Added DB connection and started creating api calls for the pages

This commit is contained in:
gulimabr
2025-11-30 15:17:23 -03:00
parent b5381ae376
commit bbbe65067b
20 changed files with 1403 additions and 152 deletions

View File

@@ -3,6 +3,7 @@ import Layout from '@/components/Layout'
import HomePage from '@/pages/HomePage'
import DashboardPage from '@/pages/DashboardPage'
import RequirementsPage from '@/pages/RequirementsPage'
import RequirementDetailPage from '@/pages/RequirementDetailPage'
import ProtectedRoute from '@/components/ProtectedRoute'
function App() {
@@ -32,6 +33,14 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/requirements/:id"
element={
<ProtectedRoute>
<RequirementDetailPage />
</ProtectedRoute>
}
/>
</Routes>
)
}

View File

@@ -1,86 +1,94 @@
import { useState, useEffect } from 'react'
import { useAuth } from '@/hooks'
import { useNavigate } from 'react-router-dom'
import { groupService, Group } from '@/services'
// Icons as components for cleaner code
const DataServicesIcon = () => (
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="12" y="8" width="40" height="48" rx="2" />
<line x1="20" y1="20" x2="44" y2="20" />
<line x1="20" y1="28" x2="44" y2="28" />
<line x1="20" y1="36" x2="44" y2="36" />
<line x1="20" y1="44" x2="36" y2="44" />
<rect x="36" y="40" width="12" height="12" rx="1" />
<line x1="40" y1="44" x2="44" y2="44" />
<line x1="40" y1="48" x2="44" y2="48" />
</svg>
)
/**
* Helper function to convert hex color to RGB values
*/
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null
}
const IntegrationIcon = () => (
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="8" y="16" width="20" height="14" rx="2" />
<rect x="36" y="16" width="20" height="14" rx="2" />
<rect x="22" y="38" width="20" height="14" rx="2" />
<line x1="18" y1="30" x2="18" y2="38" />
<line x1="18" y1="38" x2="32" y2="38" />
<line x1="46" y1="30" x2="46" y2="38" />
<line x1="46" y1="38" x2="32" y2="38" />
</svg>
)
/**
* Helper function to determine if text should be light or dark based on background color
*/
function getContrastTextColor(hexColor: string): string {
const rgb = hexToRgb(hexColor)
if (!rgb) return '#000000'
// Calculate luminance
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255
return luminance > 0.5 ? '#000000' : '#ffffff'
}
const IntelligenceIcon = () => (
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="28" cy="28" r="14" />
<line x1="38" y1="38" x2="52" y2="52" strokeWidth="4" strokeLinecap="round" />
<path d="M22 28 L26 32 L34 24" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
/**
* Helper function to lighten a hex color for hover state
*/
function lightenColor(hex: string, percent: number): string {
const rgb = hexToRgb(hex)
if (!rgb) return hex
const lighten = (value: number) => Math.min(255, Math.floor(value + (255 - value) * percent))
const r = lighten(rgb.r).toString(16).padStart(2, '0')
const g = lighten(rgb.g).toString(16).padStart(2, '0')
const b = lighten(rgb.b).toString(16).padStart(2, '0')
return `#${r}${g}${b}`
}
const UserExperienceIcon = () => (
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="32" cy="20" r="8" />
<circle cx="16" cy="36" r="4" />
<circle cx="48" cy="36" r="4" />
<circle cx="24" cy="52" r="4" />
<circle cx="40" cy="52" r="4" />
<line x1="32" y1="28" x2="32" y2="36" />
<line x1="32" y1="36" x2="16" y2="36" />
<line x1="32" y1="36" x2="48" y2="36" />
<line x1="20" y1="40" x2="24" y2="48" />
<line x1="44" y1="40" x2="40" y2="48" />
</svg>
)
const ManagementIcon = () => (
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="32" cy="32" r="12" />
<path d="M32 16 L32 20" />
<path d="M32 44 L32 48" />
<path d="M16 32 L20 32" />
<path d="M44 32 L48 32" />
<path d="M20.7 20.7 L23.5 23.5" />
<path d="M40.5 40.5 L43.3 43.3" />
<path d="M20.7 43.3 L23.5 40.5" />
<path d="M40.5 23.5 L43.3 20.7" />
<rect x="26" y="28" width="12" height="10" rx="1" />
<path d="M29 28 L29 26 L35 26 L35 28" />
<line x1="29" y1="32" x2="35" y2="32" />
</svg>
)
const TrustworthinessIcon = () => (
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 36 C12 36 20 28 32 36 C44 44 52 36 52 36" strokeLinecap="round" />
<path d="M12 36 L24 48 L32 36" strokeLinecap="round" strokeLinejoin="round" />
<path d="M52 36 L40 48 L32 36" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
/**
* Helper function to darken a hex color for border
*/
function darkenColor(hex: string, percent: number): string {
const rgb = hexToRgb(hex)
if (!rgb) return hex
const darken = (value: number) => Math.max(0, Math.floor(value * (1 - percent)))
const r = darken(rgb.r).toString(16).padStart(2, '0')
const g = darken(rgb.g).toString(16).padStart(2, '0')
const b = darken(rgb.b).toString(16).padStart(2, '0')
return `#${r}${g}${b}`
}
export default function DashboardPage() {
const { user, logout } = useAuth()
const navigate = useNavigate()
const [groups, setGroups] = useState<Group[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [hoveredGroup, setHoveredGroup] = useState<number | null>(null)
const handleCategoryClick = (group: string) => {
navigate(`/requirements?group=${encodeURIComponent(group)}`)
useEffect(() => {
const fetchGroups = async () => {
try {
setLoading(true)
const fetchedGroups = await groupService.getGroups()
setGroups(fetchedGroups)
setError(null)
} catch (err) {
console.error('Failed to fetch groups:', err)
setError('Failed to load groups')
} finally {
setLoading(false)
}
}
fetchGroups()
}, [])
const handleCategoryClick = (groupName: string) => {
navigate(`/requirements?group=${encodeURIComponent(groupName)}`)
}
const handleMyRequirementsClick = () => {
@@ -194,76 +202,63 @@ export default function DashboardPage() {
Quick Search Filters
</h2>
{/* Grid Layout matching the screenshot */}
<div className="grid grid-cols-4 gap-0 max-w-2xl mx-auto">
{/* Row 1 */}
{/* Data Services - spans 2 rows */}
<div
onClick={() => handleCategoryClick('Data Services')}
className="row-span-2 bg-blue-200 border border-blue-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-blue-300 transition-colors min-h-[200px]"
>
<div className="text-blue-800">
<DataServicesIcon />
</div>
<span className="mt-3 text-sm font-semibold text-blue-900">Data Services</span>
{/* Loading State */}
{loading && (
<div className="flex justify-center items-center min-h-[200px]">
<div className="text-gray-500">Loading groups...</div>
</div>
)}
{/* Integration - 1 row */}
<div
onClick={() => handleCategoryClick('Integration')}
className="bg-amber-200 border border-amber-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-amber-300 transition-colors min-h-[100px]"
>
<div className="text-amber-800">
<IntegrationIcon />
</div>
<span className="mt-2 text-sm font-semibold text-amber-900">Integration</span>
{/* Error State */}
{error && (
<div className="flex justify-center items-center min-h-[200px]">
<div className="text-red-500">{error}</div>
</div>
)}
{/* Intelligence - spans 2 rows */}
<div
onClick={() => handleCategoryClick('Intelligence')}
className="row-span-2 bg-purple-200 border border-purple-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-purple-300 transition-colors min-h-[200px]"
>
<div className="text-purple-800">
<IntelligenceIcon />
</div>
<span className="mt-3 text-sm font-semibold text-purple-900">Intelligence</span>
</div>
{/* Groups Grid */}
{!loading && !error && groups.length > 0 && (
<div className="grid grid-cols-3 gap-4 max-w-2xl mx-auto">
{groups.map((group) => {
const isHovered = hoveredGroup === group.id
const bgColor = isHovered
? lightenColor(group.hex_color, 0.2)
: group.hex_color
const borderColor = darkenColor(group.hex_color, 0.2)
const textColor = getContrastTextColor(group.hex_color)
{/* User Experience - spans 2 rows */}
<div
onClick={() => handleCategoryClick('User Experience')}
className="row-span-2 bg-green-200 border border-green-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-green-300 transition-colors min-h-[200px]"
>
<div className="text-green-800">
<UserExperienceIcon />
</div>
<span className="mt-3 text-sm font-semibold text-green-900">User Experience</span>
return (
<div
key={group.id}
onClick={() => handleCategoryClick(group.group_name)}
onMouseEnter={() => setHoveredGroup(group.id)}
onMouseLeave={() => setHoveredGroup(null)}
className="flex flex-col items-center justify-center p-6 cursor-pointer transition-colors min-h-[120px] rounded-lg"
style={{
backgroundColor: bgColor,
borderWidth: '2px',
borderStyle: 'solid',
borderColor: borderColor,
}}
>
<span
className="text-sm font-semibold text-center"
style={{ color: textColor }}
>
{group.group_name}
</span>
</div>
)
})}
</div>
)}
{/* Row 2 - Management and Trustworthiness */}
{/* Management - 1 row, spans 1 col */}
<div
onClick={() => handleCategoryClick('Management')}
className="bg-red-200 border border-red-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-red-300 transition-colors min-h-[100px]"
>
<div className="text-red-800">
<ManagementIcon />
</div>
<span className="mt-2 text-sm font-semibold text-red-900">Management</span>
{/* Empty State */}
{!loading && !error && groups.length === 0 && (
<div className="flex justify-center items-center min-h-[200px]">
<div className="text-gray-500">No groups found</div>
</div>
{/* Trustworthiness - 1 row */}
<div
onClick={() => handleCategoryClick('Trustworthiness')}
className="bg-green-100 border border-green-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-green-200 transition-colors min-h-[100px]"
>
<div className="text-red-400">
<TrustworthinessIcon />
</div>
<span className="mt-2 text-sm font-semibold text-red-600">Trustworthiness</span>
</div>
</div>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,476 @@
import { useState } from 'react'
import { useAuth } from '@/hooks'
import { useParams, Link } from 'react-router-dom'
// Types for requirement details
interface RequirementDetail {
id: string
tag: string
title: string
priority: number
group: string
description: string
authors: string[]
subRequirements: SubRequirement[]
coRequirements: CoRequirement[]
acceptanceCriteria: AcceptanceCriterion[]
sharedComments: Comment[]
}
interface SubRequirement {
id: string
tag: string
title: string
}
interface CoRequirement {
id: string
tag: string
title: string
}
interface AcceptanceCriterion {
id: string
description: string
validated: boolean
}
interface Comment {
id: string
author: string
date: string
content: string
}
// Tab types
type TabType = 'description' | 'sub-requirements' | 'co-requirements' | 'acceptance-criteria' | 'shared-comments' | 'validate'
// Mock data for requirement details
const mockRequirementDetails: Record<string, RequirementDetail> = {
'1': {
id: '1',
tag: 'GSR#7',
title: 'Controle e monitoramento em right-time',
priority: 0,
group: 'userExperience',
description: '',
authors: ['Ricardo Belo'],
subRequirements: [],
coRequirements: [],
acceptanceCriteria: [],
sharedComments: [],
},
'2': {
id: '2',
tag: 'GSR#10',
title: 'Interface de software DT-externo bem definida.',
priority: 1,
group: 'Intelligence',
description: 'This requirement defines the need for a well-defined software interface between the Digital Twin and external systems.',
authors: ['Ricardo Belo', 'Maria Silva'],
subRequirements: [
{ id: 'sub1', tag: 'SFR#1', title: 'API Documentation' },
{ id: 'sub2', tag: 'SFR#2', title: 'Authentication Protocol' },
],
coRequirements: [
{ id: 'co1', tag: 'GSR#5', title: 'Sincronização de dados em tempo real' },
],
acceptanceCriteria: [
{ id: 'ac1', description: 'API endpoints documented with OpenAPI spec', validated: true },
{ id: 'ac2', description: 'Authentication tested with external systems', validated: false },
],
sharedComments: [
{ id: 'c1', author: 'Ricardo Belo', date: '2025-11-28', content: 'Initial draft created' },
],
},
'3': {
id: '3',
tag: 'GSR#12',
title: 'Visualizacao',
priority: 1,
group: 'User Experience',
description: 'Visualization requirements for the Digital Twin interface.',
authors: ['Ricardo Belo'],
subRequirements: [],
coRequirements: [],
acceptanceCriteria: [],
sharedComments: [],
},
'4': {
id: '4',
tag: 'GSR#1',
title: 'Estado corrente atualizado',
priority: 1,
group: 'Data Services',
description: 'The system must maintain an updated current state of all monitored entities.',
authors: ['Ricardo Belo'],
subRequirements: [],
coRequirements: [],
acceptanceCriteria: [],
sharedComments: [],
},
'5': {
id: '5',
tag: 'GSR#5',
title: 'Sincronização de dados em tempo real',
priority: 2,
group: 'Data Services',
description: 'Real-time data synchronization between physical and digital twin.',
authors: ['Ricardo Belo'],
subRequirements: [],
coRequirements: [],
acceptanceCriteria: [],
sharedComments: [],
},
'6': {
id: '6',
tag: 'GSR#8',
title: 'Integração com sistemas legados',
priority: 1,
group: 'Integration',
description: 'Integration capabilities with legacy systems.',
authors: ['Ricardo Belo'],
subRequirements: [],
coRequirements: [],
acceptanceCriteria: [],
sharedComments: [],
},
'7': {
id: '7',
tag: 'GSR#15',
title: 'Dashboard de gestão',
priority: 3,
group: 'Management',
description: 'Management dashboard for system monitoring and control.',
authors: ['Ricardo Belo'],
subRequirements: [],
coRequirements: [],
acceptanceCriteria: [],
sharedComments: [],
},
}
export default function RequirementDetailPage() {
const { user, logout } = useAuth()
const { id } = useParams<{ id: string }>()
const [activeTab, setActiveTab] = useState<TabType>('description')
const [priority, setPriority] = useState<number>(0)
// Get requirement details from mock data
const requirement = id ? mockRequirementDetails[id] : null
// Initialize priority when requirement loads
useState(() => {
if (requirement) {
setPriority(requirement.priority)
}
})
if (!requirement) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Requirement not found</h2>
<Link to="/requirements" className="text-teal-600 hover:underline">
Back to Requirements
</Link>
</div>
</div>
)
}
const tabs: { id: TabType; label: string }[] = [
{ id: 'description', label: 'Description' },
{ id: 'sub-requirements', label: 'Sub-Requirements' },
{ id: 'co-requirements', label: 'Co-Requirements' },
{ id: 'acceptance-criteria', label: 'Acceptance Criteria' },
{ id: 'shared-comments', label: 'Shared Comments' },
{ id: 'validate', label: 'Validate' },
]
const handlePriorityChange = (delta: number) => {
setPriority(prev => Math.max(0, prev + delta))
}
const renderTabContent = () => {
switch (activeTab) {
case 'description':
return (
<div>
<h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3>
<p className="text-sm text-gray-700 mb-4">
<span className="font-semibold">Author(s):</span> {requirement.authors.join(', ')}
</p>
<div className="bg-teal-50 border border-gray-300 rounded min-h-[300px] p-4">
<p className="text-gray-700">{requirement.description || ''}</p>
</div>
<div className="mt-4">
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
Edit
</button>
</div>
</div>
)
case 'sub-requirements':
return (
<div>
<h3 className="text-xl font-bold text-gray-800 mb-4">Sub-Requirements</h3>
{requirement.subRequirements.length > 0 ? (
<div className="space-y-2">
{requirement.subRequirements.map(sub => (
<div key={sub.id} className="p-3 border border-gray-300 rounded">
<span className="font-semibold">{sub.tag}</span> - {sub.title}
</div>
))}
</div>
) : (
<p className="text-gray-500">No sub-requirements defined.</p>
)}
<div className="mt-4">
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
Add Sub-Requirement
</button>
</div>
</div>
)
case 'co-requirements':
return (
<div>
<h3 className="text-xl font-bold text-gray-800 mb-4">Co-Requirements</h3>
{requirement.coRequirements.length > 0 ? (
<div className="space-y-2">
{requirement.coRequirements.map(co => (
<div key={co.id} className="p-3 border border-gray-300 rounded">
<span className="font-semibold">{co.tag}</span> - {co.title}
</div>
))}
</div>
) : (
<p className="text-gray-500">No co-requirements defined.</p>
)}
<div className="mt-4">
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
Add Co-Requirement
</button>
</div>
</div>
)
case 'acceptance-criteria':
return (
<div>
<h3 className="text-xl font-bold text-gray-800 mb-4">Acceptance Criteria</h3>
{requirement.acceptanceCriteria.length > 0 ? (
<div className="space-y-2">
{requirement.acceptanceCriteria.map(ac => (
<div key={ac.id} className="p-3 border border-gray-300 rounded flex items-center gap-3">
<input
type="checkbox"
checked={ac.validated}
readOnly
className="w-4 h-4 rounded border-gray-300 text-teal-600"
/>
<span className={ac.validated ? 'line-through text-gray-400' : ''}>
{ac.description}
</span>
</div>
))}
</div>
) : (
<p className="text-gray-500">No acceptance criteria defined.</p>
)}
<div className="mt-4">
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
Add Criterion
</button>
</div>
</div>
)
case 'shared-comments':
return (
<div>
<h3 className="text-xl font-bold text-gray-800 mb-4">Shared Comments</h3>
{requirement.sharedComments.length > 0 ? (
<div className="space-y-3">
{requirement.sharedComments.map(comment => (
<div key={comment.id} className="p-3 border border-gray-300 rounded">
<div className="flex justify-between text-sm text-gray-500 mb-1">
<span className="font-semibold">{comment.author}</span>
<span>{comment.date}</span>
</div>
<p className="text-gray-700">{comment.content}</p>
</div>
))}
</div>
) : (
<p className="text-gray-500">No comments yet.</p>
)}
<div className="mt-4">
<textarea
placeholder="Add a comment..."
className="w-full p-3 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 mb-2"
rows={3}
/>
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
Post Comment
</button>
</div>
</div>
)
case 'validate':
return (
<div>
<h3 className="text-xl font-bold text-gray-800 mb-4">Validate Requirement</h3>
<div className="p-4 border border-gray-300 rounded bg-gray-50">
<p className="text-gray-700 mb-4">
Review all acceptance criteria and validate this requirement when ready.
</p>
<div className="space-y-2 mb-4">
<label className="flex items-center gap-2">
<input type="checkbox" className="w-4 h-4 rounded border-gray-300 text-teal-600" />
<span className="text-sm">All acceptance criteria have been met</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" className="w-4 h-4 rounded border-gray-300 text-teal-600" />
<span className="text-sm">Documentation is complete</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" className="w-4 h-4 rounded border-gray-300 text-teal-600" />
<span className="text-sm">Stakeholders have approved</span>
</label>
</div>
<button className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700">
Validate Requirement
</button>
</div>
</div>
)
default:
return null
}
}
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header className="py-6 text-center">
<h1 className="text-3xl font-semibold text-teal-700">
Digital Twin Requirements Tool
</h1>
</header>
{/* Top Bar */}
<div className="border-y border-gray-200 py-3 px-8">
<div className="flex items-center justify-between max-w-7xl mx-auto">
{/* Breadcrumb */}
<div className="text-sm">
<Link to="/dashboard" className="text-gray-600 hover:underline">Projects</Link>
<span className="mx-2 text-gray-400">»</span>
<Link to="/dashboard" className="text-gray-600 hover:underline">PeTWIN</Link>
<span className="mx-2 text-gray-400">»</span>
<Link to="/requirements" className="text-gray-600 hover:underline">Search</Link>
<span className="mx-2 text-gray-400">»</span>
<span className="font-semibold text-gray-900">Details {requirement.tag}</span>
</div>
{/* Language Toggle */}
<div className="flex items-center gap-2 text-sm text-gray-600">
<span>English</span>
<div className="relative inline-flex h-5 w-10 items-center rounded-full bg-gray-300">
<span className="inline-block h-4 w-4 transform rounded-full bg-white shadow-sm translate-x-0.5" />
</div>
<span>Portuguese</span>
</div>
{/* User Info */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
</svg>
<span className="text-sm text-gray-700">
{user?.full_name || user?.preferred_username || 'Ricardo Belo'}{' '}
<span className="text-gray-500">(admin)</span>
</span>
</div>
<button
onClick={logout}
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
>
Logout
</button>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-5xl mx-auto px-8 py-8">
{/* Requirement Header */}
<div className="text-center mb-8">
{/* Tag */}
<h2 className="text-2xl font-bold text-gray-800 mb-2">{requirement.tag}</h2>
{/* Priority */}
<div className="flex items-center justify-center gap-2 mb-3">
<span className="text-gray-700 font-medium">Priority:</span>
<button
onClick={() => handlePriorityChange(-1)}
className="w-6 h-6 rounded-full border border-gray-400 text-gray-600 hover:bg-gray-100 flex items-center justify-center text-sm"
>
</button>
<span className="text-lg font-semibold text-gray-800 min-w-[20px] text-center">{priority}</span>
<button
onClick={() => handlePriorityChange(1)}
className="w-6 h-6 rounded-full border border-gray-400 text-gray-600 hover:bg-gray-100 flex items-center justify-center text-sm"
>
+
</button>
</div>
{/* Title */}
<h3 className="text-xl text-gray-700 mb-3">{requirement.title}</h3>
{/* Group Badge */}
<span className="inline-block px-3 py-1 border border-gray-400 rounded text-sm text-gray-700">
{requirement.group}
</span>
</div>
{/* Content Area */}
<div className="flex gap-6">
{/* Sidebar Tabs */}
<div className="w-48 flex-shrink-0">
<div className="border border-gray-300 rounded overflow-hidden">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full px-4 py-3 text-left text-sm border-b border-gray-300 last:border-b-0 transition-colors ${
activeTab === tab.id
? 'bg-gray-100 font-semibold text-gray-800'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Main Content Panel */}
<div className="flex-1">
<div className="border border-gray-300 rounded p-6">
{renderTabContent()}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useAuth } from '@/hooks'
import { useSearchParams, Link } from 'react-router-dom'
import { useSearchParams, Link, useNavigate } from 'react-router-dom'
// Types for requirements
interface Requirement {
@@ -111,6 +111,7 @@ const captionItems = [
export default function RequirementsPage() {
const { user, logout } = useAuth()
const [searchParams, setSearchParams] = useSearchParams()
const navigate = useNavigate()
// State
const [searchQuery, setSearchQuery] = useState('')
@@ -174,8 +175,7 @@ export default function RequirementsPage() {
}
const handleDetails = (id: string) => {
// For now, just a placeholder - will connect to backend later
console.log('View details:', id)
navigate(`/requirements/${id}`)
}
return (

View File

@@ -1,3 +1,4 @@
export { default as HomePage } from './HomePage'
export { default as DashboardPage } from './DashboardPage'
export { default as RequirementsPage } from './RequirementsPage'
export { default as RequirementDetailPage } from './RequirementDetailPage'

View File

@@ -0,0 +1,36 @@
const API_BASE_URL = '/api'
export interface Group {
id: number
group_name: string
hex_color: string
}
class GroupService {
/**
* Get all groups from the API.
*/
async getGroups(): Promise<Group[]> {
try {
const response = await fetch(`${API_BASE_URL}/groups`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const groups: Group[] = await response.json()
return groups
} catch (error) {
console.error('Failed to fetch groups:', error)
throw error
}
}
}
export const groupService = new GroupService()

View File

@@ -1 +1,3 @@
export { authService } from './authService'
export { groupService } from './groupService'
export type { Group } from './groupService'