Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements
- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
This commit is contained in:
@@ -9,7 +9,7 @@ export default function AboutPage() {
|
||||
<header className="border-b border-studio-medium bg-studio-dark">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
|
||||
<Link href="/" className="text-2xl font-bold text-white">
|
||||
Phoenix <span className="text-phoenix-fire">Sankofa</span> Cloud
|
||||
<span className="text-sankofa-gold">Sankofa's</span> Phoenix <span className="text-phoenix-fire">Nexus</span> Cloud
|
||||
</Link>
|
||||
<nav className="flex gap-4">
|
||||
<Link href="/" className="text-gray-400 hover:text-white">
|
||||
|
||||
74
src/app/api/auth/token/route.ts
Normal file
74
src/app/api/auth/token/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
/**
|
||||
* API route for managing authentication tokens via httpOnly cookies
|
||||
*/
|
||||
|
||||
const COOKIE_NAME = 'auth_token'
|
||||
const MAX_AGE = 60 * 60 * 24 * 7 // 7 days
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { token } = await request.json()
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Token is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set(COOKIE_NAME, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: MAX_AGE,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to set token' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get(COOKIE_NAME)?.value
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No token found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ token })
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get token' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete(COOKIE_NAME)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete token' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
86
src/app/api/infrastructure/backup/route.ts
Normal file
86
src/app/api/infrastructure/backup/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as zlib from 'zlib'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const gzip = promisify(zlib.gzip)
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'docs/infrastructure/data')
|
||||
const BACKUP_DIR = path.join(process.cwd(), 'docs/infrastructure/backups')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Ensure backup directory exists
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const backupFilename = `backup-${timestamp}.json.gz`
|
||||
const backupPath = path.join(BACKUP_DIR, backupFilename)
|
||||
|
||||
// Read all data files
|
||||
const files = fs.readdirSync(DATA_DIR).filter((f) => f.endsWith('.json'))
|
||||
const backup: Record<string, any> = {}
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(DATA_DIR, file)
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
backup[file] = JSON.parse(content)
|
||||
}
|
||||
|
||||
// Compress and save
|
||||
const jsonData = JSON.stringify(backup, null, 2)
|
||||
const compressed = await gzip(jsonData)
|
||||
fs.writeFileSync(backupPath, compressed)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
filename: backupFilename,
|
||||
timestamp: new Date().toISOString(),
|
||||
files: files.length,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to create backup',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
return NextResponse.json({ backups: [] })
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(BACKUP_DIR)
|
||||
.filter((f) => f.endsWith('.json.gz'))
|
||||
.map((f) => {
|
||||
const filePath = path.join(BACKUP_DIR, f)
|
||||
const stats = fs.statSync(filePath)
|
||||
return {
|
||||
filename: f,
|
||||
size: stats.size,
|
||||
created: stats.birthtime.toISOString(),
|
||||
}
|
||||
})
|
||||
.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime())
|
||||
|
||||
return NextResponse.json({ backups: files })
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to list backups',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
114
src/app/api/infrastructure/data/[filename]/route.ts
Normal file
114
src/app/api/infrastructure/data/[filename]/route.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* API route for serving infrastructure data JSON files
|
||||
*
|
||||
* GET /api/infrastructure/data/[filename]
|
||||
*
|
||||
* Serves JSON files from docs/infrastructure/data/ with caching headers
|
||||
*/
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'docs/infrastructure/data')
|
||||
|
||||
// Allowed file extensions
|
||||
const ALLOWED_EXTENSIONS = ['.json']
|
||||
|
||||
// Security: Prevent directory traversal
|
||||
function sanitizeFilename(filename: string): string {
|
||||
// Remove any path separators and only allow alphanumeric, hyphens, underscores, and dots
|
||||
const sanitized = filename.replace(/[^a-zA-Z0-9._-]/g, '')
|
||||
// Ensure it has a valid extension
|
||||
const ext = path.extname(sanitized)
|
||||
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
||||
throw new Error('Invalid file extension')
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { filename: string } }
|
||||
) {
|
||||
try {
|
||||
// Sanitize filename to prevent directory traversal
|
||||
const filename = sanitizeFilename(params.filename)
|
||||
const filePath = path.join(DATA_DIR, filename)
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'File not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
|
||||
// Parse JSON to validate it's valid JSON
|
||||
let data: unknown
|
||||
try {
|
||||
data = JSON.parse(fileContent)
|
||||
} catch (parseError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid JSON file' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get file stats for caching
|
||||
const stats = fs.statSync(filePath)
|
||||
|
||||
// Create response
|
||||
const response = NextResponse.json({
|
||||
data,
|
||||
metadata: {
|
||||
filename,
|
||||
lastModified: stats.mtime.toISOString(),
|
||||
size: stats.size,
|
||||
},
|
||||
})
|
||||
|
||||
// Add caching headers
|
||||
const etag = `"${stats.mtime.getTime()}-${stats.size}"`
|
||||
response.headers.set('ETag', etag)
|
||||
response.headers.set('Last-Modified', stats.mtime.toUTCString())
|
||||
response.headers.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400')
|
||||
response.headers.set('Content-Type', 'application/json')
|
||||
|
||||
// Check if client has cached version (304 Not Modified)
|
||||
const ifNoneMatch = request.headers.get('if-none-match')
|
||||
if (ifNoneMatch === etag) {
|
||||
return new NextResponse(null, { status: 304 })
|
||||
}
|
||||
|
||||
const ifModifiedSince = request.headers.get('if-modified-since')
|
||||
if (ifModifiedSince) {
|
||||
const ifModifiedSinceDate = new Date(ifModifiedSince)
|
||||
if (stats.mtime <= ifModifiedSinceDate) {
|
||||
return new NextResponse(null, { status: 304 })
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
// Handle security errors
|
||||
if (error instanceof Error && error.message === 'Invalid file extension') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid file type. Only JSON files are allowed.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Log error for debugging (in production, use proper logging)
|
||||
console.error('Error serving infrastructure data:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load file' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
67
src/app/api/infrastructure/import/route.ts
Normal file
67
src/app/api/infrastructure/import/route.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as XLSX from 'xlsx'
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'docs/infrastructure/data')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File
|
||||
const targetFile = formData.get('targetFile') as string
|
||||
|
||||
if (!file || !targetFile) {
|
||||
return NextResponse.json(
|
||||
{ error: 'File and targetFile are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
const filePath = path.join(DATA_DIR, targetFile)
|
||||
|
||||
// Handle different file types
|
||||
if (file.name.endsWith('.json')) {
|
||||
const data = JSON.parse(buffer.toString())
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2))
|
||||
} else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' })
|
||||
const sheetName = workbook.SheetNames[0]
|
||||
const worksheet = workbook.Sheets[sheetName]
|
||||
const data = XLSX.utils.sheet_to_json(worksheet)
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2))
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
const csv = buffer.toString()
|
||||
const lines = csv.split('\n')
|
||||
const headers = lines[0].split(',').map((h) => h.trim().replace(/^"|"$/g, ''))
|
||||
const data = lines.slice(1).map((line) => {
|
||||
const values = line.split(',').map((v) => v.trim().replace(/^"|"$/g, ''))
|
||||
return headers.reduce((obj, header, idx) => {
|
||||
obj[header] = values[idx] || ''
|
||||
return obj
|
||||
}, {} as Record<string, any>)
|
||||
})
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2))
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Unsupported file type' }, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
filename: targetFile,
|
||||
records: Array.isArray(JSON.parse(fs.readFileSync(filePath, 'utf-8')))
|
||||
? JSON.parse(fs.readFileSync(filePath, 'utf-8')).length
|
||||
: 1,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to import file',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
52
src/app/api/infrastructure/restore/route.ts
Normal file
52
src/app/api/infrastructure/restore/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as zlib from 'zlib'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const gunzip = promisify(zlib.gunzip)
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'docs/infrastructure/data')
|
||||
const BACKUP_DIR = path.join(process.cwd(), 'docs/infrastructure/backups')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { filename } = await request.json()
|
||||
|
||||
if (!filename) {
|
||||
return NextResponse.json({ error: 'Filename is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const backupPath = path.join(BACKUP_DIR, filename)
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
return NextResponse.json({ error: 'Backup file not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Read and decompress backup
|
||||
const compressed = fs.readFileSync(backupPath)
|
||||
const decompressed = await gunzip(compressed)
|
||||
const backup = JSON.parse(decompressed.toString())
|
||||
|
||||
// Restore files
|
||||
for (const [file, data] of Object.entries(backup)) {
|
||||
const filePath = path.join(DATA_DIR, file)
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
filesRestored: Object.keys(backup).length,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to restore backup',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
122
src/app/certification/page.tsx
Normal file
122
src/app/certification/page.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Award, Book, CheckCircle, Clock } from 'lucide-react'
|
||||
|
||||
export default function CertificationPage() {
|
||||
const certifications = [
|
||||
{
|
||||
id: 'phoenix-admin',
|
||||
name: 'Phoenix Certified Administrator',
|
||||
level: 'Associate',
|
||||
duration: '3 months',
|
||||
description: 'Learn to manage and operate Phoenix infrastructure',
|
||||
skills: ['Infrastructure Management', 'Security', 'Monitoring'],
|
||||
},
|
||||
{
|
||||
id: 'phoenix-architect',
|
||||
name: 'Phoenix Certified Architect',
|
||||
level: 'Professional',
|
||||
duration: '6 months',
|
||||
description: 'Design and architect solutions on Phoenix platform',
|
||||
skills: ['Architecture Design', 'Best Practices', 'Scalability'],
|
||||
},
|
||||
{
|
||||
id: 'phoenix-developer',
|
||||
name: 'Phoenix Certified Developer',
|
||||
level: 'Associate',
|
||||
duration: '3 months',
|
||||
description: 'Build applications using Phoenix APIs and services',
|
||||
skills: ['API Development', 'SDKs', 'Integration'],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="mb-4 text-5xl font-bold text-white">Certification Program</h1>
|
||||
<p className="text-xl text-gray-400">
|
||||
Validate your expertise with Phoenix certifications
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
{certifications.map((cert) => (
|
||||
<Card key={cert.id} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Award className="h-8 w-8 text-phoenix-fire" />
|
||||
<span className="px-2 py-1 text-xs bg-studio-dark text-gray-400 rounded">
|
||||
{cert.level}
|
||||
</span>
|
||||
</div>
|
||||
<CardTitle className="text-white">{cert.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{cert.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400 mb-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>{cert.duration}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-300 mb-2">Skills covered:</p>
|
||||
<ul className="space-y-1">
|
||||
{cert.skills.map((skill) => (
|
||||
<li key={skill} className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>{skill}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/certification/${cert.id}`}>Learn More</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card className="bg-studio-dark border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Why Get Certified?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<Award className="h-6 w-6 text-phoenix-fire mb-2" />
|
||||
<h3 className="text-white font-semibold mb-2">Validate Skills</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Demonstrate your expertise to employers and clients
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Book className="h-6 w-6 text-sankofa-gold mb-2" />
|
||||
<h3 className="text-white font-semibold mb-2">Career Growth</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Advance your career with recognized credentials
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<CheckCircle className="h-6 w-6 text-neon-cyan mb-2" />
|
||||
<h3 className="text-white font-semibold mb-2">Stay Current</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Keep up with latest Phoenix features and best practices
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
171
src/app/community/page.tsx
Normal file
171
src/app/community/page.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { MessageSquare, Users, TrendingUp, Clock } from 'lucide-react'
|
||||
|
||||
export default function CommunityForumPage() {
|
||||
const categories = [
|
||||
{
|
||||
id: 'general',
|
||||
name: 'General Discussion',
|
||||
description: 'General questions and discussions',
|
||||
posts: 1250,
|
||||
latest: '2 hours ago',
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
name: 'Support',
|
||||
description: 'Get help with technical issues',
|
||||
posts: 890,
|
||||
latest: '1 hour ago',
|
||||
},
|
||||
{
|
||||
id: 'announcements',
|
||||
name: 'Announcements',
|
||||
description: 'Product updates and news',
|
||||
posts: 45,
|
||||
latest: '1 day ago',
|
||||
},
|
||||
{
|
||||
id: 'showcase',
|
||||
name: 'Showcase',
|
||||
description: 'Share your projects and solutions',
|
||||
posts: 320,
|
||||
latest: '3 hours ago',
|
||||
},
|
||||
]
|
||||
|
||||
const recentPosts = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'How to optimize costs on Phoenix?',
|
||||
author: 'john_doe',
|
||||
category: 'Support',
|
||||
replies: 12,
|
||||
views: 245,
|
||||
time: '2 hours ago',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'New feature: Cost forecasting',
|
||||
author: 'sankofa_team',
|
||||
category: 'Announcements',
|
||||
replies: 8,
|
||||
views: 189,
|
||||
time: '5 hours ago',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Showcase: My Phoenix deployment',
|
||||
author: 'dev_user',
|
||||
category: 'Showcase',
|
||||
replies: 15,
|
||||
views: 312,
|
||||
time: '1 day ago',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-12">
|
||||
<h1 className="mb-4 text-5xl font-bold text-white">Community Forum</h1>
|
||||
<p className="text-xl text-gray-400">
|
||||
Connect with other users, share knowledge, and get support
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
{categories.map((category) => (
|
||||
<Card key={category.id} className="hover:border-phoenix-fire transition-colors cursor-pointer">
|
||||
<CardHeader>
|
||||
<MessageSquare className="h-8 w-8 text-phoenix-fire mb-2" />
|
||||
<CardTitle className="text-white">{category.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{category.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between text-sm text-gray-400">
|
||||
<span>{category.posts} posts</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-4 w-4" />
|
||||
{category.latest}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="bg-studio-dark border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Recent Posts</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recentPosts.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="p-4 bg-studio-black rounded-lg hover:bg-studio-medium transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-white font-semibold">{post.title}</h3>
|
||||
<span className="px-2 py-1 text-xs bg-studio-dark text-gray-400 rounded">
|
||||
{post.category}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span>by {post.author}</span>
|
||||
<span>{post.replies} replies</span>
|
||||
<span>{post.views} views</span>
|
||||
<span>{post.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Card className="bg-studio-dark border-studio-medium mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Users className="h-5 w-5 text-phoenix-fire" />
|
||||
Community Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Members</span>
|
||||
<span className="text-white font-semibold">12,450</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Posts</span>
|
||||
<span className="text-white font-semibold">2,505</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Online</span>
|
||||
<span className="text-white font-semibold">234</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button variant="phoenix" className="w-full" asChild>
|
||||
<Link href="/community/new-post">Create New Post</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
63
src/app/company/accessibility/page.tsx
Normal file
63
src/app/company/accessibility/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Accessibility, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function AccessibilityPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">Accessibility</h1>
|
||||
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Accessibility className="h-8 w-8 text-phoenix-fire" />
|
||||
<CardTitle className="text-white">Accessibility Commitment</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Sankofa Phoenix is committed to making our platform accessible to all users.
|
||||
We follow WCAG 2.1 Level AA guidelines and continuously work to improve accessibility.
|
||||
</p>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>WCAG 2.1 Level AA compliant</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>Keyboard navigation support</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>Screen reader compatible</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>High contrast mode support</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Feedback</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
We welcome feedback on accessibility. If you encounter any accessibility issues,
|
||||
please contact our support team.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
67
src/app/company/compliance/page.tsx
Normal file
67
src/app/company/compliance/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Shield, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function CompliancePage() {
|
||||
const complianceFrameworks = [
|
||||
{ name: 'SOC 2 Type II', status: 'Certified', description: 'Security, availability, and confidentiality controls' },
|
||||
{ name: 'ISO 27001', status: 'Certified', description: 'Information security management system' },
|
||||
{ name: 'GDPR', status: 'Compliant', description: 'European data protection regulation' },
|
||||
{ name: 'HIPAA', status: 'Ready', description: 'Healthcare data protection (when applicable)' },
|
||||
{ name: 'PCI-DSS', status: 'Compliant', description: 'Payment card industry data security' },
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">Compliance</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="h-8 w-8 text-phoenix-fire" />
|
||||
<CardTitle className="text-white">Regulatory Compliance</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-6">
|
||||
Sankofa Phoenix maintains compliance with major regulatory frameworks and industry
|
||||
standards to meet enterprise and government requirements.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{complianceFrameworks.map((framework) => (
|
||||
<div key={framework.name} className="p-4 bg-studio-dark rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold text-white">{framework.name}</h3>
|
||||
<span className="px-3 py-1 bg-green-500/20 text-green-400 text-xs font-semibold rounded-full">
|
||||
{framework.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{framework.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Compliance Certifications</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
We undergo regular audits and assessments to maintain our compliance certifications.
|
||||
Certificates and audit reports are available upon request for enterprise customers.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
80
src/app/company/privacy/page.tsx
Normal file
80
src/app/company/privacy/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Lock, Eye, FileText, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">Privacy</h1>
|
||||
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Lock className="h-8 w-8 text-phoenix-fire" />
|
||||
<CardTitle className="text-white">Data Privacy & Sovereignty</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Sankofa Phoenix is committed to protecting your privacy and ensuring data sovereignty.
|
||||
We implement privacy by design principles and comply with global privacy regulations.
|
||||
</p>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>GDPR compliant</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>Data residency controls</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>Right to deletion</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>Privacy by design</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Eye className="h-8 w-8 text-sankofa-gold" />
|
||||
<CardTitle className="text-white">Transparency</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
We are transparent about how we collect, use, and protect your data. Our privacy
|
||||
policy clearly outlines our data handling practices.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="h-8 w-8 text-neon-cyan" />
|
||||
<CardTitle className="text-white">Privacy Policy</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
For detailed information about our privacy practices, please review our Privacy Policy.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
82
src/app/company/security/page.tsx
Normal file
82
src/app/company/security/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Shield, Lock, Eye, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function SecurityPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">Security</h1>
|
||||
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="h-8 w-8 text-phoenix-fire" />
|
||||
<CardTitle className="text-white">Enterprise-Grade Security</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Sankofa Phoenix is built with security as a foundational principle. Our infrastructure
|
||||
implements defense-in-depth strategies, zero-trust networking, and comprehensive
|
||||
security monitoring.
|
||||
</p>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>SOC 2 Type II certified</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>ISO 27001 compliant</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>Zero Trust architecture</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
|
||||
<span>End-to-end encryption</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Lock className="h-8 w-8 text-sankofa-gold" />
|
||||
<CardTitle className="text-white">Data Protection</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
All data is encrypted at rest and in transit using industry-standard encryption
|
||||
algorithms. We implement comprehensive access controls and audit logging.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Eye className="h-8 w-8 text-neon-cyan" />
|
||||
<CardTitle className="text-white">Security Monitoring</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Continuous security monitoring, threat detection, and incident response capabilities
|
||||
ensure rapid detection and mitigation of security threats.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
156
src/app/company/trust/page.tsx
Normal file
156
src/app/company/trust/page.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Shield, Lock, FileCheck, Globe, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function TrustPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Trust & Compliance
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Enterprise-grade security, compliance, and data sovereignty
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Compliance Standards */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Compliance & Certifications
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>DoD/MilSpec Compliance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-300 mb-4">
|
||||
STIG-compliant infrastructure with security hardening and audit controls.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Security Technical Implementation Guides (STIG)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>RMF authorization ready</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Lock className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Data Sovereignty</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-300 mb-4">
|
||||
Complete control over data location, access, and governance.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Regional data residency controls</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>No vendor lock-in</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<FileCheck className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Audit & Reporting</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-300 mb-4">
|
||||
Comprehensive audit logging and compliance reporting.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Immutable audit logs</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Compliance dashboards</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Security Features */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Security Features
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Sovereign Identity</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Keycloak-based identity management with zero Azure dependencies.
|
||||
Complete control over authentication, authorization, and user management.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Multi-factor authentication (MFA)</li>
|
||||
<li>• SSO integration (SAML, OIDC)</li>
|
||||
<li>• Role-based access control (RBAC)</li>
|
||||
<li>• Identity federation</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Zero Trust Architecture</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Every request is verified, every connection is encrypted, and access is
|
||||
granted on a least-privilege basis.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Network segmentation</li>
|
||||
<li>• End-to-end encryption</li>
|
||||
<li>• Continuous verification</li>
|
||||
<li>• Threat detection and response</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Questions About Compliance?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Contact our compliance team for detailed information
|
||||
</p>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/support">Contact Support</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
249
src/app/developers/docs/api/graphql/page.tsx
Normal file
249
src/app/developers/docs/api/graphql/page.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Code, Book, Terminal, ExternalLink } from 'lucide-react'
|
||||
|
||||
export default function GraphQLAPIPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4 max-w-6xl">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-4">GraphQL API Reference</h1>
|
||||
<p className="text-xl text-gray-300">
|
||||
Complete reference for the Phoenix Nexus GraphQL API
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Start */}
|
||||
<Card className="mb-8 border-phoenix-fire/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-phoenix-fire" />
|
||||
Quick Start
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-400 mb-2">Endpoint</h3>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
https://api.sankofa.nexus/graphql
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-400 mb-2">Authentication</h3>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
Authorization: Bearer {'<token>'}
|
||||
</code>
|
||||
</div>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/portal/developers">
|
||||
Get API Key <ExternalLink className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Queries */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-white mb-6">Queries</h2>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Get Resources</CardTitle>
|
||||
<CardDescription>Retrieve a list of resources</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`query GetResources($filter: ResourceFilter) {
|
||||
resources(filter: $filter) {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
site {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Get Current User</CardTitle>
|
||||
<CardDescription>Get authenticated user information</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`query GetMe {
|
||||
me {
|
||||
id
|
||||
email
|
||||
name
|
||||
role
|
||||
tenant {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Get Tenants</CardTitle>
|
||||
<CardDescription>List all tenants (admin only)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`query GetTenants {
|
||||
tenants {
|
||||
id
|
||||
name
|
||||
status
|
||||
createdAt
|
||||
quotas {
|
||||
compute
|
||||
storage
|
||||
network
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mutations */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-white mb-6">Mutations</h2>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Create Resource</CardTitle>
|
||||
<CardDescription>Create a new resource</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`mutation CreateResource($input: CreateResourceInput!) {
|
||||
createResource(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Create Tenant</CardTitle>
|
||||
<CardDescription>Create a new tenant (admin only)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`mutation CreateTenant($input: CreateTenantInput!) {
|
||||
createTenant(input: $input) {
|
||||
id
|
||||
name
|
||||
status
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Types */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-white mb-6">Types</h2>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Resource</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`type Resource {
|
||||
id: ID!
|
||||
name: String!
|
||||
type: ResourceType!
|
||||
status: ResourceStatus!
|
||||
site: Site
|
||||
tenant: Tenant
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Tenant</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`type Tenant {
|
||||
id: ID!
|
||||
name: String!
|
||||
status: TenantStatus!
|
||||
quotas: TenantQuotas!
|
||||
usage: TenantQuotaUsage!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Interactive Explorer */}
|
||||
<Card className="mb-8 border-sankofa-gold/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Code className="h-5 w-5 text-sankofa-gold" />
|
||||
Interactive API Explorer
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Try queries and mutations in the interactive GraphQL playground
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/portal/developers/explorer">
|
||||
Open API Explorer <ExternalLink className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Related Links */}
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/api/rest">REST API Reference →</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/guides/authentication">Authentication Guide →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
125
src/app/developers/docs/api/rest/page.tsx
Normal file
125
src/app/developers/docs/api/rest/page.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Code, Book, Terminal, ExternalLink } from 'lucide-react'
|
||||
|
||||
export default function RESTAPIPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4 max-w-6xl">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-4">REST API Reference</h1>
|
||||
<p className="text-xl text-gray-300">
|
||||
Complete reference for the Phoenix Nexus REST API
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Start */}
|
||||
<Card className="mb-8 border-phoenix-fire/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-phoenix-fire" />
|
||||
Quick Start
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-400 mb-2">Base URL</h3>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
https://api.sankofa.nexus/v1
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-400 mb-2">Authentication</h3>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
Authorization: Bearer {'<token>'}
|
||||
</code>
|
||||
</div>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/portal/developers">
|
||||
Get API Key <ExternalLink className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Endpoints */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-white mb-6">Endpoints</h2>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Get Resources</CardTitle>
|
||||
<CardDescription>GET /v1/resources</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`GET /v1/resources
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "string",
|
||||
"type": "VM|CONTAINER|STORAGE|NETWORK",
|
||||
"status": "RUNNING|STOPPED|...",
|
||||
"createdAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Create Resource</CardTitle>
|
||||
<CardDescription>POST /v1/resources</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`POST /v1/resources
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "My Resource",
|
||||
"type": "VM",
|
||||
"siteId": "uuid"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "My Resource",
|
||||
"status": "PROVISIONING",
|
||||
...
|
||||
}`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Links */}
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/api/graphql">GraphQL API Reference →</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/guides/authentication">Authentication Guide →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
130
src/app/developers/docs/guides/authentication/page.tsx
Normal file
130
src/app/developers/docs/guides/authentication/page.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Shield, Key, Lock } from 'lucide-react'
|
||||
|
||||
export default function AuthenticationGuidePage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4 max-w-4xl">
|
||||
<div className="mb-12 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-phoenix-fire/20">
|
||||
<Shield className="h-8 w-8 text-phoenix-fire" />
|
||||
</div>
|
||||
<h1 className="text-5xl font-bold text-white mb-4">Authentication Guide</h1>
|
||||
<p className="text-xl text-gray-300">
|
||||
Learn how to authenticate with Phoenix Nexus APIs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* API Keys */}
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Key className="h-6 w-6 text-sankofa-gold" />
|
||||
<div>
|
||||
<CardTitle>API Key Authentication</CardTitle>
|
||||
<CardDescription>Simple and secure API key-based authentication</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-2">1. Create an API Key</h3>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Navigate to the Developer Portal and create a new API key.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/portal/developers/keys">Create API Key →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-2">2. Use the API Key</h3>
|
||||
<p className="text-gray-300 mb-2">Include the API key in the Authorization header:</p>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`curl -X GET https://api.sankofa.nexus/graphql \\
|
||||
-H "Authorization: Bearer sk_live_your_key_here" \\
|
||||
-H "Content-Type: application/json"`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* JWT Tokens */}
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Lock className="h-6 w-6 text-neon-cyan" />
|
||||
<div>
|
||||
<CardTitle>JWT Token Authentication</CardTitle>
|
||||
<CardDescription>For user-based authentication</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-2">1. Login</h3>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`mutation {
|
||||
login(email: "user@example.com", password: "password") {
|
||||
token
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-2">2. Use the Token</h3>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`curl -X POST https://api.sankofa.nexus/graphql \\
|
||||
-H "Authorization: Bearer <jwt_token>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"query": "{ me { id email } }"}'`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security Best Practices */}
|
||||
<Card className="mb-8 border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle>Security Best Practices</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Never commit API keys or tokens to version control</li>
|
||||
<li>• Use environment variables to store credentials</li>
|
||||
<li>• Rotate API keys regularly</li>
|
||||
<li>• Use HTTPS for all API requests</li>
|
||||
<li>• Implement token refresh for long-lived sessions</li>
|
||||
<li>• Monitor API key usage for suspicious activity</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Related Links */}
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/quickstart">Quick Start Guide →</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/api/graphql">GraphQL API →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
200
src/app/developers/docs/page.tsx
Normal file
200
src/app/developers/docs/page.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Book, Code, Terminal, GitBranch, Zap, FileText, Search } from 'lucide-react'
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="mb-8 flex items-center justify-center">
|
||||
<Search className="mr-4 h-8 w-8 text-gray-400" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search documentation..."
|
||||
className="w-full max-w-md rounded-lg border border-studio-medium bg-studio-dark px-4 py-3 text-white placeholder-gray-400 focus:border-phoenix-fire focus:outline-none focus:ring-1 focus:ring-phoenix-fire"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="mb-6 text-center text-5xl font-bold text-white md:text-6xl">
|
||||
Documentation
|
||||
</h1>
|
||||
<p className="mb-8 text-center text-xl text-gray-300">
|
||||
Comprehensive guides, API references, and tutorials
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Start */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-8 text-3xl font-bold text-white">Getting Started</h2>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/developers/docs/quickstart">
|
||||
<CardHeader>
|
||||
<Zap className="mb-2 h-8 w-8 text-phoenix-fire" />
|
||||
<CardTitle>Quick Start</CardTitle>
|
||||
<CardDescription>
|
||||
Get up and running in 5 minutes
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/developers/docs/installation">
|
||||
<CardHeader>
|
||||
<Terminal className="mb-2 h-8 w-8 text-sankofa-gold" />
|
||||
<CardTitle>Installation</CardTitle>
|
||||
<CardDescription>
|
||||
Install and configure Phoenix Nexus
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/developers/docs/concepts">
|
||||
<CardHeader>
|
||||
<Book className="mb-2 h-8 w-8 text-neon-cyan" />
|
||||
<CardTitle>Core Concepts</CardTitle>
|
||||
<CardDescription>
|
||||
Understand the architecture
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Documentation Sections */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-8 text-3xl font-bold text-white">Documentation</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<Code className="mb-4 h-8 w-8 text-phoenix-fire" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">API Reference</h3>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li>
|
||||
<Link href="/developers/docs/api/graphql" className="hover:text-phoenix-fire">
|
||||
GraphQL API →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/api/rest" className="hover:text-phoenix-fire">
|
||||
REST APIs →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/api/websocket" className="hover:text-phoenix-fire">
|
||||
WebSocket APIs →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/api/authentication" className="hover:text-phoenix-fire">
|
||||
Authentication →
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FileText className="mb-4 h-8 w-8 text-sankofa-gold" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Guides</h3>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li>
|
||||
<Link href="/developers/docs/guides/architecture" className="hover:text-sankofa-gold">
|
||||
Architecture Guides →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/guides/security" className="hover:text-sankofa-gold">
|
||||
Security Guides →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/guides/compliance" className="hover:text-sankofa-gold">
|
||||
Compliance Guides →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/guides/best-practices" className="hover:text-sankofa-gold">
|
||||
Best Practices →
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Book className="mb-4 h-8 w-8 text-neon-cyan" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Tutorials</h3>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li>
|
||||
<Link href="/developers/docs/tutorials/first-app" className="hover:text-neon-cyan">
|
||||
Build Your First App →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/tutorials/multi-tenancy" className="hover:text-neon-cyan">
|
||||
Multi-Tenancy Setup →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/tutorials/integration" className="hover:text-neon-cyan">
|
||||
Integration Examples →
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<GitBranch className="mb-4 h-8 w-8 text-phoenix-fire" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">SDKs & Tools</h3>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li>
|
||||
<Link href="/developers/docs/sdks/cli" className="hover:text-phoenix-fire">
|
||||
CLI Documentation →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/sdks/typescript" className="hover:text-phoenix-fire">
|
||||
TypeScript SDK →
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/developers/docs/sdks/terraform" className="hover:text-phoenix-fire">
|
||||
Terraform Provider →
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Need More Help?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Access the developer portal for API keys and test environments
|
||||
</p>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/developers">Developer Portal</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
238
src/app/developers/docs/quickstart/page.tsx
Normal file
238
src/app/developers/docs/quickstart/page.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { CheckCircle, Code, Key, Terminal, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default function QuickStartPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4 max-w-4xl">
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="text-5xl font-bold text-white mb-4">Quick Start Guide</h1>
|
||||
<p className="text-xl text-gray-300">
|
||||
Get up and running with Phoenix Nexus Cloud in 5 minutes
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="space-y-8">
|
||||
{/* Step 1 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-phoenix-fire text-white font-bold">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Create an Account</CardTitle>
|
||||
<CardDescription>Sign up for Phoenix Nexus Cloud</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300">
|
||||
If you don't have an account yet, create one to get started.
|
||||
</p>
|
||||
<Button variant="phoenix" asChild>
|
||||
<Link href="/portal/get-started">
|
||||
Get Started <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Step 2 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-sankofa-gold text-studio-black font-bold">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Get Your API Key</CardTitle>
|
||||
<CardDescription>Generate an API key from the Developer Portal</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300">
|
||||
Navigate to the Developer Portal and create your first API key.
|
||||
</p>
|
||||
<ol className="list-decimal list-inside space-y-2 text-gray-300">
|
||||
<li>Go to Developer Portal</li>
|
||||
<li>Click "Create API Key"</li>
|
||||
<li>Give it a descriptive name</li>
|
||||
<li>Copy the key (you won't see it again!)</li>
|
||||
</ol>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/portal/developers/keys">
|
||||
<Key className="mr-2 h-4 w-4" />
|
||||
Manage API Keys
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Step 3 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-neon-cyan text-studio-black font-bold">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Install the CLI</CardTitle>
|
||||
<CardDescription>Install the Phoenix Nexus CLI tool</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300">
|
||||
Install the CLI using your preferred package manager:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400 mb-1">npm</p>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
npm install -g @sankofa/phoenix-cli
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400 mb-1">pnpm</p>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
pnpm add -g @sankofa/phoenix-cli
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Step 4 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-phoenix-fire text-white font-bold">
|
||||
4
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Authenticate</CardTitle>
|
||||
<CardDescription>Configure the CLI with your API key</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300">
|
||||
Set your API key as an environment variable or use the login command:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400 mb-1">Environment Variable</p>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
export PHOENIX_API_KEY=sk_live_your_key_here
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400 mb-1">Or use login</p>
|
||||
<code className="block rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
phoenix login
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Step 5 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-sankofa-gold text-studio-black font-bold">
|
||||
5
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Make Your First API Call</CardTitle>
|
||||
<CardDescription>Test the connection with a simple query</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300">
|
||||
Test your setup with a simple GraphQL query:
|
||||
</p>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`phoenix query '
|
||||
query {
|
||||
me {
|
||||
id
|
||||
email
|
||||
name
|
||||
}
|
||||
}
|
||||
'`}
|
||||
</pre>
|
||||
<p className="text-sm text-gray-400">
|
||||
Or use curl to make a direct HTTP request:
|
||||
</p>
|
||||
<pre className="rounded bg-studio-dark p-4 text-sm text-white overflow-x-auto">
|
||||
{`curl -X POST https://api.sankofa.nexus/graphql \\
|
||||
-H "Authorization: Bearer $PHOENIX_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"query": "{ me { id email name } }"}'`}
|
||||
</pre>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Next Steps */}
|
||||
<Card className="mt-12 border-phoenix-fire/50">
|
||||
<CardHeader>
|
||||
<CardTitle>Next Steps</CardTitle>
|
||||
<CardDescription>Continue your journey with Phoenix Nexus</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="mt-1 h-5 w-5 text-phoenix-fire flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-white mb-1">Read the API Reference</h4>
|
||||
<p className="text-sm text-gray-400 mb-2">
|
||||
Learn about all available queries and mutations
|
||||
</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/developers/docs/api/graphql">API Reference →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="mt-1 h-5 w-5 text-sankofa-gold flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-white mb-1">Explore Examples</h4>
|
||||
<p className="text-sm text-gray-400 mb-2">
|
||||
See real-world integration examples
|
||||
</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/developers/docs/tutorials">Tutorials →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
105
src/app/developers/docs/tutorials/page.tsx
Normal file
105
src/app/developers/docs/tutorials/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { BookOpen, Code, Rocket, Zap } from 'lucide-react'
|
||||
|
||||
export default function TutorialsPage() {
|
||||
const tutorials = [
|
||||
{
|
||||
title: 'Getting Started with Phoenix Nexus',
|
||||
description: 'Build your first application using the Phoenix Nexus API',
|
||||
icon: Rocket,
|
||||
href: '/developers/docs/tutorials/getting-started',
|
||||
level: 'Beginner',
|
||||
},
|
||||
{
|
||||
title: 'Building a Multi-Tenant Application',
|
||||
description: 'Learn how to leverage multi-tenancy in your applications',
|
||||
icon: Code,
|
||||
href: '/developers/docs/tutorials/multi-tenant',
|
||||
level: 'Intermediate',
|
||||
},
|
||||
{
|
||||
title: 'Real-Time Resource Monitoring',
|
||||
description: 'Implement real-time monitoring with GraphQL subscriptions',
|
||||
icon: Zap,
|
||||
href: '/developers/docs/tutorials/realtime-monitoring',
|
||||
level: 'Advanced',
|
||||
},
|
||||
{
|
||||
title: 'Integrating with Keycloak SSO',
|
||||
description: 'Add single sign-on to your application',
|
||||
icon: BookOpen,
|
||||
href: '/developers/docs/tutorials/keycloak-sso',
|
||||
level: 'Intermediate',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4 max-w-6xl">
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="text-5xl font-bold text-white mb-4">Tutorials</h1>
|
||||
<p className="text-xl text-gray-300">
|
||||
Step-by-step guides to help you build with Phoenix Nexus
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{tutorials.map((tutorial) => {
|
||||
const Icon = tutorial.icon
|
||||
return (
|
||||
<Card key={tutorial.href} className="border-studio-medium hover:border-phoenix-fire/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="rounded-lg bg-phoenix-fire/20 p-3">
|
||||
<Icon className="h-6 w-6 text-phoenix-fire" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<CardTitle>{tutorial.title}</CardTitle>
|
||||
<span className="rounded-full bg-studio-medium px-2 py-1 text-xs text-gray-400">
|
||||
{tutorial.level}
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription>{tutorial.description}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={tutorial.href}>Start Tutorial →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card className="mt-12 border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle>More Resources</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/api/graphql">API Reference</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/quickstart">Quick Start</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/guides/authentication">Authentication</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
182
src/app/developers/page.tsx
Normal file
182
src/app/developers/page.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Code, Book, Key, Terminal, Zap, GitBranch } from 'lucide-react'
|
||||
|
||||
export default function DevelopersPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Developer Resources
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Build on Sankofa's Phoenix Nexus Cloud with comprehensive APIs, SDKs, and documentation
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/developers/docs">View Documentation</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/portal/developers">Developer Portal</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Links */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Get Started
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/developers/docs">
|
||||
<CardHeader>
|
||||
<Book className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Documentation</CardTitle>
|
||||
<CardDescription>
|
||||
API references, guides, tutorials, and architecture blueprints
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/developers">
|
||||
<CardHeader>
|
||||
<Key className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Developer Portal</CardTitle>
|
||||
<CardDescription>
|
||||
API keys, test environments, logs, and developer tools
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/developers/quickstart">
|
||||
<CardHeader>
|
||||
<Zap className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Quick Start</CardTitle>
|
||||
<CardDescription>
|
||||
Get up and running in minutes with our step-by-step guides
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Developer Tools */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Developer Tools & Resources
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<Code className="mb-4 h-8 w-8 text-phoenix-fire" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">GraphQL API</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Powerful GraphQL API with real-time subscriptions, comprehensive type system,
|
||||
and built-in authentication.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Interactive API explorer</li>
|
||||
<li>• TypeScript SDK</li>
|
||||
<li>• WebSocket subscriptions</li>
|
||||
<li>• Rate limiting and quotas</li>
|
||||
</ul>
|
||||
<Button variant="outline" className="mt-4" asChild>
|
||||
<Link href="/developers/docs/api">API Reference →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Terminal className="mb-4 h-8 w-8 text-sankofa-gold" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">CLI & SDKs</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Command-line tools and SDKs for popular languages to integrate Phoenix Nexus
|
||||
into your workflow.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Phoenix CLI (Node.js, Python, Go)</li>
|
||||
<li>• Terraform provider</li>
|
||||
<li>• Kubernetes operators</li>
|
||||
<li>• CI/CD integrations</li>
|
||||
</ul>
|
||||
<Button variant="outline" className="mt-4" asChild>
|
||||
<Link href="/developers/docs/sdks">View SDKs →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<GitBranch className="mb-4 h-8 w-8 text-neon-cyan" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">GitOps & Infrastructure</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Infrastructure as Code with Crossplane, ArgoCD, and GitOps workflows for
|
||||
declarative infrastructure management.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Crossplane providers</li>
|
||||
<li>• ArgoCD integration</li>
|
||||
<li>• GitOps templates</li>
|
||||
<li>• Infrastructure blueprints</li>
|
||||
</ul>
|
||||
<Button variant="outline" className="mt-4" asChild>
|
||||
<Link href="/developers/docs/gitops">GitOps Guide →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Zap className="mb-4 h-8 w-8 text-phoenix-fire" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Test Environments</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Sandbox environments for testing and development, with instant provisioning
|
||||
and automatic cleanup.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Free tier for development</li>
|
||||
<li>• Isolated test environments</li>
|
||||
<li>• Pre-configured templates</li>
|
||||
<li>• Integration testing tools</li>
|
||||
</ul>
|
||||
<Button variant="outline" className="mt-4" asChild>
|
||||
<Link href="/portal/developers">Get API Keys →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Build?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Start building on sovereign cloud infrastructure today
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/developers">Access Developer Portal</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/developers/docs">Read Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
131
src/app/docs/api/graphql/page.tsx
Normal file
131
src/app/docs/api/graphql/page.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client'
|
||||
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { useState } from 'react'
|
||||
import { Play, Code, Book } from 'lucide-react'
|
||||
|
||||
export default function GraphQLExplorerPage() {
|
||||
const [query, setQuery] = useState(`query {
|
||||
resources {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
}
|
||||
}`)
|
||||
const [variables, setVariables] = useState('{}')
|
||||
const [response, setResponse] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const executeQuery = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// In production, this would call the actual GraphQL API
|
||||
const res = await fetch('/api/graphql', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, variables: JSON.parse(variables) }),
|
||||
})
|
||||
const data = await res.json()
|
||||
setResponse(JSON.stringify(data, null, 2))
|
||||
} catch (error) {
|
||||
setResponse(JSON.stringify({ error: error.message }, null, 2))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-8">
|
||||
<h1 className="mb-4 text-4xl font-bold text-white">GraphQL API Explorer</h1>
|
||||
<p className="text-gray-400">
|
||||
Interactive GraphQL API explorer with live query execution and schema documentation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card className="bg-studio-dark border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Code className="h-5 w-5 text-phoenix-fire" />
|
||||
Query Editor
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<textarea
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-full h-64 p-4 bg-studio-black border border-studio-medium rounded text-white font-mono text-sm"
|
||||
placeholder="Enter your GraphQL query..."
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-studio-dark border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Code className="h-5 w-5 text-sankofa-gold" />
|
||||
Variables (JSON)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<textarea
|
||||
value={variables}
|
||||
onChange={(e) => setVariables(e.target.value)}
|
||||
className="w-full h-64 p-4 bg-studio-black border border-studio-medium rounded text-white font-mono text-sm"
|
||||
placeholder='{"key": "value"}'
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={executeQuery}
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-phoenix-fire text-white rounded-lg hover:bg-phoenix-fire/90 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
{loading ? 'Executing...' : 'Execute Query'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{response && (
|
||||
<Card className="bg-studio-dark border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Book className="h-5 w-5 text-neon-cyan" />
|
||||
Response
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="p-4 bg-studio-black border border-studio-medium rounded text-white font-mono text-sm overflow-auto">
|
||||
{response}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="mt-6 bg-studio-dark border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Schema Documentation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Explore the GraphQL schema to discover available queries, mutations, and types.
|
||||
</p>
|
||||
<button className="px-4 py-2 bg-studio-medium text-white rounded hover:bg-studio-medium/80 transition-colors">
|
||||
View Schema Documentation →
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
73
src/app/docs/api/page.tsx
Normal file
73
src/app/docs/api/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function APIDocsPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">API Reference</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">GraphQL API</CardTitle>
|
||||
<CardDescription>
|
||||
Comprehensive GraphQL API with real-time subscriptions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Our GraphQL API provides a flexible, type-safe interface for querying and mutating
|
||||
infrastructure resources, with built-in authentication and real-time subscriptions.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/api/graphql">View GraphQL Schema →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">REST API</CardTitle>
|
||||
<CardDescription>
|
||||
Traditional REST endpoints for resource management
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
RESTful API endpoints following standard HTTP conventions for managing
|
||||
resources, with comprehensive OpenAPI documentation.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/api/rest">View REST API Docs →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">SDKs</CardTitle>
|
||||
<CardDescription>
|
||||
Client libraries for popular programming languages
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Official SDKs for TypeScript, Python, Go, and more, with type definitions
|
||||
and comprehensive examples.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/api/sdks">View SDK Documentation →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
73
src/app/docs/governance/page.tsx
Normal file
73
src/app/docs/governance/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function GovernancePage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">Governance & Architecture</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Architecture Blueprints</CardTitle>
|
||||
<CardDescription>
|
||||
Reference architectures for common patterns
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Pre-built architecture blueprints for common deployment patterns,
|
||||
including multi-region, high-availability, and disaster recovery setups.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/governance/blueprints">View Blueprints →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Compliance Guides</CardTitle>
|
||||
<CardDescription>
|
||||
Compliance frameworks and requirements
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Guides for meeting compliance requirements including SOC 2, ISO 27001,
|
||||
GDPR, HIPAA, and other regulatory frameworks.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/governance/compliance">View Compliance Guides →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Security Frameworks</CardTitle>
|
||||
<CardDescription>
|
||||
Security best practices and frameworks
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Security frameworks, threat modeling, and best practices for securing
|
||||
your infrastructure and applications.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/governance/security">View Security Frameworks →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
73
src/app/docs/guides/page.tsx
Normal file
73
src/app/docs/guides/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function GuidesPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">Conceptual Guides</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Architecture Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Understanding the Sankofa Phoenix architecture
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Learn about the multi-tier architecture, global deployment model, and
|
||||
how components work together to deliver sovereign cloud infrastructure.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/guides/architecture">Read Architecture Guide →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Well-Architected Framework</CardTitle>
|
||||
<CardDescription>
|
||||
Best practices for building on Sankofa Phoenix
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Guidelines for building reliable, secure, performant, and cost-effective
|
||||
applications on the Phoenix platform.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/guides/well-architected">View Framework →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Best Practices</CardTitle>
|
||||
<CardDescription>
|
||||
Recommended patterns and practices
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Learn from real-world examples and recommended patterns for common
|
||||
use cases and scenarios.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/guides/best-practices">View Best Practices →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
134
src/app/docs/page.tsx
Normal file
134
src/app/docs/page.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Book, Code, GraduationCap, Shield, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default function DocsPage() {
|
||||
const docSections = [
|
||||
{
|
||||
title: 'API Reference',
|
||||
description: 'REST API documentation, GraphQL schema, and SDK references',
|
||||
icon: Code,
|
||||
href: '/docs/api',
|
||||
categories: ['REST API', 'GraphQL', 'SDKs'],
|
||||
},
|
||||
{
|
||||
title: 'Conceptual Guides',
|
||||
description: 'Architecture overview, Well-Architected Framework, and best practices',
|
||||
icon: Book,
|
||||
href: '/docs/guides',
|
||||
categories: ['Architecture', 'Best Practices', 'Framework'],
|
||||
},
|
||||
{
|
||||
title: 'Tutorials & Quickstarts',
|
||||
description: 'Getting started guides, step-by-step tutorials, and sample code',
|
||||
icon: GraduationCap,
|
||||
href: '/docs/tutorials',
|
||||
categories: ['Quickstarts', 'Tutorials', 'Samples'],
|
||||
},
|
||||
{
|
||||
title: 'Governance & Architecture',
|
||||
description: 'Architecture blueprints, compliance guides, and security frameworks',
|
||||
icon: Shield,
|
||||
href: '/docs/governance',
|
||||
categories: ['Blueprints', 'Compliance', 'Security'],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex min-h-[40vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Documentation & Learning
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Comprehensive guides, tutorials, and references for building on Sankofa Phoenix
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Documentation Sections */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{docSections.map((section) => {
|
||||
const Icon = section.icon
|
||||
return (
|
||||
<Card key={section.href} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Icon className="h-8 w-8 text-phoenix-fire" />
|
||||
<CardTitle className="text-2xl text-white">{section.title}</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="text-gray-400">
|
||||
{section.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{section.categories.map((category) => (
|
||||
<span
|
||||
key={category}
|
||||
className="px-2 py-1 text-xs bg-studio-dark text-gray-400 rounded"
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={section.href}>
|
||||
Explore {section.title}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Links */}
|
||||
<section className="py-16 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h2 className="mb-8 text-center text-3xl font-bold text-white">
|
||||
Quick Links
|
||||
</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link
|
||||
href="/docs/api/graphql"
|
||||
className="p-4 bg-studio-black rounded-lg hover:border-phoenix-fire border border-studio-medium transition-colors"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">GraphQL API</h3>
|
||||
<p className="text-sm text-gray-400">Interactive API explorer</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/tutorials/getting-started"
|
||||
className="p-4 bg-studio-black rounded-lg hover:border-phoenix-fire border border-studio-medium transition-colors"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Getting Started</h3>
|
||||
<p className="text-sm text-gray-400">Your first deployment</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/guides/architecture"
|
||||
className="p-4 bg-studio-black rounded-lg hover:border-phoenix-fire border border-studio-medium transition-colors"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Architecture</h3>
|
||||
<p className="text-sm text-gray-400">System architecture overview</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
73
src/app/docs/tutorials/page.tsx
Normal file
73
src/app/docs/tutorials/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function TutorialsPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="mb-8 text-4xl font-bold text-white">Tutorials & Quickstarts</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Getting Started</CardTitle>
|
||||
<CardDescription>
|
||||
Your first deployment on Sankofa Phoenix
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Get up and running in minutes with our step-by-step getting started guide.
|
||||
Deploy your first application and learn the basics.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/tutorials/getting-started">Start Tutorial →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Step-by-Step Tutorials</CardTitle>
|
||||
<CardDescription>
|
||||
Comprehensive tutorials for common scenarios
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Follow along with detailed tutorials covering infrastructure provisioning,
|
||||
application deployment, monitoring, and more.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/tutorials/step-by-step">Browse Tutorials →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Sample Code</CardTitle>
|
||||
<CardDescription>
|
||||
Ready-to-use code samples and examples
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Explore sample code, configuration files, and complete examples that you
|
||||
can use as starting points for your projects.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs/tutorials/samples">View Samples →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
260
src/app/enterprise/page.tsx
Normal file
260
src/app/enterprise/page.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Shield, Lock, Globe, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function EnterprisePage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex min-h-[60vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Enterprise & Government
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Sovereign cloud infrastructure built for enterprise scale and government compliance
|
||||
</p>
|
||||
|
||||
<p className="mb-12 text-lg text-gray-400">
|
||||
Trust, security, and compliance at the core of every deployment
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Contact Sales</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/company/trust">Learn About Trust</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Trust & Compliance Section */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Trust & Compliance
|
||||
</h2>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-phoenix-fire">Security</CardTitle>
|
||||
<CardDescription>
|
||||
Enterprise-grade security
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
SOC 2 Type II certified
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
ISO 27001 compliant
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
Zero Trust architecture
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
End-to-end encryption
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Lock className="mb-4 h-12 w-12 text-sankofa-gold" />
|
||||
<CardTitle className="text-sankofa-gold">Privacy</CardTitle>
|
||||
<CardDescription>
|
||||
Data sovereignty and privacy
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
GDPR compliant
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
Data residency controls
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
Right to deletion
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
Privacy by design
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Globe className="mb-4 h-12 w-12 text-neon-cyan" />
|
||||
<CardTitle className="text-neon-cyan">Compliance</CardTitle>
|
||||
<CardDescription>
|
||||
Regulatory compliance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
HIPAA ready
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
PCI-DSS compliant
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
FedRAMP in progress
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
Regional compliance
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sovereignty Messaging */}
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
True Technological Sovereignty
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Sankofa Phoenix delivers complete control over infrastructure, data, and destiny.
|
||||
No vendor lock-in. No dependencies on foreign cloud providers.
|
||||
Sovereign identity and self-determined policy frameworks.
|
||||
</p>
|
||||
<p className="text-lg text-gray-400">
|
||||
Built for organizations that require true sovereignty, cultural alignment,
|
||||
and infrastructure that honors identity while delivering world-class performance.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Case Studies / Use Cases */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Enterprise Use Cases
|
||||
</h2>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Government & Public Sector</CardTitle>
|
||||
<CardDescription>
|
||||
Sovereign infrastructure for government agencies
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Data residency and sovereignty requirements</li>
|
||||
<li>• Compliance with government regulations</li>
|
||||
<li>• Secure multi-agency collaboration</li>
|
||||
<li>• Disaster recovery and business continuity</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Services</CardTitle>
|
||||
<CardDescription>
|
||||
Secure, compliant infrastructure for financial institutions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• PCI-DSS compliant infrastructure</li>
|
||||
<li>• Real-time transaction processing</li>
|
||||
<li>• Regulatory reporting and compliance</li>
|
||||
<li>• High-availability and fault tolerance</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Healthcare & Life Sciences</CardTitle>
|
||||
<CardDescription>
|
||||
HIPAA-compliant infrastructure for healthcare organizations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
<li>• Protected health information (PHI) handling</li>
|
||||
<li>• HIPAA compliance and audit trails</li>
|
||||
<li>• Research data management</li>
|
||||
<li>• Interoperability and data exchange</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Global Enterprises</CardTitle>
|
||||
<CardDescription>
|
||||
Multi-region infrastructure for global operations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• 325-region global deployment</li>
|
||||
<li>• Cultural intelligence and localization</li>
|
||||
<li>• Multi-tenant isolation</li>
|
||||
<li>• Enterprise-scale resource management</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Contact our enterprise sales team to discuss your requirements
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Contact Sales</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/company/trust">View Trust Center</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
|
||||
body {
|
||||
@apply bg-studio-black text-foreground;
|
||||
/* Cross-browser text size adjustment */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -34,6 +38,12 @@
|
||||
box-shadow: 0 0 20px rgba(0, 255, 209, 0.5);
|
||||
}
|
||||
|
||||
/* Cross-browser backdrop filter support */
|
||||
.backdrop-blur-safari {
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -56,5 +66,45 @@
|
||||
clip: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Chart container styles */
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Status indicator colors */
|
||||
.status-pass {
|
||||
background-color: #00FF88;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-fail {
|
||||
background-color: #FF0040;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: #FFB800;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Dynamic color utilities - use data attributes for dynamic colors */
|
||||
[data-pillar-color] {
|
||||
color: var(--pillar-color);
|
||||
}
|
||||
|
||||
[data-score-color] {
|
||||
color: var(--score-color);
|
||||
}
|
||||
|
||||
[data-health-color] {
|
||||
color: var(--health-color);
|
||||
}
|
||||
|
||||
[data-pillar-bg] {
|
||||
background-color: var(--pillar-bg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
13
src/app/infrastructure/docs/compliance/page.tsx
Normal file
13
src/app/infrastructure/docs/compliance/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ComplianceMapping } from '@/components/infrastructure/ComplianceMapping'
|
||||
import { InfrastructureErrorBoundary } from '@/components/infrastructure/InfrastructureErrorBoundary'
|
||||
|
||||
export default function CompliancePage() {
|
||||
return (
|
||||
<InfrastructureErrorBoundary>
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<ComplianceMapping />
|
||||
</div>
|
||||
</InfrastructureErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
13
src/app/infrastructure/docs/costs/page.tsx
Normal file
13
src/app/infrastructure/docs/costs/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { CostEstimates } from '@/components/infrastructure/CostEstimates'
|
||||
import { InfrastructureErrorBoundary } from '@/components/infrastructure/InfrastructureErrorBoundary'
|
||||
|
||||
export default function CostsPage() {
|
||||
return (
|
||||
<InfrastructureErrorBoundary>
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<CostEstimates />
|
||||
</div>
|
||||
</InfrastructureErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
20
src/app/infrastructure/docs/layout.tsx
Normal file
20
src/app/infrastructure/docs/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { SkipLink, useKeyboardNavigation, FocusIndicator } from '@/components/infrastructure/AccessibilityEnhancements'
|
||||
|
||||
export default function InfrastructureLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
useKeyboardNavigation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkipLink />
|
||||
<FocusIndicator />
|
||||
<div id="main-content" className="min-h-screen">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
13
src/app/infrastructure/docs/page.tsx
Normal file
13
src/app/infrastructure/docs/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DocsDashboard } from '@/components/infrastructure/DocsDashboard'
|
||||
import { InfrastructureErrorBoundary } from '@/components/infrastructure/InfrastructureErrorBoundary'
|
||||
|
||||
export default function InfrastructureDocsPage() {
|
||||
return (
|
||||
<InfrastructureErrorBoundary>
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<DocsDashboard />
|
||||
</div>
|
||||
</InfrastructureErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
13
src/app/infrastructure/docs/timeline/page.tsx
Normal file
13
src/app/infrastructure/docs/timeline/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DeploymentTimeline } from '@/components/infrastructure/DeploymentTimeline'
|
||||
import { InfrastructureErrorBoundary } from '@/components/infrastructure/InfrastructureErrorBoundary'
|
||||
|
||||
export default function TimelinePage() {
|
||||
return (
|
||||
<InfrastructureErrorBoundary>
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<DeploymentTimeline />
|
||||
</div>
|
||||
</InfrastructureErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
13
src/app/infrastructure/docs/topology/page.tsx
Normal file
13
src/app/infrastructure/docs/topology/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NetworkTopologyDocs } from '@/components/infrastructure/NetworkTopologyDocs'
|
||||
import { InfrastructureErrorBoundary } from '@/components/infrastructure/InfrastructureErrorBoundary'
|
||||
|
||||
export default function TopologyPage() {
|
||||
return (
|
||||
<InfrastructureErrorBoundary>
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<NetworkTopologyDocs />
|
||||
</div>
|
||||
</InfrastructureErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import { Providers } from './providers'
|
||||
@@ -10,8 +10,19 @@ const inter = Inter({
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Phoenix Sankofa Cloud',
|
||||
title: {
|
||||
default: "Sankofa's Phoenix Nexus Cloud",
|
||||
template: "%s | Sankofa's Phoenix Nexus Cloud",
|
||||
},
|
||||
description: 'The sovereign cloud born of fire and ancestral wisdom.',
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'),
|
||||
}
|
||||
|
||||
// Viewport configuration without maximum-scale and user-scalable for accessibility compliance
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
// Note: maximum-scale and user-scalable removed per accessibility best practices
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function ManifestoPage() {
|
||||
<header className="border-b border-studio-medium bg-studio-dark">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
|
||||
<Link href="/" className="text-2xl font-bold text-white">
|
||||
Phoenix <span className="text-phoenix-fire">Sankofa</span> Cloud
|
||||
<span className="text-sankofa-gold">Sankofa's</span> Phoenix <span className="text-phoenix-fire">Nexus</span> Cloud
|
||||
</Link>
|
||||
<nav className="flex gap-4">
|
||||
<Link href="/" className="text-gray-400 hover:text-white">
|
||||
@@ -137,7 +137,10 @@ export default function ManifestoPage() {
|
||||
This is the Sankofa cycle. This is the Phoenix transformation.
|
||||
</p>
|
||||
<p className="text-center text-2xl font-bold text-phoenix-fire">
|
||||
This is Phoenix Sankofa Cloud.
|
||||
This is Sankofa.
|
||||
</p>
|
||||
<p className="text-center text-2xl font-bold text-phoenix-fire">
|
||||
This is Sankofa Phoenix.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
113
src/app/marketplace/api/page.tsx
Normal file
113
src/app/marketplace/api/page.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Globe, Star, TrendingUp } from 'lucide-react'
|
||||
|
||||
export default function APIMarketplacePage() {
|
||||
const apis = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Weather API',
|
||||
provider: 'WeatherCorp',
|
||||
description: 'Real-time weather data and forecasts',
|
||||
rating: 4.7,
|
||||
requests: '10M+',
|
||||
category: 'Data',
|
||||
pricing: 'Free tier available',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Payment Gateway',
|
||||
provider: 'PayFlow',
|
||||
description: 'Secure payment processing API',
|
||||
rating: 4.9,
|
||||
requests: '50M+',
|
||||
category: 'Payment',
|
||||
pricing: 'Pay per transaction',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'AI Image Processing',
|
||||
provider: 'VisionAI',
|
||||
description: 'Advanced image recognition and processing',
|
||||
rating: 4.6,
|
||||
requests: '5M+',
|
||||
category: 'AI/ML',
|
||||
pricing: 'Usage-based',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-12">
|
||||
<h1 className="mb-4 text-5xl font-bold text-white">API Marketplace</h1>
|
||||
<p className="text-xl text-gray-400">
|
||||
Discover and integrate third-party APIs into your applications
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{apis.map((api) => (
|
||||
<Card key={api.id} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<Globe className="h-8 w-8 text-phoenix-fire" />
|
||||
<span className="px-2 py-1 text-xs bg-studio-dark text-gray-400 rounded">
|
||||
{api.category}
|
||||
</span>
|
||||
</div>
|
||||
<CardTitle className="text-white">{api.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
by {api.provider}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-300 mb-4">{api.description}</p>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Star className="h-4 w-4 text-yellow-400 fill-yellow-400" />
|
||||
<span>{api.rating}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
<span>{api.requests} requests/month</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{api.pricing}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" asChild>
|
||||
<Link href={`/marketplace/api/${api.id}`}>
|
||||
View Docs
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="phoenix" asChild>
|
||||
<Link href={`/marketplace/api/${api.id}/subscribe`}>
|
||||
Subscribe
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<h2 className="mb-4 text-2xl font-bold text-white">Publish Your API</h2>
|
||||
<p className="mb-6 text-gray-400">
|
||||
Monetize your API and reach developers worldwide
|
||||
</p>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/developers/api/publish">Publish API</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
238
src/app/marketplace/deployments/[id]/page.tsx
Normal file
238
src/app/marketplace/deployments/[id]/page.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { ArrowLeft, Trash2, RefreshCw } from 'lucide-react'
|
||||
|
||||
async function fetchDeployment(id: string) {
|
||||
const response = await fetch('/api/graphql', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query GetDeployment($id: ID!) {
|
||||
deployment(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
deploymentType
|
||||
region
|
||||
parameters
|
||||
outputs
|
||||
errorMessage
|
||||
createdAt
|
||||
startedAt
|
||||
completedAt
|
||||
logs(limit: 100) {
|
||||
id
|
||||
level
|
||||
message
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id },
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return data.data?.deployment
|
||||
}
|
||||
|
||||
export default function DeploymentDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const id = params.id as string
|
||||
|
||||
const { data: deployment, isLoading, refetch } = useQuery({
|
||||
queryKey: ['deployment', id],
|
||||
queryFn: () => fetchDeployment(id),
|
||||
refetchInterval: 5000,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-muted rounded w-1/3" />
|
||||
<div className="h-64 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!deployment) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<p className="text-muted-foreground mb-4">Deployment not found</p>
|
||||
<Button onClick={() => router.push('/marketplace/deployments')}>
|
||||
Back to Deployments
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<Button variant="ghost" onClick={() => router.back()}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => refetch()}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="text-4xl font-bold mb-2">{deployment.name}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>{deployment.status}</Badge>
|
||||
<Badge variant="outline">{deployment.deploymentType}</Badge>
|
||||
{deployment.region && (
|
||||
<Badge variant="outline">Region: {deployment.region}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="outputs">Outputs</TabsTrigger>
|
||||
<TabsTrigger value="parameters">Parameters</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Status:</span>
|
||||
<Badge>{deployment.status}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Type:</span>
|
||||
<span>{deployment.deploymentType}</span>
|
||||
</div>
|
||||
{deployment.region && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Region:</span>
|
||||
<span>{deployment.region}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Created:</span>
|
||||
<span>{new Date(deployment.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
{deployment.startedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Started:</span>
|
||||
<span>{new Date(deployment.startedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
{deployment.completedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Completed:</span>
|
||||
<span>{new Date(deployment.completedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
{deployment.errorMessage && (
|
||||
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded">
|
||||
<p className="text-sm font-medium text-red-800">Error:</p>
|
||||
<p className="text-sm text-red-600">{deployment.errorMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logs">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Logs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{deployment.logs && deployment.logs.length > 0 ? (
|
||||
deployment.logs.map((log: any) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className={`p-2 rounded text-sm font-mono ${
|
||||
log.level === 'ERROR'
|
||||
? 'bg-red-50 text-red-800'
|
||||
: log.level === 'WARN'
|
||||
? 'bg-yellow-50 text-yellow-800'
|
||||
: 'bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
[{new Date(log.createdAt).toLocaleTimeString()}] [{log.level}]
|
||||
</span>{' '}
|
||||
{log.message}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground">No logs available</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="outputs">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Outputs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{deployment.outputs && Object.keys(deployment.outputs).length > 0 ? (
|
||||
<pre className="p-4 bg-muted rounded overflow-auto">
|
||||
{JSON.stringify(deployment.outputs, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No outputs available</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="parameters">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Parameters</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{deployment.parameters && Object.keys(deployment.parameters).length > 0 ? (
|
||||
<pre className="p-4 bg-muted rounded overflow-auto">
|
||||
{JSON.stringify(deployment.parameters, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No parameters available</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
145
src/app/marketplace/deployments/page.tsx
Normal file
145
src/app/marketplace/deployments/page.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Clock, CheckCircle, XCircle, Loader } from 'lucide-react'
|
||||
|
||||
async function fetchDeployments() {
|
||||
const response = await fetch('/api/graphql', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query GetDeployments {
|
||||
deployments {
|
||||
id
|
||||
name
|
||||
status
|
||||
deploymentType
|
||||
region
|
||||
createdAt
|
||||
completedAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return data.data?.deployments || []
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
PENDING: 'bg-yellow-100 text-yellow-800',
|
||||
PROVISIONING: 'bg-blue-100 text-blue-800',
|
||||
DEPLOYING: 'bg-blue-100 text-blue-800',
|
||||
RUNNING: 'bg-green-100 text-green-800',
|
||||
FAILED: 'bg-red-100 text-red-800',
|
||||
STOPPED: 'bg-gray-100 text-gray-800',
|
||||
}
|
||||
|
||||
const STATUS_ICONS: Record<string, any> = {
|
||||
PENDING: Clock,
|
||||
PROVISIONING: Loader,
|
||||
DEPLOYING: Loader,
|
||||
RUNNING: CheckCircle,
|
||||
FAILED: XCircle,
|
||||
STOPPED: Clock,
|
||||
}
|
||||
|
||||
export default function DeploymentsPage() {
|
||||
const { data: deployments = [], isLoading } = useQuery({
|
||||
queryKey: ['deployments'],
|
||||
queryFn: fetchDeployments,
|
||||
refetchInterval: 5000, // Refresh every 5 seconds
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-muted rounded w-1/3" />
|
||||
<div className="h-64 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">Deployments</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Monitor and manage your deployments
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/marketplace">
|
||||
<Button>New Deployment</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{deployments.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<p className="text-muted-foreground mb-4">No deployments found</p>
|
||||
<Link href="/marketplace">
|
||||
<Button>Browse Marketplace</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{deployments.map((deployment: any) => {
|
||||
const StatusIcon = STATUS_ICONS[deployment.status] || Clock
|
||||
return (
|
||||
<Card key={deployment.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<CardTitle className="text-xl">
|
||||
<Link
|
||||
href={`/marketplace/deployments/${deployment.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{deployment.name}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<StatusIcon
|
||||
className={`h-5 w-5 ${
|
||||
deployment.status === 'RUNNING'
|
||||
? 'text-green-500'
|
||||
: deployment.status === 'FAILED'
|
||||
? 'text-red-500'
|
||||
: 'text-blue-500'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<CardDescription>
|
||||
{deployment.region && (
|
||||
<span className="mr-2">Region: {deployment.region}</span>
|
||||
)}
|
||||
<span>Type: {deployment.deploymentType}</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge className={STATUS_COLORS[deployment.status] || ''}>
|
||||
{deployment.status}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(deployment.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
113
src/app/marketplace/developers/page.tsx
Normal file
113
src/app/marketplace/developers/page.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Code, Star, Download, ExternalLink } from 'lucide-react'
|
||||
|
||||
export default function DeveloperMarketplacePage() {
|
||||
const tools = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Phoenix CLI',
|
||||
author: 'Sankofa Team',
|
||||
description: 'Command-line interface for managing Phoenix resources',
|
||||
rating: 4.9,
|
||||
downloads: 5000,
|
||||
category: 'CLI Tools',
|
||||
language: 'TypeScript',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Terraform Provider',
|
||||
author: 'Community',
|
||||
description: 'Terraform provider for infrastructure as code',
|
||||
rating: 4.7,
|
||||
downloads: 3200,
|
||||
category: 'IaC',
|
||||
language: 'Go',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Python SDK',
|
||||
author: 'Sankofa Team',
|
||||
description: 'Official Python SDK for Phoenix API',
|
||||
rating: 4.8,
|
||||
downloads: 2800,
|
||||
category: 'SDK',
|
||||
language: 'Python',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-12">
|
||||
<h1 className="mb-4 text-5xl font-bold text-white">Developer Marketplace</h1>
|
||||
<p className="text-xl text-gray-400">
|
||||
Tools, libraries, and integrations built by the community
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{tools.map((tool) => (
|
||||
<Card key={tool.id} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<Code className="h-8 w-8 text-phoenix-fire" />
|
||||
<span className="px-2 py-1 text-xs bg-studio-dark text-gray-400 rounded">
|
||||
{tool.category}
|
||||
</span>
|
||||
</div>
|
||||
<CardTitle className="text-white">{tool.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
by {tool.author}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-300 mb-4">{tool.description}</p>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4 text-sm text-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-4 w-4 text-yellow-400 fill-yellow-400" />
|
||||
<span>{tool.rating}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-4 w-4" />
|
||||
<span>{tool.downloads.toLocaleString()}</span>
|
||||
</div>
|
||||
<span className="text-xs">{tool.language}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" asChild>
|
||||
<Link href={`/marketplace/developers/${tool.id}`}>
|
||||
View Details
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="phoenix" asChild>
|
||||
<Link href={`/marketplace/developers/${tool.id}/install`}>
|
||||
Install
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<h2 className="mb-4 text-2xl font-bold text-white">Publish Your Tool</h2>
|
||||
<p className="mb-6 text-gray-400">
|
||||
Share your tools and libraries with the developer community
|
||||
</p>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/developers/marketplace/publish">Publish Tool</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
156
src/app/marketplace/page.tsx
Normal file
156
src/app/marketplace/page.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ProductCard } from '@/components/marketplace/ProductCard'
|
||||
import { Search, Filter } from 'lucide-react'
|
||||
|
||||
const PRODUCT_CATEGORIES = [
|
||||
{ value: 'COMPUTE', label: 'Compute' },
|
||||
{ value: 'NETWORK_INFRA', label: 'Network & Infrastructure' },
|
||||
{ value: 'BLOCKCHAIN_STACK', label: 'Blockchain Stacks' },
|
||||
{ value: 'BLOCKCHAIN_TOOLS', label: 'Blockchain Tools' },
|
||||
{ value: 'FINANCIAL_MESSAGING', label: 'Financial Messaging' },
|
||||
{ value: 'INTERNET_REGISTRY', label: 'Internet Registry' },
|
||||
{ value: 'AI_LLM_AGENT', label: 'AI/LLM Agent' },
|
||||
]
|
||||
|
||||
async function fetchProducts(filter?: any) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (filter?.category) queryParams.set('category', filter.category)
|
||||
if (filter?.search) queryParams.set('search', filter.search)
|
||||
if (filter?.featured !== undefined) queryParams.set('featured', String(filter.featured))
|
||||
|
||||
const response = await fetch(`/api/graphql?query=${encodeURIComponent(`
|
||||
query GetProducts($filter: ProductFilter) {
|
||||
products(filter: $filter) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
category
|
||||
shortDescription
|
||||
featured
|
||||
iconUrl
|
||||
publisher {
|
||||
displayName
|
||||
verified
|
||||
}
|
||||
averageRating
|
||||
reviewCount
|
||||
}
|
||||
}
|
||||
`)}&variables=${encodeURIComponent(JSON.stringify({ filter }))}`)
|
||||
|
||||
const data = await response.json()
|
||||
return data.data?.products || []
|
||||
}
|
||||
|
||||
export default function MarketplacePage() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | undefined>()
|
||||
const [showFeatured, setShowFeatured] = useState(false)
|
||||
|
||||
const filter = {
|
||||
...(selectedCategory && { category: selectedCategory }),
|
||||
...(searchQuery && { search: searchQuery }),
|
||||
...(showFeatured && { featured: true }),
|
||||
}
|
||||
|
||||
const { data: products = [], isLoading } = useQuery({
|
||||
queryKey: ['marketplace-products', filter],
|
||||
queryFn: () => fetchProducts(filter),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-2">Phoenix Marketplace</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Discover and deploy cloud resources, blockchain stacks, and developer tools
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="mb-8 space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search products..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant={showFeatured ? 'default' : 'outline'}
|
||||
onClick={() => setShowFeatured(!showFeatured)}
|
||||
>
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
Featured Only
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Category Filters */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant={!selectedCategory ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedCategory(undefined)}
|
||||
>
|
||||
All Categories
|
||||
</Button>
|
||||
{PRODUCT_CATEGORIES.map((category) => (
|
||||
<Button
|
||||
key={category.value}
|
||||
variant={selectedCategory === category.value ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setSelectedCategory(
|
||||
selectedCategory === category.value ? undefined : category.value
|
||||
)
|
||||
}
|
||||
>
|
||||
{category.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<Card key={i} className="animate-pulse">
|
||||
<CardHeader>
|
||||
<div className="h-4 bg-muted rounded w-3/4 mb-2" />
|
||||
<div className="h-3 bg-muted rounded w-1/2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-20 bg-muted rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : products.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<p className="text-muted-foreground">No products found. Try adjusting your filters.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{products.map((product: any) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
112
src/app/marketplace/partners/page.tsx
Normal file
112
src/app/marketplace/partners/page.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Star, Download, ExternalLink } from 'lucide-react'
|
||||
|
||||
export default function PartnerMarketplacePage() {
|
||||
const solutions = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Enterprise Security Suite',
|
||||
partner: 'SecureCorp',
|
||||
description: 'Comprehensive security solution for enterprise deployments',
|
||||
rating: 4.8,
|
||||
downloads: 1250,
|
||||
category: 'Security',
|
||||
logo: '🔒',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Data Analytics Platform',
|
||||
partner: 'DataFlow Inc',
|
||||
description: 'Advanced analytics and reporting for cloud infrastructure',
|
||||
rating: 4.6,
|
||||
downloads: 890,
|
||||
category: 'Analytics',
|
||||
logo: '📊',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'DevOps Automation Tools',
|
||||
partner: 'AutoDev Solutions',
|
||||
description: 'Automated CI/CD pipelines and deployment workflows',
|
||||
rating: 4.9,
|
||||
downloads: 2100,
|
||||
category: 'DevOps',
|
||||
logo: '⚙️',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-12">
|
||||
<h1 className="mb-4 text-5xl font-bold text-white">Partner Marketplace</h1>
|
||||
<p className="text-xl text-gray-400">
|
||||
Discover solutions from our certified partners
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{solutions.map((solution) => (
|
||||
<Card key={solution.id} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="text-4xl">{solution.logo}</div>
|
||||
<span className="px-2 py-1 text-xs bg-studio-dark text-gray-400 rounded">
|
||||
{solution.category}
|
||||
</span>
|
||||
</div>
|
||||
<CardTitle className="text-white">{solution.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
by {solution.partner}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-300 mb-4">{solution.description}</p>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4 text-sm text-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-4 w-4 text-yellow-400 fill-yellow-400" />
|
||||
<span>{solution.rating}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-4 w-4" />
|
||||
<span>{solution.downloads.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" asChild>
|
||||
<Link href={`/marketplace/partners/${solution.id}`}>
|
||||
View Details
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="phoenix" asChild>
|
||||
<Link href={`/marketplace/partners/${solution.id}/install`}>
|
||||
Install
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<h2 className="mb-4 text-2xl font-bold text-white">Become a Partner</h2>
|
||||
<p className="mb-6 text-gray-400">
|
||||
List your solution in our marketplace and reach thousands of customers
|
||||
</p>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/partners/register">Register as Partner</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
291
src/app/marketplace/products/[slug]/page.tsx
Normal file
291
src/app/marketplace/products/[slug]/page.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DeploymentWizard } from '@/components/marketplace/DeploymentWizard'
|
||||
import { Star, Verified, ArrowLeft, ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
async function fetchProduct(slug: string) {
|
||||
const response = await fetch(`/api/graphql?query=${encodeURIComponent(`
|
||||
query GetProduct($slug: String!) {
|
||||
productBySlug(slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
category
|
||||
description
|
||||
shortDescription
|
||||
featured
|
||||
iconUrl
|
||||
documentationUrl
|
||||
supportUrl
|
||||
metadata
|
||||
tags
|
||||
publisher {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
verified
|
||||
websiteUrl
|
||||
logoUrl
|
||||
}
|
||||
versions {
|
||||
id
|
||||
version
|
||||
isLatest
|
||||
status
|
||||
releasedAt
|
||||
}
|
||||
pricing {
|
||||
id
|
||||
pricingType
|
||||
basePrice
|
||||
currency
|
||||
billingPeriod
|
||||
}
|
||||
averageRating
|
||||
reviewCount
|
||||
}
|
||||
}
|
||||
`)}&variables=${encodeURIComponent(JSON.stringify({ slug }))}`)
|
||||
|
||||
const data = await response.json()
|
||||
return data.data?.productBySlug
|
||||
}
|
||||
|
||||
export default function ProductDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const slug = params.slug as string
|
||||
const [showDeployWizard, setShowDeployWizard] = useState(false)
|
||||
|
||||
const { data: product, isLoading } = useQuery({
|
||||
queryKey: ['product', slug],
|
||||
queryFn: () => fetchProduct(slug),
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-muted rounded w-1/3" />
|
||||
<div className="h-4 bg-muted rounded w-1/2" />
|
||||
<div className="h-64 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<p className="text-muted-foreground mb-4">Product not found</p>
|
||||
<Link href="/marketplace">
|
||||
<Button>Back to Marketplace</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-6"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
{product.iconUrl && (
|
||||
<img
|
||||
src={product.iconUrl}
|
||||
alt={product.name}
|
||||
className="w-20 h-20 rounded-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h1 className="text-4xl font-bold mb-2">{product.name}</h1>
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-2">
|
||||
{product.publisher && (
|
||||
<>
|
||||
<span>by {product.publisher.displayName}</span>
|
||||
{product.publisher.verified && (
|
||||
<Verified className="h-4 w-4 text-blue-500" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary">{product.category}</Badge>
|
||||
{product.featured && <Badge>Featured</Badge>}
|
||||
{product.averageRating !== undefined && product.averageRating > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
|
||||
<span className="text-sm font-medium">
|
||||
{product.averageRating.toFixed(1)}
|
||||
</span>
|
||||
{product.reviewCount !== undefined && product.reviewCount > 0 && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({product.reviewCount} reviews)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Description</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground whitespace-pre-wrap">
|
||||
{product.description || product.shortDescription || 'No description available'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Versions */}
|
||||
{product.versions && product.versions.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Versions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{product.versions.map((version: any) => (
|
||||
<div
|
||||
key={version.id}
|
||||
className="flex items-center justify-between p-2 border rounded"
|
||||
>
|
||||
<div>
|
||||
<span className="font-medium">v{version.version}</span>
|
||||
{version.isLatest && (
|
||||
<Badge variant="outline" className="ml-2">
|
||||
Latest
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{version.releasedAt && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(version.releasedAt).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{product.tags && product.tags.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tags</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{product.tags.map((tag: string) => (
|
||||
<Badge key={tag} variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Pricing */}
|
||||
{product.pricing && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Pricing</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{product.pricing.pricingType === 'FREE' ? (
|
||||
<div className="text-2xl font-bold">Free</div>
|
||||
) : (
|
||||
<div>
|
||||
{product.pricing.basePrice !== null && (
|
||||
<div className="text-2xl font-bold">
|
||||
${product.pricing.basePrice}
|
||||
{product.pricing.billingPeriod && (
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
/{product.pricing.billingPeriod.toLowerCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
{product.pricing.pricingType}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Button
|
||||
className="w-full mb-2"
|
||||
onClick={() => setShowDeployWizard(true)}
|
||||
>
|
||||
Deploy Now
|
||||
</Button>
|
||||
{product.documentationUrl && (
|
||||
<Link href={product.documentationUrl} target="_blank">
|
||||
<Button variant="outline" className="w-full mb-2">
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
Documentation
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{product.supportUrl && (
|
||||
<Link href={product.supportUrl} target="_blank">
|
||||
<Button variant="outline" className="w-full">
|
||||
Get Support
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deployment Wizard Modal */}
|
||||
{showDeployWizard && (
|
||||
<DeploymentWizard
|
||||
product={product}
|
||||
onClose={() => setShowDeployWizard(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
<span className="bg-gradient-to-r from-phoenix-fire to-sankofa-gold bg-clip-text text-transparent">
|
||||
Sankofa's
|
||||
</span>{' '}
|
||||
Phoenix{' '}
|
||||
<span className="bg-gradient-to-r from-phoenix-fire to-sankofa-gold bg-clip-text text-transparent">
|
||||
Sankofa
|
||||
Nexus
|
||||
</span>{' '}
|
||||
Cloud
|
||||
</h1>
|
||||
@@ -104,7 +109,8 @@ export default function Home() {
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
158
src/app/partners/page.tsx
Normal file
158
src/app/partners/page.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Handshake, TrendingUp, Users, Award, Zap, BookOpen } from 'lucide-react'
|
||||
|
||||
export default function PartnersPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Partner Program
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Build, sell, and grow with Sankofa's Phoenix Nexus Cloud
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/partners">Join as Partner</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/partners/benefits">View Benefits</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Partner Benefits */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Partner Benefits
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<TrendingUp className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Revenue Opportunities</CardTitle>
|
||||
<CardDescription>
|
||||
Competitive margins and co-sell opportunities
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Attractive partner margins</li>
|
||||
<li>• Co-sell deal registration</li>
|
||||
<li>• Recurring revenue streams</li>
|
||||
<li>• Marketing development funds</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Zap className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Technical Enablement</CardTitle>
|
||||
<CardDescription>
|
||||
Comprehensive training and support resources
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Technical onboarding programs</li>
|
||||
<li>• Solution architecture support</li>
|
||||
<li>• Dedicated partner engineering</li>
|
||||
<li>• Test environments and sandboxes</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Users className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Go-to-Market Support</CardTitle>
|
||||
<CardDescription>
|
||||
Marketing resources and co-marketing opportunities
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Co-marketing campaigns</li>
|
||||
<li>• Partner portal access</li>
|
||||
<li>• Sales enablement materials</li>
|
||||
<li>• Joint customer events</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Partner Types */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Partner Types
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<Handshake className="mb-4 h-8 w-8 text-phoenix-fire" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Solution Partners</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Build and deliver solutions on Phoenix Nexus Cloud. Integrate your
|
||||
products and services with our platform.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Solution marketplace listing</li>
|
||||
<li>• Technical integration support</li>
|
||||
<li>• Joint go-to-market</li>
|
||||
<li>• Solution certification</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Award className="mb-4 h-8 w-8 text-sankofa-gold" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Reseller Partners</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Sell Phoenix Nexus Cloud to your customers. Leverage our infrastructure
|
||||
to deliver value to your client base.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Reseller discounts</li>
|
||||
<li>• Deal registration</li>
|
||||
<li>• Sales training</li>
|
||||
<li>• Customer support tools</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Partner With Us?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Join our partner program and grow your business
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/partners">Access Partner Portal</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/support">Contact Partner Team</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
161
src/app/portal/admin/page.tsx
Normal file
161
src/app/portal/admin/page.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Users, Building2, CreditCard, Settings, Shield, BarChart3 } from 'lucide-react'
|
||||
|
||||
export default function AdminPortalPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-white">Tenant Administration</h1>
|
||||
<p className="mt-2 text-gray-400">
|
||||
Manage your organization, users, permissions, and billing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="border-studio-medium hover:border-phoenix-fire/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-phoenix-fire/20 p-3">
|
||||
<Building2 className="h-6 w-6 text-phoenix-fire" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Organization</CardTitle>
|
||||
<CardDescription>Manage organization settings</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-gray-400">
|
||||
Configure organization details, domains, and branding
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href="/portal/admin/organization">Manage Organization</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-studio-medium hover:border-phoenix-fire/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-sankofa-gold/20 p-3">
|
||||
<Users className="h-6 w-6 text-sankofa-gold" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Users & Roles</CardTitle>
|
||||
<CardDescription>Manage team members</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-gray-400">
|
||||
Invite users, assign roles, and manage permissions
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href="/portal/admin/users">Manage Users</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-studio-medium hover:border-phoenix-fire/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-neon-cyan/20 p-3">
|
||||
<Shield className="h-6 w-6 text-neon-cyan" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Permissions</CardTitle>
|
||||
<CardDescription>Configure access controls</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-gray-400">
|
||||
Set up role-based access control and permissions
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href="/portal/admin/permissions">Manage Permissions</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-studio-medium hover:border-phoenix-fire/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-phoenix-fire/20 p-3">
|
||||
<CreditCard className="h-6 w-6 text-phoenix-fire" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Billing</CardTitle>
|
||||
<CardDescription>Manage subscriptions</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-gray-400">
|
||||
View invoices, manage payment methods, and billing settings
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href="/portal/admin/billing">Manage Billing</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-studio-medium hover:border-phoenix-fire/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-sankofa-gold/20 p-3">
|
||||
<BarChart3 className="h-6 w-6 text-sankofa-gold" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Usage & Analytics</CardTitle>
|
||||
<CardDescription>Monitor resource usage</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-gray-400">
|
||||
Track usage, costs, and performance metrics
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href="/portal/admin/analytics">View Analytics</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-studio-medium hover:border-phoenix-fire/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-neon-cyan/20 p-3">
|
||||
<Settings className="h-6 w-6 text-neon-cyan" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Settings</CardTitle>
|
||||
<CardDescription>Configure preferences</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-gray-400">
|
||||
Manage security, notifications, and integrations
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href="/portal/admin/settings">Manage Settings</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
58
src/app/portal/callback/page.tsx
Normal file
58
src/app/portal/callback/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { handleSSOCallback, getPortalForRole } from '@/lib/auth/sso'
|
||||
|
||||
export default function SSOCallbackPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams?.get('code')
|
||||
const state = searchParams?.get('state')
|
||||
|
||||
if (code && state) {
|
||||
const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://keycloak.sankofa.nexus'
|
||||
const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'sankofa'
|
||||
const clientId = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'phoenix-portal'
|
||||
const redirectUri = `${window.location.origin}/portal/callback`
|
||||
|
||||
handleSSOCallback(code, state, {
|
||||
keycloakUrl,
|
||||
realm,
|
||||
clientId,
|
||||
redirectUri,
|
||||
})
|
||||
.then(({ token, user, role }) => {
|
||||
// Store token
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('auth_token', token)
|
||||
localStorage.setItem('phoenix_token', token)
|
||||
localStorage.setItem('phoenix_user', JSON.stringify(user))
|
||||
}
|
||||
|
||||
// Route to appropriate portal
|
||||
const portal = getPortalForRole(role)
|
||||
router.push(portal)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('SSO callback error:', error)
|
||||
router.push('/portal/signin?error=sso_failed')
|
||||
})
|
||||
} else {
|
||||
// No code/state, redirect to sign in
|
||||
router.push('/portal/signin')
|
||||
}
|
||||
}, [searchParams, router])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-studio-black">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 inline-block h-8 w-8 animate-spin rounded-full border-4 border-phoenix-fire border-t-transparent" />
|
||||
<p className="text-gray-400">Completing sign in...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
394
src/app/portal/developers/environments/page.tsx
Normal file
394
src/app/portal/developers/environments/page.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Terminal, Plus, Trash2, Play, Square, RefreshCw, Loader2 } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
GET_TEST_ENVIRONMENTS,
|
||||
CREATE_TEST_ENVIRONMENT,
|
||||
START_TEST_ENVIRONMENT,
|
||||
STOP_TEST_ENVIRONMENT,
|
||||
DELETE_TEST_ENVIRONMENT,
|
||||
} from '@/lib/graphql/queries/test-environments'
|
||||
|
||||
interface Environment {
|
||||
id: string
|
||||
name: string
|
||||
status: 'RUNNING' | 'STOPPED' | 'PROVISIONING' | 'ERROR' | 'DELETING'
|
||||
region: string
|
||||
resources: {
|
||||
vms: number
|
||||
storage: string
|
||||
network: string
|
||||
}
|
||||
created: string
|
||||
expires?: string
|
||||
}
|
||||
|
||||
export default function EnvironmentsPage() {
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [newEnvName, setNewEnvName] = useState('')
|
||||
const [newEnvRegion, setNewEnvRegion] = useState('us-east-1')
|
||||
|
||||
// Fetch test environments
|
||||
const { data, loading, error, refetch } = useQuery(GET_TEST_ENVIRONMENTS, {
|
||||
onCompleted: (data) => {
|
||||
// Data is handled in the component
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error fetching test environments:', error)
|
||||
},
|
||||
})
|
||||
|
||||
// Create environment mutation
|
||||
const [createEnvironment, { loading: creating }] = useMutation(CREATE_TEST_ENVIRONMENT, {
|
||||
onCompleted: () => {
|
||||
setNewEnvName('')
|
||||
setNewEnvRegion('us-east-1')
|
||||
setShowCreate(false)
|
||||
refetch()
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error creating environment:', error)
|
||||
alert('Failed to create environment: ' + error.message)
|
||||
},
|
||||
})
|
||||
|
||||
// Start environment mutation
|
||||
const [startEnvironment, { loading: starting }] = useMutation(START_TEST_ENVIRONMENT, {
|
||||
onCompleted: () => {
|
||||
refetch()
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error starting environment:', error)
|
||||
alert('Failed to start environment: ' + error.message)
|
||||
},
|
||||
})
|
||||
|
||||
// Stop environment mutation
|
||||
const [stopEnvironment, { loading: stopping }] = useMutation(STOP_TEST_ENVIRONMENT, {
|
||||
onCompleted: () => {
|
||||
refetch()
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error stopping environment:', error)
|
||||
alert('Failed to stop environment: ' + error.message)
|
||||
},
|
||||
})
|
||||
|
||||
// Delete environment mutation
|
||||
const [deleteEnvironment, { loading: deleting }] = useMutation(DELETE_TEST_ENVIRONMENT, {
|
||||
onCompleted: () => {
|
||||
refetch()
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error deleting environment:', error)
|
||||
alert('Failed to delete environment: ' + error.message)
|
||||
},
|
||||
})
|
||||
|
||||
const environments: Environment[] = data?.testEnvironments?.map((env: any) => ({
|
||||
id: env.id,
|
||||
name: env.name,
|
||||
status: env.status,
|
||||
region: env.region,
|
||||
resources: env.resources,
|
||||
created: new Date(env.createdAt).toLocaleDateString(),
|
||||
expires: env.expiresAt ? new Date(env.expiresAt).toLocaleDateString() : undefined,
|
||||
})) || []
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!newEnvName.trim()) return
|
||||
|
||||
createEnvironment({
|
||||
variables: {
|
||||
input: {
|
||||
name: newEnvName,
|
||||
region: newEnvRegion,
|
||||
resources: {
|
||||
vms: 1,
|
||||
storage: '20 GB',
|
||||
network: '10.0.0.0/16',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleStart = (id: string) => {
|
||||
startEnvironment({ variables: { id } })
|
||||
}
|
||||
|
||||
const handleStop = (id: string) => {
|
||||
stopEnvironment({ variables: { id } })
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this environment? All data will be lost.')) {
|
||||
deleteEnvironment({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RUNNING':
|
||||
return 'bg-green-500'
|
||||
case 'STOPPED':
|
||||
return 'bg-gray-500'
|
||||
case 'PROVISIONING':
|
||||
return 'bg-yellow-500'
|
||||
case 'ERROR':
|
||||
return 'bg-red-500'
|
||||
default:
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-phoenix-fire" />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-white">Test Environments</h1>
|
||||
<p className="mt-2 text-gray-400">
|
||||
Provision and manage sandbox environments for testing
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="phoenix"
|
||||
onClick={() => setShowCreate(!showCreate)}
|
||||
disabled={creating}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Environment
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Card className="mb-8 border-red-500/50">
|
||||
<CardContent className="py-4">
|
||||
<p className="text-red-500">
|
||||
Error loading environments: {error.message}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showCreate && (
|
||||
<Card className="mb-8 border-phoenix-fire/50">
|
||||
<CardHeader>
|
||||
<CardTitle>Create New Test Environment</CardTitle>
|
||||
<CardDescription>
|
||||
Provision a new sandbox environment for testing
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Environment Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newEnvName}
|
||||
onChange={(e) => setNewEnvName(e.target.value)}
|
||||
placeholder="e.g., Development Sandbox"
|
||||
className="w-full rounded bg-studio-dark px-4 py-2 text-white border border-studio-medium"
|
||||
disabled={creating}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Region
|
||||
</label>
|
||||
<select
|
||||
value={newEnvRegion}
|
||||
onChange={(e) => setNewEnvRegion(e.target.value)}
|
||||
className="w-full rounded bg-studio-dark px-4 py-2 text-white border border-studio-medium"
|
||||
disabled={creating}
|
||||
>
|
||||
<option value="us-east-1">US East (N. Virginia)</option>
|
||||
<option value="us-west-2">US West (Oregon)</option>
|
||||
<option value="eu-west-1">EU (Ireland)</option>
|
||||
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={!newEnvName.trim() || creating}
|
||||
>
|
||||
{creating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Environment'
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowCreate(false)}
|
||||
disabled={creating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{environments.length === 0 && !loading ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<Terminal className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
||||
<h3 className="mb-2 text-lg font-semibold text-white">No Environments</h3>
|
||||
<p className="mb-4 text-gray-400">
|
||||
Create a test environment to start developing
|
||||
</p>
|
||||
<Button variant="phoenix" onClick={() => setShowCreate(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Environment
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{environments.map((env) => (
|
||||
<Card key={env.id}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-sankofa-gold" />
|
||||
{env.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
{env.region} • Created {env.created}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`h-3 w-3 rounded-full ${getStatusColor(env.status)}`} />
|
||||
<Badge variant="outline">{env.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-400">Resources</h4>
|
||||
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-500">VMs</div>
|
||||
<div className="font-semibold text-white">{env.resources.vms}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Storage</div>
|
||||
<div className="font-semibold text-white">{env.resources.storage}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Network</div>
|
||||
<div className="font-semibold text-white">{env.resources.network}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{env.expires && (
|
||||
<div className="rounded bg-yellow-500/10 px-3 py-2 text-sm text-yellow-500">
|
||||
Expires: {env.expires}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
{env.status === 'STOPPED' ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleStart(env.id)}
|
||||
disabled={starting}
|
||||
>
|
||||
{starting ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Start
|
||||
</Button>
|
||||
) : env.status === 'RUNNING' ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleStop(env.id)}
|
||||
disabled={stopping}
|
||||
>
|
||||
{stopping ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Square className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Stop
|
||||
</Button>
|
||||
) : env.status === 'PROVISIONING' ? (
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Provisioning...
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(env.id)}
|
||||
disabled={deleting || env.status === 'PROVISIONING'}
|
||||
>
|
||||
{deleting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="mt-8 border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle>About Test Environments</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Environments are automatically cleaned up after 30 days</li>
|
||||
<li>• Free tier includes 2 environments with basic resources</li>
|
||||
<li>• All data is isolated and secure</li>
|
||||
<li>• Environments can be started/stopped on demand</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
359
src/app/portal/developers/keys/page.tsx
Normal file
359
src/app/portal/developers/keys/page.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Key, Plus, Trash2, Eye, EyeOff, Copy, CheckCircle, Loader2 } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
GET_API_KEYS,
|
||||
CREATE_API_KEY,
|
||||
REVOKE_API_KEY,
|
||||
} from '@/lib/graphql/queries/api-keys'
|
||||
|
||||
interface ApiKey {
|
||||
id: string
|
||||
name: string
|
||||
keyPrefix: string
|
||||
created: string
|
||||
lastUsed: string
|
||||
permissions: string[]
|
||||
masked: boolean
|
||||
fullKey?: string // Only available immediately after creation
|
||||
}
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
const [keys, setKeys] = useState<ApiKey[]>([])
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [newKeyName, setNewKeyName] = useState('')
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
const [newlyCreatedKey, setNewlyCreatedKey] = useState<string | null>(null)
|
||||
|
||||
// Fetch API keys
|
||||
const { data, loading, error, refetch } = useQuery(GET_API_KEYS, {
|
||||
onCompleted: (data) => {
|
||||
if (data?.apiKeys) {
|
||||
setKeys(
|
||||
data.apiKeys.map((key: any) => ({
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
keyPrefix: key.keyPrefix,
|
||||
key: `${key.keyPrefix}...`,
|
||||
created: new Date(key.createdAt).toLocaleDateString(),
|
||||
lastUsed: key.lastUsedAt
|
||||
? new Date(key.lastUsedAt).toLocaleDateString()
|
||||
: 'Never',
|
||||
permissions: key.permissions || [],
|
||||
masked: true,
|
||||
}))
|
||||
)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error fetching API keys:', error)
|
||||
},
|
||||
})
|
||||
|
||||
// Create API key mutation
|
||||
const [createApiKey, { loading: creating }] = useMutation(CREATE_API_KEY, {
|
||||
onCompleted: (data) => {
|
||||
if (data?.createApiKey) {
|
||||
const newKey = data.createApiKey
|
||||
setNewlyCreatedKey(newKey.key)
|
||||
setKeys([
|
||||
{
|
||||
id: newKey.id,
|
||||
name: newKey.name,
|
||||
keyPrefix: newKey.key.substring(0, 12),
|
||||
key: newKey.key,
|
||||
created: new Date(newKey.createdAt).toLocaleDateString(),
|
||||
lastUsed: 'Never',
|
||||
permissions: ['read', 'write'],
|
||||
masked: false,
|
||||
fullKey: newKey.key,
|
||||
},
|
||||
...keys,
|
||||
])
|
||||
setNewKeyName('')
|
||||
setShowCreate(false)
|
||||
// Refetch to get updated list
|
||||
refetch()
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error creating API key:', error)
|
||||
alert('Failed to create API key: ' + error.message)
|
||||
},
|
||||
})
|
||||
|
||||
// Revoke API key mutation
|
||||
const [revokeApiKey, { loading: revoking }] = useMutation(REVOKE_API_KEY, {
|
||||
onCompleted: () => {
|
||||
refetch()
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error revoking API key:', error)
|
||||
alert('Failed to revoke API key: ' + error.message)
|
||||
},
|
||||
})
|
||||
|
||||
const handleCreateKey = () => {
|
||||
if (!newKeyName.trim()) return
|
||||
|
||||
createApiKey({
|
||||
variables: {
|
||||
input: {
|
||||
name: newKeyName,
|
||||
permissions: ['read', 'write'],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteKey = (id: string) => {
|
||||
if (confirm('Are you sure you want to revoke this API key? It will no longer work.')) {
|
||||
revokeApiKey({
|
||||
variables: { id },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMask = (id: string) => {
|
||||
setKeys(keys.map((k) => (k.id === id ? { ...k, masked: !k.masked } : k)))
|
||||
}
|
||||
|
||||
const copyKey = (key: string, id: string) => {
|
||||
navigator.clipboard.writeText(key)
|
||||
setCopiedId(id)
|
||||
setTimeout(() => setCopiedId(null), 2000)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-phoenix-fire" />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
<div className="container mx-auto py-12 px-4">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-white">API Keys</h1>
|
||||
<p className="mt-2 text-gray-400">
|
||||
Manage API keys for your applications
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="phoenix"
|
||||
onClick={() => setShowCreate(!showCreate)}
|
||||
disabled={creating}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create API Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Card className="mb-8 border-red-500/50">
|
||||
<CardContent className="py-4">
|
||||
<p className="text-red-500">
|
||||
Error loading API keys: {error.message}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{newlyCreatedKey && (
|
||||
<Card className="mb-8 border-green-500/50 bg-green-500/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-green-500">API Key Created!</CardTitle>
|
||||
<CardDescription>
|
||||
Copy this key now. You won't be able to see it again.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white break-all">
|
||||
{newlyCreatedKey}
|
||||
</code>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(newlyCreatedKey)
|
||||
setNewlyCreatedKey(null)
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showCreate && (
|
||||
<Card className="mb-8 border-phoenix-fire/50">
|
||||
<CardHeader>
|
||||
<CardTitle>Create New API Key</CardTitle>
|
||||
<CardDescription>
|
||||
Give your API key a descriptive name to identify its purpose
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="key-name">Key Name</Label>
|
||||
<Input
|
||||
id="key-name"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
placeholder="e.g., Production App, Test Environment"
|
||||
className="mt-2"
|
||||
disabled={creating}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleCreateKey}
|
||||
disabled={!newKeyName.trim() || creating}
|
||||
>
|
||||
{creating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Key'
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowCreate(false)}
|
||||
disabled={creating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{keys.length === 0 && !loading ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<Key className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
||||
<h3 className="mb-2 text-lg font-semibold text-white">No API Keys</h3>
|
||||
<p className="mb-4 text-gray-400">
|
||||
Create your first API key to start using the Phoenix Nexus API
|
||||
</p>
|
||||
<Button variant="phoenix" onClick={() => setShowCreate(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create API Key
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{keys.map((key) => (
|
||||
<Card key={key.id}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Key className="h-5 w-5 text-phoenix-fire" />
|
||||
{key.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
Created {key.created} • Last used {key.lastUsed}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteKey(key.id)}
|
||||
disabled={revoking}
|
||||
>
|
||||
{revoking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded bg-studio-dark px-4 py-2 font-mono text-sm text-white">
|
||||
{key.masked && !key.fullKey ? key.key : key.fullKey || key.key}
|
||||
</code>
|
||||
{key.fullKey && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => toggleMask(key.id)}
|
||||
>
|
||||
{key.masked ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => copyKey(key.fullKey || key.key, key.id)}
|
||||
>
|
||||
{copiedId === key.id ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{key.permissions.map((perm) => (
|
||||
<span
|
||||
key={perm}
|
||||
className="rounded-full bg-phoenix-fire/20 px-3 py-1 text-xs text-phoenix-fire"
|
||||
>
|
||||
{perm}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="mt-8 border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle>Security Best Practices</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Never commit API keys to version control</li>
|
||||
<li>• Use environment variables to store keys</li>
|
||||
<li>• Rotate keys regularly</li>
|
||||
<li>• Delete unused keys immediately</li>
|
||||
<li>• Use different keys for different environments</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
183
src/app/portal/developers/page.tsx
Normal file
183
src/app/portal/developers/page.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Key, Terminal, FileText, Activity, Code, Settings } from 'lucide-react'
|
||||
|
||||
export default function DeveloperPortalPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Developer Portal
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Manage API keys, test environments, and developer resources
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/developers/keys">Get API Keys</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/developers/docs">View Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Developer Tools */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Developer Tools
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/developers/keys">
|
||||
<CardHeader>
|
||||
<Key className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>API Keys</CardTitle>
|
||||
<CardDescription>
|
||||
Create and manage API keys for your applications
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Generate keys, set permissions, and monitor usage
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/developers/environments">
|
||||
<CardHeader>
|
||||
<Terminal className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Test Environments</CardTitle>
|
||||
<CardDescription>
|
||||
Provision and manage sandbox environments
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Instant provisioning with automatic cleanup
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/developers/logs">
|
||||
<CardHeader>
|
||||
<FileText className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>API Logs</CardTitle>
|
||||
<CardDescription>
|
||||
View and analyze API request logs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Real-time log streaming and search
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/developers/analytics">
|
||||
<CardHeader>
|
||||
<Activity className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Usage Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
Monitor API usage and performance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Track requests, errors, and quotas
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/developers/docs">
|
||||
<CardHeader>
|
||||
<Code className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Documentation</CardTitle>
|
||||
<CardDescription>
|
||||
API references and integration guides
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Comprehensive guides and examples
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/developers/settings">
|
||||
<CardHeader>
|
||||
<Settings className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Configure developer account settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Manage preferences and notifications
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-8 text-3xl font-bold text-white">Quick Actions</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Get Started</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-gray-300">
|
||||
New to Phoenix Nexus? Create your first API key and start building.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/developers/docs/quickstart">Quick Start Guide →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Need Help?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Check the documentation or contact our developer support team.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/support">Contact Support →</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
139
src/app/portal/partners/page.tsx
Normal file
139
src/app/portal/partners/page.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Handshake, TrendingUp, FileText, Users, Award, Settings } from 'lucide-react'
|
||||
|
||||
export default function PartnerPortalPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Partner Portal
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Manage deals, solutions, and partner resources
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Partner Tools */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Partner Resources
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/partners/deals">
|
||||
<CardHeader>
|
||||
<TrendingUp className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Deal Registration</CardTitle>
|
||||
<CardDescription>
|
||||
Register and track co-sell opportunities
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Submit deals, track status, and manage opportunities
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/partners/solutions">
|
||||
<CardHeader>
|
||||
<FileText className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Solution Marketplace</CardTitle>
|
||||
<CardDescription>
|
||||
List and manage your solutions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Submit solutions for marketplace listing
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/partners/onboarding">
|
||||
<CardHeader>
|
||||
<Users className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Technical Onboarding</CardTitle>
|
||||
<CardDescription>
|
||||
Access onboarding resources and training
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Technical enablement and certification
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/partners/resources">
|
||||
<CardHeader>
|
||||
<Award className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Marketing Resources</CardTitle>
|
||||
<CardDescription>
|
||||
Co-marketing materials and assets
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Logos, templates, and campaign materials
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/partners/analytics">
|
||||
<CardHeader>
|
||||
<TrendingUp className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Partner Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
Track performance and revenue
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
View deal pipeline and performance metrics
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/portal/partners/settings">
|
||||
<CardHeader>
|
||||
<Settings className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Partner Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Manage partner account and preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400">
|
||||
Update profile and notification preferences
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
242
src/app/portal/signin/page.tsx
Normal file
242
src/app/portal/signin/page.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Key, ArrowRight, Loader2 } from 'lucide-react'
|
||||
import { initiateSSO, getPortalForRole, isAuthenticated } from '@/lib/auth/sso'
|
||||
|
||||
export default function SignInPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [ssoLoading, setSsoLoading] = useState(false)
|
||||
|
||||
// Check if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated()) {
|
||||
router.push('/portal')
|
||||
}
|
||||
}, [router])
|
||||
|
||||
// Handle Keycloak SSO callback
|
||||
useEffect(() => {
|
||||
const code = searchParams?.get('code')
|
||||
const state = searchParams?.get('state')
|
||||
|
||||
if (code && state) {
|
||||
// Handle SSO callback - would integrate with actual Keycloak
|
||||
// For now, redirect to portal
|
||||
router.push('/portal')
|
||||
}
|
||||
}, [searchParams, router])
|
||||
|
||||
const handleSignIn = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
// TODO: Integrate with actual authentication API
|
||||
// For now, use GraphQL login mutation
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation Login($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password) {
|
||||
token
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { email, password },
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.errors) {
|
||||
setError(result.errors[0].message || 'Invalid email or password')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (result.data?.login) {
|
||||
// Store token
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('auth_token', result.data.login.token)
|
||||
localStorage.setItem('phoenix_token', result.data.login.token)
|
||||
}
|
||||
|
||||
// Determine portal based on role
|
||||
const role = result.data.login.user.role.toLowerCase()
|
||||
const portal = getPortalForRole(role)
|
||||
router.push(portal)
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to sign in. Please try again.')
|
||||
console.error('Sign in error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeycloakSSO = async () => {
|
||||
setSsoLoading(true)
|
||||
try {
|
||||
const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://keycloak.sankofa.nexus'
|
||||
const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'sankofa'
|
||||
const clientId = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'phoenix-portal'
|
||||
const redirectUri = `${window.location.origin}/portal/signin`
|
||||
|
||||
const ssoUrl = await initiateSSO({
|
||||
keycloakUrl,
|
||||
realm,
|
||||
clientId,
|
||||
redirectUri,
|
||||
})
|
||||
|
||||
window.location.href = ssoUrl
|
||||
} catch (err) {
|
||||
setError('Failed to initiate SSO. Please try again.')
|
||||
console.error('SSO error:', err)
|
||||
setSsoLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isKeycloakConfigured =
|
||||
typeof window !== 'undefined' &&
|
||||
(process.env.NEXT_PUBLIC_KEYCLOAK_URL || process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black flex items-center justify-center px-4">
|
||||
<Card className="w-full max-w-md border-studio-medium">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-phoenix-fire/20">
|
||||
<Key className="h-6 w-6 text-phoenix-fire" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Sign In</CardTitle>
|
||||
<CardDescription>
|
||||
Access your Phoenix Nexus portals
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSignIn} className="space-y-4">
|
||||
{error && (
|
||||
<div className="rounded bg-red-500/10 px-4 py-3 text-sm text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link
|
||||
href="/portal/forgot-password"
|
||||
className="text-sm text-phoenix-fire hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="phoenix"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Sign In
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-gray-400">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/portal/get-started" className="text-phoenix-fire hover:underline">
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{isKeycloakConfigured && (
|
||||
<div className="mt-6 border-t border-studio-medium pt-6">
|
||||
<p className="mb-4 text-center text-sm text-gray-400">
|
||||
Or continue with
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={handleKeycloakSSO}
|
||||
disabled={ssoLoading || loading}
|
||||
>
|
||||
{ssoLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
'Keycloak SSO'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
254
src/app/products/[slug]/page.tsx
Normal file
254
src/app/products/[slug]/page.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ArrowLeft, Check } from 'lucide-react'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
// Product catalog with details
|
||||
const products = {
|
||||
'phoenixcore-compute': {
|
||||
name: 'PhoenixCore Compute',
|
||||
category: 'Compute',
|
||||
description: 'Core compute engine powered by Phoenix fire',
|
||||
longDescription: 'PhoenixCore Compute delivers high-performance virtual machines and container orchestration with enterprise-grade reliability. Built on our global edge infrastructure, it provides low-latency compute resources across 325+ regions worldwide.',
|
||||
features: [
|
||||
'High-performance CPUs (AMD EPYC / Intel Xeon)',
|
||||
'NVMe storage for ultra-fast I/O',
|
||||
'Global edge deployment',
|
||||
'Auto-scaling capabilities',
|
||||
'99.99% uptime SLA',
|
||||
'Blockchain-verified resource tracking',
|
||||
],
|
||||
pricing: {
|
||||
base: 0.05, // per vCPU hour
|
||||
memory: 0.01, // per GB hour
|
||||
storage: 0.001, // per GB hour
|
||||
currency: 'USD',
|
||||
},
|
||||
tiers: [
|
||||
{ name: 'Starter', vcpu: 2, memory: 4, price: 50 },
|
||||
{ name: 'Professional', vcpu: 8, memory: 16, price: 200 },
|
||||
{ name: 'Enterprise', vcpu: 32, memory: 64, price: 800 },
|
||||
],
|
||||
},
|
||||
'sankofaedge-nodes': {
|
||||
name: 'SankofaEdge Nodes',
|
||||
category: 'Compute',
|
||||
description: 'Edge nodes that remember and return data',
|
||||
longDescription: 'SankofaEdge Nodes provide low-latency compute at the network edge, bringing your applications closer to users worldwide. With 250+ edge locations, reduce latency and improve user experience.',
|
||||
features: [
|
||||
'250+ global edge locations',
|
||||
'Sub-10ms latency',
|
||||
'CDN integration',
|
||||
'Edge caching',
|
||||
'Regional data residency',
|
||||
],
|
||||
pricing: {
|
||||
base: 0.03,
|
||||
currency: 'USD',
|
||||
},
|
||||
},
|
||||
'okravault-storage': {
|
||||
name: 'OkraVault Storage',
|
||||
category: 'Storage',
|
||||
description: 'Storage for the soul of your data',
|
||||
longDescription: 'OkraVault provides secure, scalable object and block storage with enterprise-grade durability. Your data is protected with encryption at rest and in transit, with blockchain-verified integrity.',
|
||||
features: [
|
||||
'S3-compatible API',
|
||||
'99.999999999% durability',
|
||||
'Encryption at rest and in transit',
|
||||
'Multi-region replication',
|
||||
'Versioning and lifecycle policies',
|
||||
],
|
||||
pricing: {
|
||||
storage: 0.023, // per GB/month
|
||||
egress: 0.09, // per GB
|
||||
currency: 'USD',
|
||||
},
|
||||
},
|
||||
'firebird-ai-engine': {
|
||||
name: 'Firebird AI Engine',
|
||||
category: 'AI & Machine Learning',
|
||||
description: 'AI that transforms like fire',
|
||||
longDescription: 'Firebird AI Engine provides scalable machine learning infrastructure with GPU acceleration, model training, and inference capabilities. Built for enterprise AI workloads.',
|
||||
features: [
|
||||
'GPU acceleration (NVIDIA A100/H100)',
|
||||
'Distributed training',
|
||||
'Model serving',
|
||||
'Auto-scaling inference',
|
||||
'MLOps integration',
|
||||
],
|
||||
pricing: {
|
||||
training: 2.50, // per GPU hour
|
||||
inference: 0.50, // per GPU hour
|
||||
currency: 'USD',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(products).map((slug) => ({ slug }))
|
||||
}
|
||||
|
||||
export default function ProductDetailPage({ params }: { params: { slug: string } }) {
|
||||
const product = products[params.slug as keyof typeof products]
|
||||
|
||||
if (!product) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Header */}
|
||||
<header className="border-b border-studio-medium bg-studio-dark">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
|
||||
<Link href="/" className="text-2xl font-bold text-white">
|
||||
<span className="text-sankofa-gold">Sankofa's</span> Phoenix <span className="text-phoenix-fire">Nexus</span> Cloud
|
||||
</Link>
|
||||
<nav className="flex gap-4">
|
||||
<Link href="/" className="text-gray-400 hover:text-white">
|
||||
Home
|
||||
</Link>
|
||||
<Link href="/products" className="text-gray-400 hover:text-white">
|
||||
Products
|
||||
</Link>
|
||||
<Link href="/about" className="text-gray-400 hover:text-white">
|
||||
About
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<div className="mx-auto max-w-7xl px-4 py-4">
|
||||
<Link
|
||||
href="/products"
|
||||
className="inline-flex items-center gap-2 text-gray-400 hover:text-white"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Products
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Product Detail */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="grid gap-8 lg:grid-cols-2">
|
||||
{/* Product Info */}
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<span className="rounded-full bg-phoenix-fire/20 px-3 py-1 text-sm text-phoenix-fire">
|
||||
{product.category}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="mb-4 text-4xl font-bold text-white">{product.name}</h1>
|
||||
<p className="mb-6 text-xl text-gray-300">{product.description}</p>
|
||||
<p className="mb-8 text-gray-400">{product.longDescription}</p>
|
||||
|
||||
{/* Features */}
|
||||
<div className="mb-8">
|
||||
<h2 className="mb-4 text-2xl font-bold text-white">Features</h2>
|
||||
<ul className="space-y-2">
|
||||
{product.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start gap-2 text-gray-300">
|
||||
<Check className="mt-1 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Pricing */}
|
||||
{product.pricing && (
|
||||
<div className="mb-8">
|
||||
<h2 className="mb-4 text-2xl font-bold text-white">Pricing</h2>
|
||||
<Card className="border-studio-medium bg-studio-dark">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Pay-as-you-go</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{product.pricing.base && (
|
||||
<p className="text-gray-300">
|
||||
Base: ${product.pricing.base.toFixed(3)} per hour
|
||||
</p>
|
||||
)}
|
||||
{product.pricing.storage && (
|
||||
<p className="text-gray-300">
|
||||
Storage: ${product.pricing.storage.toFixed(3)} per GB/month
|
||||
</p>
|
||||
)}
|
||||
{product.pricing.training && (
|
||||
<p className="text-gray-300">
|
||||
Training: ${product.pricing.training.toFixed(2)} per GPU hour
|
||||
</p>
|
||||
)}
|
||||
{product.pricing.inference && (
|
||||
<p className="text-gray-300">
|
||||
Inference: ${product.pricing.inference.toFixed(2)} per GPU hour
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tiers */}
|
||||
{product.tiers && (
|
||||
<div className="mb-8">
|
||||
<h2 className="mb-4 text-2xl font-bold text-white">Pricing Tiers</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{product.tiers.map((tier, index) => (
|
||||
<Card key={index} className="border-studio-medium bg-studio-dark">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">{tier.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-2 text-3xl font-bold text-phoenix-fire">
|
||||
${tier.price}
|
||||
<span className="text-sm text-gray-400">/month</span>
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{tier.vcpu} vCPU, {tier.memory}GB RAM
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<div className="flex gap-4">
|
||||
<Button variant="phoenix" size="lg">
|
||||
Get Started
|
||||
</Button>
|
||||
<Button variant="outline" size="lg">
|
||||
Contact Sales
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div>
|
||||
<Card className="border-studio-medium bg-studio-dark">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Ready to Start?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-gray-400">
|
||||
Start building on Sankofa Phoenix today. No credit card required for free tier.
|
||||
</p>
|
||||
<Button variant="phoenix" className="w-full" size="lg">
|
||||
Create Account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" size="lg">
|
||||
Schedule Demo
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
111
src/app/products/ai-ml/page.tsx
Normal file
111
src/app/products/ai-ml/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Sparkles, Brain, Network } from 'lucide-react'
|
||||
|
||||
export default function AIMLPage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'Firebird AI Engine',
|
||||
icon: Sparkles,
|
||||
description: 'AI that transforms like fire',
|
||||
features: [
|
||||
'Model inference',
|
||||
'Real-time AI',
|
||||
'Edge AI deployment',
|
||||
'GPU acceleration',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Sankofa Memory Model',
|
||||
icon: Brain,
|
||||
description: 'AI models that remember and learn',
|
||||
features: [
|
||||
'Recursive learning',
|
||||
'Memory-augmented AI',
|
||||
'Context-aware models',
|
||||
'Continuous improvement',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ancestral Neural Fabric',
|
||||
icon: Network,
|
||||
description: 'Distributed AI with ancestral patterns',
|
||||
features: [
|
||||
'Distributed training',
|
||||
'Federated learning',
|
||||
'Knowledge graphs',
|
||||
'Multi-region deployment',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
AI & Machine Learning
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
AI services powered by ancestral wisdom and modern technology
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
126
src/app/products/core-infrastructure/page.tsx
Normal file
126
src/app/products/core-infrastructure/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Server, HardDrive, Network } from 'lucide-react'
|
||||
|
||||
export default function CoreInfrastructurePage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'PhoenixCore Compute',
|
||||
icon: Server,
|
||||
description: 'Core compute engine powered by Phoenix fire',
|
||||
features: [
|
||||
'Virtual machines and containers',
|
||||
'Serverless functions',
|
||||
'Auto-scaling capabilities',
|
||||
'Multi-region deployment',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'OkraVault Storage',
|
||||
icon: HardDrive,
|
||||
description: 'Storage for the soul of your data',
|
||||
features: [
|
||||
'Object and block storage',
|
||||
'High-performance NVMe storage',
|
||||
'Multi-region replication',
|
||||
'S3-compatible API',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'SankofaGrid Global Mesh',
|
||||
icon: Network,
|
||||
description: 'Global network mesh that remembers',
|
||||
features: [
|
||||
'325-region global network',
|
||||
'Low-latency routing',
|
||||
'Private network connections',
|
||||
'DDoS protection',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Core Infrastructure
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
The foundation of sovereign cloud infrastructure
|
||||
</p>
|
||||
|
||||
<p className="mb-12 text-lg text-gray-400">
|
||||
Compute, storage, and networking services that power the Phoenix Nexus Cloud
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Products Grid */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Build on sovereign cloud infrastructure
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/docs">View Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
111
src/app/products/data/page.tsx
Normal file
111
src/app/products/data/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Database, Layers, Clock } from 'lucide-react'
|
||||
|
||||
export default function DataServicesPage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'SankofaGraph Database',
|
||||
icon: Database,
|
||||
description: 'Graph database that remembers relationships',
|
||||
features: [
|
||||
'Knowledge graphs',
|
||||
'Relationship mapping',
|
||||
'Network analysis',
|
||||
'GraphQL interface',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'PhoenixFire NoSQL',
|
||||
icon: Layers,
|
||||
description: 'Fast, powerful NoSQL database',
|
||||
features: [
|
||||
'Document stores',
|
||||
'Key-value stores',
|
||||
'High-performance queries',
|
||||
'Auto-scaling',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Nananom Time Series',
|
||||
icon: Clock,
|
||||
description: 'Time-series database for historical data',
|
||||
features: [
|
||||
'Metrics storage',
|
||||
'Monitoring data',
|
||||
'Historical analysis',
|
||||
'High-throughput ingestion',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Data Services
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Databases and data services for modern applications
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
100
src/app/products/developer/page.tsx
Normal file
100
src/app/products/developer/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Code, Hammer } from 'lucide-react'
|
||||
|
||||
export default function DeveloperServicesPage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'SankofaDev Platform',
|
||||
icon: Code,
|
||||
description: 'Development that remembers best practices',
|
||||
features: [
|
||||
'CI/CD pipelines',
|
||||
'Developer tools',
|
||||
'Platform services',
|
||||
'GitOps workflows',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'PhoenixForge Build Engine',
|
||||
icon: Hammer,
|
||||
description: 'Build and deployment services',
|
||||
features: [
|
||||
'Build systems',
|
||||
'Deployment automation',
|
||||
'Container builds',
|
||||
'Artifact management',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Developer Services
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Tools and services for modern development workflows
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
100
src/app/products/identity/page.tsx
Normal file
100
src/app/products/identity/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { User, Key } from 'lucide-react'
|
||||
|
||||
export default function IdentityPage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'OkraID',
|
||||
icon: User,
|
||||
description: 'Soul-powered identity framework',
|
||||
features: [
|
||||
'Self-sovereign identity',
|
||||
'Decentralized identity',
|
||||
'User authentication',
|
||||
'Identity verification',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'AkanAuth Sovereign Identity Plane',
|
||||
icon: Key,
|
||||
description: 'Sovereign authentication platform',
|
||||
features: [
|
||||
'Multi-factor authentication',
|
||||
'Single sign-on (SSO)',
|
||||
'Identity federation',
|
||||
'Sovereign identity management',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Identity Services
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Sovereign identity management for the modern web
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
100
src/app/products/management/page.tsx
Normal file
100
src/app/products/management/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Settings, Globe2 } from 'lucide-react'
|
||||
|
||||
export default function ManagementPage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'SankofaControl Plane',
|
||||
icon: Settings,
|
||||
description: 'Control that remembers and learns',
|
||||
features: [
|
||||
'Infrastructure as Code',
|
||||
'Orchestration',
|
||||
'Resource management',
|
||||
'Policy enforcement',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'PhoenixRealm Management',
|
||||
icon: Globe2,
|
||||
description: 'Multi-cloud and multi-region management',
|
||||
features: [
|
||||
'Multi-cloud management',
|
||||
'Global orchestration',
|
||||
'Region management',
|
||||
'Unified control plane',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Management Services
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Infrastructure management and orchestration for global deployments
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
111
src/app/products/networking/page.tsx
Normal file
111
src/app/products/networking/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Network, Globe, Zap } from 'lucide-react'
|
||||
|
||||
export default function NetworkingPage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'SankofaGrid Global Mesh',
|
||||
icon: Network,
|
||||
description: 'Global network mesh that remembers',
|
||||
features: [
|
||||
'325-region global network',
|
||||
'Low-latency routing',
|
||||
'Private network connections',
|
||||
'Multi-region connectivity',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'AkanSphere Edge Routing',
|
||||
icon: Globe,
|
||||
description: 'Edge routing with cultural intelligence',
|
||||
features: [
|
||||
'Edge routing',
|
||||
'Traffic management',
|
||||
'CDN integration',
|
||||
'Geographic routing',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'PhoenixFlight Network Fabric',
|
||||
icon: Zap,
|
||||
description: 'High-performance network fabric',
|
||||
features: [
|
||||
'High-bandwidth connections',
|
||||
'Data center networking',
|
||||
'Low-latency fabric',
|
||||
'Network automation',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Networking Services
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Global networking infrastructure for sovereign cloud
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function ProductsPage() {
|
||||
<header className="border-b border-studio-medium bg-studio-dark">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
|
||||
<Link href="/" className="text-2xl font-bold text-white">
|
||||
Phoenix <span className="text-phoenix-fire">Sankofa</span> Cloud
|
||||
<span className="text-sankofa-gold">Sankofa's</span> Phoenix <span className="text-phoenix-fire">Nexus</span> Cloud
|
||||
</Link>
|
||||
<nav className="flex gap-4">
|
||||
<Link href="/" className="text-gray-400 hover:text-white">
|
||||
@@ -96,18 +96,26 @@ export default function ProductsPage() {
|
||||
{category.category}
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{category.items.map((item) => (
|
||||
<Card key={item.name}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{item.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-gray-400">
|
||||
{item.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{category.items.map((item) => {
|
||||
const slug = item.name.toLowerCase().replace(/\s+/g, '-')
|
||||
return (
|
||||
<Card key={item.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{item.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-gray-400 mb-4">
|
||||
{item.description}
|
||||
</CardDescription>
|
||||
<Link href={`/products/${slug}`}>
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
Learn More
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
111
src/app/products/security/page.tsx
Normal file
111
src/app/products/security/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Shield, Lock, Eye } from 'lucide-react'
|
||||
|
||||
export default function SecurityPage() {
|
||||
const products = [
|
||||
{
|
||||
name: 'Aegis of Akan Shield',
|
||||
icon: Shield,
|
||||
description: 'Comprehensive security platform',
|
||||
features: [
|
||||
'Threat protection',
|
||||
'DDoS mitigation',
|
||||
'Security monitoring',
|
||||
'Zero Trust networking',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'PhoenixGuard IAM',
|
||||
icon: Lock,
|
||||
description: 'Identity and access management',
|
||||
features: [
|
||||
'Multi-factor authentication',
|
||||
'Single sign-on (SSO)',
|
||||
'Role-based access control',
|
||||
'Identity federation',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Nsamankom Sentinel',
|
||||
icon: Eye,
|
||||
description: 'Security monitoring with ancestral protection',
|
||||
features: [
|
||||
'Security monitoring',
|
||||
'Threat detection',
|
||||
'Compliance reporting',
|
||||
'Incident response',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black">
|
||||
<section className="relative flex min-h-[50vh] flex-col items-center justify-center overflow-hidden px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
|
||||
Security Services
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
|
||||
Enterprise-grade security for sovereign cloud infrastructure
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{products.map((product) => {
|
||||
const Icon = product.icon
|
||||
return (
|
||||
<Card key={product.name} className="hover:border-phoenix-fire transition-colors">
|
||||
<CardHeader>
|
||||
<Icon className="mb-4 h-12 w-12 text-phoenix-fire" />
|
||||
<CardTitle className="text-xl">{product.name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
{product.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300 mb-6">
|
||||
{product.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-phoenix-fire mt-1">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link href={`/products/${product.name.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 bg-studio-dark">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { ApolloProvider } from '@apollo/client'
|
||||
import { apolloClient } from '@/lib/graphql/client'
|
||||
import { getApolloClient } from '@/lib/graphql/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
|
||||
@@ -33,6 +33,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||
})
|
||||
)
|
||||
|
||||
// Create Apollo Client only on client side
|
||||
const apolloClient = useMemo(() => getApolloClient(), [])
|
||||
|
||||
return (
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
183
src/app/solutions/enterprise/page.tsx
Normal file
183
src/app/solutions/enterprise/page.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Check, Shield, Globe, Lock, Zap, Users } from 'lucide-react'
|
||||
|
||||
export default function EnterprisePage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Enterprise Cloud Infrastructure
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Sovereign cloud infrastructure designed for enterprise scale, security, and compliance
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/get-started">Start Free Trial</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/company/trust">View Compliance</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Trust & Compliance */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Enterprise-Grade Trust & Compliance
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Security First</CardTitle>
|
||||
<CardDescription>
|
||||
DoD/MilSpec compliance, zero-trust architecture, and sovereign identity management
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>STIG-compliant infrastructure</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Keycloak-based sovereign identity (no Azure dependencies)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>End-to-end encryption</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Lock className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Compliance Ready</CardTitle>
|
||||
<CardDescription>
|
||||
Built for regulated industries and government requirements
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>RMF authorization ready</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Audit logging and compliance reporting</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Data sovereignty controls</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Globe className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Global Infrastructure</CardTitle>
|
||||
<CardDescription>
|
||||
325-region deployment with edge computing and regional data residency
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Multi-region redundancy</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Edge computing for low latency</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Data residency controls</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Enterprise Features */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Enterprise Capabilities
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<Zap className="mb-4 h-8 w-8 text-phoenix-fire" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Advanced Multi-Tenancy</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Superior tenant isolation, per-second billing granularity, and comprehensive quota management.
|
||||
Built for enterprises managing multiple business units or customer tenants.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Tenant-level resource isolation</li>
|
||||
<li>• Per-second billing with cost forecasting</li>
|
||||
<li>• Budget management and alerts</li>
|
||||
<li>• Custom quota enforcement</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Users className="mb-4 h-8 w-8 text-sankofa-gold" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Role-Based Access Control</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Enterprise-grade RBAC with fine-grained permissions, audit trails, and integration
|
||||
with your existing identity providers.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Custom roles and permissions</li>
|
||||
<li>• SSO integration (SAML, OIDC)</li>
|
||||
<li>• Audit logging for all actions</li>
|
||||
<li>• Multi-factor authentication</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Transform Your Infrastructure?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Join enterprises leveraging sovereign cloud infrastructure
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/get-started">Contact Sales</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/developers">View Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
169
src/app/solutions/government/page.tsx
Normal file
169
src/app/solutions/government/page.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Shield, Lock, FileCheck, Globe, CheckCircle, Building2 } from 'lucide-react'
|
||||
|
||||
export default function GovernmentPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Government Cloud Infrastructure
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Sovereign cloud infrastructure designed for government agencies and public sector organizations
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/get-started">Request Demo</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/company/trust">View Compliance</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Government Features */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Built for Government Requirements
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>DoD/MilSpec Compliance</CardTitle>
|
||||
<CardDescription>
|
||||
STIG-compliant infrastructure meeting Department of Defense standards
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Security Technical Implementation Guides (STIG)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>RMF authorization ready</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>FedRAMP alignment (in progress)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Lock className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Data Sovereignty</CardTitle>
|
||||
<CardDescription>
|
||||
Complete control over data location and access
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>On-premises deployment options</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Air-gapped configurations</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Regional data residency controls</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<FileCheck className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Audit & Compliance</CardTitle>
|
||||
<CardDescription>
|
||||
Comprehensive audit logging and compliance reporting
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Immutable audit logs</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Compliance dashboards</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Automated compliance reporting</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Building2 className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Government Partnerships</CardTitle>
|
||||
<CardDescription>
|
||||
Dedicated support for government agencies
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Dedicated government support team</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>GSA schedule (in progress)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Custom contract vehicles</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Transform Government IT?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Contact our government team to learn more
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/support">Contact Government Team</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/developers">View Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
183
src/app/solutions/institutional/page.tsx
Normal file
183
src/app/solutions/institutional/page.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { GraduationCap, Users, Shield, BookOpen, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function InstitutionalPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Institutional Cloud Infrastructure
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Sovereign cloud infrastructure for educational institutions, research organizations, and non-profits
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/portal/get-started">Get Started</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/solutions/institutional/pricing">View Pricing</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Institutional Features */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Built for Institutions
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<GraduationCap className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Education-Focused</CardTitle>
|
||||
<CardDescription>
|
||||
Designed for universities, colleges, and research institutions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Student and faculty management</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Research computing resources</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-phoenix-fire" />
|
||||
<span>Academic pricing and discounts</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Users className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Multi-Tenant Support</CardTitle>
|
||||
<CardDescription>
|
||||
Manage multiple departments, schools, or research groups
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Department-level isolation</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Budget allocation and tracking</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-sankofa-gold" />
|
||||
<span>Usage reporting by department</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Privacy & Security</CardTitle>
|
||||
<CardDescription>
|
||||
FERPA and HIPAA considerations for educational data
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Data privacy controls</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Secure research environments</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-neon-cyan" />
|
||||
<span>Compliance with educational regulations</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Use Cases */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Institutional Use Cases
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<BookOpen className="mb-4 h-8 w-8 text-phoenix-fire" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Research Computing</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
High-performance computing resources for research projects,
|
||||
data analysis, and computational research.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• GPU clusters for ML/AI research</li>
|
||||
<li>• Large-scale data processing</li>
|
||||
<li>• Collaborative research environments</li>
|
||||
<li>• Research data management</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Users className="mb-4 h-8 w-8 text-sankofa-gold" />
|
||||
<h3 className="mb-4 text-2xl font-bold text-white">Campus IT Infrastructure</h3>
|
||||
<p className="mb-4 text-gray-300">
|
||||
Modern cloud infrastructure for campus-wide IT services,
|
||||
student portals, and administrative systems.
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li>• Student information systems</li>
|
||||
<li>• Learning management systems</li>
|
||||
<li>• Campus-wide applications</li>
|
||||
<li>• Administrative computing</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Ready to Modernize Your Institution?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Contact our institutional team to learn more
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/support">Contact Institutional Team</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/developers">View Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
179
src/app/support/page.tsx
Normal file
179
src/app/support/page.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { HelpCircle, Book, MessageSquare, FileText, Search, Headphones } from 'lucide-react'
|
||||
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-studio-black">
|
||||
{/* Hero */}
|
||||
<section className="relative border-b border-studio-medium bg-gradient-to-br from-phoenix-fire/10 via-transparent to-sankofa-gold/10 py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
|
||||
Support Center
|
||||
</h1>
|
||||
<p className="mb-8 text-xl text-gray-300">
|
||||
Get help when you need it
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Support Options */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
How Can We Help?
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/developers/docs">
|
||||
<CardHeader>
|
||||
<Book className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>Documentation</CardTitle>
|
||||
<CardDescription>
|
||||
Comprehensive guides, API references, and tutorials
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/support/kb">
|
||||
<CardHeader>
|
||||
<FileText className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Knowledge Base</CardTitle>
|
||||
<CardDescription>
|
||||
Search articles, FAQs, and troubleshooting guides
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/support/contact">
|
||||
<CardHeader>
|
||||
<MessageSquare className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Contact Support</CardTitle>
|
||||
<CardDescription>
|
||||
Get in touch with our support team
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/support/status">
|
||||
<CardHeader>
|
||||
<Search className="mb-2 h-10 w-10 text-phoenix-fire" />
|
||||
<CardTitle>System Status</CardTitle>
|
||||
<CardDescription>
|
||||
Check service status and incident reports
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/support/community">
|
||||
<CardHeader>
|
||||
<Headphones className="mb-2 h-10 w-10 text-sankofa-gold" />
|
||||
<CardTitle>Community</CardTitle>
|
||||
<CardDescription>
|
||||
Connect with other users and experts
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:border-phoenix-fire transition-colors cursor-pointer" asChild>
|
||||
<Link href="/support/training">
|
||||
<CardHeader>
|
||||
<HelpCircle className="mb-2 h-10 w-10 text-neon-cyan" />
|
||||
<CardTitle>Training</CardTitle>
|
||||
<CardDescription>
|
||||
Learn through courses and workshops
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Support Tiers */}
|
||||
<section className="border-t border-studio-medium bg-studio-dark py-24 px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-12 text-center text-4xl font-bold text-white">
|
||||
Support Plans
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Community Support</CardTitle>
|
||||
<CardDescription>Free</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Community forums</li>
|
||||
<li>• Documentation access</li>
|
||||
<li>• Knowledge base</li>
|
||||
<li>• Best effort responses</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-phoenix-fire">
|
||||
<CardHeader>
|
||||
<CardTitle>Standard Support</CardTitle>
|
||||
<CardDescription>Included with subscription</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• Email support (24-48h response)</li>
|
||||
<li>• Priority documentation access</li>
|
||||
<li>• System status notifications</li>
|
||||
<li>• Business hours support</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Enterprise Support</CardTitle>
|
||||
<CardDescription>Premium</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li>• 24/7 phone and email support</li>
|
||||
<li>• Dedicated support engineer</li>
|
||||
<li>• SLA guarantees</li>
|
||||
<li>• On-site support available</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-bold text-white">
|
||||
Need Immediate Help?
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-gray-400">
|
||||
Contact our support team
|
||||
</p>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<Link href="/support/contact">Contact Support</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
107
src/app/white-label/page.tsx
Normal file
107
src/app/white-label/page.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { PublicLayout } from '@/components/layout/public-layout'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Palette, Globe, Shield, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default function WhiteLabelPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-studio-black py-24 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="mb-4 text-5xl font-bold text-white">White-Label Solution</h1>
|
||||
<p className="text-xl text-gray-400">
|
||||
Customize Phoenix with your brand for enterprise deployments
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Palette className="h-8 w-8 text-phoenix-fire mb-2" />
|
||||
<CardTitle className="text-white">Custom Branding</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Custom logo and colors</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Custom domain</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Branded email templates</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Globe className="h-8 w-8 text-sankofa-gold mb-2" />
|
||||
<CardTitle className="text-white">Multi-Tenant</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Isolated tenant environments</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Custom subdomains</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Tenant-specific branding</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="h-8 w-8 text-neon-cyan mb-2" />
|
||||
<CardTitle className="text-white">Enterprise Features</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>SLA guarantees</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Dedicated support</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span>Custom integrations</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-studio-dark border-studio-medium">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Get Started with White-Label</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-300 mb-6">
|
||||
Contact our enterprise sales team to discuss white-label options for your organization.
|
||||
</p>
|
||||
<Button variant="phoenix" size="lg" asChild>
|
||||
<a href="/enterprise">Contact Sales</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
116
src/components/__tests__/Dashboard.test.tsx
Normal file
116
src/components/__tests__/Dashboard.test.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Dashboard Component Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { MockedProvider } from '@apollo/client/testing'
|
||||
import Dashboard from '../dashboards/Dashboard'
|
||||
import { GET_REGIONS } from '@/lib/graphql/queries/resources'
|
||||
import { GET_RESOURCES } from '@/lib/graphql/queries'
|
||||
import { GET_METRICS } from '@/lib/graphql/queries/metrics'
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: GET_REGIONS,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
regions: [
|
||||
{
|
||||
id: 'region-1',
|
||||
name: 'US East',
|
||||
code: 'us-east-1',
|
||||
country: 'USA',
|
||||
coordinates: { latitude: 39.8283, longitude: -98.5795 },
|
||||
sites: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GET_RESOURCES,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
resources: [
|
||||
{
|
||||
id: 'resource-1',
|
||||
name: 'test-resource',
|
||||
type: 'VM',
|
||||
status: 'RUNNING',
|
||||
site: { id: 'site-1', name: 'Test Site', region: 'us-east-1' },
|
||||
metadata: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GET_METRICS,
|
||||
variables: {
|
||||
resourceId: 'resource-1',
|
||||
metricType: 'CPU_USAGE',
|
||||
timeRange: {
|
||||
start: expect.any(String),
|
||||
end: expect.any(String),
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
metrics: {
|
||||
resource: { id: 'resource-1', name: 'test-resource' },
|
||||
metricType: 'CPU_USAGE',
|
||||
values: [
|
||||
{ timestamp: new Date().toISOString(), value: 50, labels: {} },
|
||||
{ timestamp: new Date().toISOString(), value: 55, labels: {} },
|
||||
],
|
||||
timeRange: {
|
||||
start: new Date().toISOString(),
|
||||
end: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe('Dashboard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render dashboard with metrics', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<Dashboard />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Dashboard')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Check for metric cards
|
||||
expect(screen.getByText('Total Regions')).toBeInTheDocument()
|
||||
expect(screen.getByText('Active Services')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display loading state', () => {
|
||||
render(
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<Dashboard />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
88
src/components/__tests__/ResourceList.test.tsx
Normal file
88
src/components/__tests__/ResourceList.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* ResourceList Component Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ResourceList } from '../resources/ResourceList'
|
||||
|
||||
// Mock Apollo Client
|
||||
vi.mock('@/lib/graphql/client', () => ({
|
||||
apolloClient: {
|
||||
query: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('ResourceList', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should render loading state', () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ResourceList />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByText(/loading resources/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render resources when loaded', async () => {
|
||||
const mockResources = [
|
||||
{
|
||||
id: 'resource-1',
|
||||
name: 'Test Resource',
|
||||
type: 'VM',
|
||||
status: 'RUNNING',
|
||||
site: {
|
||||
id: 'site-1',
|
||||
name: 'Test Site',
|
||||
region: 'us-east-1',
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
|
||||
const { apolloClient } = await import('@/lib/graphql/client')
|
||||
vi.mocked(apolloClient.query).mockResolvedValueOnce({
|
||||
data: { resources: mockResources },
|
||||
} as any)
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ResourceList />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Resource')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render error state', async () => {
|
||||
const { apolloClient } = await import('@/lib/graphql/client')
|
||||
vi.mocked(apolloClient.query).mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ResourceList />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/error/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
98
src/components/__tests__/WAFDashboard.test.tsx
Normal file
98
src/components/__tests__/WAFDashboard.test.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* WAF Dashboard Component Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { MockedProvider } from '@apollo/client/testing'
|
||||
import WAFDashboard from '../well-architected/WAFDashboard'
|
||||
import { GET_PILLARS, GET_FINDINGS } from '@/lib/graphql/queries/well-architected'
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: GET_PILLARS,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
pillars: [
|
||||
{
|
||||
id: 'pillar-1',
|
||||
code: 'SECURITY',
|
||||
name: 'Security',
|
||||
description: 'Security pillar',
|
||||
controls: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GET_FINDINGS,
|
||||
variables: {
|
||||
filter: undefined,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
findings: [
|
||||
{
|
||||
id: 'finding-1',
|
||||
control: {
|
||||
id: 'control-1',
|
||||
code: 'SECURITY-001',
|
||||
name: 'Encryption',
|
||||
pillar: { code: 'SECURITY', name: 'Security' },
|
||||
},
|
||||
resource: {
|
||||
id: 'resource-1',
|
||||
name: 'test-resource',
|
||||
type: 'VM',
|
||||
},
|
||||
status: 'PASS',
|
||||
severity: 'LOW',
|
||||
title: 'Encryption enabled',
|
||||
description: 'Resource has encryption at rest enabled',
|
||||
recommendation: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe('WAFDashboard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render WAF dashboard with pillars', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<WAFDashboard />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Well-Architected Framework')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.getByText('Findings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should filter findings by selected lens', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<WAFDashboard />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Well-Architected Framework')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
102
src/components/auth/LoginForm.tsx
Normal file
102
src/components/auth/LoginForm.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { gql } from 'graphql-tag'
|
||||
import { apolloClient } from '@/lib/graphql/client'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
const LOGIN_MUTATION = gql`
|
||||
mutation Login($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password) {
|
||||
token
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function LoginForm() {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: async (variables: { email: string; password: string }) => {
|
||||
const result = await apolloClient.mutate({
|
||||
mutation: LOGIN_MUTATION,
|
||||
variables,
|
||||
})
|
||||
return result.data.login
|
||||
},
|
||||
onSuccess: async (data) => {
|
||||
// Store token in httpOnly cookie
|
||||
const { setAuthToken } = await import('@/lib/auth-storage')
|
||||
await setAuthToken(data.token)
|
||||
router.push('/dashboard')
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
loginMutation.mutate({ email, password })
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md mx-auto border-studio-medium bg-studio-dark">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl text-white text-center">
|
||||
Sign In to Sankofa Phoenix
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="admin@sankofa.nexus"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{loginMutation.isError && (
|
||||
<div className="text-red-400 text-sm">
|
||||
{(loginMutation.error as Error).message || 'Login failed'}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="phoenix"
|
||||
className="w-full"
|
||||
disabled={loginMutation.isPending}
|
||||
>
|
||||
{loginMutation.isPending ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,135 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import MetricsCard from './MetricsCard'
|
||||
import { MetricsChart } from './MetricsChart'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { GET_REGIONS } from '@/lib/graphql/queries/resources'
|
||||
import { GET_RESOURCES } from '@/lib/graphql/queries'
|
||||
import { GET_METRICS } from '@/lib/graphql/queries/metrics'
|
||||
import { useMemo, useEffect, useState } from 'react'
|
||||
import { useResourceUpdate, useMetricsUpdate, useHealthChange } from '@/lib/graphql/hooks/useSubscriptions'
|
||||
|
||||
export default function Dashboard() {
|
||||
// Fetch regions
|
||||
const { data: regionsData, loading: regionsLoading } = useQuery(GET_REGIONS, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Fetch all resources
|
||||
const { data: resourcesData, loading: resourcesLoading } = useQuery(GET_RESOURCES, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Calculate metrics from data
|
||||
const regions = regionsData?.regions || []
|
||||
const resources = resourcesData?.resources || []
|
||||
|
||||
// Calculate active services (running resources)
|
||||
const activeServices = resources.filter((r: any) => r.status === 'RUNNING').length
|
||||
const totalResources = resources.length
|
||||
|
||||
// Calculate network health (simplified - would use actual health metrics)
|
||||
const healthyResources = resources.filter((r: any) => r.health === 'HEALTHY' || !r.health).length
|
||||
const networkHealth = totalResources > 0 ? Math.round((healthyResources / totalResources) * 100) : 0
|
||||
|
||||
// Get a sample resource for metrics (first running resource)
|
||||
const sampleResource = resources.find((r: any) => r.status === 'RUNNING')
|
||||
const timeRange = useMemo(() => {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 60 * 60 * 1000) // Last hour
|
||||
return { start, end }
|
||||
}, [])
|
||||
|
||||
// Fetch metrics for sample resource
|
||||
const { data: metricsData, loading: metricsLoading } = useQuery(GET_METRICS, {
|
||||
variables: {
|
||||
resourceId: sampleResource?.id || '',
|
||||
metricType: 'CPU_USAGE',
|
||||
timeRange: {
|
||||
start: timeRange.start.toISOString(),
|
||||
end: timeRange.end.toISOString(),
|
||||
},
|
||||
},
|
||||
skip: !sampleResource?.id,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Fetch cost metrics
|
||||
const { data: costMetricsData } = useQuery(GET_METRICS, {
|
||||
variables: {
|
||||
resourceId: sampleResource?.id || '',
|
||||
metricType: 'COST',
|
||||
timeRange: {
|
||||
start: timeRange.start.toISOString(),
|
||||
end: timeRange.end.toISOString(),
|
||||
},
|
||||
},
|
||||
skip: !sampleResource?.id,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Real-time subscriptions for live updates
|
||||
const { resource: updatedResource } = useResourceUpdate(sampleResource?.id || '')
|
||||
const { metric: updatedMetric } = useMetricsUpdate(sampleResource?.id || '', 'CPU_USAGE')
|
||||
const { health: updatedHealth } = useHealthChange(sampleResource?.id || '')
|
||||
|
||||
// Update local state when subscription data arrives
|
||||
useEffect(() => {
|
||||
if (updatedResource) {
|
||||
// Trigger refetch or update local cache
|
||||
// Apollo Client will handle this automatically with cache updates
|
||||
}
|
||||
}, [updatedResource])
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedMetric) {
|
||||
// Add new metric point to the chart data
|
||||
// This would typically update the Apollo cache or local state
|
||||
}
|
||||
}, [updatedMetric])
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedHealth) {
|
||||
// Update health status in real-time
|
||||
}
|
||||
}, [updatedHealth])
|
||||
|
||||
// Transform metrics data for charts
|
||||
const cpuMetricsData = useMemo(() => {
|
||||
if (!metricsData?.metrics?.values) return []
|
||||
return metricsData.metrics.values.map((v: any) => ({
|
||||
timestamp: v.timestamp,
|
||||
value: v.value,
|
||||
}))
|
||||
}, [metricsData])
|
||||
|
||||
const costMetricsData = useMemo(() => {
|
||||
if (!costMetricsData?.metrics?.values) return []
|
||||
return costMetricsData.metrics.values.map((v: any) => ({
|
||||
timestamp: v.timestamp,
|
||||
value: v.value,
|
||||
}))
|
||||
}, [costMetricsData])
|
||||
|
||||
// Calculate cost efficiency (simplified)
|
||||
const costEfficiency = useMemo(() => {
|
||||
if (!costMetricsData?.metrics?.values || costMetricsData.metrics.values.length === 0) return 87
|
||||
const values = costMetricsData.metrics.values.map((v: any) => v.value)
|
||||
const avgCost = values.reduce((a: number, b: number) => a + b, 0) / values.length
|
||||
// Simplified calculation - in production would compare against baseline
|
||||
return Math.max(70, Math.min(100, 100 - (avgCost / 1000) * 10))
|
||||
}, [costMetricsData])
|
||||
|
||||
if (regionsLoading || resourcesLoading) {
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<h1 className="text-3xl font-bold text-white">Dashboard</h1>
|
||||
<div className="text-white">Loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<h1 className="text-3xl font-bold text-white">Dashboard</h1>
|
||||
@@ -10,44 +137,70 @@ export default function Dashboard() {
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<MetricsCard
|
||||
title="Total Regions"
|
||||
value="325"
|
||||
health={95}
|
||||
value={regions.length.toString()}
|
||||
health={regions.length > 0 ? 95 : 0}
|
||||
trend="up"
|
||||
description="Active global regions"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Active Services"
|
||||
value="1,247"
|
||||
health={88}
|
||||
trend="stable"
|
||||
value={activeServices.toLocaleString()}
|
||||
health={totalResources > 0 ? Math.round((activeServices / totalResources) * 100) : 0}
|
||||
trend={activeServices > 0 ? "up" : "stable"}
|
||||
description="Running services"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Network Health"
|
||||
value="92%"
|
||||
health={92}
|
||||
trend="up"
|
||||
value={`${networkHealth}%`}
|
||||
health={networkHealth}
|
||||
trend={networkHealth >= 90 ? "up" : networkHealth >= 70 ? "stable" : "down"}
|
||||
description="Overall network status"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Cost Efficiency"
|
||||
value="87%"
|
||||
health={87}
|
||||
value={`${costEfficiency}%`}
|
||||
health={costEfficiency}
|
||||
trend="up"
|
||||
description="Cost optimization score"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-studio-medium bg-studio-dark p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold text-white">Performance Metrics</h2>
|
||||
<p className="text-gray-400">Chart placeholder - ECharts integration</p>
|
||||
</div>
|
||||
<Card className="border-studio-medium bg-studio-dark">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Performance Metrics</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{metricsLoading ? (
|
||||
<div className="text-white">Loading metrics...</div>
|
||||
) : cpuMetricsData.length > 0 ? (
|
||||
<MetricsChart
|
||||
data={cpuMetricsData}
|
||||
title="CPU Usage"
|
||||
metricType="CPU_USAGE"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white">No metrics available</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="rounded-lg border border-studio-medium bg-studio-dark p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold text-white">Cost Analysis</h2>
|
||||
<p className="text-gray-400">Chart placeholder - ECharts integration</p>
|
||||
</div>
|
||||
<Card className="border-studio-medium bg-studio-dark">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Cost Analysis</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{costMetricsData.length > 0 ? (
|
||||
<MetricsChart
|
||||
data={costMetricsData}
|
||||
title="Cost Over Time"
|
||||
metricType="COST"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white">No cost data available</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -31,7 +31,8 @@ export default function MetricsCard({
|
||||
{health !== undefined && (
|
||||
<span
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: healthColor }}
|
||||
data-health-color
|
||||
style={{ '--health-color': healthColor } as React.CSSProperties}
|
||||
>
|
||||
{health}%
|
||||
</span>
|
||||
|
||||
83
src/components/dashboards/MetricsChart.tsx
Normal file
83
src/components/dashboards/MetricsChart.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
interface MetricsChartProps {
|
||||
data: Array<{ timestamp: string; value: number }>
|
||||
title?: string
|
||||
metricType?: string
|
||||
}
|
||||
|
||||
export function MetricsChart({ data, title, metricType }: MetricsChartProps) {
|
||||
const option = useMemo(() => {
|
||||
const times = data.map((d) => new Date(d.timestamp).toLocaleTimeString())
|
||||
const values = data.map((d) => d.value)
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: title || metricType || 'Metrics',
|
||||
textStyle: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: '#1A1A1A',
|
||||
borderColor: '#FF4500',
|
||||
textStyle: { color: '#FFFFFF' },
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: times,
|
||||
axisLine: { lineStyle: { color: '#2A2A2A' } },
|
||||
axisLabel: { color: '#999999' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { lineStyle: { color: '#2A2A2A' } },
|
||||
axisLabel: { color: '#999999' },
|
||||
splitLine: { lineStyle: { color: '#2A2A2A' } },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: metricType || 'Value',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: values,
|
||||
lineStyle: {
|
||||
color: '#FF4500',
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(255, 69, 0, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(255, 69, 0, 0.0)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}, [data, title, metricType])
|
||||
|
||||
return (
|
||||
<div className="h-[400px] w-full">
|
||||
<ReactECharts option={option} className="chart-container" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
135
src/components/editors/ResourceGraphEditor.tsx
Normal file
135
src/components/editors/ResourceGraphEditor.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
Edge,
|
||||
addEdge,
|
||||
Connection,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Controls,
|
||||
MiniMap,
|
||||
Background,
|
||||
MarkerType,
|
||||
} from 'reactflow'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { gql } from 'graphql-tag'
|
||||
import { apolloClient } from '@/lib/graphql/client'
|
||||
import 'reactflow/dist/style.css'
|
||||
|
||||
const GET_RESOURCE_GRAPH = gql`
|
||||
query GetResourceGraph($query: ResourceGraphQuery) {
|
||||
resourceGraph(query: $query) {
|
||||
nodes {
|
||||
id
|
||||
resourceType
|
||||
provider
|
||||
name
|
||||
region
|
||||
}
|
||||
edges {
|
||||
id
|
||||
source
|
||||
target
|
||||
relationshipType
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function ResourceGraphEditor({ query }: { query?: any }) {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['resourceGraph', query],
|
||||
queryFn: async () => {
|
||||
const result = await apolloClient.query({
|
||||
query: GET_RESOURCE_GRAPH,
|
||||
variables: { query },
|
||||
})
|
||||
return result.data.resourceGraph
|
||||
},
|
||||
})
|
||||
|
||||
const initialNodes = useMemo<Node[]>(() => {
|
||||
if (!data?.nodes) return []
|
||||
|
||||
return data.nodes.map((node: any, index: number) => ({
|
||||
id: node.id,
|
||||
type: 'default',
|
||||
data: { label: node.name },
|
||||
position: {
|
||||
x: (index % 10) * 150 + 50,
|
||||
y: Math.floor(index / 10) * 150 + 50,
|
||||
},
|
||||
style: {
|
||||
background: getProviderColor(node.provider),
|
||||
color: '#FFFFFF',
|
||||
border: '2px solid #FF4500',
|
||||
borderRadius: '8px',
|
||||
padding: '10px',
|
||||
},
|
||||
}))
|
||||
}, [data?.nodes])
|
||||
|
||||
const initialEdges = useMemo<Edge[]>(() => {
|
||||
if (!data?.edges) return []
|
||||
|
||||
return data.edges.map((edge: any) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.relationshipType,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: '#FF4500',
|
||||
},
|
||||
style: {
|
||||
stroke: '#FF4500',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
}))
|
||||
}, [data?.edges])
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
function getProviderColor(provider: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
PROXMOX: '#FF4500',
|
||||
KUBERNETES: '#326CE5',
|
||||
CLOUDFLARE: '#F38020',
|
||||
CEPH: '#FF6B35',
|
||||
MINIO: '#FFD700',
|
||||
}
|
||||
return colors[provider] || '#2A2A2A'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[800px] w-full rounded-lg border border-studio-medium bg-studio-black">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
className="bg-studio-black"
|
||||
>
|
||||
<Controls className="bg-studio-dark border-studio-medium" />
|
||||
<MiniMap
|
||||
className="bg-studio-dark border-studio-medium"
|
||||
nodeColor={(node) => {
|
||||
return node.style?.background as string || '#2A2A2A'
|
||||
}}
|
||||
/>
|
||||
<Background color="#2A2A2A" gap={16} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
41
src/components/i18n/LanguageSwitcher.tsx
Normal file
41
src/components/i18n/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Globe } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { supportedLanguages, type LanguageCode } from '@/lib/i18n/config'
|
||||
import { useLanguage } from '@/hooks/useLanguage'
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { language, setLanguage } = useLanguage()
|
||||
const currentLang = supportedLanguages.find(lang => lang.code === language) || supportedLanguages[0]
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-2 px-3 py-2 text-sm text-gray-400 hover:text-phoenix-fire transition-colors">
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{currentLang.nativeName}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
{supportedLanguages.map((lang) => (
|
||||
<DropdownMenuItem
|
||||
key={lang.code}
|
||||
onClick={() => setLanguage(lang.code)}
|
||||
className={language === lang.code ? 'bg-phoenix-fire/10' : ''}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{lang.nativeName}</span>
|
||||
<span className="text-xs text-gray-400">{lang.name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
53
src/components/infrastructure/AccessibilityEnhancements.tsx
Normal file
53
src/components/infrastructure/AccessibilityEnhancements.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function SkipLink() {
|
||||
return (
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-phoenix-fire focus:text-white focus:rounded-md"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export function useKeyboardNavigation() {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Tab navigation enhancement
|
||||
if (e.key === 'Tab') {
|
||||
document.body.classList.add('keyboard-navigation')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseDown = () => {
|
||||
document.body.classList.remove('keyboard-navigation')
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
window.addEventListener('mousedown', handleMouseDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
window.removeEventListener('mousedown', handleMouseDown)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
// Add focus indicators for keyboard navigation
|
||||
export function FocusIndicator() {
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
.keyboard-navigation *:focus {
|
||||
outline: 2px solid #f59e0b !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
return () => document.head.removeChild(style)
|
||||
}, [])
|
||||
}
|
||||
|
||||
332
src/components/infrastructure/AdvancedFilters.tsx
Normal file
332
src/components/infrastructure/AdvancedFilters.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { X, Save, Filter } from 'lucide-react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
|
||||
interface FilterPreset {
|
||||
id: string
|
||||
name: string
|
||||
filters: Record<string, any>
|
||||
}
|
||||
|
||||
interface AdvancedFiltersProps {
|
||||
onFiltersChange: (filters: Record<string, any>) => void
|
||||
filterConfig: {
|
||||
multiSelect?: Array<{ key: string; label: string; options: string[] }>
|
||||
dateRange?: Array<{ key: string; label: string }>
|
||||
costRange?: Array<{ key: string; label: string; min: number; max: number }>
|
||||
}
|
||||
presets?: FilterPreset[]
|
||||
onSavePreset?: (preset: FilterPreset) => void
|
||||
}
|
||||
|
||||
export function AdvancedFilters({
|
||||
onFiltersChange,
|
||||
filterConfig,
|
||||
presets = [],
|
||||
onSavePreset,
|
||||
}: AdvancedFiltersProps) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [filters, setFilters] = useState<Record<string, any>>({})
|
||||
const [presetName, setPresetName] = useState('')
|
||||
|
||||
// Load filters from URL
|
||||
useEffect(() => {
|
||||
const urlFilters: Record<string, any> = {}
|
||||
searchParams.forEach((value, key) => {
|
||||
if (key.startsWith('filter_')) {
|
||||
const filterKey = key.replace('filter_', '')
|
||||
try {
|
||||
urlFilters[filterKey] = JSON.parse(value)
|
||||
} catch {
|
||||
urlFilters[filterKey] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
if (Object.keys(urlFilters).length > 0) {
|
||||
setFilters(urlFilters)
|
||||
onFiltersChange(urlFilters)
|
||||
}
|
||||
}, [searchParams, onFiltersChange])
|
||||
|
||||
// Sync filters to URL
|
||||
const updateURL = useCallback(
|
||||
(newFilters: Record<string, any>) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
|
||||
// Remove old filter params
|
||||
Array.from(params.keys())
|
||||
.filter((key) => key.startsWith('filter_'))
|
||||
.forEach((key) => params.delete(key))
|
||||
|
||||
// Add new filter params
|
||||
Object.entries(newFilters).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
params.set(
|
||||
`filter_${key}`,
|
||||
typeof value === 'object' ? JSON.stringify(value) : String(value)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
router.replace(`?${params.toString()}`, { scroll: false })
|
||||
},
|
||||
[searchParams, router]
|
||||
)
|
||||
|
||||
const handleFilterChange = (key: string, value: any) => {
|
||||
const newFilters = { ...filters, [key]: value }
|
||||
setFilters(newFilters)
|
||||
onFiltersChange(newFilters)
|
||||
updateURL(newFilters)
|
||||
}
|
||||
|
||||
const handleMultiSelectChange = (key: string, value: string, checked: boolean) => {
|
||||
const current = (filters[key] as string[]) || []
|
||||
const newValue = checked
|
||||
? [...current, value]
|
||||
: current.filter((v) => v !== value)
|
||||
handleFilterChange(key, newValue.length > 0 ? newValue : null)
|
||||
}
|
||||
|
||||
const handleDateRangeChange = (key: string, field: 'start' | 'end', value: string) => {
|
||||
const current = filters[key] || { start: '', end: '' }
|
||||
const newRange = { ...current, [field]: value }
|
||||
handleFilterChange(
|
||||
key,
|
||||
newRange.start || newRange.end ? newRange : null
|
||||
)
|
||||
}
|
||||
|
||||
const handleCostRangeChange = (key: string, field: 'min' | 'max', value: number) => {
|
||||
const current = filters[key] || { min: 0, max: 1000000 }
|
||||
const newRange = { ...current, [field]: value }
|
||||
handleFilterChange(key, newRange)
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({})
|
||||
onFiltersChange({})
|
||||
updateURL({})
|
||||
}
|
||||
|
||||
const applyPreset = (preset: FilterPreset) => {
|
||||
setFilters(preset.filters)
|
||||
onFiltersChange(preset.filters)
|
||||
updateURL(preset.filters)
|
||||
}
|
||||
|
||||
const saveCurrentAsPreset = () => {
|
||||
if (!presetName.trim() || !onSavePreset) return
|
||||
|
||||
const preset: FilterPreset = {
|
||||
id: `preset-${Date.now()}`,
|
||||
name: presetName,
|
||||
filters: { ...filters },
|
||||
}
|
||||
onSavePreset(preset)
|
||||
setPresetName('')
|
||||
}
|
||||
|
||||
const activeFilterCount = Object.values(filters).filter(
|
||||
(v) => v !== null && v !== undefined && v !== '' && (Array.isArray(v) ? v.length > 0 : true)
|
||||
).length
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Filter className="h-4 w-4" />
|
||||
Advanced Filters
|
||||
{activeFilterCount > 0 && (
|
||||
<Badge variant="default" className="ml-2">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
{activeFilterCount > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={clearFilters}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Filter Options</CardTitle>
|
||||
<CardDescription>Apply multiple filters to refine your results</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Multi-select filters */}
|
||||
{filterConfig.multiSelect?.map((config) => (
|
||||
<div key={config.key}>
|
||||
<Label>{config.label}</Label>
|
||||
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto">
|
||||
{config.options.map((option) => {
|
||||
const selected = (filters[config.key] as string[])?.includes(option) || false
|
||||
return (
|
||||
<div key={option} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${config.key}-${option}`}
|
||||
checked={selected}
|
||||
onChange={(e) =>
|
||||
handleMultiSelectChange(config.key, option, e.target.checked)
|
||||
}
|
||||
className="rounded border-studio-medium"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`${config.key}-${option}`}
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
{option}
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{(filters[config.key] as string[])?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{(filters[config.key] as string[]).map((value) => (
|
||||
<Badge
|
||||
key={value}
|
||||
variant="outline"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleMultiSelectChange(config.key, value, false)}
|
||||
>
|
||||
{value}
|
||||
<X className="h-3 w-3 ml-1" />
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Date range filters */}
|
||||
{filterConfig.dateRange?.map((config) => (
|
||||
<div key={config.key} className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>{config.label} - Start</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={(filters[config.key] as { start?: string })?.start || ''}
|
||||
onChange={(e) => handleDateRangeChange(config.key, 'start', e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{config.label} - End</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={(filters[config.key] as { end?: string })?.end || ''}
|
||||
onChange={(e) => handleDateRangeChange(config.key, 'end', e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Cost range filters */}
|
||||
{filterConfig.costRange?.map((config) => (
|
||||
<div key={config.key}>
|
||||
<Label>{config.label}</Label>
|
||||
<div className="grid grid-cols-2 gap-4 mt-2">
|
||||
<div>
|
||||
<Label className="text-xs text-studio-medium">Min</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={config.min}
|
||||
max={config.max}
|
||||
value={(filters[config.key] as { min?: number })?.min || config.min}
|
||||
onChange={(e) =>
|
||||
handleCostRangeChange(config.key, 'min', Number(e.target.value))
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-studio-medium">Max</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={config.min}
|
||||
max={config.max}
|
||||
value={(filters[config.key] as { max?: number })?.max || config.max}
|
||||
onChange={(e) =>
|
||||
handleCostRangeChange(config.key, 'max', Number(e.target.value))
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Presets */}
|
||||
{presets.length > 0 && (
|
||||
<div>
|
||||
<Label>Saved Presets</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{presets.map((preset) => (
|
||||
<Button
|
||||
key={preset.id}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => applyPreset(preset)}
|
||||
>
|
||||
{preset.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save preset */}
|
||||
{onSavePreset && (
|
||||
<div>
|
||||
<Label>Save Current Filters as Preset</Label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Input
|
||||
placeholder="Preset name"
|
||||
value={presetName}
|
||||
onChange={(e) => setPresetName(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={saveCurrentAsPreset}
|
||||
disabled={!presetName.trim()}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
219
src/components/infrastructure/AuditLogViewer.tsx
Normal file
219
src/components/infrastructure/AuditLogViewer.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Download, Filter } from 'lucide-react'
|
||||
import type { AuditLogEntry, AuditAction, AuditEntityType } from '@/lib/services/auditLog'
|
||||
|
||||
interface AuditLogViewerProps {
|
||||
logs: AuditLogEntry[]
|
||||
onFilter?: (filters: any) => void
|
||||
}
|
||||
|
||||
export function AuditLogViewer({ logs, onFilter }: AuditLogViewerProps) {
|
||||
const [filteredLogs, setFilteredLogs] = useState<AuditLogEntry[]>(logs)
|
||||
const [filters, setFilters] = useState<{
|
||||
action?: AuditAction
|
||||
entityType?: AuditEntityType
|
||||
search?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
let filtered = [...logs]
|
||||
|
||||
if (filters.action) {
|
||||
filtered = filtered.filter((log) => log.action === filters.action)
|
||||
}
|
||||
if (filters.entityType) {
|
||||
filtered = filtered.filter((log) => log.entityType === filters.entityType)
|
||||
}
|
||||
if (filters.search) {
|
||||
const searchLower = filters.search.toLowerCase()
|
||||
filtered = filtered.filter(
|
||||
(log) =>
|
||||
log.entityName.toLowerCase().includes(searchLower) ||
|
||||
log.entityId.toLowerCase().includes(searchLower)
|
||||
)
|
||||
}
|
||||
|
||||
setFilteredLogs(filtered)
|
||||
onFilter?.(filters)
|
||||
}, [logs, filters, onFilter])
|
||||
|
||||
const actionColors: Record<AuditAction, string> = {
|
||||
create: 'bg-green-500/20 text-green-400',
|
||||
update: 'bg-blue-500/20 text-blue-400',
|
||||
delete: 'bg-red-500/20 text-red-400',
|
||||
export: 'bg-purple-500/20 text-purple-400',
|
||||
import: 'bg-orange-500/20 text-orange-400',
|
||||
backup: 'bg-yellow-500/20 text-yellow-400',
|
||||
restore: 'bg-cyan-500/20 text-cyan-400',
|
||||
}
|
||||
|
||||
const exportLogs = () => {
|
||||
const csv = [
|
||||
['Timestamp', 'Action', 'Entity Type', 'Entity ID', 'Entity Name', 'User ID'].join(','),
|
||||
...filteredLogs.map((log) =>
|
||||
[
|
||||
log.timestamp,
|
||||
log.action,
|
||||
log.entityType,
|
||||
log.entityId,
|
||||
log.entityName,
|
||||
log.userId || '',
|
||||
].join(',')
|
||||
),
|
||||
].join('\n')
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `audit-logs-${new Date().toISOString().split('T')[0]}.csv`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Audit Log</CardTitle>
|
||||
<CardDescription>
|
||||
Track all changes and operations ({filteredLogs.length} entries)
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={exportLogs}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4 mb-4">
|
||||
<div className="flex gap-4">
|
||||
<Select
|
||||
value={filters.action || 'all'}
|
||||
onValueChange={(value) =>
|
||||
setFilters({ ...filters, action: value === 'all' ? undefined : (value as AuditAction) })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Action" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Actions</SelectItem>
|
||||
<SelectItem value="create">Create</SelectItem>
|
||||
<SelectItem value="update">Update</SelectItem>
|
||||
<SelectItem value="delete">Delete</SelectItem>
|
||||
<SelectItem value="export">Export</SelectItem>
|
||||
<SelectItem value="import">Import</SelectItem>
|
||||
<SelectItem value="backup">Backup</SelectItem>
|
||||
<SelectItem value="restore">Restore</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={filters.entityType || 'all'}
|
||||
onValueChange={(value) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
entityType: value === 'all' ? undefined : (value as AuditEntityType),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Entity Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem value="compliance">Compliance</SelectItem>
|
||||
<SelectItem value="milestone">Milestone</SelectItem>
|
||||
<SelectItem value="cost">Cost</SelectItem>
|
||||
<SelectItem value="topology">Topology</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={filters.search || ''}
|
||||
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
||||
className="flex-1 max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-studio-medium rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
<TableHead>Action</TableHead>
|
||||
<TableHead>Entity</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Changes</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredLogs.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-studio-medium">
|
||||
No audit log entries found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredLogs.map((log) => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell className="text-sm">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={actionColors[log.action] || ''}>{log.action}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{log.entityType}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{log.entityName}</TableCell>
|
||||
<TableCell className="text-studio-medium">
|
||||
{log.userId || 'System'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.changes ? (
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-studio-medium">
|
||||
{Object.keys(log.changes).length} change(s)
|
||||
</summary>
|
||||
<pre className="mt-2 p-2 bg-studio-black rounded text-xs overflow-auto max-h-32">
|
||||
{JSON.stringify(log.changes, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
) : (
|
||||
<span className="text-studio-medium">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
55
src/components/infrastructure/BulkActions.tsx
Normal file
55
src/components/infrastructure/BulkActions.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Trash2, Edit, CheckSquare } from 'lucide-react'
|
||||
|
||||
interface BulkActionsProps {
|
||||
selectedCount: number
|
||||
onBulkDelete?: () => void
|
||||
onBulkEdit?: () => void
|
||||
actions?: Array<{
|
||||
label: string
|
||||
onClick: () => void
|
||||
icon?: React.ReactNode
|
||||
}>
|
||||
}
|
||||
|
||||
export function BulkActions({
|
||||
selectedCount,
|
||||
onBulkDelete,
|
||||
onBulkEdit,
|
||||
actions = [],
|
||||
}: BulkActionsProps) {
|
||||
if (selectedCount === 0) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 p-4 bg-studio-medium/20 border border-studio-medium rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckSquare className="h-4 w-4 text-studio-light" />
|
||||
<Badge variant="outline">{selectedCount} selected</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{onBulkEdit && (
|
||||
<Button size="sm" variant="outline" onClick={onBulkEdit}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
{onBulkDelete && (
|
||||
<Button size="sm" variant="destructive" onClick={onBulkDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
{actions.map((action, idx) => (
|
||||
<Button key={idx} size="sm" variant="outline" onClick={action.onClick}>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
154
src/components/infrastructure/ComplianceGapAnalysis.tsx
Normal file
154
src/components/infrastructure/ComplianceGapAnalysis.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { AlertTriangle, CheckCircle } from 'lucide-react'
|
||||
import type { ComplianceRequirement } from '@/lib/types/infrastructure'
|
||||
|
||||
interface ComplianceGapAnalysisProps {
|
||||
requirements: ComplianceRequirement[]
|
||||
targetFrameworks?: string[]
|
||||
}
|
||||
|
||||
export function ComplianceGapAnalysis({
|
||||
requirements,
|
||||
targetFrameworks = ['GDPR', 'PCI-DSS', 'HIPAA', 'SOC 2'],
|
||||
}: ComplianceGapAnalysisProps) {
|
||||
const analysis = useMemo(() => {
|
||||
const gaps: Array<{
|
||||
country: string
|
||||
missingFrameworks: string[]
|
||||
status: string
|
||||
}> = []
|
||||
|
||||
const frameworkCoverage: Record<string, { total: number; compliant: number }> = {}
|
||||
|
||||
targetFrameworks.forEach((framework) => {
|
||||
frameworkCoverage[framework] = { total: 0, compliant: 0 }
|
||||
})
|
||||
|
||||
requirements.forEach((req) => {
|
||||
const missing = targetFrameworks.filter((f) => !req.frameworks.includes(f))
|
||||
if (missing.length > 0) {
|
||||
gaps.push({
|
||||
country: req.country,
|
||||
missingFrameworks: missing,
|
||||
status: req.status,
|
||||
})
|
||||
}
|
||||
|
||||
targetFrameworks.forEach((framework) => {
|
||||
frameworkCoverage[framework].total++
|
||||
if (req.frameworks.includes(framework)) {
|
||||
frameworkCoverage[framework].compliant++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const overallProgress =
|
||||
requirements.length > 0
|
||||
? (requirements.filter((r) => r.status === 'Compliant').length / requirements.length) * 100
|
||||
: 0
|
||||
|
||||
return {
|
||||
gaps,
|
||||
frameworkCoverage,
|
||||
overallProgress,
|
||||
totalCountries: requirements.length,
|
||||
compliantCountries: requirements.filter((r) => r.status === 'Compliant').length,
|
||||
}
|
||||
}, [requirements, targetFrameworks])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Overall Compliance Progress</CardTitle>
|
||||
<CardDescription>
|
||||
{analysis.compliantCountries} of {analysis.totalCountries} countries are fully compliant
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-studio-medium">Progress</span>
|
||||
<span className="text-studio-light font-semibold">
|
||||
{analysis.overallProgress.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={analysis.overallProgress} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Framework Coverage</CardTitle>
|
||||
<CardDescription>Compliance status by framework</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(analysis.frameworkCoverage).map(([framework, coverage]) => {
|
||||
const percentage = coverage.total > 0 ? (coverage.compliant / coverage.total) * 100 : 0
|
||||
return (
|
||||
<div key={framework}>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium">{framework}</span>
|
||||
<Badge
|
||||
variant={percentage >= 80 ? 'default' : percentage >= 50 ? 'outline' : 'destructive'}
|
||||
>
|
||||
{coverage.compliant}/{coverage.total}
|
||||
</Badge>
|
||||
</div>
|
||||
<Progress value={percentage} className="h-2" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Compliance Gaps</CardTitle>
|
||||
<CardDescription>
|
||||
Countries missing required frameworks ({analysis.gaps.length} found)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{analysis.gaps.length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-green-400">
|
||||
<CheckCircle className="h-5 w-5" />
|
||||
<span>All countries meet the required frameworks</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{analysis.gaps.map((gap) => (
|
||||
<div
|
||||
key={gap.country}
|
||||
className="p-3 border border-studio-medium rounded-lg flex items-start gap-3"
|
||||
>
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-studio-light">{gap.country}</div>
|
||||
<div className="text-sm text-studio-medium mt-1">
|
||||
Missing: {gap.missingFrameworks.join(', ')}
|
||||
</div>
|
||||
<Badge className="mt-2" variant="outline">
|
||||
{gap.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
116
src/components/infrastructure/ComplianceMapView.tsx
Normal file
116
src/components/infrastructure/ComplianceMapView.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import Map, { Marker, Popup, Layer, Source } from 'react-map-gl'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import type { ComplianceRequirement } from '@/lib/types/infrastructure'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface ComplianceMapViewProps {
|
||||
requirements: ComplianceRequirement[]
|
||||
onCountryClick?: (country: string) => void
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
Compliant: '#10b981',
|
||||
Partial: '#f59e0b',
|
||||
Pending: '#3b82f6',
|
||||
'Non-Compliant': '#ef4444',
|
||||
}
|
||||
|
||||
// Basic country coordinates (in production, use a proper geocoding service)
|
||||
const countryCoordinates: Record<string, { lat: number; lng: number }> = {
|
||||
Italy: { lat: 41.9028, lng: 12.4964 },
|
||||
Germany: { lat: 51.1657, lng: 10.4515 },
|
||||
France: { lat: 46.2276, lng: 2.2137 },
|
||||
Spain: { lat: 40.4637, lng: -3.7492 },
|
||||
// Add more as needed
|
||||
}
|
||||
|
||||
export function ComplianceMapView({ requirements, onCountryClick }: ComplianceMapViewProps) {
|
||||
const [popupInfo, setPopupInfo] = useState<ComplianceRequirement | null>(null)
|
||||
const [viewState, setViewState] = useState({
|
||||
longitude: 12.4964,
|
||||
latitude: 41.9028,
|
||||
zoom: 3,
|
||||
})
|
||||
|
||||
const handleMarkerClick = useCallback(
|
||||
(requirement: ComplianceRequirement) => {
|
||||
setPopupInfo(requirement)
|
||||
onCountryClick?.(requirement.country)
|
||||
},
|
||||
[onCountryClick]
|
||||
)
|
||||
|
||||
const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || ''
|
||||
|
||||
if (!mapboxToken) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center">
|
||||
<p className="text-studio-medium">
|
||||
Mapbox token not configured. Please set NEXT_PUBLIC_MAPBOX_TOKEN in your environment
|
||||
variables.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-96 rounded-lg overflow-hidden border border-studio-medium">
|
||||
<Map
|
||||
{...viewState}
|
||||
onMove={(evt) => setViewState(evt.viewState)}
|
||||
mapboxAccessToken={mapboxToken}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
mapStyle="mapbox://styles/mapbox/dark-v10"
|
||||
>
|
||||
{requirements.map((req) => {
|
||||
const coords = countryCoordinates[req.country]
|
||||
if (!coords) return null
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={req.country}
|
||||
longitude={coords.lng}
|
||||
latitude={coords.lat}
|
||||
anchor="bottom"
|
||||
onClick={() => handleMarkerClick(req)}
|
||||
>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full border-2 border-white cursor-pointer"
|
||||
style={{ backgroundColor: statusColors[req.status] || '#6b7280' }}
|
||||
/>
|
||||
</Marker>
|
||||
)
|
||||
})}
|
||||
|
||||
{popupInfo && (
|
||||
<Popup
|
||||
longitude={countryCoordinates[popupInfo.country]?.lng || 0}
|
||||
latitude={countryCoordinates[popupInfo.country]?.lat || 0}
|
||||
anchor="top"
|
||||
onClose={() => setPopupInfo(null)}
|
||||
closeButton
|
||||
closeOnClick={false}
|
||||
>
|
||||
<div className="p-2 min-w-[200px]">
|
||||
<h3 className="font-semibold text-sm mb-2">{popupInfo.country}</h3>
|
||||
<Badge className={statusColors[popupInfo.status] ? `bg-${statusColors[popupInfo.status]}/20 text-${statusColors[popupInfo.status]}` : ''}>
|
||||
{popupInfo.status}
|
||||
</Badge>
|
||||
<div className="mt-2 text-xs">
|
||||
<div>Frameworks: {popupInfo.frameworks.join(', ')}</div>
|
||||
<div>Requirements: {popupInfo.requirements.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
)}
|
||||
</Map>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
376
src/components/infrastructure/ComplianceMapping.tsx
Normal file
376
src/components/infrastructure/ComplianceMapping.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { AdvancedFilters } from './AdvancedFilters'
|
||||
import { AuditLogViewer } from './AuditLogViewer'
|
||||
import { auditLogService } from '@/lib/services/auditLog'
|
||||
import { useComplianceRequirements } from '@/lib/hooks/useInfrastructureData'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { RegionSelector } from './RegionSelector'
|
||||
import { EditModeToggle } from './EditModeToggle'
|
||||
import { EditComplianceForm } from './forms/EditComplianceForm'
|
||||
import { ComplianceMapView } from './lazy'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { SkeletonTable } from './SkeletonCard'
|
||||
import { BulkActions } from './BulkActions'
|
||||
import { ConfirmDialog } from './ConfirmDialog'
|
||||
import { ComplianceGapAnalysis } from './lazy'
|
||||
import { Download, Search } from 'lucide-react'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import type { ComplianceRequirement } from '@/lib/types/infrastructure'
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
Compliant: 'bg-green-500/20 text-green-400',
|
||||
Partial: 'bg-yellow-500/20 text-yellow-400',
|
||||
Pending: 'bg-blue-500/20 text-blue-400',
|
||||
'Non-Compliant': 'bg-red-500/20 text-red-400',
|
||||
}
|
||||
|
||||
export function ComplianceMapping() {
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>('All')
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('All')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [editingRequirement, setEditingRequirement] = useState<ComplianceRequirement | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set())
|
||||
const [confirmBulkDelete, setConfirmBulkDelete] = useState(false)
|
||||
const [showAuditLog, setShowAuditLog] = useState(false)
|
||||
const [auditLogs, setAuditLogs] = useState<any[]>([])
|
||||
const { toast } = useToast()
|
||||
|
||||
useEffect(() => {
|
||||
if (showAuditLog) {
|
||||
const logs = auditLogService.getLogs({ entityType: 'compliance' })
|
||||
setAuditLogs(logs)
|
||||
}
|
||||
}, [showAuditLog])
|
||||
|
||||
const filter = {
|
||||
region: selectedRegion === 'All' ? undefined : selectedRegion,
|
||||
status: selectedStatus === 'All' ? undefined : selectedStatus,
|
||||
}
|
||||
|
||||
const { requirements, loading, error } = useComplianceRequirements(filter)
|
||||
|
||||
const filteredRequirements = useMemo(
|
||||
() =>
|
||||
requirements.filter((req) =>
|
||||
searchQuery
|
||||
? req.country.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
req.frameworks.some((f) => f.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: true
|
||||
),
|
||||
[requirements, searchQuery]
|
||||
)
|
||||
|
||||
const handleExportCSV = () => {
|
||||
const headers = ['Country', 'Region', 'Frameworks', 'Status', 'Requirements', 'Last Audit']
|
||||
const rows = filteredRequirements.map((req) => [
|
||||
req.country,
|
||||
req.region,
|
||||
req.frameworks.join('; '),
|
||||
req.status,
|
||||
req.requirements.join('; '),
|
||||
req.lastAuditDate || 'N/A',
|
||||
])
|
||||
|
||||
const csv = [headers, ...rows].map((row) => row.map((cell) => `"${cell}"`).join(',')).join('\n')
|
||||
const blob = new Blob([csv], { type: 'text/csv' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `compliance-requirements-${new Date().toISOString().split('T')[0]}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Compliance Mapping</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
Track compliance requirements by country and framework
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<SkeletonTable rows={5} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Error Loading Compliance Data</CardTitle>
|
||||
<CardDescription>{error.message}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Compliance Mapping</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
Track compliance requirements by country and framework
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleExportCSV}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Compliance Requirements</CardTitle>
|
||||
<CardDescription>
|
||||
{filteredRequirements.length} requirement{filteredRequirements.length !== 1 ? 's' : ''} found
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-studio-medium" />
|
||||
<Input
|
||||
placeholder="Search countries or frameworks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8 w-64"
|
||||
/>
|
||||
</div>
|
||||
<RegionSelector
|
||||
value={selectedRegion}
|
||||
onChange={setSelectedRegion}
|
||||
className="w-48"
|
||||
/>
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className="h-10 rounded-md border border-studio-medium bg-studio-dark px-3 text-sm"
|
||||
>
|
||||
<option value="All">All Status</option>
|
||||
<option value="Compliant">Compliant</option>
|
||||
<option value="Partial">Partial</option>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Non-Compliant">Non-Compliant</option>
|
||||
</select>
|
||||
<EditModeToggle enabled={editMode} onToggle={setEditMode} />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{editMode && selectedItems.size > 0 && (
|
||||
<BulkActions
|
||||
selectedCount={selectedItems.size}
|
||||
onBulkDelete={() => setConfirmBulkDelete(true)}
|
||||
/>
|
||||
)}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{editMode && (
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={selectedItems.size === filteredRequirements.length && filteredRequirements.length > 0}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedItems(new Set(filteredRequirements.map((r) => r.country)))
|
||||
} else {
|
||||
setSelectedItems(new Set())
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead>Country</TableHead>
|
||||
<TableHead>Region</TableHead>
|
||||
<TableHead>Frameworks</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Requirements</TableHead>
|
||||
<TableHead>Last Audit</TableHead>
|
||||
{editMode && <TableHead>Actions</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredRequirements.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={editMode ? 8 : 6} className="text-center">
|
||||
<EmptyState
|
||||
title="No compliance requirements found"
|
||||
description="Try adjusting your filters or search query."
|
||||
action={
|
||||
editMode
|
||||
? {
|
||||
label: 'Add Requirement',
|
||||
onClick: () => {
|
||||
// TODO: Add create functionality
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRequirements.map((req) => (
|
||||
<TableRow key={req.country}>
|
||||
{editMode && (
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItems.has(req.country)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newSet = new Set(selectedItems)
|
||||
if (checked) {
|
||||
newSet.add(req.country)
|
||||
} else {
|
||||
newSet.delete(req.country)
|
||||
}
|
||||
setSelectedItems(newSet)
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="font-medium">{req.country}</TableCell>
|
||||
<TableCell>{req.region}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{req.frameworks.map((framework) => (
|
||||
<Badge key={framework} variant="outline" className="text-xs">
|
||||
{framework}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={statusColors[req.status] || ''}>{req.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-md truncate" title={req.requirements.join(', ')}>
|
||||
{req.requirements.length} requirement{req.requirements.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{req.lastAuditDate
|
||||
? new Date(req.lastAuditDate).toLocaleDateString()
|
||||
: 'N/A'}
|
||||
</TableCell>
|
||||
{editMode && (
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditingRequirement(req)
|
||||
setDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Map view */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Geographic View</CardTitle>
|
||||
<CardDescription>Interactive map showing compliance status by country</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{filteredRequirements.length > 0 ? (
|
||||
<ComplianceMapView
|
||||
requirements={filteredRequirements}
|
||||
onCountryClick={(country) => {
|
||||
setSearchQuery(country)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="No compliance data"
|
||||
description="No compliance requirements match your current filters."
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{filteredRequirements.length > 0 && (
|
||||
<ComplianceGapAnalysis requirements={filteredRequirements} />
|
||||
)}
|
||||
|
||||
{editingRequirement && (
|
||||
<EditComplianceForm
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
requirement={editingRequirement}
|
||||
onSuccess={() => {
|
||||
setEditingRequirement(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmBulkDelete}
|
||||
onOpenChange={setConfirmBulkDelete}
|
||||
title="Delete Selected Items"
|
||||
description={`Are you sure you want to delete ${selectedItems.size} compliance requirement(s)? This action cannot be undone.`}
|
||||
confirmLabel="Delete"
|
||||
variant="destructive"
|
||||
onConfirm={() => {
|
||||
// TODO: Implement bulk delete
|
||||
selectedItems.forEach((country) => {
|
||||
auditLogService.log({
|
||||
action: 'delete',
|
||||
entityType: 'compliance',
|
||||
entityId: country,
|
||||
entityName: country,
|
||||
})
|
||||
})
|
||||
setSelectedItems(new Set())
|
||||
toast({
|
||||
title: 'Deleted',
|
||||
description: `${selectedItems.size} item(s) deleted`,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{showAuditLog && (
|
||||
<AuditLogViewer
|
||||
logs={auditLogs}
|
||||
onFilter={(filters) => {
|
||||
const logs = auditLogService.getLogs(filters)
|
||||
setAuditLogs(logs)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
58
src/components/infrastructure/ConfirmDialog.tsx
Normal file
58
src/components/infrastructure/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
title: string
|
||||
description: string
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
variant?: 'default' | 'destructive'
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
variant = 'default',
|
||||
onConfirm,
|
||||
}: ConfirmDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{cancelLabel}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant={variant}
|
||||
onClick={() => {
|
||||
onConfirm()
|
||||
onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
{confirmLabel}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
472
src/components/infrastructure/CostEstimates.tsx
Normal file
472
src/components/infrastructure/CostEstimates.tsx
Normal file
@@ -0,0 +1,472 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useCostEstimates } from '@/lib/hooks/useInfrastructureData'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { RegionSelector } from './RegionSelector'
|
||||
import { EntitySelector } from './EntitySelector'
|
||||
import { EditModeToggle } from './EditModeToggle'
|
||||
import { EditCostEstimateForm } from './forms/EditCostEstimateForm'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { CostForecast } from './lazy'
|
||||
import { Download } from 'lucide-react'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import * as XLSX from 'xlsx'
|
||||
import type { CostEstimate } from '@/lib/types/infrastructure'
|
||||
|
||||
function generateCostCharts(estimates: any[]) {
|
||||
const byRegion = estimates.reduce((acc, e) => {
|
||||
acc[e.region] = (acc[e.region] || 0) + e.annual
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
const byCategory = estimates.reduce((acc, e) => {
|
||||
acc[e.category] = (acc[e.category] || 0) + e.annual
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
const barChartOption = {
|
||||
title: {
|
||||
text: 'Annual Costs by Region',
|
||||
textStyle: { color: '#e5e7eb' },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Object.keys(byRegion),
|
||||
axisLabel: { color: '#9ca3af' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#9ca3af',
|
||||
formatter: (value: number) => `$${(value / 1000000).toFixed(1)}M`,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: Object.values(byRegion),
|
||||
type: 'bar',
|
||||
itemStyle: { color: '#3b82f6' },
|
||||
},
|
||||
],
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
|
||||
const pieChartOption = {
|
||||
title: {
|
||||
text: 'Cost Breakdown by Category',
|
||||
textStyle: { color: '#e5e7eb' },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: ${c} ({d}%)',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
data: Object.entries(byCategory).map(([name, value]) => ({
|
||||
value,
|
||||
name,
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
|
||||
return { barChartOption, pieChartOption }
|
||||
}
|
||||
|
||||
export function CostEstimates() {
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>('All')
|
||||
const [selectedEntity, setSelectedEntity] = useState<string>('All')
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [editingEstimate, setEditingEstimate] = useState<CostEstimate | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const { toast } = useToast()
|
||||
|
||||
const filter = {
|
||||
region: selectedRegion === 'All' ? undefined : selectedRegion,
|
||||
entity: selectedEntity === 'All' ? undefined : selectedEntity,
|
||||
}
|
||||
|
||||
const { estimates, loading, error } = useCostEstimates(filter)
|
||||
|
||||
const handleExportExcel = () => {
|
||||
if (estimates.length === 0) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: 'No data to export',
|
||||
variant: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const workbook = XLSX.utils.book_new()
|
||||
|
||||
// Summary sheet
|
||||
const totalMonthly = estimates.reduce((sum, e) => sum + e.monthly, 0)
|
||||
const totalAnnual = estimates.reduce((sum, e) => sum + e.annual, 0)
|
||||
const summaryData = [
|
||||
['Cost Estimates Summary'],
|
||||
[],
|
||||
['Total Monthly (USD)', totalMonthly],
|
||||
['Total Annual (USD)', totalAnnual],
|
||||
['Number of Estimates', estimates.length],
|
||||
['Export Date', new Date().toLocaleDateString()],
|
||||
['Region Filter', selectedRegion],
|
||||
['Entity Filter', selectedEntity],
|
||||
]
|
||||
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData)
|
||||
XLSX.utils.book_append_sheet(workbook, summarySheet, 'Summary')
|
||||
|
||||
// Detailed breakdown sheet
|
||||
const detailData = estimates.map((e) => ({
|
||||
Region: e.region,
|
||||
Entity: e.entity,
|
||||
Category: e.category,
|
||||
'Monthly (USD)': e.monthly,
|
||||
'Annual (USD)': e.annual,
|
||||
'Compute (USD)': e.breakdown.compute || 0,
|
||||
'Storage (USD)': e.breakdown.storage || 0,
|
||||
'Network (USD)': e.breakdown.network || 0,
|
||||
'Licenses (USD)': e.breakdown.licenses || 0,
|
||||
'Personnel (USD)': e.breakdown.personnel || 0,
|
||||
Currency: e.currency || 'USD',
|
||||
'Last Updated': e.lastUpdated ? new Date(e.lastUpdated).toLocaleDateString() : '',
|
||||
}))
|
||||
const detailSheet = XLSX.utils.json_to_sheet(detailData)
|
||||
|
||||
// Format currency columns
|
||||
const range = XLSX.utils.decode_range(detailSheet['!ref'] || 'A1')
|
||||
for (let C = 3; C <= 10; ++C) {
|
||||
for (let R = 1; R <= range.e.r; ++R) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
|
||||
if (!detailSheet[cellAddress]) continue
|
||||
detailSheet[cellAddress].z = '$#,##0.00'
|
||||
}
|
||||
}
|
||||
|
||||
// Set column widths
|
||||
detailSheet['!cols'] = [
|
||||
{ wch: 15 }, // Region
|
||||
{ wch: 20 }, // Entity
|
||||
{ wch: 15 }, // Category
|
||||
{ wch: 15 }, // Monthly
|
||||
{ wch: 15 }, // Annual
|
||||
{ wch: 15 }, // Compute
|
||||
{ wch: 15 }, // Storage
|
||||
{ wch: 15 }, // Network
|
||||
{ wch: 15 }, // Licenses
|
||||
{ wch: 15 }, // Personnel
|
||||
{ wch: 10 }, // Currency
|
||||
{ wch: 12 }, // Last Updated
|
||||
]
|
||||
|
||||
XLSX.utils.book_append_sheet(workbook, detailSheet, 'Detailed Breakdown')
|
||||
|
||||
// By region sheet
|
||||
const byRegion = estimates.reduce((acc, e) => {
|
||||
acc[e.region] = (acc[e.region] || 0) + e.annual
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
const regionData = Object.entries(byRegion)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([region, annual]) => ({
|
||||
Region: region,
|
||||
'Annual Cost (USD)': annual,
|
||||
'Monthly Cost (USD)': annual / 12,
|
||||
}))
|
||||
const regionSheet = XLSX.utils.json_to_sheet(regionData)
|
||||
|
||||
// Format currency columns
|
||||
const regionRange = XLSX.utils.decode_range(regionSheet['!ref'] || 'A1')
|
||||
for (let C = 1; C <= 2; ++C) {
|
||||
for (let R = 1; R <= regionRange.e.r; ++R) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
|
||||
if (!regionSheet[cellAddress] || R === 0) continue
|
||||
regionSheet[cellAddress].z = '$#,##0.00'
|
||||
}
|
||||
}
|
||||
|
||||
XLSX.utils.book_append_sheet(workbook, regionSheet, 'By Region')
|
||||
|
||||
// By category sheet
|
||||
const byCategory = estimates.reduce((acc, e) => {
|
||||
acc[e.category] = (acc[e.category] || 0) + e.annual
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
const categoryData = Object.entries(byCategory)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([category, annual]) => ({
|
||||
Category: category,
|
||||
'Annual Cost (USD)': annual,
|
||||
'Monthly Cost (USD)': annual / 12,
|
||||
}))
|
||||
const categorySheet = XLSX.utils.json_to_sheet(categoryData)
|
||||
|
||||
// Format currency columns
|
||||
const categoryRange = XLSX.utils.decode_range(categorySheet['!ref'] || 'A1')
|
||||
for (let C = 1; C <= 2; ++C) {
|
||||
for (let R = 1; R <= categoryRange.e.r; ++R) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
|
||||
if (!categorySheet[cellAddress] || R === 0) continue
|
||||
categorySheet[cellAddress].z = '$#,##0.00'
|
||||
}
|
||||
}
|
||||
|
||||
XLSX.utils.book_append_sheet(workbook, categorySheet, 'By Category')
|
||||
|
||||
// Save workbook
|
||||
XLSX.writeFile(
|
||||
workbook,
|
||||
`cost-estimates-${selectedRegion}-${selectedEntity}-${Date.now()}.xlsx`
|
||||
)
|
||||
|
||||
toast({
|
||||
title: 'Export successful',
|
||||
description: 'Cost estimates exported as Excel',
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
variant: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const totalAnnual = useMemo(
|
||||
() => estimates.reduce((sum, e) => sum + e.annual, 0),
|
||||
[estimates]
|
||||
)
|
||||
const totalMonthly = useMemo(
|
||||
() => estimates.reduce((sum, e) => sum + e.monthly, 0),
|
||||
[estimates]
|
||||
)
|
||||
|
||||
const { barChartOption, pieChartOption } = useMemo(
|
||||
() => generateCostCharts(estimates),
|
||||
[estimates]
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Cost Estimates</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
View and manage cost estimates by region, entity, and category
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="h-[400px] flex items-center justify-center">
|
||||
<div className="text-studio-medium">Loading cost estimates...</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Error Loading Cost Estimates</CardTitle>
|
||||
<CardDescription>{error.message}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Cost Estimates</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
View and manage cost estimates by region, entity, and category
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleExportExcel} disabled={!estimates.length}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export Excel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Monthly</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-studio-light">
|
||||
${(totalMonthly / 1000).toFixed(0)}K
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Annual</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-studio-light">
|
||||
${(totalAnnual / 1000000).toFixed(1)}M
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Estimates</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-studio-light">{estimates.length}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Cost Visualizations</CardTitle>
|
||||
<CardDescription>Charts showing cost breakdown</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<RegionSelector
|
||||
value={selectedRegion}
|
||||
onChange={setSelectedRegion}
|
||||
className="w-48"
|
||||
/>
|
||||
<EntitySelector
|
||||
value={selectedEntity}
|
||||
onChange={setSelectedEntity}
|
||||
className="w-64"
|
||||
/>
|
||||
<EditModeToggle enabled={editMode} onToggle={setEditMode} />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<ReactECharts option={barChartOption} style={{ height: '400px' }} />
|
||||
</div>
|
||||
<div>
|
||||
<ReactECharts option={pieChartOption} style={{ height: '400px' }} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Detailed Breakdown</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Region</TableHead>
|
||||
<TableHead>Entity</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Monthly</TableHead>
|
||||
<TableHead>Annual</TableHead>
|
||||
<TableHead>Breakdown</TableHead>
|
||||
{editMode && <TableHead>Actions</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{estimates.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={editMode ? 7 : 6} className="text-center">
|
||||
<EmptyState
|
||||
title="No cost estimates found"
|
||||
description="No cost estimates match your current filters."
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
estimates.map((estimate, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="font-medium">{estimate.region}</TableCell>
|
||||
<TableCell>{estimate.entity}</TableCell>
|
||||
<TableCell>{estimate.category}</TableCell>
|
||||
<TableCell>${(estimate.monthly / 1000).toFixed(0)}K</TableCell>
|
||||
<TableCell>${(estimate.annual / 1000).toFixed(0)}K</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-studio-medium">
|
||||
Compute: ${((estimate.breakdown.compute || 0) / 1000).toFixed(0)}K
|
||||
<br />
|
||||
Storage: ${((estimate.breakdown.storage || 0) / 1000).toFixed(0)}K
|
||||
<br />
|
||||
Network: ${((estimate.breakdown.network || 0) / 1000).toFixed(0)}K
|
||||
</div>
|
||||
</TableCell>
|
||||
{editMode && (
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditingEstimate(estimate)
|
||||
setDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{estimates.length > 0 && (
|
||||
<CostForecast estimates={estimates} months={12} />
|
||||
)}
|
||||
|
||||
{editingEstimate && (
|
||||
<EditCostEstimateForm
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen(open)
|
||||
if (!open) setEditingEstimate(null)
|
||||
}}
|
||||
estimate={editingEstimate}
|
||||
onSuccess={() => {
|
||||
setEditingEstimate(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
165
src/components/infrastructure/CostForecast.tsx
Normal file
165
src/components/infrastructure/CostForecast.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import type { CostEstimate } from '@/lib/types/infrastructure'
|
||||
|
||||
interface CostForecastProps {
|
||||
estimates: CostEstimate[]
|
||||
months: number
|
||||
}
|
||||
|
||||
export function CostForecast({ estimates, months = 12 }: CostForecastProps) {
|
||||
const forecast = useMemo(() => {
|
||||
if (estimates.length === 0) return null
|
||||
|
||||
// Calculate average growth rate from historical data
|
||||
const sortedByDate = estimates
|
||||
.filter((e) => e.lastUpdated)
|
||||
.sort((a, b) => new Date(a.lastUpdated!).getTime() - new Date(b.lastUpdated!).getTime())
|
||||
|
||||
let growthRate = 0.02 // Default 2% monthly growth
|
||||
if (sortedByDate.length >= 2) {
|
||||
const first = sortedByDate[0].monthly
|
||||
const last = sortedByDate[sortedByDate.length - 1].monthly
|
||||
if (first > 0) {
|
||||
growthRate = (last - first) / first / sortedByDate.length
|
||||
}
|
||||
}
|
||||
|
||||
// Generate forecast
|
||||
const currentTotal = estimates.reduce((sum, e) => sum + e.monthly, 0)
|
||||
const forecastData = []
|
||||
const dates = []
|
||||
|
||||
for (let i = 0; i < months; i++) {
|
||||
const date = new Date()
|
||||
date.setMonth(date.getMonth() + i)
|
||||
dates.push(date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }))
|
||||
forecastData.push(currentTotal * Math.pow(1 + growthRate, i))
|
||||
}
|
||||
|
||||
// Calculate confidence intervals (±10%)
|
||||
const upper = forecastData.map((v) => v * 1.1)
|
||||
const lower = forecastData.map((v) => v * 0.9)
|
||||
|
||||
return {
|
||||
dates,
|
||||
forecast: forecastData,
|
||||
upper,
|
||||
lower,
|
||||
growthRate,
|
||||
}
|
||||
}, [estimates, months])
|
||||
|
||||
if (!forecast) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cost Forecast</CardTitle>
|
||||
<CardDescription>Insufficient data for forecasting</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const chartOption = {
|
||||
title: {
|
||||
text: `Cost Forecast (${months} months)`,
|
||||
textStyle: { color: '#e5e7eb' },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
const [forecast, upper, lower] = params
|
||||
return `
|
||||
<div>
|
||||
<strong>${forecast.name}</strong><br/>
|
||||
Forecast: $${(forecast.value / 1000).toFixed(0)}K<br/>
|
||||
Range: $${(lower.value / 1000).toFixed(0)}K - $${(upper.value / 1000).toFixed(0)}K
|
||||
</div>
|
||||
`
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['Forecast', 'Upper Bound', 'Lower Bound'],
|
||||
textStyle: { color: '#9ca3af' },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: forecast.dates,
|
||||
axisLabel: { color: '#9ca3af' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#9ca3af',
|
||||
formatter: (value: number) => `$${(value / 1000).toFixed(0)}K`,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Forecast',
|
||||
type: 'line',
|
||||
data: forecast.forecast,
|
||||
smooth: true,
|
||||
itemStyle: { color: '#3b82f6' },
|
||||
},
|
||||
{
|
||||
name: 'Upper Bound',
|
||||
type: 'line',
|
||||
data: forecast.upper,
|
||||
smooth: true,
|
||||
lineStyle: { type: 'dashed', color: '#10b981' },
|
||||
itemStyle: { opacity: 0 },
|
||||
},
|
||||
{
|
||||
name: 'Lower Bound',
|
||||
type: 'line',
|
||||
data: forecast.lower,
|
||||
smooth: true,
|
||||
lineStyle: { type: 'dashed', color: '#ef4444' },
|
||||
itemStyle: { opacity: 0 },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(59, 130, 246, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(59, 130, 246, 0.1)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Cost Forecast</CardTitle>
|
||||
<CardDescription>
|
||||
Projected costs based on historical trends
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{forecast.growthRate > 0 ? '+' : ''}
|
||||
{(forecast.growthRate * 100).toFixed(1)}% monthly growth
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReactECharts option={chartOption} style={{ height: '400px' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
684
src/components/infrastructure/DeploymentTimeline.tsx
Normal file
684
src/components/infrastructure/DeploymentTimeline.tsx
Normal file
@@ -0,0 +1,684 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'
|
||||
import { useDeploymentMilestones } from '@/lib/hooks/useInfrastructureData'
|
||||
import { useMutation } from '@apollo/client'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { RegionSelector } from './RegionSelector'
|
||||
import { EntitySelector } from './EntitySelector'
|
||||
import { EditModeToggle } from './EditModeToggle'
|
||||
import { EditMilestoneForm } from './forms/EditMilestoneForm'
|
||||
import { Download, Plus, GripVertical, HelpCircle } from 'lucide-react'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { KeyboardShortcuts, useKeyboardShortcuts } from './KeyboardShortcuts'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import jsPDF from 'jspdf'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { UPDATE_DEPLOYMENT_MILESTONE } from '@/lib/graphql/queries/infrastructure'
|
||||
import type { DeploymentMilestone } from '@/lib/types/infrastructure'
|
||||
|
||||
const priorityColors: Record<string, string> = {
|
||||
Critical: 'bg-red-500/20 text-red-400',
|
||||
High: 'bg-orange-500/20 text-orange-400',
|
||||
Medium: 'bg-yellow-500/20 text-yellow-400',
|
||||
Low: 'bg-blue-500/20 text-blue-400',
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
Planned: 'bg-gray-500/20 text-gray-400',
|
||||
'In Progress': 'bg-blue-500/20 text-blue-400',
|
||||
Complete: 'bg-green-500/20 text-green-400',
|
||||
Blocked: 'bg-red-500/20 text-red-400',
|
||||
}
|
||||
|
||||
// Sortable milestone item component
|
||||
function SortableMilestoneItem({
|
||||
milestone,
|
||||
editMode,
|
||||
onEdit,
|
||||
}: {
|
||||
milestone: DeploymentMilestone
|
||||
editMode: boolean
|
||||
onEdit: () => void
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: milestone.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="border border-studio-medium rounded-lg p-4 hover:border-studio-light transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
{editMode && (
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="mr-2 cursor-grab active:cursor-grabbing text-studio-medium hover:text-studio-light"
|
||||
>
|
||||
<GripVertical className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
<h3 className="font-semibold text-studio-light inline">{milestone.title}</h3>
|
||||
<p className="text-sm text-studio-medium mt-1">{milestone.description}</p>
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<Badge className={priorityColors[milestone.priority] || ''}>
|
||||
{milestone.priority}
|
||||
</Badge>
|
||||
<Badge className={statusColors[milestone.status] || ''}>
|
||||
{milestone.status}
|
||||
</Badge>
|
||||
<span className="text-sm text-studio-medium">
|
||||
{milestone.region} • {milestone.entity}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-studio-medium">
|
||||
{new Date(milestone.startDate).toLocaleDateString()} -{' '}
|
||||
{new Date(milestone.endDate).toLocaleDateString()}
|
||||
</div>
|
||||
{milestone.cost && (
|
||||
<div className="text-sm font-semibold text-studio-light mt-1">
|
||||
${(milestone.cost / 1000).toFixed(0)}K
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{editMode && (
|
||||
<div className="ml-4">
|
||||
<Button size="sm" variant="outline" onClick={onEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function generateGanttData(milestones: DeploymentMilestone[]) {
|
||||
const data = milestones.map((m) => ({
|
||||
name: m.title,
|
||||
value: [
|
||||
m.startDate,
|
||||
m.endDate,
|
||||
m.status,
|
||||
m.priority,
|
||||
m.region,
|
||||
],
|
||||
}))
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
formatter: (params: any) => {
|
||||
const [start, end, status, priority, region] = params.value
|
||||
return `
|
||||
<div>
|
||||
<strong>${params.name}</strong><br/>
|
||||
Region: ${region}<br/>
|
||||
Priority: ${priority}<br/>
|
||||
Status: ${status}<br/>
|
||||
Start: ${new Date(start).toLocaleDateString()}<br/>
|
||||
End: ${new Date(end).toLocaleDateString()}
|
||||
</div>
|
||||
`
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
top: '10%',
|
||||
bottom: '10%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: milestones.map((m) => m.title),
|
||||
inverse: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'custom',
|
||||
renderItem: (params: any, api: any) => {
|
||||
const categoryIndex = api.value(4)
|
||||
const start = api.coord([api.value(0), categoryIndex])
|
||||
const end = api.coord([api.value(1), categoryIndex])
|
||||
const height = api.size([0, 1])[1] * 0.6
|
||||
|
||||
return {
|
||||
type: 'rect',
|
||||
shape: {
|
||||
x: start[0],
|
||||
y: start[1] - height / 2,
|
||||
width: end[0] - start[0],
|
||||
height: height,
|
||||
},
|
||||
style: {
|
||||
fill: statusColors[api.value(2)]?.split(' ')[0] || '#6b7280',
|
||||
},
|
||||
}
|
||||
},
|
||||
data: data,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function DeploymentTimeline() {
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>('All')
|
||||
const [selectedEntity, setSelectedEntity] = useState<string>('All')
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('All')
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [editingMilestone, setEditingMilestone] = useState<DeploymentMilestone | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
||||
const [localMilestones, setLocalMilestones] = useState<DeploymentMilestone[]>([])
|
||||
const [shortcutsOpen, setShortcutsOpen] = useState(false)
|
||||
const ganttChartRef = useRef<HTMLDivElement>(null)
|
||||
const { toast } = useToast()
|
||||
|
||||
// Keyboard shortcuts
|
||||
useKeyboardShortcuts([
|
||||
{
|
||||
keys: ['Ctrl', 'e'],
|
||||
handler: () => setEditMode(!editMode),
|
||||
description: 'Toggle edit mode',
|
||||
},
|
||||
{
|
||||
keys: ['Ctrl', 'n'],
|
||||
handler: () => {
|
||||
if (editMode) {
|
||||
setCreateDialogOpen(true)
|
||||
}
|
||||
},
|
||||
description: 'Create milestone',
|
||||
},
|
||||
{
|
||||
keys: ['Escape'],
|
||||
handler: () => {
|
||||
setEditMode(false)
|
||||
setDialogOpen(false)
|
||||
setCreateDialogOpen(false)
|
||||
},
|
||||
description: 'Cancel/Exit',
|
||||
},
|
||||
{
|
||||
keys: ['Ctrl', '/'],
|
||||
handler: () => setShortcutsOpen(true),
|
||||
description: 'Show shortcuts',
|
||||
},
|
||||
])
|
||||
|
||||
const [updateMilestone, { loading: updating }] = useMutation(UPDATE_DEPLOYMENT_MILESTONE, {
|
||||
refetchQueries: ['GetDeploymentMilestones'],
|
||||
})
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
)
|
||||
|
||||
// Sync local milestones with fetched data
|
||||
useEffect(() => {
|
||||
if (milestones.length > 0) {
|
||||
setLocalMilestones(milestones)
|
||||
}
|
||||
}, [milestones])
|
||||
|
||||
const filter = {
|
||||
region: selectedRegion === 'All' ? undefined : selectedRegion,
|
||||
entity: selectedEntity === 'All' ? undefined : selectedEntity,
|
||||
status: selectedStatus === 'All' ? undefined : selectedStatus,
|
||||
}
|
||||
|
||||
const { milestones, loading, error } = useDeploymentMilestones(filter)
|
||||
|
||||
const handleDragEnd = useCallback(async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
|
||||
if (!over || active.id === over.id) return
|
||||
|
||||
const oldIndex = localMilestones.findIndex((m) => m.id === active.id)
|
||||
const newIndex = localMilestones.findIndex((m) => m.id === over.id)
|
||||
|
||||
if (oldIndex === -1 || newIndex === -1) return
|
||||
|
||||
// Calculate new dates based on position
|
||||
const movedMilestone = localMilestones[oldIndex]
|
||||
const referenceMilestone = localMilestones[newIndex]
|
||||
|
||||
// Calculate duration
|
||||
const duration = new Date(movedMilestone.endDate).getTime() - new Date(movedMilestone.startDate).getTime()
|
||||
|
||||
// Set new dates based on reference milestone
|
||||
let newStartDate: Date
|
||||
let newEndDate: Date
|
||||
|
||||
if (newIndex > oldIndex) {
|
||||
// Moving down - start after reference end date
|
||||
newStartDate = new Date(referenceMilestone.endDate)
|
||||
newStartDate.setDate(newStartDate.getDate() + 1) // Add 1 day gap
|
||||
} else {
|
||||
// Moving up - start before reference start date
|
||||
newStartDate = new Date(referenceMilestone.startDate)
|
||||
newStartDate.setDate(newStartDate.getDate() - Math.ceil(duration / (1000 * 60 * 60 * 24)) - 1) // Subtract duration + 1 day gap
|
||||
}
|
||||
|
||||
newEndDate = new Date(newStartDate.getTime() + duration)
|
||||
|
||||
// Validate dates
|
||||
if (newStartDate < new Date()) {
|
||||
toast({
|
||||
title: 'Invalid date',
|
||||
description: 'Cannot move milestone to the past',
|
||||
variant: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Update milestone dates
|
||||
try {
|
||||
await updateMilestone({
|
||||
variables: {
|
||||
id: movedMilestone.id,
|
||||
input: {
|
||||
startDate: newStartDate.toISOString(),
|
||||
endDate: newEndDate.toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Update local state
|
||||
const reordered = arrayMove(localMilestones, oldIndex, newIndex)
|
||||
const updated = reordered.map((m) =>
|
||||
m.id === movedMilestone.id
|
||||
? {
|
||||
...m,
|
||||
startDate: newStartDate.toISOString(),
|
||||
endDate: newEndDate.toISOString(),
|
||||
}
|
||||
: m
|
||||
)
|
||||
setLocalMilestones(updated)
|
||||
|
||||
toast({
|
||||
title: 'Milestone rescheduled',
|
||||
description: 'Dates updated successfully',
|
||||
variant: 'success',
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Update failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to update milestone dates',
|
||||
variant: 'error',
|
||||
})
|
||||
}
|
||||
}, [localMilestones, updateMilestone, toast])
|
||||
|
||||
const displayMilestones = localMilestones.length > 0 ? localMilestones : milestones
|
||||
|
||||
const handleExportPDF = async () => {
|
||||
if (!ganttChartRef.current || milestones.length === 0) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: 'No data to export',
|
||||
variant: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setExporting(true)
|
||||
try {
|
||||
const pdf = new jsPDF('landscape', 'mm', 'a4')
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
const pageHeight = pdf.internal.pageSize.getHeight()
|
||||
|
||||
// Page 1: Title and metadata
|
||||
pdf.setFontSize(20)
|
||||
pdf.text('Deployment Timeline', 20, 20)
|
||||
pdf.setFontSize(12)
|
||||
pdf.text(`Region: ${selectedRegion}`, 20, 30)
|
||||
pdf.text(`Entity: ${selectedEntity}`, 20, 35)
|
||||
pdf.text(`Status: ${selectedStatus}`, 20, 40)
|
||||
pdf.text(`Export Date: ${new Date().toLocaleDateString()}`, 20, 45)
|
||||
pdf.text(`Total Milestones: ${milestones.length}`, 20, 50)
|
||||
|
||||
// Page 2: Gantt chart
|
||||
const chartElement = ganttChartRef.current.querySelector('.echarts-for-react')
|
||||
if (chartElement) {
|
||||
const canvas = await html2canvas(chartElement as HTMLElement, {
|
||||
backgroundColor: '#000000',
|
||||
scale: 2,
|
||||
logging: false,
|
||||
})
|
||||
const imgData = canvas.toDataURL('image/png')
|
||||
pdf.addPage()
|
||||
const imgWidth = pageWidth - 20
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width
|
||||
pdf.addImage(imgData, 'PNG', 10, 10, imgWidth, Math.min(imgHeight, pageHeight - 20))
|
||||
}
|
||||
|
||||
// Page 3: Milestone list
|
||||
pdf.addPage()
|
||||
pdf.setFontSize(16)
|
||||
pdf.text('Milestone List', 20, 20)
|
||||
|
||||
let y = 30
|
||||
milestones.forEach((milestone, index) => {
|
||||
if (y > pageHeight - 30) {
|
||||
pdf.addPage()
|
||||
y = 20
|
||||
}
|
||||
|
||||
pdf.setFontSize(12)
|
||||
pdf.text(`${index + 1}. ${milestone.title}`, 20, y)
|
||||
pdf.setFontSize(10)
|
||||
pdf.text(
|
||||
`Status: ${milestone.status} | Priority: ${milestone.priority} | Region: ${milestone.region}`,
|
||||
20,
|
||||
y + 5
|
||||
)
|
||||
pdf.text(
|
||||
`Dates: ${new Date(milestone.startDate).toLocaleDateString()} - ${new Date(
|
||||
milestone.endDate
|
||||
).toLocaleDateString()}`,
|
||||
20,
|
||||
y + 10
|
||||
)
|
||||
if (milestone.cost) {
|
||||
pdf.text(`Cost: $${(milestone.cost / 1000).toFixed(0)}K`, 20, y + 15)
|
||||
y += 20
|
||||
} else {
|
||||
y += 15
|
||||
}
|
||||
})
|
||||
|
||||
// Save PDF
|
||||
pdf.save(`deployment-timeline-${selectedRegion}-${selectedEntity}-${Date.now()}.pdf`)
|
||||
|
||||
toast({
|
||||
title: 'Export successful',
|
||||
description: 'Timeline exported as PDF',
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
variant: 'error',
|
||||
})
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Deployment Timeline</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
Manage infrastructure deployment milestones and schedules
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="h-[600px] flex items-center justify-center">
|
||||
<div className="text-studio-medium">Loading timeline...</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Error Loading Timeline</CardTitle>
|
||||
<CardDescription>{error.message}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const ganttOptions = generateGanttData(displayMilestones)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Deployment Timeline</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
Manage infrastructure deployment milestones and schedules
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleExportPDF} disabled={exporting || !milestones.length}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{exporting ? 'Exporting...' : 'Export PDF'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShortcutsOpen(true)}>
|
||||
<HelpCircle className="h-4 w-4 mr-2" />
|
||||
Shortcuts
|
||||
</Button>
|
||||
{editMode && (
|
||||
<Button onClick={() => setCreateDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Milestone
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Gantt Chart</CardTitle>
|
||||
<CardDescription>
|
||||
{milestones.length} milestone{milestones.length !== 1 ? 's' : ''} found
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<RegionSelector
|
||||
value={selectedRegion}
|
||||
onChange={setSelectedRegion}
|
||||
className="w-48"
|
||||
/>
|
||||
<EntitySelector
|
||||
value={selectedEntity}
|
||||
onChange={setSelectedEntity}
|
||||
className="w-64"
|
||||
/>
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className="h-10 rounded-md border border-studio-medium bg-studio-dark px-3 text-sm"
|
||||
>
|
||||
<option value="All">All Status</option>
|
||||
<option value="Planned">Planned</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="Complete">Complete</option>
|
||||
<option value="Blocked">Blocked</option>
|
||||
</select>
|
||||
<EditModeToggle enabled={editMode} onToggle={setEditMode} />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div ref={ganttChartRef} id="gantt-chart">
|
||||
{displayMilestones.length > 0 ? (
|
||||
<ReactECharts option={ganttOptions} style={{ height: '600px' }} />
|
||||
) : (
|
||||
<EmptyState
|
||||
title="No milestones found"
|
||||
description="No milestones match your current filters."
|
||||
action={
|
||||
editMode
|
||||
? {
|
||||
label: 'Create Milestone',
|
||||
onClick: () => setCreateDialogOpen(true),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Milestone List</CardTitle>
|
||||
{editMode && (
|
||||
<CardDescription>
|
||||
Drag milestones to reschedule. Dates will be automatically updated.
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{editMode ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={displayMilestones.map((m) => m.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{displayMilestones.map((milestone) => (
|
||||
<SortableMilestoneItem
|
||||
key={milestone.id}
|
||||
milestone={milestone}
|
||||
editMode={editMode}
|
||||
onEdit={() => {
|
||||
setEditingMilestone(milestone)
|
||||
setDialogOpen(true)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{displayMilestones.map((milestone) => (
|
||||
<div
|
||||
key={milestone.id}
|
||||
className="border border-studio-medium rounded-lg p-4 hover:border-studio-light transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-studio-light">{milestone.title}</h3>
|
||||
<p className="text-sm text-studio-medium mt-1">{milestone.description}</p>
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<Badge className={priorityColors[milestone.priority] || ''}>
|
||||
{milestone.priority}
|
||||
</Badge>
|
||||
<Badge className={statusColors[milestone.status] || ''}>
|
||||
{milestone.status}
|
||||
</Badge>
|
||||
<span className="text-sm text-studio-medium">
|
||||
{milestone.region} • {milestone.entity}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-studio-medium">
|
||||
{new Date(milestone.startDate).toLocaleDateString()} -{' '}
|
||||
{new Date(milestone.endDate).toLocaleDateString()}
|
||||
</div>
|
||||
{milestone.cost && (
|
||||
<div className="text-sm font-semibold text-studio-light mt-1">
|
||||
${(milestone.cost / 1000).toFixed(0)}K
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{editingMilestone && (
|
||||
<EditMilestoneForm
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen(open)
|
||||
if (!open) setEditingMilestone(null)
|
||||
}}
|
||||
milestone={editingMilestone}
|
||||
onSuccess={() => {
|
||||
setEditingMilestone(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditMilestoneForm
|
||||
open={createDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setCreateDialogOpen(open)
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setCreateDialogOpen(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
<KeyboardShortcuts
|
||||
open={shortcutsOpen}
|
||||
onOpenChange={setShortcutsOpen}
|
||||
shortcuts={[
|
||||
{ keys: ['Ctrl', 'E'], description: 'Toggle edit mode', category: 'General' },
|
||||
{ keys: ['Ctrl', 'N'], description: 'Create milestone', category: 'General' },
|
||||
{ keys: ['Escape'], description: 'Cancel/Exit', category: 'General' },
|
||||
{ keys: ['Ctrl', '/'], description: 'Show shortcuts', category: 'General' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
188
src/components/infrastructure/DocsDashboard.tsx
Normal file
188
src/components/infrastructure/DocsDashboard.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useInfrastructureSummary } from '@/lib/hooks/useInfrastructureData'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GlobalSearch } from './GlobalSearch'
|
||||
import Link from 'next/link'
|
||||
import { Network2, Shield, Calendar, DollarSign, Search } from 'lucide-react'
|
||||
|
||||
export function DocsDashboard() {
|
||||
const { summary, loading, error } = useInfrastructureSummary()
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: 'Network Topology',
|
||||
description: 'View and edit regional network topology diagrams',
|
||||
icon: Network2,
|
||||
href: '/infrastructure/docs/topology',
|
||||
color: 'text-blue-400',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
stats: summary
|
||||
? {
|
||||
label: 'Regions',
|
||||
value: summary.totalRegions,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
{
|
||||
title: 'Compliance Mapping',
|
||||
description: 'Track compliance requirements by country and region',
|
||||
icon: Shield,
|
||||
href: '/infrastructure/docs/compliance',
|
||||
color: 'text-green-400',
|
||||
bgColor: 'bg-green-500/10',
|
||||
stats: summary
|
||||
? {
|
||||
label: 'Countries',
|
||||
value: summary.totalCountries,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
{
|
||||
title: 'Deployment Timeline',
|
||||
description: 'Manage infrastructure deployment milestones and schedules',
|
||||
icon: Calendar,
|
||||
href: '/infrastructure/docs/timeline',
|
||||
color: 'text-purple-400',
|
||||
bgColor: 'bg-purple-500/10',
|
||||
stats: summary
|
||||
? {
|
||||
label: 'In Progress',
|
||||
value: summary.deploymentProgress.inProgress,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
{
|
||||
title: 'Cost Estimates',
|
||||
description: 'View and manage cost estimates by region and category',
|
||||
icon: DollarSign,
|
||||
href: '/infrastructure/docs/costs',
|
||||
color: 'text-yellow-400',
|
||||
bgColor: 'bg-yellow-500/10',
|
||||
stats: summary
|
||||
? {
|
||||
label: 'Total Annual',
|
||||
value: `$${(summary.totalCost / 1000000).toFixed(1)}M`,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<Card key={card.title} className="animate-pulse">
|
||||
<CardHeader>
|
||||
<div className="h-6 w-32 bg-studio-medium rounded" />
|
||||
<div className="h-4 w-48 bg-studio-medium rounded mt-2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-8 w-16 bg-studio-medium rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Error Loading Dashboard</CardTitle>
|
||||
<CardDescription>{error.message}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Infrastructure Documentation</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
Manage network topology, compliance, deployment timelines, and cost estimates
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setSearchOpen(true)}>
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<Link key={card.title} href={card.href} className="block">
|
||||
<Card className="hover:border-studio-light transition-colors cursor-pointer h-full">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<Icon className={`h-8 w-8 ${card.color}`} />
|
||||
<div className={`h-12 w-12 rounded-lg ${card.bgColor} flex items-center justify-center`}>
|
||||
<Icon className={`h-6 w-6 ${card.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
<CardTitle className="mt-4">{card.title}</CardTitle>
|
||||
<CardDescription>{card.description}</CardDescription>
|
||||
</CardHeader>
|
||||
{card.stats && (
|
||||
<CardContent>
|
||||
<div className="mt-4">
|
||||
<div className="text-2xl font-bold text-studio-light">{card.stats.value}</div>
|
||||
<div className="text-sm text-studio-medium">{card.stats.label}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{summary && (
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Progress</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-studio-medium">Planned</span>
|
||||
<span className="font-semibold">{summary.deploymentProgress.planned}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-studio-medium">In Progress</span>
|
||||
<span className="font-semibold text-yellow-400">
|
||||
{summary.deploymentProgress.inProgress}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-studio-medium">Complete</span>
|
||||
<span className="font-semibold text-green-400">
|
||||
{summary.deploymentProgress.complete}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-studio-medium">Blocked</span>
|
||||
<span className="font-semibold text-red-400">
|
||||
{summary.deploymentProgress.blocked}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GlobalSearch open={searchOpen} onOpenChange={setSearchOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
22
src/components/infrastructure/EditModeToggle.tsx
Normal file
22
src/components/infrastructure/EditModeToggle.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface EditModeToggleProps {
|
||||
enabled: boolean
|
||||
onToggle: (enabled: boolean) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function EditModeToggle({ enabled, onToggle, className }: EditModeToggleProps) {
|
||||
return (
|
||||
<div className={`flex items-center space-x-2 ${className}`}>
|
||||
<Switch id="edit-mode" checked={enabled} onCheckedChange={onToggle} />
|
||||
<Label htmlFor="edit-mode" className="cursor-pointer">
|
||||
Edit Mode
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
50
src/components/infrastructure/EmptyState.tsx
Normal file
50
src/components/infrastructure/EmptyState.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Inbox, Plus, Search } from 'lucide-react'
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string
|
||||
description: string
|
||||
icon?: React.ReactNode
|
||||
action?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
secondaryAction?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
action,
|
||||
secondaryAction,
|
||||
}: EmptyStateProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12 px-6">
|
||||
<div className="mb-4 text-studio-medium">
|
||||
{icon || <Inbox className="h-12 w-12" />}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-studio-light mb-2">{title}</h3>
|
||||
<p className="text-sm text-studio-medium text-center max-w-md mb-6">{description}</p>
|
||||
{action && (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={action.onClick}>{action.label}</Button>
|
||||
{secondaryAction && (
|
||||
<Button variant="outline" onClick={secondaryAction.onClick}>
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
40
src/components/infrastructure/EntitySelector.tsx
Normal file
40
src/components/infrastructure/EntitySelector.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
|
||||
interface EntitySelectorProps {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const entities = [
|
||||
'Sovereign Order of Hospitallers',
|
||||
'Solace Bank Group LTD',
|
||||
'TAJ Private Single Family Trust Company',
|
||||
'Mann Li Family Office LPBC',
|
||||
'Organisation Mondiale Du Numerique',
|
||||
'Elemental Imperium LPBC',
|
||||
'Aseret Mortgage Bank',
|
||||
'Digital Bank of International Settlements',
|
||||
'International Criminal Courts of Commerce',
|
||||
'All',
|
||||
]
|
||||
|
||||
export function EntitySelector({ value, onChange, className }: EntitySelectorProps) {
|
||||
return (
|
||||
<Select value={value || 'All'} onValueChange={onChange}>
|
||||
<SelectTrigger className={className}>
|
||||
<SelectValue placeholder="Select entity" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{entities.map((entity) => (
|
||||
<SelectItem key={entity} value={entity}>
|
||||
{entity}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
191
src/components/infrastructure/GlobalSearch.tsx
Normal file
191
src/components/infrastructure/GlobalSearch.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import { useCountries, useNetworkTopologies, useComplianceRequirements, useDeploymentMilestones, useCostEstimates } from '@/lib/hooks/useInfrastructureData'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Search, X } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface SearchResult {
|
||||
type: 'country' | 'topology' | 'compliance' | 'milestone' | 'cost'
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
route: string
|
||||
}
|
||||
|
||||
export function GlobalSearch({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const { countries } = useCountries()
|
||||
const { topologies } = useNetworkTopologies()
|
||||
const { requirements } = useComplianceRequirements()
|
||||
const { milestones } = useDeploymentMilestones()
|
||||
const { estimates } = useCostEstimates()
|
||||
|
||||
const results = useMemo<SearchResult[]>(() => {
|
||||
if (!query.trim()) return []
|
||||
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const allResults: SearchResult[] = []
|
||||
|
||||
// Search countries
|
||||
countries
|
||||
.filter((c) => c.name.toLowerCase().includes(lowerQuery) || c.region.toLowerCase().includes(lowerQuery))
|
||||
.forEach((country) => {
|
||||
allResults.push({
|
||||
type: 'country',
|
||||
id: country.name,
|
||||
title: country.name,
|
||||
description: `${country.region} • ${country.relationshipType}`,
|
||||
route: `/infrastructure/docs/compliance?country=${encodeURIComponent(country.name)}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Search topologies
|
||||
topologies
|
||||
.filter((t) => t.region.toLowerCase().includes(lowerQuery) || t.entity.toLowerCase().includes(lowerQuery))
|
||||
.forEach((topology) => {
|
||||
allResults.push({
|
||||
type: 'topology',
|
||||
id: topology.id || 'default',
|
||||
title: `${topology.region} - ${topology.entity}`,
|
||||
description: `${topology.nodes.length} nodes, ${topology.edges.length} edges`,
|
||||
route: `/infrastructure/docs/topology?region=${encodeURIComponent(topology.region)}&entity=${encodeURIComponent(topology.entity)}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Search compliance
|
||||
requirements
|
||||
.filter((r) => r.country.toLowerCase().includes(lowerQuery) || r.frameworks.some((f) => f.toLowerCase().includes(lowerQuery)))
|
||||
.forEach((req) => {
|
||||
allResults.push({
|
||||
type: 'compliance',
|
||||
id: req.country,
|
||||
title: req.country,
|
||||
description: `${req.status} • ${req.frameworks.join(', ')}`,
|
||||
route: `/infrastructure/docs/compliance?country=${encodeURIComponent(req.country)}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Search milestones
|
||||
milestones
|
||||
.filter((m) => m.title.toLowerCase().includes(lowerQuery) || m.region.toLowerCase().includes(lowerQuery))
|
||||
.forEach((milestone) => {
|
||||
allResults.push({
|
||||
type: 'milestone',
|
||||
id: milestone.id,
|
||||
title: milestone.title,
|
||||
description: `${milestone.status} • ${milestone.region}`,
|
||||
route: `/infrastructure/docs/timeline?milestone=${encodeURIComponent(milestone.id)}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Search cost estimates
|
||||
estimates
|
||||
.filter((e) => e.region.toLowerCase().includes(lowerQuery) || e.entity.toLowerCase().includes(lowerQuery))
|
||||
.forEach((estimate) => {
|
||||
allResults.push({
|
||||
type: 'cost',
|
||||
id: `${estimate.region}-${estimate.entity}-${estimate.category}`,
|
||||
title: `${estimate.region} - ${estimate.category}`,
|
||||
description: `$${(estimate.annual / 1000).toFixed(0)}K annually`,
|
||||
route: `/infrastructure/docs/costs?region=${encodeURIComponent(estimate.region)}&entity=${encodeURIComponent(estimate.entity)}`,
|
||||
})
|
||||
})
|
||||
|
||||
return allResults.slice(0, 20) // Limit to 20 results
|
||||
}, [query, countries, topologies, requirements, milestones, estimates])
|
||||
|
||||
const handleResultClick = useCallback((result: SearchResult) => {
|
||||
router.push(result.route)
|
||||
onOpenChange(false)
|
||||
setQuery('')
|
||||
}, [router, onOpenChange])
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
country: 'bg-blue-500/20 text-blue-400',
|
||||
topology: 'bg-green-500/20 text-green-400',
|
||||
compliance: 'bg-yellow-500/20 text-yellow-400',
|
||||
milestone: 'bg-purple-500/20 text-purple-400',
|
||||
cost: 'bg-orange-500/20 text-orange-400',
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Global Search</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-studio-medium" />
|
||||
<Input
|
||||
placeholder="Search countries, topologies, compliance, milestones, costs..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
autoFocus
|
||||
/>
|
||||
{query && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute right-2 top-1"
|
||||
onClick={() => setQuery('')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{query && (
|
||||
<div className="max-h-96 overflow-y-auto space-y-1">
|
||||
{results.length === 0 ? (
|
||||
<div className="text-center py-8 text-studio-medium">
|
||||
No results found for "{query}"
|
||||
</div>
|
||||
) : (
|
||||
results.map((result) => (
|
||||
<button
|
||||
key={`${result.type}-${result.id}`}
|
||||
onClick={() => handleResultClick(result)}
|
||||
className="w-full text-left p-3 rounded-lg border border-studio-medium hover:border-studio-light hover:bg-studio-medium/20 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge className={typeColors[result.type] || ''}>
|
||||
{result.type}
|
||||
</Badge>
|
||||
<span className="font-semibold text-studio-light">{result.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-studio-medium">{result.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!query && (
|
||||
<div className="text-center py-8 text-studio-medium">
|
||||
Start typing to search across all infrastructure data...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
fallback?: React.ComponentType<{ error: Error; reset: () => void }>
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Error boundary component for infrastructure views
|
||||
* Catches errors in the component tree and displays a user-friendly error message
|
||||
*/
|
||||
export class InfrastructureErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false, error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Infrastructure error:', error, errorInfo)
|
||||
|
||||
// Log to Sentry if available
|
||||
if (typeof window !== 'undefined' && (window as any).Sentry) {
|
||||
;(window as any).Sentry.captureException(error, {
|
||||
contexts: {
|
||||
react: errorInfo,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.setState({ hasError: false, error: null })
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
const Fallback = this.props.fallback
|
||||
return <Fallback error={this.state.error!} reset={this.reset} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="m-4">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-red-400" />
|
||||
<CardTitle>Something went wrong</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
An error occurred while loading infrastructure data. Please try again or contact
|
||||
support if the problem persists.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||
<div className="mb-4 p-4 bg-red-950/20 border border-red-500/20 rounded-lg">
|
||||
<pre className="text-sm text-red-400 overflow-auto">
|
||||
{this.state.error.message}
|
||||
{'\n\n'}
|
||||
{this.state.error.stack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={this.reset} variant="outline">
|
||||
Try Again
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
102
src/components/infrastructure/KeyboardShortcuts.tsx
Normal file
102
src/components/infrastructure/KeyboardShortcuts.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface KeyboardShortcut {
|
||||
keys: string[]
|
||||
description: string
|
||||
category: string
|
||||
}
|
||||
|
||||
interface KeyboardShortcutsProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
shortcuts: KeyboardShortcut[]
|
||||
}
|
||||
|
||||
export function KeyboardShortcuts({ open, onOpenChange, shortcuts }: KeyboardShortcutsProps) {
|
||||
const grouped = shortcuts.reduce((acc, shortcut) => {
|
||||
if (!acc[shortcut.category]) {
|
||||
acc[shortcut.category] = []
|
||||
}
|
||||
acc[shortcut.category].push(shortcut)
|
||||
return acc
|
||||
}, {} as Record<string, KeyboardShortcut[]>)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Keyboard Shortcuts</DialogTitle>
|
||||
<DialogDescription>Available keyboard shortcuts for this view</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-6 mt-4">
|
||||
{Object.entries(grouped).map(([category, items]) => (
|
||||
<div key={category}>
|
||||
<h3 className="font-semibold text-studio-light mb-2">{category}</h3>
|
||||
<div className="space-y-2">
|
||||
{items.map((shortcut, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between py-2 border-b border-studio-medium">
|
||||
<span className="text-sm text-studio-medium">{shortcut.description}</span>
|
||||
<div className="flex gap-1">
|
||||
{shortcut.keys.map((key, keyIdx) => (
|
||||
<Badge key={keyIdx} variant="outline" className="font-mono text-xs">
|
||||
{key}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function useKeyboardShortcuts(
|
||||
shortcuts: Array<{
|
||||
keys: string[]
|
||||
handler: () => void
|
||||
description?: string
|
||||
}>
|
||||
) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase()
|
||||
const ctrl = e.ctrlKey || e.metaKey
|
||||
const shift = e.shiftKey
|
||||
const alt = e.altKey
|
||||
|
||||
for (const shortcut of shortcuts) {
|
||||
const matches = shortcut.keys.every((k) => {
|
||||
const lower = k.toLowerCase()
|
||||
if (lower === 'ctrl' || lower === 'cmd') return ctrl
|
||||
if (lower === 'shift') return shift
|
||||
if (lower === 'alt') return alt
|
||||
return key === lower
|
||||
})
|
||||
|
||||
if (matches) {
|
||||
e.preventDefault()
|
||||
shortcut.handler()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [shortcuts])
|
||||
}
|
||||
|
||||
86
src/components/infrastructure/MobileResponsiveWrapper.tsx
Normal file
86
src/components/infrastructure/MobileResponsiveWrapper.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
|
||||
interface MobileResponsiveWrapperProps {
|
||||
children: React.ReactNode
|
||||
sidebar?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MobileResponsiveWrapper({
|
||||
children,
|
||||
sidebar,
|
||||
className = '',
|
||||
}: MobileResponsiveWrapperProps) {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768)
|
||||
}
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [])
|
||||
|
||||
if (!sidebar) {
|
||||
return <div className={className}>{children}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex ${className}`}>
|
||||
{/* Mobile sidebar toggle */}
|
||||
{isMobile && (
|
||||
<div className="fixed top-4 left-4 z-50">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
>
|
||||
{sidebarOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
${isMobile ? 'fixed inset-y-0 left-0 z-40 transform' : 'relative'}
|
||||
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
transition-transform duration-300 ease-in-out
|
||||
${isMobile ? 'w-64 bg-studio-dark border-r border-studio-medium' : 'w-64'}
|
||||
`}
|
||||
>
|
||||
{sidebar}
|
||||
{isMobile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-4 right-4"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{/* Overlay for mobile */}
|
||||
{isMobile && sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-30"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<main className={`flex-1 ${isMobile ? 'w-full' : ''}`}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
709
src/components/infrastructure/NetworkTopologyDocs.tsx
Normal file
709
src/components/infrastructure/NetworkTopologyDocs.tsx
Normal file
@@ -0,0 +1,709 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'
|
||||
import { useNetworkTopologies } from '@/lib/hooks/useInfrastructureData'
|
||||
import { useMutation } from '@apollo/client'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RegionSelector } from './RegionSelector'
|
||||
import { EntitySelector } from './EntitySelector'
|
||||
import { EditModeToggle } from './EditModeToggle'
|
||||
import { EditTopologyNodeForm } from './forms/EditTopologyNodeForm'
|
||||
import { Download, Plus, Trash2, Undo, Redo, HelpCircle } from 'lucide-react'
|
||||
import { ReactFlowTopology } from './lazy'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { KeyboardShortcuts, useKeyboardShortcuts } from './KeyboardShortcuts'
|
||||
import { ConfirmDialog } from './ConfirmDialog'
|
||||
import { NodeDetailsPanel } from './NodeDetailsPanel'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { UPDATE_NETWORK_TOPOLOGY } from '@/lib/graphql/queries/infrastructure'
|
||||
import type { NetworkTopology, TopologyNode, TopologyEdge } from '@/lib/types/infrastructure'
|
||||
|
||||
// Use React Flow for topology visualization (fallback to SVG if React Flow fails)
|
||||
function TopologyVisualizationWrapper({
|
||||
topology,
|
||||
editMode,
|
||||
containerRef,
|
||||
onNodeDrag,
|
||||
onNodeClick,
|
||||
onNodeDelete,
|
||||
onEdgeDelete,
|
||||
selectedNodeId,
|
||||
connectingFrom,
|
||||
useReactFlow = true,
|
||||
}: {
|
||||
topology: NetworkTopology
|
||||
editMode: boolean
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
onNodeDrag?: (nodeId: string, x: number, y: number) => void
|
||||
onNodeClick?: (nodeId: string) => void
|
||||
onNodeDelete?: (nodeId: string) => void
|
||||
onEdgeDelete?: (edgeId: string) => void
|
||||
selectedNodeId?: string | null
|
||||
connectingFrom?: string | null
|
||||
useReactFlow?: boolean
|
||||
}) {
|
||||
if (useReactFlow) {
|
||||
return (
|
||||
<div ref={containerRef} id="topology-container">
|
||||
<ReactFlowTopology
|
||||
topology={topology}
|
||||
editMode={editMode}
|
||||
onNodesChange={(nodes) => {
|
||||
// Update local topology when nodes change
|
||||
const updatedNodes = nodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: node.type as any,
|
||||
label: node.data.label,
|
||||
region: node.data.region,
|
||||
entity: node.data.entity,
|
||||
position: node.position,
|
||||
metadata: node.data.metadata || {},
|
||||
}))
|
||||
// This will be handled by parent component
|
||||
}}
|
||||
onNodeClick={(node) => onNodeClick?.(node.id)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback to simple SVG visualization
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
id="topology-container"
|
||||
className="relative w-full h-[600px] border border-studio-medium rounded-lg bg-studio-black overflow-hidden"
|
||||
>
|
||||
<svg
|
||||
id="topology-svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 800 600"
|
||||
className="absolute inset-0"
|
||||
>
|
||||
{/* Simple SVG fallback */}
|
||||
{topology.edges.map((edge) => {
|
||||
const sourceNode = topology.nodes.find((n) => n.id === edge.source)
|
||||
const targetNode = topology.nodes.find((n) => n.id === edge.target)
|
||||
if (!sourceNode || !targetNode) return null
|
||||
return (
|
||||
<line
|
||||
key={edge.id}
|
||||
x1={sourceNode.position.x}
|
||||
y1={sourceNode.position.y}
|
||||
x2={targetNode.position.x}
|
||||
y2={targetNode.position.y}
|
||||
stroke="#3b82f6"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{topology.nodes.map((node) => (
|
||||
<circle
|
||||
key={node.id}
|
||||
cx={node.position.x}
|
||||
cy={node.position.y}
|
||||
r={20}
|
||||
fill="#3b82f6"
|
||||
onClick={() => onNodeClick?.(node.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NetworkTopologyDocs() {
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>('All')
|
||||
const [selectedEntity, setSelectedEntity] = useState<string>('All')
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [localTopology, setLocalTopology] = useState<NetworkTopology | null>(null)
|
||||
const [history, setHistory] = useState<NetworkTopology[]>([])
|
||||
const [historyIndex, setHistoryIndex] = useState(-1)
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
|
||||
const [connectingFrom, setConnectingFrom] = useState<string | null>(null)
|
||||
const [editingNode, setEditingNode] = useState<TopologyNode | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [shortcutsOpen, setShortcutsOpen] = useState(false)
|
||||
const [confirmDelete, setConfirmDelete] = useState<{ type: 'node' | 'edge'; id: string } | null>(null)
|
||||
const topologyRef = useRef<HTMLDivElement>(null)
|
||||
const { toast } = useToast()
|
||||
|
||||
// Keyboard shortcuts
|
||||
useKeyboardShortcuts([
|
||||
{
|
||||
keys: ['Ctrl', 'e'],
|
||||
handler: () => setEditMode(!editMode),
|
||||
description: 'Toggle edit mode',
|
||||
},
|
||||
{
|
||||
keys: ['Ctrl', 's'],
|
||||
handler: () => {
|
||||
if (editMode && localTopology) {
|
||||
handleSave()
|
||||
}
|
||||
},
|
||||
description: 'Save changes',
|
||||
},
|
||||
{
|
||||
keys: ['Escape'],
|
||||
handler: () => {
|
||||
setEditMode(false)
|
||||
setSelectedNodeId(null)
|
||||
setConnectingFrom(null)
|
||||
},
|
||||
description: 'Cancel/Exit',
|
||||
},
|
||||
{
|
||||
keys: ['Ctrl', '/'],
|
||||
handler: () => setShortcutsOpen(true),
|
||||
description: 'Show shortcuts',
|
||||
},
|
||||
])
|
||||
|
||||
const [updateTopology, { loading: saving }] = useMutation(UPDATE_NETWORK_TOPOLOGY, {
|
||||
refetchQueries: ['GetNetworkTopologies'],
|
||||
})
|
||||
|
||||
const filter = {
|
||||
region: selectedRegion === 'All' ? undefined : selectedRegion,
|
||||
entity: selectedEntity === 'All' ? undefined : selectedEntity,
|
||||
}
|
||||
|
||||
const { topologies, loading, error } = useNetworkTopologies(filter)
|
||||
|
||||
// Initialize local topology when data loads
|
||||
useEffect(() => {
|
||||
if (topologies.length > 0 && !localTopology) {
|
||||
const topology = topologies[0]
|
||||
setLocalTopology(topology)
|
||||
setHistory([topology])
|
||||
setHistoryIndex(0)
|
||||
}
|
||||
}, [topologies, localTopology])
|
||||
|
||||
// Save state to history for undo/redo
|
||||
const saveToHistory = useCallback((topology: NetworkTopology) => {
|
||||
setHistory((prev) => {
|
||||
const newHistory = prev.slice(0, historyIndex + 1)
|
||||
newHistory.push(topology)
|
||||
return newHistory.slice(-50) // Keep last 50 states
|
||||
})
|
||||
setHistoryIndex((prev) => Math.min(prev + 1, 49))
|
||||
}, [historyIndex])
|
||||
|
||||
const handleNodeDrag = useCallback((nodeId: string, x: number, y: number) => {
|
||||
if (!localTopology) return
|
||||
|
||||
const updatedNodes = localTopology.nodes.map((node) =>
|
||||
node.id === nodeId ? { ...node, position: { x, y } } : node
|
||||
)
|
||||
|
||||
const updated = { ...localTopology, nodes: updatedNodes }
|
||||
setLocalTopology(updated)
|
||||
}, [localTopology])
|
||||
|
||||
const handleNodeClick = useCallback((nodeId: string) => {
|
||||
if (connectingFrom) {
|
||||
// Create edge
|
||||
if (connectingFrom !== nodeId && localTopology) {
|
||||
const newEdge: TopologyEdge = {
|
||||
id: `edge-${Date.now()}`,
|
||||
source: connectingFrom,
|
||||
target: nodeId,
|
||||
type: 'network-route',
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...localTopology,
|
||||
edges: [...localTopology.edges, newEdge],
|
||||
}
|
||||
setLocalTopology(updated)
|
||||
saveToHistory(updated)
|
||||
setConnectingFrom(null)
|
||||
toast({
|
||||
title: 'Edge created',
|
||||
description: 'Connection added successfully',
|
||||
})
|
||||
} else {
|
||||
setConnectingFrom(null)
|
||||
}
|
||||
} else {
|
||||
setSelectedNodeId(nodeId)
|
||||
}
|
||||
}, [connectingFrom, localTopology, saveToHistory, toast])
|
||||
|
||||
const selectedNode = displayTopology?.nodes.find((n) => n.id === selectedNodeId) || null
|
||||
|
||||
const handleNodeDelete = useCallback((nodeId: string) => {
|
||||
if (!localTopology) return
|
||||
|
||||
const updated = {
|
||||
...localTopology,
|
||||
nodes: localTopology.nodes.filter((n) => n.id !== nodeId),
|
||||
edges: localTopology.edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
||||
}
|
||||
setLocalTopology(updated)
|
||||
saveToHistory(updated)
|
||||
setSelectedNodeId(null)
|
||||
toast({
|
||||
title: 'Node deleted',
|
||||
description: 'Node and its connections removed',
|
||||
})
|
||||
}, [localTopology, saveToHistory, toast])
|
||||
|
||||
const handleEdgeDelete = useCallback((edgeId: string) => {
|
||||
if (!localTopology) return
|
||||
|
||||
const updated = {
|
||||
...localTopology,
|
||||
edges: localTopology.edges.filter((e) => e.id !== edgeId),
|
||||
}
|
||||
setLocalTopology(updated)
|
||||
saveToHistory(updated)
|
||||
toast({
|
||||
title: 'Edge deleted',
|
||||
description: 'Connection removed',
|
||||
})
|
||||
}, [localTopology, saveToHistory, toast])
|
||||
|
||||
const handleAddNode = useCallback(() => {
|
||||
if (!localTopology) return
|
||||
|
||||
const newNode: TopologyNode = {
|
||||
id: `node-${Date.now()}`,
|
||||
type: 'service',
|
||||
label: 'New Node',
|
||||
region: localTopology.region,
|
||||
entity: localTopology.entity,
|
||||
position: { x: 400, y: 300 },
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...localTopology,
|
||||
nodes: [...localTopology.nodes, newNode],
|
||||
}
|
||||
setLocalTopology(updated)
|
||||
saveToHistory(updated)
|
||||
setSelectedNodeId(newNode.id)
|
||||
toast({
|
||||
title: 'Node added',
|
||||
description: 'New node created',
|
||||
})
|
||||
}, [localTopology, saveToHistory, toast])
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
if (historyIndex > 0) {
|
||||
const newIndex = historyIndex - 1
|
||||
setHistoryIndex(newIndex)
|
||||
setLocalTopology(history[newIndex])
|
||||
}
|
||||
}, [history, historyIndex])
|
||||
|
||||
const handleRedo = useCallback(() => {
|
||||
if (historyIndex < history.length - 1) {
|
||||
const newIndex = historyIndex + 1
|
||||
setHistoryIndex(newIndex)
|
||||
setLocalTopology(history[newIndex])
|
||||
}
|
||||
}, [history, historyIndex])
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!localTopology) return
|
||||
|
||||
try {
|
||||
await updateTopology({
|
||||
variables: {
|
||||
id: localTopology.id || 'default',
|
||||
input: {
|
||||
nodes: localTopology.nodes,
|
||||
edges: localTopology.edges,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
toast({
|
||||
title: 'Saved',
|
||||
description: 'Topology changes saved successfully',
|
||||
variant: 'success',
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Save failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to save topology',
|
||||
variant: 'error',
|
||||
})
|
||||
}
|
||||
}, [localTopology, updateTopology, toast])
|
||||
|
||||
const handleExportPNG = async () => {
|
||||
if (!topologyRef.current) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: 'Topology container not found',
|
||||
variant: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setExporting(true)
|
||||
try {
|
||||
const canvas = await html2canvas(topologyRef.current, {
|
||||
backgroundColor: '#000000',
|
||||
scale: 2, // High resolution
|
||||
logging: false,
|
||||
useCORS: true,
|
||||
})
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: 'Failed to create image blob',
|
||||
variant: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `topology-${selectedRegion}-${selectedEntity}-${Date.now()}.png`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
toast({
|
||||
title: 'Export successful',
|
||||
description: 'Topology exported as PNG',
|
||||
})
|
||||
},
|
||||
'image/png',
|
||||
1.0
|
||||
)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
variant: 'error',
|
||||
})
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportSVG = () => {
|
||||
const svgElement = topologyRef.current?.querySelector('#topology-svg') as SVGElement
|
||||
if (!svgElement) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: 'SVG element not found',
|
||||
variant: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Clone the SVG to avoid modifying the original
|
||||
const svgClone = svgElement.cloneNode(true) as SVGElement
|
||||
|
||||
// Set explicit dimensions
|
||||
const bbox = svgElement.getBBox()
|
||||
svgClone.setAttribute('width', bbox.width.toString())
|
||||
svgClone.setAttribute('height', bbox.height.toString())
|
||||
svgClone.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`)
|
||||
|
||||
// Serialize to string
|
||||
const svgData = new XMLSerializer().serializeToString(svgClone)
|
||||
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
|
||||
const url = URL.createObjectURL(svgBlob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `topology-${selectedRegion}-${selectedEntity}-${Date.now()}.svg`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
toast({
|
||||
title: 'Export successful',
|
||||
description: 'Topology exported as SVG',
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Export failed',
|
||||
description: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
variant: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Network Topology</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
Visualize and manage network topology by region and entity
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="h-[600px] flex items-center justify-center">
|
||||
<div className="text-studio-medium">Loading topology...</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Error Loading Topology</CardTitle>
|
||||
<CardDescription>{error.message}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const displayTopology = useMemo(() => localTopology || topologies[0], [localTopology, topologies])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-studio-light">Network Topology</h1>
|
||||
<p className="text-studio-medium mt-2">
|
||||
Visualize and manage network topology by region and entity
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{editMode && (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleUndo} disabled={historyIndex <= 0}>
|
||||
<Undo className="h-4 w-4 mr-2" />
|
||||
Undo
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleRedo} disabled={historyIndex >= history.length - 1}>
|
||||
<Redo className="h-4 w-4 mr-2" />
|
||||
Redo
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={saving || !localTopology}>
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button variant="outline" onClick={handleExportPNG} disabled={exporting || !topologies.length}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{exporting ? 'Exporting...' : 'Export PNG'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleExportSVG} disabled={!topologies.length}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export SVG
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShortcutsOpen(true)}>
|
||||
<HelpCircle className="h-4 w-4 mr-2" />
|
||||
Shortcuts
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Topology Diagram</CardTitle>
|
||||
<CardDescription>
|
||||
{topologies.length} topology{topologies.length !== 1 ? 'ies' : ''} found
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<RegionSelector
|
||||
value={selectedRegion}
|
||||
onChange={setSelectedRegion}
|
||||
className="w-48"
|
||||
/>
|
||||
<EntitySelector
|
||||
value={selectedEntity}
|
||||
onChange={setSelectedEntity}
|
||||
className="w-64"
|
||||
/>
|
||||
<EditModeToggle enabled={editMode} onToggle={setEditMode} />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{displayTopology ? (
|
||||
<>
|
||||
{editMode && (
|
||||
<div className="mb-4 flex gap-2 p-4 bg-studio-medium/20 rounded-lg">
|
||||
<Button size="sm" onClick={handleAddNode}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Node
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={connectingFrom ? 'default' : 'outline'}
|
||||
onClick={() => {
|
||||
if (connectingFrom) {
|
||||
setConnectingFrom(null)
|
||||
} else if (selectedNodeId) {
|
||||
setConnectingFrom(selectedNodeId)
|
||||
} else {
|
||||
toast({
|
||||
title: 'Select a node',
|
||||
description: 'Please select a node to start connecting',
|
||||
variant: 'error',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{connectingFrom ? 'Cancel Connection' : 'Connect Nodes'}
|
||||
</Button>
|
||||
{selectedNodeId && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const node = displayTopology.nodes.find((n) => n.id === selectedNodeId)
|
||||
if (node) {
|
||||
setEditingNode(node)
|
||||
setDialogOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Node
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setConfirmDelete({ type: 'node', id: selectedNodeId })
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Node
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<TopologyVisualizationWrapper
|
||||
topology={displayTopology}
|
||||
editMode={editMode}
|
||||
containerRef={topologyRef}
|
||||
onNodeDrag={handleNodeDrag}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeDelete={handleNodeDelete}
|
||||
onEdgeDelete={handleEdgeDelete}
|
||||
selectedNodeId={selectedNodeId}
|
||||
connectingFrom={connectingFrom}
|
||||
useReactFlow={true}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="No topology data"
|
||||
description="No topology data available for the selected filters."
|
||||
action={{
|
||||
label: 'Clear Filters',
|
||||
onClick: () => {
|
||||
setSelectedRegion('All')
|
||||
setSelectedEntity('All')
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{editingNode && displayTopology && (
|
||||
<EditTopologyNodeForm
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen(open)
|
||||
if (!open) {
|
||||
setEditingNode(null)
|
||||
setSelectedNodeId(null)
|
||||
}
|
||||
}}
|
||||
node={editingNode}
|
||||
topologyId={displayTopology.id || 'default'}
|
||||
onSuccess={() => {
|
||||
// Refresh topology after edit
|
||||
if (topologies.length > 0) {
|
||||
setLocalTopology(topologies[0])
|
||||
}
|
||||
setEditingNode(null)
|
||||
setSelectedNodeId(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<KeyboardShortcuts
|
||||
open={shortcutsOpen}
|
||||
onOpenChange={setShortcutsOpen}
|
||||
shortcuts={[
|
||||
{ keys: ['Ctrl', 'E'], description: 'Toggle edit mode', category: 'General' },
|
||||
{ keys: ['Ctrl', 'S'], description: 'Save changes', category: 'General' },
|
||||
{ keys: ['Escape'], description: 'Cancel/Exit', category: 'General' },
|
||||
{ keys: ['Ctrl', '/'], description: 'Show shortcuts', category: 'General' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedNode && displayTopology && (
|
||||
<NodeDetailsPanel
|
||||
node={selectedNode}
|
||||
topology={displayTopology}
|
||||
onClose={() => setSelectedNodeId(null)}
|
||||
onEdit={() => {
|
||||
setEditingNode(selectedNode)
|
||||
setDialogOpen(true)
|
||||
}}
|
||||
onDelete={() => {
|
||||
setConfirmDelete({ type: 'node', id: selectedNode.id })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{confirmDelete && (
|
||||
<ConfirmDialog
|
||||
open={!!confirmDelete}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setConfirmDelete(null)
|
||||
}}
|
||||
title={confirmDelete.type === 'node' ? 'Delete Node' : 'Delete Edge'}
|
||||
description={
|
||||
confirmDelete.type === 'node'
|
||||
? 'Are you sure you want to delete this node and all its connections? This action cannot be undone.'
|
||||
: 'Are you sure you want to delete this connection? This action cannot be undone.'
|
||||
}
|
||||
confirmLabel="Delete"
|
||||
variant="destructive"
|
||||
onConfirm={() => {
|
||||
if (confirmDelete.type === 'node') {
|
||||
handleNodeDelete(confirmDelete.id)
|
||||
} else {
|
||||
handleEdgeDelete(confirmDelete.id)
|
||||
}
|
||||
setConfirmDelete(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
116
src/components/infrastructure/NodeDetailsPanel.tsx
Normal file
116
src/components/infrastructure/NodeDetailsPanel.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { X } from 'lucide-react'
|
||||
import type { TopologyNode, NetworkTopology } from '@/lib/types/infrastructure'
|
||||
|
||||
interface NodeDetailsPanelProps {
|
||||
node: TopologyNode
|
||||
topology: NetworkTopology
|
||||
onClose: () => void
|
||||
onEdit?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export function NodeDetailsPanel({
|
||||
node,
|
||||
topology,
|
||||
onClose,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: NodeDetailsPanelProps) {
|
||||
const connections = topology.edges.filter(
|
||||
(e) => e.source === node.id || e.target === node.id
|
||||
)
|
||||
|
||||
const connectedNodes = connections.map((edge) => {
|
||||
const otherId = edge.source === node.id ? edge.target : edge.source
|
||||
return topology.nodes.find((n) => n.id === otherId)
|
||||
}).filter(Boolean) as TopologyNode[]
|
||||
|
||||
return (
|
||||
<Card className="fixed right-4 top-4 w-80 z-50 max-h-[80vh] overflow-y-auto">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle>{node.label}</CardTitle>
|
||||
<CardDescription>{node.type}</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-studio-medium mb-2">Details</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-studio-medium">Region:</span>{' '}
|
||||
<span className="text-studio-light">{node.region}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-studio-medium">Entity:</span>{' '}
|
||||
<span className="text-studio-light">{node.entity}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-studio-medium">Position:</span>{' '}
|
||||
<span className="text-studio-light">
|
||||
({Math.round(node.position.x)}, {Math.round(node.position.y)})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.keys(node.metadata || {}).length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-studio-medium mb-2">Metadata</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
{Object.entries(node.metadata || {}).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<span className="text-studio-medium">{key}:</span>{' '}
|
||||
<span className="text-studio-light">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connectedNodes.length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-studio-medium mb-2">Connections</div>
|
||||
<div className="space-y-2">
|
||||
{connectedNodes.map((connectedNode) => (
|
||||
<div
|
||||
key={connectedNode.id}
|
||||
className="p-2 bg-studio-medium/20 rounded text-sm"
|
||||
>
|
||||
<div className="font-medium">{connectedNode.label}</div>
|
||||
<div className="text-xs text-studio-medium">{connectedNode.type}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-2 border-t border-studio-medium">
|
||||
{onEdit && (
|
||||
<Button size="sm" variant="outline" onClick={onEdit} className="flex-1">
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button size="sm" variant="destructive" onClick={onDelete} className="flex-1">
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
36
src/components/infrastructure/RegionSelector.tsx
Normal file
36
src/components/infrastructure/RegionSelector.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
|
||||
interface RegionSelectorProps {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const regions = [
|
||||
'All',
|
||||
'Africa (Sub-Saharan)',
|
||||
'Middle East & North Africa',
|
||||
'Americas',
|
||||
'Asia-Pacific',
|
||||
'Europe',
|
||||
]
|
||||
|
||||
export function RegionSelector({ value, onChange, className }: RegionSelectorProps) {
|
||||
return (
|
||||
<Select value={value || 'All'} onValueChange={onChange}>
|
||||
<SelectTrigger className={className}>
|
||||
<SelectValue placeholder="Select region" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{regions.map((region) => (
|
||||
<SelectItem key={region} value={region}>
|
||||
{region}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
40
src/components/infrastructure/SkeletonCard.tsx
Normal file
40
src/components/infrastructure/SkeletonCard.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
|
||||
export function SkeletonCard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-4 w-32 mt-2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function SkeletonTable({ rows = 5 }: { rows?: number }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4 pb-2 border-b border-studio-medium">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
<div key={i} className="flex gap-4 py-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user