diff --git a/README.md b/README.md index 6fbdff7..8c6b087 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ ## Features -- ๐ŸŽจ **13+ AI Models** โ€” Choose from Nano Banana Pro, GPT 1.5, Seedream, Veo 3.1, Seedance and more -- โšก **Generous Limits** โ€” 5000~ Z-image turbo generations per day +- ๐ŸŽจ **6 AI Models** โ€” Choose from Flux Schnell, Z-Image Turbo, GPT Image, FLUX.2 Klein, and Seedance +- โšก **Generous Limits** โ€” Up to 900,000 Flux Schnell generations per month (180 high-quality images) - ๐Ÿ“ **Full Dimension Control** โ€” Create logos, banners, or any custom image size - ๐Ÿ’พ **Cloud History** โ€” Access your generated images from any device - ๐Ÿ” **Secure Auth** โ€” Powered by Clerk with SSO support diff --git a/app/layout.tsx b/app/layout.tsx index ba427ad..251b590 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -39,7 +39,7 @@ export const metadata: Metadata = { template: "%s | Bloom Studio", }, description: - "Bloom Studio, a cheap and powerful AI image generator and video generator studio. Featuring Nano Banana Pro, Veo, GPT 1.5, Seedream 4.5 and more. Try for free.", + "Bloom Studio, a cheap and powerful AI image generator and video generator studio. Featuring Flux Schnell, Z-Image Turbo, GPT Image, FLUX.2 Klein, and Seedance. Try for free.", metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || "https://bloomstudio.fun"), applicationName: "Bloom Studio", authors: [{ name: "Bloom Studio Team" }], @@ -85,7 +85,7 @@ export const metadata: Metadata = { url: "/", title: "Bloom Studio - Cheap and Powerful AI Image & Video Generator", description: - "Bloom Studio, a cheap and powerful AI image generator and video generator studio. Featuring Nano Banana Pro, Veo, GPT 1.5, Seedream 4.5 and more. Try for free.", + "Bloom Studio, a cheap and powerful AI image generator and video generator studio. Featuring Flux Schnell, Z-Image Turbo, GPT Image, FLUX.2 Klein, and Seedance. Try for free.", siteName: "Bloom Studio", images: [ { @@ -100,7 +100,7 @@ export const metadata: Metadata = { card: "summary_large_image", title: "Bloom Studio - Cheap and Powerful AI Image & Video Generator", description: - "Bloom Studio, a cheap and powerful AI image generator and video generator studio. Featuring Nano Banana Pro, Veo, GPT 1.5, Seedream 4.5 and more. Try for free.", + "Bloom Studio, a cheap and powerful AI image generator and video generator studio. Featuring Flux Schnell, Z-Image Turbo, GPT Image, FLUX.2 Klein, and Seedance. Try for free.", images: ["/branding/bloom-studio_logo.png"], creator: "@bloomstudio", }, diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 44b57a1..68431b1 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -47,7 +47,7 @@ export default function PricingPage() { "@context": "https://schema.org", "@type": "Product", name: "Bloom Studio Pro", - description: "Professional AI image and video generation subscription. Includes 180 images/month, all models (Flux, Veo, etc), and private generations.", + description: "Professional AI image and video generation subscription. Includes 180 images/month, all active models (Flux, GPT Image, Z-Image, Klein, Seedance), and private generations.", image: "https://bloomstudio.fun/branding/bloom-studio_logo.png", offers: { "@type": "Offer", @@ -121,26 +121,7 @@ export default function PricingPage() {
-

- {tier.description.includes("Nano Banana Pro") ? ( - - {tier.description.split("Nano Banana Pro")[0]} - - Gemini - Nano Banana Pro - - {tier.description.split("Nano Banana Pro")[1]} - - ) : ( - tier.description - )} -

+

{tier.description}

