diff --git a/apps/landing/src/app/docs/layout.tsx b/apps/landing/src/app/docs/layout.tsx index 67e1f43..ea7575e 100644 --- a/apps/landing/src/app/docs/layout.tsx +++ b/apps/landing/src/app/docs/layout.tsx @@ -6,6 +6,7 @@ import { SubsectionNav } from '@/components/shared/SubsectionNav'; import { DocsSidebar } from '@/components/docs/layout/DocsSidebar'; import { ThreeColumnLayout } from '@/components/layout/ThreeColumnLayout'; import { ApiKeyWidget } from '@/components/shared/ApiKeyWidget/apikey-widget'; +import { ApiKeyProvider } from '@/components/shared/ApiKeyWidget/apikey-context'; /** * Root Documentation Layout @@ -45,33 +46,35 @@ export default function DocsRootLayout({ children }: DocsRootLayoutProps) { const pathname = usePathname(); return ( -
- {/* Animated gradient background (matching landing page) */} -
-
-
-
+ +
+ {/* Animated gradient background (matching landing page) */} +
+
+
+
- {/* Subsection Navigation */} - } - /> - - {/* Three-column Documentation Layout */} -
- - -
- } - center={children} + {/* Subsection Navigation */} + } /> + + {/* Three-column Documentation Layout */} +
+ + +
+ } + center={children} + /> +
- + ); } diff --git a/apps/landing/src/components/shared/ApiKeyWidget/apikey-context.tsx b/apps/landing/src/components/shared/ApiKeyWidget/apikey-context.tsx index bc8f795..b82124d 100644 --- a/apps/landing/src/components/shared/ApiKeyWidget/apikey-context.tsx +++ b/apps/landing/src/components/shared/ApiKeyWidget/apikey-context.tsx @@ -1,17 +1,229 @@ -/* +'use client'; -TODO: create ApiKeyProvider and useApiKey hook to access provider values +/** + * API Key Context Provider + * + * Centralized state management for API key functionality across demo pages. + * Provides: + * - API key validation and storage + * - UI state management (expanded, visibility) + * - Focus method for external components + * - Automatic validation of stored keys on mount + * + * Phase 1 of centralizing apikey functionality (previously duplicated across demo pages) + */ -usage example: const { organizationSlug, projectSlug, apiKey, onRevoke } = useApiKey(); +import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react'; +import { + validateApiKeyRequest, + getStoredKey, + setStoredKey, + clearStoredKey, + type ApiKeyContextValue, + type ApiKeyProviderProps, + type ApiKeyInfo, +} from '@/lib/apikey'; -see ApiKeyWidget component (src/components/shared/ApiKeyWidget/apikey-widget.tsx) +// Create context with undefined default (will error if used outside provider) +const ApiKeyContext = createContext(undefined); -the functionality should be strictly copied from src/app/demo/tti/page.tsx (see apikey related functionality) and MinimizedApiKey +/** + * API Key Provider Component + * + * Wraps the application to provide global API key state and actions + */ +export function ApiKeyProvider({ children, onValidationSuccess }: ApiKeyProviderProps) { + // Data State + const [apiKey, setApiKeyState] = useState(''); + const [apiKeyValidated, setApiKeyValidated] = useState(false); + const [apiKeyInfo, setApiKeyInfo] = useState(null); + const [apiKeyError, setApiKeyError] = useState(''); + const [validatingKey, setValidatingKey] = useState(false); + const [isReady, setIsReady] = useState(false); -Move apikey simple actions in src/lib/apikey + // UI State (grouped) + const [expanded, setExpandedState] = useState(false); + const [keyVisible, setKeyVisible] = useState(false); -Provider should centralize apikey information and organize actions call from lib and then provide apikey related functionality down to children + // Refs + const inputRef = useRef(null); + const containerRef = useRef(null); + /** + * Validate stored API key on mount + */ + const validateStoredApiKey = useCallback( + async (keyToValidate: string) => { + setValidatingKey(true); + setApiKeyError(''); -This is phase 1 of a task of centralizing apikey functionality that now is not DRY in demo pages -*/ \ No newline at end of file + const result = await validateApiKeyRequest(keyToValidate); + + if (result.success && result.keyInfo) { + setApiKeyValidated(true); + setApiKeyInfo(result.keyInfo); + setApiKeyState(keyToValidate); + + // Call optional success callback + onValidationSuccess?.(result.keyInfo); + } else { + // Stored key is invalid, clear it + clearStoredKey(); + setApiKeyError(result.error || 'Stored API key is invalid or expired'); + setApiKeyValidated(false); + } + + setValidatingKey(false); + setIsReady(true); + }, + [onValidationSuccess] + ); + + /** + * Initialize: check for stored API key + */ + useEffect(() => { + const storedKey = getStoredKey(); + if (storedKey) { + validateStoredApiKey(storedKey); + } else { + setIsReady(true); + } + }, [validateStoredApiKey]); + + /** + * Validate API key (user-triggered) + */ + const validateApiKey = useCallback(async () => { + setValidatingKey(true); + setApiKeyError(''); + + const result = await validateApiKeyRequest(apiKey); + + if (result.success && result.keyInfo) { + setApiKeyValidated(true); + setApiKeyInfo(result.keyInfo); + setStoredKey(apiKey); + + // Collapse widget after successful validation + setExpandedState(false); + + // Call optional success callback + onValidationSuccess?.(result.keyInfo); + } else { + setApiKeyError(result.error || 'Validation failed'); + setApiKeyValidated(false); + } + + setValidatingKey(false); + }, [apiKey, onValidationSuccess]); + + /** + * Revoke API key + * Optionally accepts a cleanup function for page-specific state + */ + const revokeApiKey = useCallback((clearPageState?: () => void) => { + clearStoredKey(); + setApiKeyState(''); + setApiKeyValidated(false); + setApiKeyInfo(null); + setApiKeyError(''); + setExpandedState(true); // Expand to show input after revoke + + // Call optional page-specific cleanup + clearPageState?.(); + }, []); + + /** + * Toggle API key visibility + */ + const toggleKeyVisibility = useCallback(() => { + setKeyVisible((prev) => !prev); + }, []); + + /** + * Set expanded state + */ + const setExpanded = useCallback((value: boolean) => { + setExpandedState(value); + }, []); + + /** + * Set API key value + */ + const setApiKey = useCallback((key: string) => { + setApiKeyState(key); + setApiKeyError(''); // Clear error when user types + }, []); + + /** + * Focus method - for external components to trigger focus + * Expands widget, focuses input, and scrolls into view + */ + const focus = useCallback(() => { + setExpandedState(true); + + // Wait for expansion animation, then focus + setTimeout(() => { + inputRef.current?.focus(); + containerRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + }, 100); + }, []); + + const value: ApiKeyContextValue = { + // Data State + apiKey, + apiKeyValidated, + apiKeyInfo, + apiKeyError, + validatingKey, + isReady, + + // UI State + ui: { + expanded, + keyVisible, + }, + + // Actions + setApiKey, + validateApiKey, + revokeApiKey, + toggleKeyVisibility, + setExpanded, + + // Focus method + focus, + }; + + return ( + +
{children}
+
+ ); +} + +/** + * useApiKey Hook + * + * Access API key context values and actions + * Must be used within ApiKeyProvider + * + * @example + * const { apiKey, apiKeyValidated, validateApiKey, focus } = useApiKey(); + */ +export function useApiKey(): ApiKeyContextValue { + const context = useContext(ApiKeyContext); + + if (context === undefined) { + throw new Error('useApiKey must be used within an ApiKeyProvider'); + } + + return context; +} + +// Export inputRef for ApiKeyWidget to use +export { ApiKeyContext }; diff --git a/apps/landing/src/components/shared/ApiKeyWidget/apikey-widget.tsx b/apps/landing/src/components/shared/ApiKeyWidget/apikey-widget.tsx index 4a49fe9..ec41750 100644 --- a/apps/landing/src/components/shared/ApiKeyWidget/apikey-widget.tsx +++ b/apps/landing/src/components/shared/ApiKeyWidget/apikey-widget.tsx @@ -1,35 +1,96 @@ 'use client'; -import { useState } from 'react'; +/** + * API Key Widget Component + * + * Global API key management widget displayed in navigation bar. + * Features: + * - Inline collapsed state (fits in nav rightSlot) + * - Popup expanded state (absolute positioned dropdown) + * - API key validation and management + * - Click outside and Escape key to close + * - Focus method accessible via useApiKey hook + * + * Usage: + * Must be used within ApiKeyProvider context + */ -interface ApiKeyWidgetProps { - organizationSlug: string; - projectSlug: string; - apiKey: string; - onRevoke: () => void; -} +import { useEffect, useRef } from 'react'; +import { useApiKey } from './apikey-context'; export function ApiKeyWidget() { - const [expanded, setExpanded] = useState(false); - const [keyVisible, setKeyVisible] = useState(false); - const { organizationSlug, projectSlug, apiKey, onRevoke } = useApiKey(); + const { + apiKey, + apiKeyValidated, + apiKeyInfo, + apiKeyError, + validatingKey, + ui, + setApiKey, + validateApiKey, + revokeApiKey, + toggleKeyVisibility, + setExpanded, + } = useApiKey(); + + const containerRef = useRef(null); + const inputRef = useRef(null); + + // Click outside to close + useEffect(() => { + if (!ui.expanded) return; + + function handleClickOutside(event: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setExpanded(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [ui.expanded, setExpanded]); + + // Escape key to close + useEffect(() => { + if (!ui.expanded) return; + + function handleEscape(event: KeyboardEvent) { + if (event.key === 'Escape') { + setExpanded(false); + } + } + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [ui.expanded, setExpanded]); + + // Handle Enter key for validation + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !validatingKey) { + validateApiKey(); + } + }; return ( -
- {!expanded ? ( - // Minimized badge +
+ {!ui.expanded ? ( + // COLLAPSED STATE - Inline badge in nav ) : ( - // Expanded card -
-
+ // EXPANDED STATE - Popup dropdown +
+ {/* Header */} +
-

API Key Active

-

- {organizationSlug} / {projectSlug} -

+

+ {apiKeyValidated ? 'API Key Active' : 'Enter API Key'} +

+ {apiKeyValidated && apiKeyInfo && ( +

+ {apiKeyInfo.organizationSlug} / {apiKeyInfo.projectSlug} +

+ )}
+ {/* API Key Input/Display */}
- +
-
- {keyVisible ? apiKey : '•'.repeat(32)} -
+ setApiKey(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter your API key" + disabled={apiKeyValidated} + className="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-gray-300 font-mono placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all" + />
- + {/* Error Message */} + {apiKeyError && ( +
+

{apiKeyError}

+
+ )} + + {/* Actions */} +
+ {!apiKeyValidated ? ( + + ) : ( + + )} +
+ + {/* Helper Text */} + {!apiKeyValidated && ( +

+ Press Enter or click Validate to verify your API key +

+ )}
)}
diff --git a/apps/landing/src/lib/apikey/constants.ts b/apps/landing/src/lib/apikey/constants.ts new file mode 100644 index 0000000..9e35fef --- /dev/null +++ b/apps/landing/src/lib/apikey/constants.ts @@ -0,0 +1,26 @@ +/** + * API Key Management Constants + * + * Centralized constants for API key functionality across demo pages + */ + +/** + * LocalStorage key for persisting API keys + */ +export const API_KEY_STORAGE_KEY = 'banatie_demo_api_key'; + +/** + * Base URL for API requests + * Uses environment variable or falls back to localhost + */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; + +/** + * API endpoints + */ +export const API_ENDPOINTS = { + INFO: '/api/info', + TEXT_TO_IMAGE: '/api/text-to-image', + UPLOAD: '/api/upload', + IMAGES_GENERATED: '/api/images/generated', +} as const; diff --git a/apps/landing/src/lib/apikey/index.ts b/apps/landing/src/lib/apikey/index.ts new file mode 100644 index 0000000..b5db538 --- /dev/null +++ b/apps/landing/src/lib/apikey/index.ts @@ -0,0 +1,24 @@ +/** + * API Key Management Library + * + * Centralized utilities for API key validation, storage, and management + * Used across demo pages and components + */ + +// Constants +export { API_KEY_STORAGE_KEY, API_BASE_URL, API_ENDPOINTS } from './constants'; + +// Types +export type { + ApiKeyInfo, + ApiInfoResponse, + ValidationResult, + ApiKeyContextValue, + ApiKeyProviderProps, +} from './types'; + +// Validation utilities +export { validateApiKeyRequest, parseKeyInfo } from './validation'; + +// Storage utilities +export { getStoredKey, setStoredKey, clearStoredKey } from './storage'; diff --git a/apps/landing/src/lib/apikey/storage.ts b/apps/landing/src/lib/apikey/storage.ts new file mode 100644 index 0000000..e19a6e3 --- /dev/null +++ b/apps/landing/src/lib/apikey/storage.ts @@ -0,0 +1,69 @@ +/** + * API Key Storage Utilities + * + * localStorage utilities for persisting API keys + */ + +import { API_KEY_STORAGE_KEY } from './constants'; + +/** + * Check if localStorage is available (browser environment) + */ +function isLocalStorageAvailable(): boolean { + try { + return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'; + } catch { + return false; + } +} + +/** + * Get stored API key from localStorage + * + * @returns The stored API key or null if not found + */ +export function getStoredKey(): string | null { + if (!isLocalStorageAvailable()) { + return null; + } + + try { + return localStorage.getItem(API_KEY_STORAGE_KEY); + } catch (error) { + console.error('Error reading API key from localStorage:', error); + return null; + } +} + +/** + * Save API key to localStorage + * + * @param apiKey - The API key to store + */ +export function setStoredKey(apiKey: string): void { + if (!isLocalStorageAvailable()) { + console.warn('localStorage is not available'); + return; + } + + try { + localStorage.setItem(API_KEY_STORAGE_KEY, apiKey); + } catch (error) { + console.error('Error saving API key to localStorage:', error); + } +} + +/** + * Remove API key from localStorage + */ +export function clearStoredKey(): void { + if (!isLocalStorageAvailable()) { + return; + } + + try { + localStorage.removeItem(API_KEY_STORAGE_KEY); + } catch (error) { + console.error('Error removing API key from localStorage:', error); + } +} diff --git a/apps/landing/src/lib/apikey/types.ts b/apps/landing/src/lib/apikey/types.ts new file mode 100644 index 0000000..ce04218 --- /dev/null +++ b/apps/landing/src/lib/apikey/types.ts @@ -0,0 +1,72 @@ +/** + * API Key Management Types + * + * TypeScript interfaces and types for API key functionality + */ + +/** + * Information about an API key extracted from validation response + */ +export interface ApiKeyInfo { + organizationSlug?: string; + projectSlug?: string; +} + +/** + * Response from API info endpoint + */ +export interface ApiInfoResponse { + message?: string; + keyInfo?: { + organizationSlug?: string; + organizationId?: string; + projectSlug?: string; + projectId?: string; + }; +} + +/** + * Result of API key validation + */ +export interface ValidationResult { + success: boolean; + keyInfo?: ApiKeyInfo; + error?: string; +} + +/** + * Context value for API Key Provider + */ +export interface ApiKeyContextValue { + // Data State + apiKey: string; + apiKeyValidated: boolean; + apiKeyInfo: ApiKeyInfo | null; + apiKeyError: string; + validatingKey: boolean; + isReady: boolean; // true when initial validation complete + + // UI State (grouped) + ui: { + expanded: boolean; + keyVisible: boolean; + }; + + // Actions + setApiKey: (key: string) => void; + validateApiKey: () => Promise; + revokeApiKey: (clearPageState?: () => void) => void; + toggleKeyVisibility: () => void; + setExpanded: (expanded: boolean) => void; + + // Focus method for external components + focus: () => void; +} + +/** + * Props for ApiKeyProvider component + */ +export interface ApiKeyProviderProps { + children: React.ReactNode; + onValidationSuccess?: (keyInfo: ApiKeyInfo) => void; +} diff --git a/apps/landing/src/lib/apikey/validation.ts b/apps/landing/src/lib/apikey/validation.ts new file mode 100644 index 0000000..5a6ff98 --- /dev/null +++ b/apps/landing/src/lib/apikey/validation.ts @@ -0,0 +1,79 @@ +/** + * API Key Validation Logic + * + * Core validation functions for API key management + */ + +import { API_BASE_URL, API_ENDPOINTS } from './constants'; +import type { ApiKeyInfo, ApiInfoResponse, ValidationResult } from './types'; + +/** + * Parse API key info from API response + * Handles various response formats (organizationSlug vs organizationId) + */ +export function parseKeyInfo(data: ApiInfoResponse): ApiKeyInfo { + if (data.keyInfo) { + return { + organizationSlug: data.keyInfo.organizationSlug || data.keyInfo.organizationId || 'Unknown', + projectSlug: data.keyInfo.projectSlug || data.keyInfo.projectId || 'Unknown', + }; + } + + return { + organizationSlug: 'Unknown', + projectSlug: 'Unknown', + }; +} + +/** + * Validate API key by making request to /api/info endpoint + * + * @param apiKey - The API key to validate + * @returns ValidationResult with success status, keyInfo, or error + */ +export async function validateApiKeyRequest(apiKey: string): Promise { + if (!apiKey.trim()) { + return { + success: false, + error: 'Please enter an API key', + }; + } + + try { + const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.INFO}`, { + headers: { + 'X-API-Key': apiKey, + }, + }); + + if (response.ok) { + const data: ApiInfoResponse = await response.json(); + const keyInfo = parseKeyInfo(data); + + return { + success: true, + keyInfo, + }; + } else { + // Try to parse error message from response + try { + const error = await response.json(); + return { + success: false, + error: error.message || 'Invalid API key', + }; + } catch { + return { + success: false, + error: response.status === 401 ? 'Invalid API key' : `Request failed with status ${response.status}`, + }; + } + } + } catch (error) { + console.error('API key validation error:', error); + return { + success: false, + error: 'Failed to validate API key. Please check your connection.', + }; + } +}