Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions app/api/enhance-prompt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,17 @@ interface EnhancePromptErrorResponse {
type EnhancePromptResponse = EnhancePromptSuccessResponse | EnhancePromptErrorResponse

/**
* POST /api/enhance-prompt
* Handle POST /api/enhance-prompt: authenticate the user, enforce per-user rate limits, validate input,
* and return an enhanced prompt or negative prompt generated by the LLM.
*
* Enhances an image generation prompt or negative prompt using LLM.
* Uses the request.signal to handle client disconnection/cancellation.
* Validates that `prompt` is present and `type` is either `"prompt"` or `"negative"`, then calls the
* appropriate enhancement routine while passing the request's abort signal for cancellation support.
*
* @param request - Incoming NextRequest; `request.signal` is used to cancel in-flight enhancement calls.
* @returns On success, an object with `{ success: true, data: { enhancedText: string } }`.
* On failure, an object with `{ success: false, error: { code: string, message: string } }`
* and an appropriate HTTP status code (e.g., 401 for unauthenticated, 429 for rate limit,
* 400 for validation errors, 499 for client-cancelled, 4xx/5xx for other failures).
*/
export async function POST(
request: NextRequest
Expand Down Expand Up @@ -198,4 +205,3 @@ export async function POST(
)
}
}

10 changes: 5 additions & 5 deletions app/api/suggestions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ interface SuggestionsErrorResponse {
type SuggestionsResponse = SuggestionsSuccessResponse | SuggestionsErrorResponse

/**
* POST /api/suggestions
* Produce contextual prompt suggestions for an authenticated user based on the request body.
*
* Generates contextual prompt suggestions based on user input.
* Optimized for speed with minimal processing.
*/
* Enforces authentication and per-user rate limiting; includes rate-limit headers on responses.
* Empty prompts produce an empty suggestions array. If suggestion generation is cancelled, returns a 499 CANCELLED error; on unexpected errors returns an empty suggestions array (`success: true`) to avoid disrupting the UI.
*
* @returns A SuggestionsResponse: on success (`success: true`) contains `data.suggestions` (string[]); on failure (`success: false`) contains `error` with `code` and `message`. Rate-limit and authentication failures are returned with appropriate HTTP status codes and accompanying headers.
export async function POST(
request: NextRequest
): Promise<NextResponse<SuggestionsResponse>> {
Expand Down Expand Up @@ -147,4 +148,3 @@ export async function POST(
})
}
}

11 changes: 7 additions & 4 deletions app/api/user/balance/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!)
const POLLINATIONS_BALANCE_URL = "https://gen.pollinations.ai/api/usage?limit=100"

/**
* GET /api/user/balance
* Retrieve the authenticated user's Pollinations pending spend/balance.
*
* Fetches the user's pending spend/balance from Pollinations API.
* Requires authentication and a stored Pollinations API key.
* Proxies a request to the Pollinations balance endpoint using the user's stored (and decrypted) Pollinations API key.
* Requires the requester to be authenticated; responds with appropriate HTTP status codes for authentication failures,
* missing API key, decryption or unexpected errors, or with the original Pollinations API status when that request fails.
*
* @returns A NextResponse containing the Pollinations balance JSON on success, or a JSON error payload with an appropriate HTTP status (`401`, `404`, `500`, or the Pollinations API's status code) on failure.
*/
export async function GET(): Promise<NextResponse> {
try {
Expand Down Expand Up @@ -115,4 +118,4 @@ export async function GET(): Promise<NextResponse> {
{ status: 500 }
)
}
}
}
9 changes: 8 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ export const metadata: Metadata = {
},
}