{tier.poweredBy && tier.name !== "Pro" && (
- ) : feature.includes("Nano Banana Pro") ? ( - - {feature.split("Nano Banana Pro")[0]} - - Gemini - Nano Banana Pro - - {feature.split("Nano Banana Pro")[1]} - ) : ( feature )} diff --git a/app/pricing/pricing-data.tsx b/app/pricing/pricing-data.tsx index 7f995ba..1d5ea27 100644 --- a/app/pricing/pricing-data.tsx +++ b/app/pricing/pricing-data.tsx @@ -36,8 +36,8 @@ export const pricingTiers: PricingTier[] = [ name: "Pro", description: "Access to every feature and model", poweredBy: { - name: "Nano Banana Pro", - logo: "/image-models/google.svg", + name: "Flux Schnell", + logo: "/image-models/flux.svg", }, price: 3, icon: Crown, @@ -46,9 +46,9 @@ export const pricingTiers: PricingTier[] = [ cta: "Upgrade to Pro", ctaVariant: "default", features: [ - "180 Nano Banana Pro images/month", + "180 high-quality images/month", "Daily resets", - "13+ AI models included", + "6 AI models included (Flux, GPT Image, Z-Image, Klein, Seedance)", "Advanced prompt enhancement", "Custom prompt library", "1,000 image batch queue", @@ -79,7 +79,7 @@ export const pricingFaqs = [ { question: "How does the free plan work?", answer: - "Sign up and get 180 Nano Banana Pro images every month for free. Your quota resets daily (approx. 6 images/day). No credit card required.", + "Sign up and get 180 high-quality images every month for free. Your quota resets daily (approx. 6 images/day). No credit card required.", }, { question: "What happens if I need more images?", @@ -90,7 +90,7 @@ export const pricingFaqs = [ question: "What AI models are included?", answer: ( - We offer 13+ cutting-edge and popular models including Flux, GPT-4 Image, Seedream, and more. Both Free and Pro users get access to all models.{" "} + We offer 6 high-quality AI models including Flux Schnell, GPT Image, Z-Image Turbo, FLUX.2 Klein, and Seedance. Both Free and Pro users get access to all active models.{" "} View the full list of supported models here. @@ -128,7 +128,7 @@ export const featureComparison = [ }, }, { feature: "Batch queue size", starter: "1,000", pro: "1,000", competitors: "10-20" }, - { feature: "AI Models", starter: "13+", pro: "13+", competitors: "13+" }, + { feature: "AI Models", starter: "6", pro: "6", competitors: "Varies" }, { feature: "Prompt library", starter: true, pro: true, competitors: false }, { feature: "Prompt enhancement", starter: true, pro: true, competitors: "Basic" }, { feature: "Private gallery", starter: true, pro: true, competitors: "Varies" }, diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c8cf796 --- /dev/null +++ b/biome.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "files": { + "ignore": ["convex/_generated/**", ".next/**", "node_modules/**"] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true + } +} diff --git a/components/gallery/history-filters.test.tsx b/components/gallery/history-filters.test.tsx index 017e809..97207e1 100644 --- a/components/gallery/history-filters.test.tsx +++ b/components/gallery/history-filters.test.tsx @@ -47,7 +47,7 @@ if (!global.PointerEvent) { this.isPrimary = params.isPrimary || false } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.PointerEvent = PointerEvent as any } diff --git a/components/gallery/history-filters.tsx b/components/gallery/history-filters.tsx index debaadb..0a8a257 100644 --- a/components/gallery/history-filters.tsx +++ b/components/gallery/history-filters.tsx @@ -44,39 +44,25 @@ interface HistoryFiltersProps { const IMAGE_MODELS: ModelDefinition[] = Object.values(MODEL_REGISTRY).filter( (model): model is ModelDefinition => model.type === "image", ); - // Get all video models from the registry const VIDEO_MODELS: ModelDefinition[] = Object.values(MODEL_REGISTRY).filter( (model): model is ModelDefinition => model.type === "video", ); -/** - * Legacy models that are no longer available for generation but may exist in user history. - * These are shown separately in the filter dropdown so users can still filter by them. - */ -const LEGACY_FILTER_MODELS: { id: string; displayName: string }[] = [ - // Currently empty - 'flux' was moved back to MODEL_REGISTRY as an active model -]; - -/** Combined list of all filterable models (image + video + legacy), sorted alphabetically */ +/** Combined list of all filterable models (image + video), sorted alphabetically */ const ALL_FILTERABLE_MODELS = [ ...IMAGE_MODELS.map((m) => ({ id: m.id, displayName: m.displayName, - isLegacy: false, + isLegacy: m.isLegacy ?? false, type: "image" as const, })), ...VIDEO_MODELS.map((m) => ({ id: m.id, displayName: m.displayName, - isLegacy: false, + isLegacy: m.isLegacy ?? false, type: "video" as const, })), - ...LEGACY_FILTER_MODELS.map((m) => ({ - ...m, - isLegacy: true, - type: "image" as const, - })), ].sort((a, b) => a.displayName.localeCompare(b.displayName, undefined, { numeric: true }), ); @@ -294,7 +280,7 @@ export function HistoryFiltersDropdown({ handleModelToggle(model.id)} - className="gap-2" + className={cn("gap-2", model.isLegacy && "opacity-60")} > ({ // Mock Next.js Image vi.mock("next/image", () => ({ default: ({ src, alt, ...props }: React.ImgHTMLAttributes & { fill?: boolean }) => { - // eslint-disable-next-line @next/next/no-img-element + return {alt} }, diff --git a/components/images/image-lightbox.test.tsx b/components/images/image-lightbox.test.tsx index f595881..d1a9f9f 100644 --- a/components/images/image-lightbox.test.tsx +++ b/components/images/image-lightbox.test.tsx @@ -7,23 +7,66 @@ import { ImageLightbox } from "./image-lightbox"; // Mock Clerk auth vi.mock("@clerk/nextjs", () => ({ - useAuth: () => ({ isSignedIn: true }), + useAuth: () => ({ isSignedIn: true }), +})); + +vi.mock("@/hooks/use-image-edit", () => ({ + useImageEdit: () => ({ + isEditPanelOpen: false, + openEditPanel: vi.fn(), + closeEditPanel: vi.fn(), + editPrompt: "", + setEditPrompt: vi.fn(), + selectedModel: "kontext", + setSelectedModel: vi.fn(), + isGenerating: false, + error: null, + submitEdit: vi.fn(), + cancelGeneration: vi.fn(), + reset: vi.fn(), + editModels: [], + canSubmit: false, + selectedAspectRatio: "1:1", + setSelectedAspectRatio: vi.fn(), + selectedResolutionTier: "hd", + setSelectedResolutionTier: vi.fn(), + outputWidth: 1024, + outputHeight: 1024, + sourceFormatInfo: null, + availableAspectRatios: [], + initializeFromSource: vi.fn(), + }), })); // Mock react-zoom-pan-pinch interface TransformWrapperChildrenArgs { - zoomIn: () => void; - zoomOut: () => void; - resetTransform: () => void; - state: { scale: number }; + zoomIn: () => void; + zoomOut: () => void; + resetTransform: () => void; + state: { scale: number }; } vi.mock("react-zoom-pan-pinch", () => ({ - TransformWrapper: ({ children }: { children: (args: TransformWrapperChildrenArgs) => ReactNode }) => { - // Simulate zoom callback availability if needed - return
{children({ zoomIn: vi.fn(), zoomOut: vi.fn(), resetTransform: vi.fn(), state: { scale: 1 } })}
; - }, - TransformComponent: ({ children }: { children?: ReactNode }) =>
{children}
, + TransformWrapper: ({ + children, + }: { + children: (args: TransformWrapperChildrenArgs) => ReactNode; + }) => { + // Simulate zoom callback availability if needed + return ( +
+ {children({ + zoomIn: vi.fn(), + zoomOut: vi.fn(), + resetTransform: vi.fn(), + state: { scale: 1 }, + })} +
+ ); + }, + TransformComponent: ({ children }: { children?: ReactNode }) => ( +
{children}
+ ), })); // Mock React hooks that the component uses @@ -33,441 +76,744 @@ const mockHandleInsert = vi.fn(); let capturedOnInsertComplete: (() => void) | undefined; interface UsePromptLibraryMockProps { - onInsertComplete?: () => void; - onInsert: (content: string) => void; - onClose: () => void; + onInsertComplete?: () => void; + onInsert: (content: string) => void; + onClose: () => void; } interface PromptMock { - _id: string; - title: string; - content: string; - type: string; - tags: string[]; + _id: string; + title: string; + content: string; + type: string; + tags: string[]; } vi.mock("@/hooks/use-prompt-library", () => ({ - usePromptLibrary: (props: UsePromptLibraryMockProps) => { - capturedOnInsertComplete = props.onInsertComplete; - return { - searchQuery: "", - setSearchQuery: vi.fn(), - searchInputRef: { current: null }, - viewState: "list", - setViewState: vi.fn(), - selectedPrompt: null, - selectPrompt: vi.fn(), - typeFilter: "all", - setTypeFilter: vi.fn(), - prompts: [{ _id: "1", title: "Test Prompt", content: "Test Content", type: "positive", tags: [] } satisfies PromptMock], - isLoading: false, - handleCopy: vi.fn(), - handleInsert: (content: string) => { - mockHandleInsert(content); - props.onInsert(content); - props.onClose(); - props.onInsertComplete?.(); - }, - handleRemove: vi.fn(), - showSaveForm: vi.fn(), - goBackToList: vi.fn(), - }; - }, + usePromptLibrary: (props: UsePromptLibraryMockProps) => { + capturedOnInsertComplete = props.onInsertComplete; + return { + searchQuery: "", + setSearchQuery: vi.fn(), + searchInputRef: { current: null }, + viewState: "list", + setViewState: vi.fn(), + selectedPrompt: null, + selectPrompt: vi.fn(), + typeFilter: "all", + setTypeFilter: vi.fn(), + prompts: [ + { + _id: "1", + title: "Test Prompt", + content: "Test Content", + type: "positive", + tags: [], + } satisfies PromptMock, + ], + isLoading: false, + handleCopy: vi.fn(), + handleInsert: (content: string) => { + mockHandleInsert(content); + props.onInsert(content); + props.onClose(); + props.onInsertComplete?.(); + }, + handleRemove: vi.fn(), + showSaveForm: vi.fn(), + goBackToList: vi.fn(), + }; + }, })); // Mock the useImageLightbox hook vi.mock("@/hooks/use-image-lightbox", () => ({ - useImageLightbox: () => ({ - copied: false, - isZoomed: false, - toggleZoom: vi.fn(), - handleCopyPrompt: vi.fn(), - handleImageLoad: vi.fn(), - canZoom: true, - isHovering: true, - setIsHovering: vi.fn(), - // Legacy/unused props that might be destructured - naturalSize: { width: 1000, height: 1000 }, - isDragging: false, - scrollContainerRef: { current: null }, - handleMouseDown: vi.fn(), - handleMouseMove: vi.fn(), - handleMouseUp: vi.fn(), - handleMouseLeave: vi.fn(), - }), + useImageLightbox: () => ({ + copied: false, + isZoomed: false, + toggleZoom: vi.fn(), + handleCopyPrompt: vi.fn(), + handleImageLoad: vi.fn(), + canZoom: true, + isHovering: true, + setIsHovering: vi.fn(), + naturalSize: { width: 1000, height: 1000 }, + isDragging: false, + scrollContainerRef: { current: null }, + handleMouseDown: vi.fn(), + handleMouseMove: vi.fn(), + handleMouseUp: vi.fn(), + handleMouseLeave: vi.fn(), + hasDragged: { current: false }, + }), })); // Mock the image details query interface ImageDetailsMock { - url: string; - prompt: string; - model: string; - width?: number; - height?: number; - contentType?: string; - _id?: string; + url: string; + prompt: string; + model: string; + width?: number; + height?: number; + contentType?: string; + _id?: string; } // Use vi.hoisted() to ensure the mock is available before the hoisted vi.mock runs -const useImageDetailsMock = vi.hoisted(() => vi.fn<(imageId: string | null) => ImageDetailsMock | null | undefined>(() => null)); +const useImageDetailsMock = vi.hoisted(() => + vi.fn<(imageId: string | null) => ImageDetailsMock | null | undefined>( + () => null, + ), +); vi.mock("@/hooks/queries/use-image-history", () => ({ - useImageDetails: useImageDetailsMock, + useImageDetails: useImageDetailsMock, })); // Mock MediaPlayer vi.mock("@/components/ui/media-player", () => ({ - MediaPlayer: ({ - url, - alt, - contentType, - onLoadedMetadata, - onLoad, - }: { - url: string; - alt: string; - contentType?: string; - onLoadedMetadata?: ReactEventHandler; - onLoad?: ReactEventHandler; - }) => { - const isVideo = contentType?.startsWith("video/") || url?.match(/\.(mp4|webm|mov)$/i); - if (isVideo) { - return