Redesign API documentation page with custom layout template

- Replace sidebar-based layout with full-width documentation website design
- Add custom layout.tsx for /docs route without sidebar
- Implement gradient header with API branding and feature highlights
- Add statistics cards showcasing API capabilities
- Create feature highlight sections for AI Classification, Image Captioning, and Batch Processing
- Integrate Swagger UI within professionally styled container
- Add comprehensive Tailwind CSS styling with dark mode support
- Include interactive documentation footer with status indicators
- Maintain all existing functionality while improving visual presentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-08-27 17:28:57 -05:00
parent a204168c00
commit 4aacfb8ff1
4 changed files with 541 additions and 20 deletions

11
src/app/docs/layout.tsx Normal file
View File

@ -0,0 +1,11 @@
export default function DocsLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{children}
</div>
)
}

View File

@ -2,16 +2,44 @@
import { useState, useEffect } from 'react'
import dynamic from 'next/dynamic'
import './swagger-ui-tailwind.css'
// Dynamically import Swagger UI to avoid SSR issues
const SwaggerUI = dynamic(() => import('swagger-ui-react'), {
ssr: false,
loading: () => <div className="text-center p-8">Loading Swagger UI...</div>
loading: () => <div className="text-center p-8 text-gray-600 dark:text-gray-400">Loading Swagger UI...</div>
})
export default function ApiDocsPage() {
const [spec, setSpec] = useState(null)
const [error, setError] = useState(null)
const [isDark, setIsDark] = useState(false)
// Dark mode detection
useEffect(() => {
const checkDarkMode = () => {
setIsDark(
window.matchMedia('(prefers-color-scheme: dark)').matches ||
document.documentElement.classList.contains('dark')
)
}
checkDarkMode()
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', checkDarkMode)
// Watch for class changes on html element
const observer = new MutationObserver(checkDarkMode)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
})
return () => {
mediaQuery.removeEventListener('change', checkDarkMode)
observer.disconnect()
}
}, [])
useEffect(() => {
fetch('/api/docs')
@ -62,27 +90,162 @@ export default function ApiDocsPage() {
}
return (
<div className="min-h-screen bg-white dark:bg-gray-900">
<div className="container mx-auto px-4 py-8">
<div className="mb-8 text-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Photo Gallery AI API Documentation
</h1>
<p className="text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Complete API reference for the AI-powered photo organization system.
All endpoints run locally with no cloud dependencies.
</p>
<div className={`min-h-screen transition-colors duration-200 ${isDark ? 'dark' : ''}`}>
{/* Full-width Header */}
<header className="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
<div className="container mx-auto px-6 py-12">
<div className="max-w-4xl mx-auto text-center">
<div className="inline-flex items-center gap-4 mb-6">
<div className="w-16 h-16 bg-white/20 backdrop-blur-sm rounded-2xl flex items-center justify-center">
<span className="text-white font-bold text-2xl">API</span>
</div>
<div className="text-left">
<h1 className="text-4xl font-bold mb-2">
Photo Gallery AI API
</h1>
<p className="text-blue-100 text-lg">
Interactive Documentation
</p>
</div>
</div>
<p className="text-xl text-blue-50 leading-relaxed max-w-3xl mx-auto mb-8">
Complete API reference for AI-powered photo organization.
Features dual-model classification, image captioning, and comprehensive tag management.
</p>
<div className="flex items-center justify-center gap-6 text-blue-100">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-sm font-medium">100% Local Processing</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-yellow-400 rounded-full"></div>
<span className="text-sm font-medium">No Cloud Dependencies</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-purple-400 rounded-full"></div>
<span className="text-sm font-medium">OpenAPI 3.0</span>
</div>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<SwaggerUI
spec={spec}
docExpansion="list"
defaultModelsExpandDepth={1}
tryItOutEnabled={true}
/>
</header>
{/* Main Content */}
<main className="bg-white dark:bg-gray-900">
<div className="container mx-auto px-6 py-12 max-w-6xl">
{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12">
<div className="text-center">
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-2">8+</div>
<div className="text-sm text-gray-600 dark:text-gray-400">API Endpoints</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-green-600 dark:text-green-400 mb-2">3</div>
<div className="text-sm text-gray-600 dark:text-gray-400">AI Models</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">25+</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Tags per Photo</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-yellow-600 dark:text-yellow-400 mb-2"></div>
<div className="text-sm text-gray-600 dark:text-gray-400">Privacy First</div>
</div>
</div>
{/* Feature Highlights */}
<div className="grid md:grid-cols-3 gap-6 mb-12">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 p-6 rounded-xl border border-blue-200 dark:border-blue-800">
<div className="w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
AI Classification
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">
ViT + CLIP models for comprehensive object and style recognition
</p>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 p-6 rounded-xl border border-green-200 dark:border-green-800">
<div className="w-12 h-12 bg-green-600 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Image Captioning
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">
BLIP model generates natural language descriptions
</p>
</div>
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 p-6 rounded-xl border border-purple-200 dark:border-purple-800">
<div className="w-12 h-12 bg-purple-600 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Batch Processing
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Process entire photo libraries with progress tracking
</p>
</div>
</div>
{/* API Documentation Section */}
<div className="bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 overflow-hidden shadow-xl">
{/* Documentation Content */}
<div className="p-8">
<SwaggerUI
spec={spec}
docExpansion="list"
defaultModelsExpandDepth={1}
tryItOutEnabled={true}
displayOperationId={false}
displayRequestDuration={true}
filter={true}
showExtensions={true}
showCommonExtensions={true}
requestInterceptor={(req) => {
return req
}}
responseInterceptor={(res) => {
return res
}}
/>
</div>
</div>
</div>
</div>
</main>
{/* Footer */}
<footer className="bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-6 py-8 text-center">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600 dark:text-gray-400">
Generated automatically from code annotations Always up-to-date
</div>
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<span className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
OpenAPI 3.0
</span>
<span className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
Interactive Testing
</span>
</div>
</div>
</div>
</footer>
</div>
)
}

View File

@ -0,0 +1,312 @@
/* Swagger UI Tailwind Integration */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
/* Root variables using Tailwind theme */
.swagger-ui {
/* Color scheme */
--swagger-ui-bg-primary: theme('colors.white');
--swagger-ui-bg-secondary: theme('colors.gray.50');
--swagger-ui-bg-tertiary: theme('colors.gray.100');
--swagger-ui-text-primary: theme('colors.gray.900');
--swagger-ui-text-secondary: theme('colors.gray.600');
--swagger-ui-text-muted: theme('colors.gray.400');
--swagger-ui-border: theme('colors.gray.200');
--swagger-ui-accent: theme('colors.blue.600');
--swagger-ui-accent-hover: theme('colors.blue.700');
--swagger-ui-success: theme('colors.green.600');
--swagger-ui-warning: theme('colors.yellow.600');
--swagger-ui-error: theme('colors.red.600');
/* Typography */
--swagger-ui-font-family: theme('fontFamily.sans');
--swagger-ui-font-mono: theme('fontFamily.mono');
--swagger-ui-font-size-base: theme('fontSize.sm');
--swagger-ui-font-size-lg: theme('fontSize.base');
--swagger-ui-font-size-xl: theme('fontSize.lg');
/* Spacing */
--swagger-ui-spacing-xs: theme('spacing.1');
--swagger-ui-spacing-sm: theme('spacing.2');
--swagger-ui-spacing-md: theme('spacing.4');
--swagger-ui-spacing-lg: theme('spacing.6');
--swagger-ui-spacing-xl: theme('spacing.8');
/* Shadows */
--swagger-ui-shadow-sm: theme('boxShadow.sm');
--swagger-ui-shadow-md: theme('boxShadow.md');
--swagger-ui-shadow-lg: theme('boxShadow.lg');
/* Border radius */
--swagger-ui-radius-sm: theme('borderRadius.sm');
--swagger-ui-radius-md: theme('borderRadius.md');
--swagger-ui-radius-lg: theme('borderRadius.lg');
}
/* Dark mode variables */
.dark .swagger-ui {
--swagger-ui-bg-primary: theme('colors.gray.900');
--swagger-ui-bg-secondary: theme('colors.gray.800');
--swagger-ui-bg-tertiary: theme('colors.gray.700');
--swagger-ui-text-primary: theme('colors.white');
--swagger-ui-text-secondary: theme('colors.gray.300');
--swagger-ui-text-muted: theme('colors.gray.500');
--swagger-ui-border: theme('colors.gray.600');
--swagger-ui-accent: theme('colors.blue.400');
--swagger-ui-accent-hover: theme('colors.blue.300');
--swagger-ui-success: theme('colors.green.400');
--swagger-ui-warning: theme('colors.yellow.400');
--swagger-ui-error: theme('colors.red.400');
}
/* Base styling with Tailwind utilities */
.swagger-ui {
@apply font-sans text-sm;
background-color: var(--swagger-ui-bg-primary);
color: var(--swagger-ui-text-primary);
font-family: var(--swagger-ui-font-family);
}
/* Header styling */
.swagger-ui .info {
@apply border-b border-gray-200 dark:border-gray-700 pb-6 mb-8;
}
.swagger-ui .info .title {
@apply text-3xl font-bold text-gray-900 dark:text-white mb-4;
color: var(--swagger-ui-text-primary);
}
.swagger-ui .info .description {
@apply text-gray-600 dark:text-gray-300 leading-relaxed;
color: var(--swagger-ui-text-secondary);
}
/* Operation blocks */
.swagger-ui .opblock {
@apply border border-gray-200 dark:border-gray-700 rounded-lg mb-4 overflow-hidden shadow-sm;
background-color: var(--swagger-ui-bg-primary);
border-color: var(--swagger-ui-border);
box-shadow: var(--swagger-ui-shadow-sm);
border-radius: var(--swagger-ui-radius-lg);
}
.swagger-ui .opblock .opblock-summary {
@apply px-4 py-3 cursor-pointer transition-colors duration-200;
background-color: var(--swagger-ui-bg-secondary);
}
.swagger-ui .opblock .opblock-summary:hover {
@apply bg-gray-100 dark:bg-gray-700;
background-color: var(--swagger-ui-bg-tertiary);
}
/* HTTP method colors */
.swagger-ui .opblock.opblock-get .opblock-summary {
@apply bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500;
}
.swagger-ui .opblock.opblock-post .opblock-summary {
@apply bg-green-50 dark:bg-green-900/20 border-l-4 border-green-500;
}
.swagger-ui .opblock.opblock-put .opblock-summary {
@apply bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-500;
}
.swagger-ui .opblock.opblock-delete .opblock-summary {
@apply bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500;
}
.swagger-ui .opblock.opblock-patch .opblock-summary {
@apply bg-purple-50 dark:bg-purple-900/20 border-l-4 border-purple-500;
}
/* Method labels */
.swagger-ui .opblock .opblock-summary-method {
@apply px-3 py-1 rounded text-white text-xs font-semibold uppercase;
border-radius: var(--swagger-ui-radius-sm);
}
.swagger-ui .opblock.opblock-get .opblock-summary-method {
@apply bg-blue-600 dark:bg-blue-500;
}
.swagger-ui .opblock.opblock-post .opblock-summary-method {
@apply bg-green-600 dark:bg-green-500;
}
.swagger-ui .opblock.opblock-put .opblock-summary-method {
@apply bg-yellow-600 dark:bg-yellow-500;
}
.swagger-ui .opblock.opblock-delete .opblock-summary-method {
@apply bg-red-600 dark:bg-red-500;
}
.swagger-ui .opblock.opblock-patch .opblock-summary-method {
@apply bg-purple-600 dark:bg-purple-500;
}
/* Operation summary text */
.swagger-ui .opblock-summary-path {
@apply font-mono text-sm text-gray-700 dark:text-gray-300;
font-family: var(--swagger-ui-font-mono);
}
.swagger-ui .opblock-summary-description {
@apply text-gray-600 dark:text-gray-400 text-sm ml-4;
}
/* Parameters and request body */
.swagger-ui .parameters-container {
@apply bg-gray-50 dark:bg-gray-800 p-4;
background-color: var(--swagger-ui-bg-secondary);
}
.swagger-ui .parameter__name {
@apply font-mono text-sm font-medium text-gray-900 dark:text-white;
font-family: var(--swagger-ui-font-mono);
color: var(--swagger-ui-text-primary);
}
.swagger-ui .parameter__type {
@apply text-blue-600 dark:text-blue-400 text-xs;
color: var(--swagger-ui-accent);
}
.swagger-ui .parameter__deprecated {
@apply line-through text-gray-400;
}
/* Response section */
.swagger-ui .responses-wrapper {
@apply border-t border-gray-200 dark:border-gray-700;
border-color: var(--swagger-ui-border);
}
.swagger-ui .response-col_status {
@apply font-mono font-bold;
font-family: var(--swagger-ui-font-mono);
}
/* Status code colors */
.swagger-ui .response-col_status .response-undocumented {
@apply text-gray-600 dark:text-gray-400;
}
.swagger-ui .responses-table td {
@apply p-3 border-b border-gray-100 dark:border-gray-700;
border-color: var(--swagger-ui-border);
}
/* Try it out button */
.swagger-ui .btn.try-out__btn {
@apply bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors;
background-color: var(--swagger-ui-accent);
border-radius: var(--swagger-ui-radius-md);
}
.swagger-ui .btn.try-out__btn:hover {
background-color: var(--swagger-ui-accent-hover);
}
.swagger-ui .btn.execute {
@apply bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-md font-medium transition-colors;
background-color: var(--swagger-ui-success);
}
.swagger-ui .btn.cancel {
@apply bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-md transition-colors;
}
.swagger-ui .btn.clear {
@apply bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md transition-colors;
}
/* Input fields */
.swagger-ui input[type=text],
.swagger-ui input[type=email],
.swagger-ui input[type=password],
.swagger-ui textarea,
.swagger-ui select {
@apply border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent;
border-color: var(--swagger-ui-border);
background-color: var(--swagger-ui-bg-primary);
color: var(--swagger-ui-text-primary);
border-radius: var(--swagger-ui-radius-md);
}
/* Code blocks */
.swagger-ui .highlight-code,
.swagger-ui .microlight {
@apply bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md p-4 font-mono text-sm;
background-color: var(--swagger-ui-bg-secondary);
border-color: var(--swagger-ui-border);
font-family: var(--swagger-ui-font-mono);
border-radius: var(--swagger-ui-radius-md);
}
/* Models section */
.swagger-ui .model-container {
@apply bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg;
background-color: var(--swagger-ui-bg-secondary);
border-color: var(--swagger-ui-border);
border-radius: var(--swagger-ui-radius-lg);
}
.swagger-ui .model-box {
@apply p-4;
}
.swagger-ui .model .property {
@apply py-2 border-b border-gray-100 dark:border-gray-700;
border-color: var(--swagger-ui-border);
}
/* Schema section */
.swagger-ui .model-title {
@apply text-lg font-semibold text-gray-900 dark:text-white mb-2;
color: var(--swagger-ui-text-primary);
}
/* Tags */
.swagger-ui .opblock-tag {
@apply text-2xl font-bold text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2 mb-4;
color: var(--swagger-ui-text-primary);
border-color: var(--swagger-ui-border);
}
/* Authorization */
.swagger-ui .auth-container {
@apply bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4;
border-radius: var(--swagger-ui-radius-lg);
}
/* Loading and errors */
.swagger-ui .loading-container {
@apply text-center py-8;
}
.swagger-ui .errors-wrapper {
@apply bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300;
background-color: color-mix(in srgb, var(--swagger-ui-error) 10%, transparent);
border-color: var(--swagger-ui-error);
border-radius: var(--swagger-ui-radius-lg);
}
/* Responsive design */
@media (max-width: 768px) {
.swagger-ui .opblock-summary {
@apply px-3 py-2;
}
.swagger-ui .info .title {
@apply text-2xl;
}
.swagger-ui .parameters-container {
@apply p-3;
}
}

View File

@ -0,0 +1,35 @@
'use client'
import { useState } from 'react'
import { IconApi, IconBook, IconExternalLink } from '@tabler/icons-react'
export default function ApiDocsLink() {
const [showTooltip, setShowTooltip] = useState(false)
return (
<div className="relative">
<a
href="/docs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors duration-200"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<IconApi className="w-4 h-4" />
<span className="hidden sm:inline">API Docs</span>
<IconExternalLink className="w-3 h-3 opacity-50" />
</a>
{showTooltip && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded-lg shadow-lg whitespace-nowrap z-10">
<div className="flex items-center gap-2">
<IconBook className="w-3 h-3" />
Interactive API Documentation
</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-2 h-2 bg-gray-900 dark:bg-gray-700 rotate-45"></div>
</div>
)}
</div>
)
}