/**
* Root application layout that wraps pages with global providers, theming, fonts, and shared UI.
*
* Wraps `children` with HTML/body and a hierarchy of providers (theme, auth, data, query), renders the site header, toast container, performance insights, and analytics.
*
* @param children - The page content to render inside the layout
* @returns The root HTML element containing global providers and the rendered page content
*/
export default function RootLayout({
children,
}: Readonly<{
Expand Down Expand Up @@ -147,4 +155,3 @@ export default function RootLayout({
</html>
)
}

9 changes: 8 additions & 1 deletion app/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ const featureComparison = [
{ feature: "NSFW Generations", starter: true, pro: true, competitors: false },
]

/**
* Render the pricing page UI and coordinate user checkout and Stripe redirect feedback.
*
* Renders the full pricing experience (hero/value proposition, tier cards with CTAs, feature comparison table, FAQs, final CTA, and footer). Handles CTA actions and checkout flows: redirects immediately for the Starter trial, shows an informational message for Competitors, requires sign-in before starting a paid subscription, verifies Stripe configuration, initiates a Pro checkout session via the Convex action, redirects to Stripe Checkout, and shows toast notifications for Stripe success or cancellation redirects.
*
* @returns The React element for the pricing page UI.
*/
function PricingContent() {
const [loadingTier, setLoadingTier] = useState<TierName | null>(null)
const { isSignedIn } = useUser()
Expand Down Expand Up @@ -526,4 +533,4 @@ export default function PricingPage() {
<PricingContent />
</Suspense>
)
}
}
9 changes: 8 additions & 1 deletion app/robots.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { MetadataRoute } from "next"

/**
* Create the Next.js robots metadata describing crawl rules and sitemap location.
*
* The sitemap URL is constructed from the NEXT_PUBLIC_APP_URL environment variable, falling back to "https://bloomstudio.fun" when unset.
*
* @returns A MetadataRoute.Robots object with crawling rules (userAgent `"*"`, allow `"/"`, disallow `["/api/", "/studio/"]`) and `sitemap` set to the site's sitemap URL.
*/
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://bloomstudio.fun"

Expand All @@ -12,4 +19,4 @@ export default function robots(): MetadataRoute.Robots {
},
sitemap: `${baseUrl}/sitemap.xml`,
}
}
}
9 changes: 8 additions & 1 deletion app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { MetadataRoute } from "next"

/**
* Produce the sitemap configuration for the application.
*
* The returned array contains sitemap entries for the site; the root entry's URL is taken from `NEXT_PUBLIC_APP_URL` or falls back to "https://bloomstudio.fun", with `lastModified` set to the current time, `changeFrequency` set to `"daily"`, and `priority` set to `1`.
*
* @returns An array of sitemap entries for the site, including the root entry described above.
*/
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://bloomstudio.fun"

Expand All @@ -12,4 +19,4 @@ export default function sitemap(): MetadataRoute.Sitemap {
},
// Add other static pages here
]
}
}
9 changes: 6 additions & 3 deletions components/gallery/image-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import { Loader2 } from "lucide-react"
import Image from "next/image"

/**
* Component to display the user's generated image history.
* Supports infinite scrolling with a "Load More" button.
* Render the user's generated image history as a responsive grid with controls for visibility, deletion, and pagination.
*
* Displays skeleton placeholders while the first page is loading, an empty state when there are no images, and a grid of image cards otherwise. Each card shows the prompt and model name and provides a visibility toggle and delete action. When additional pages are available, a "Load More" button is shown to fetch more items.
*
* @returns The React element representing the image history UI
*/
export function ImageHistory() {
const { results, status, loadMore } = useImageHistoryWithDisplayData()
Expand Down Expand Up @@ -115,4 +118,4 @@ export function ImageHistory() {
)}
</div>
)
}
}
14 changes: 13 additions & 1 deletion components/studio/api-key-onboarding-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ interface ApiKeyOnboardingModalProps {
onClose?: () => void
}

/**
* Displays a modal that guides authenticated users through obtaining and saving a Pollinations API key.
*
* The modal can operate in automatic mode (opens for authenticated users who do not yet have a saved API key)
* or in controlled mode when `forceOpen` is provided. It validates the entered key, posts it to the server,
* and invokes callbacks on completion or close.
*
* @param onComplete - Optional callback invoked after a key is successfully saved.
* @param forceOpen - When provided, forces the modal's open state (enables controlled mode).
* @param onClose - Optional callback invoked when the modal is closed (used in controlled mode).
* @returns The onboarding modal JSX; in automatic mode returns `null` when the modal should not be shown.
*/
export function ApiKeyOnboardingModal({ onComplete, forceOpen, onClose }: ApiKeyOnboardingModalProps) {
const [apiKey, setApiKey] = React.useState("")
const [isSaving, setIsSaving] = React.useState(false)
Expand Down Expand Up @@ -287,4 +299,4 @@ export function ApiKeyOnboardingModal({ onComplete, forceOpen, onClose }: ApiKey
</DialogContent>
</Dialog>
)
}
}
19 changes: 9 additions & 10 deletions components/studio/features/history/gallery-feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@ export interface GalleryFeatureProps {
}

/**
* GalleryFeature component - composes hook logic with view
*
* @example
* ```tsx
* <GalleryFeature
* activeImageId={currentImage?.id}
* onSelectImage={handleSelectGalleryImage}
* />
* ```
* Render a gallery UI by forwarding gallery-related props to GalleryView.
*
* Renders the GalleryView component with the provided active image, selection callback, and thumbnail size.
*
* @param activeImageId - ID of the currently active/highlighted image, if any
* @param onSelectImage - Callback invoked with the selected thumbnail's data when a thumbnail is chosen
* @param thumbnailSize - Size of thumbnails to display ("sm", "md", or "lg")
* @returns The gallery view element
*/
export function GalleryFeature({
activeImageId,
Expand All @@ -46,4 +45,4 @@ export function GalleryFeature({
thumbnailSize={thumbnailSize}
/>
)
}
}
19 changes: 6 additions & 13 deletions components/studio/layout/studio-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,11 @@ export interface StudioShellProps {
}

/**
* StudioShell - The main Studio composition component
*
* This component:
* 1. Initializes all feature hooks at the top level
* 2. Provides contexts for cross-feature communication
* 3. Handles generation orchestration (combining prompt + settings)
* 4. Renders the layout with composed features
*
* @example
* ```tsx
* <StudioShell defaultLayout={{ sidebar: 22, gallery: 18 }} />
* ```
* Compose and render the Studio interface while wiring feature hooks, cross-feature contexts, and image generation orchestration.
*
* Initializes prompt, generation, UI, and gallery hooks; coordinates single and batch image generation flows; manages upgrade and keyboard shortcuts; and renders the header, sidebar, canvas, gallery, and associated modals (API key onboarding, upgrade modal, and image lightbox).
*
* @param defaultLayout - Optional mapping of layout regions to their default sizes (e.g., `{ sidebar: 22, gallery: 18 }`) used to seed the studio layout
*/
export function StudioShell({ defaultLayout }: StudioShellProps) {
// ========================================
Expand Down Expand Up @@ -429,4 +422,4 @@ export function StudioShell({ defaultLayout }: StudioShellProps) {
/>
</div>
)
}
}
9 changes: 8 additions & 1 deletion components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { CheckIcon, MinusIcon } from "lucide-react"

import { cn } from "@/lib/utils"

/**
* A styled checkbox that wraps Radix UI's Checkbox root and shows a check or minus icon for indeterminate state.
*
* @param className - Optional additional CSS class names to apply to the root element
* @param props - All other props are forwarded to Radix UI's Checkbox root (for example: `checked`, `disabled`, `onCheckedChange`)
* @returns The rendered CheckboxPrimitive.Root element with an indicator that shows a minus icon when `checked` is `"indeterminate"` and a check icon otherwise
*/
function Checkbox({
className,
...props
Expand Down Expand Up @@ -33,4 +40,4 @@ function Checkbox({
)
}

export { Checkbox }
export { Checkbox }
22 changes: 21 additions & 1 deletion hooks/use-image-lightbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ interface UseImageLightboxProps {
isOpen: boolean
}

/**
* Provides state and event handlers for displaying and interacting with an image lightbox.
*
* Manages copy-to-clipboard state for the image prompt, zooming, natural vs rendered image sizes,
* and drag-to-scroll behavior when zoomed; also exposes a scroll container ref.
*
* @param image - The image object shown in the lightbox (may be null); its optional `prompt` is used by copy behavior.
* @param isOpen - Whether the lightbox is currently open; used to reset transient state when changed.
* @returns An object with:
* - `copied`: `true` when the image prompt was recently copied to the clipboard.
* - `isZoomed`: `true` when the image is displayed zoomed in.
* - `naturalSize`: The image's natural `{ width, height }` in pixels.
* - `isDragging`: `true` while a drag-to-scroll operation is active.
* - `scrollContainerRef`: Ref to the scrollable container element.
* - `canZoom`: `true` when the image's natural size is sufficiently larger than its rendered size.
* - `handleCopyPrompt`: Click handler that copies the image prompt to the clipboard.
* - `handleImageLoad`: Image load handler that updates natural and rendered sizes.
* - `toggleZoom`: Click handler that toggles zoom state (centers content when enabling zoom).
* - `handleMouseDown`, `handleMouseMove`, `handleMouseUp`, `handleMouseLeave`: Mouse handlers for drag-to-scroll when zoomed.
*/
export function useImageLightbox({ image, isOpen }: UseImageLightboxProps) {
const [copied, setCopied] = React.useState(false)
const [isZoomed, setIsZoomed] = React.useState(false)
Expand Down Expand Up @@ -147,4 +167,4 @@ export function useImageLightbox({ image, isOpen }: UseImageLightboxProps) {
handleMouseUp,
handleMouseLeave
}
}
}
12 changes: 7 additions & 5 deletions hooks/use-studio-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export interface UseStudioUIReturn {
}

/**
* Hook for managing Studio UI state.
*
* Manage Studio UI state for sidebar, gallery, and fullscreen/lightbox with stable callbacks.
*
* @example
* ```tsx
* const {
Expand All @@ -56,13 +56,15 @@ export interface UseStudioUIReturn {
* showGallery,
* openLightbox,
* } = useStudioUI()
*
*
* // Toggle sidebar
* <Button onClick={toggleLeftSidebar}>Toggle Sidebar</Button>
*
*
* // Open lightbox
* <ImageThumbnail onClick={() => openLightbox(image)} />
* ```
*
* @returns An object exposing sidebar and gallery visibility (`showLeftSidebar`, `showGallery`) with their setters and toggle callbacks, fullscreen state (`isFullscreen`) with its setter, the current `lightboxImage` with its setter, and `openLightbox` / `closeLightbox` handlers.
*/
export function useStudioUI(): UseStudioUIReturn {
// ========================================
Expand Down Expand Up @@ -127,4 +129,4 @@ export function useStudioUI(): UseStudioUIReturn {
openLightbox,
closeLightbox,
}
}
}
21 changes: 17 additions & 4 deletions lib/storage/r2-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
import { withRetry, isRetryableError } from "./retry"
import crypto from "crypto"

// Validate required environment variables
/**
* Retrieve a required environment variable by name.
*
* @param name - The environment variable key to read from process.env
* @returns The value of the environment variable
* @throws Error if the environment variable is not set or is empty
*/
function getEnvVar(name: string): string {
const value = process.env[name]
if (!value) {
Expand Down Expand Up @@ -122,9 +128,16 @@ export async function imageExists(key: string): Promise<boolean> {
}

/**
* Generate a unique object key for an image
* Generate a unique storage key for an image.
*
* Format: {type}/{userId}/{timestamp}-{randomId}.{ext}
* The key is formatted as `{type}/{userHash}/{timestamp}-{randomId}.{ext}` where `userHash`
* is the SHA-256 hash of `userId`, `timestamp` is milliseconds since the Unix epoch,
* `randomId` is a UUID, and `ext` is derived from `contentType` (defaults to `jpg` if absent).
*
* @param userId - The identifier for the user; its SHA-256 hash is used in the key
* @param type - Top-level path segment, either `"generated"` or `"reference"`
* @param contentType - MIME type used to derive the file extension (e.g., `"image/png"`)
* @returns The generated object key string
*/
export function generateImageKey(
userId: string,
Expand Down Expand Up @@ -152,4 +165,4 @@ export function getPublicUrl(key: string): string {
*/
export function _resetClient(): void {
_client = null
}
}