session-wrap-dashboard 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/auth.ts.html +307 -0
- package/coverage/src/components/Header.tsx.html +262 -0
- package/coverage/src/components/Sidebar.tsx.html +262 -0
- package/coverage/src/components/index.html +131 -0
- package/coverage/src/hooks/index.html +131 -0
- package/coverage/src/hooks/useAuth.ts.html +292 -0
- package/coverage/src/hooks/useWorkspace.ts.html +475 -0
- package/coverage/src/index.html +116 -0
- package/index.html +13 -0
- package/package.json +49 -0
- package/playwright.config.ts +38 -0
- package/postcss.config.js +6 -0
- package/src/App.tsx +67 -0
- package/src/api.ts +130 -0
- package/src/auth.ts +74 -0
- package/src/components/AgentLeaderboard.tsx +84 -0
- package/src/components/AnalyticsDashboard.tsx +223 -0
- package/src/components/Header.tsx +62 -0
- package/src/components/IntegrationManager.tsx +292 -0
- package/src/components/RoleManager.tsx +230 -0
- package/src/components/Sidebar.tsx +62 -0
- package/src/components/TrendChart.tsx +92 -0
- package/src/components/WorkspaceSelector.tsx +157 -0
- package/src/components/__tests__/Header.test.tsx +64 -0
- package/src/components/__tests__/Sidebar.test.tsx +39 -0
- package/src/components/index.ts +8 -0
- package/src/hooks/__tests__/useAuth.test.ts +88 -0
- package/src/hooks/__tests__/useWorkspace.test.ts +101 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useAnalytics.ts +76 -0
- package/src/hooks/useApi.ts +62 -0
- package/src/hooks/useAuth.ts +69 -0
- package/src/hooks/useWorkspace.ts +130 -0
- package/src/index.css +57 -0
- package/src/main.tsx +13 -0
- package/src/pages/DashboardPage.tsx +13 -0
- package/src/pages/HomePage.tsx +156 -0
- package/src/pages/IntegrationsPage.tsx +9 -0
- package/src/pages/RolesPage.tsx +9 -0
- package/src/pages/SettingsPage.tsx +118 -0
- package/src/pages/WorkspacesPage.tsx +9 -0
- package/src/pages/index.ts +6 -0
- package/src/test/setup.ts +31 -0
- package/src/test/utils.tsx +15 -0
- package/src/types.ts +132 -0
- package/tailwind.config.js +11 -0
- package/tests/e2e/auth.spec.ts +42 -0
- package/tests/e2e/dashboard.spec.ts +52 -0
- package/tests/e2e/integrations.spec.ts +91 -0
- package/tests/e2e/workspaces.spec.ts +78 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +26 -0
- package/vitest.config.ts +31 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import * as authService from '../auth'
|
|
3
|
+
import { userAPI } from '../api'
|
|
4
|
+
import * as types from '../types'
|
|
5
|
+
|
|
6
|
+
interface UseAuthReturn {
|
|
7
|
+
user: types.User | null
|
|
8
|
+
isAuthenticated: boolean
|
|
9
|
+
isLoading: boolean
|
|
10
|
+
login: (claudeToken: string) => Promise<void>
|
|
11
|
+
logout: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useAuth = (): UseAuthReturn => {
|
|
15
|
+
const [user, setUser] = useState<types.User | null>(null)
|
|
16
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
|
17
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
18
|
+
|
|
19
|
+
// Initialize auth state
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const initAuth = async () => {
|
|
22
|
+
const authState = await authService.initAuth()
|
|
23
|
+
setIsAuthenticated(authState.isAuthenticated)
|
|
24
|
+
|
|
25
|
+
if (authState.isAuthenticated) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await userAPI.getProfile()
|
|
28
|
+
setUser(response.data.user)
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to fetch user profile:', error)
|
|
31
|
+
authService.logout()
|
|
32
|
+
setIsAuthenticated(false)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setIsLoading(false)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
initAuth()
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
const login = useCallback(async (claudeToken: string) => {
|
|
43
|
+
setIsLoading(true)
|
|
44
|
+
try {
|
|
45
|
+
const response = await authService.login(claudeToken)
|
|
46
|
+
setUser(response.user)
|
|
47
|
+
setIsAuthenticated(true)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Login failed:', error)
|
|
50
|
+
throw error
|
|
51
|
+
} finally {
|
|
52
|
+
setIsLoading(false)
|
|
53
|
+
}
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const logout = useCallback(() => {
|
|
57
|
+
authService.logout()
|
|
58
|
+
setUser(null)
|
|
59
|
+
setIsAuthenticated(false)
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
user,
|
|
64
|
+
isAuthenticated,
|
|
65
|
+
isLoading,
|
|
66
|
+
login,
|
|
67
|
+
logout
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { workspaceAPI, rbacAPI } from '../api'
|
|
3
|
+
import * as types from '../types'
|
|
4
|
+
|
|
5
|
+
interface UseWorkspaceReturn {
|
|
6
|
+
workspaces: types.Workspace[]
|
|
7
|
+
currentWorkspace: types.Workspace | null
|
|
8
|
+
members: types.WorkspaceMember[]
|
|
9
|
+
roles: types.Role[]
|
|
10
|
+
isLoading: boolean
|
|
11
|
+
error: string | null
|
|
12
|
+
setCurrentWorkspace: (workspace: types.Workspace | null) => void
|
|
13
|
+
createWorkspace: (name: string, isPublic: boolean) => Promise<void>
|
|
14
|
+
addMember: (userId: string, roleName: string) => Promise<void>
|
|
15
|
+
removeMember: (userId: string) => Promise<void>
|
|
16
|
+
updateMember: (userId: string, roleName: string) => Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const useWorkspace = (): UseWorkspaceReturn => {
|
|
20
|
+
const [workspaces, setWorkspaces] = useState<types.Workspace[]>([])
|
|
21
|
+
const [currentWorkspace, setCurrentWorkspace] = useState<types.Workspace | null>(null)
|
|
22
|
+
const [members, setMembers] = useState<types.WorkspaceMember[]>([])
|
|
23
|
+
const [roles, setRoles] = useState<types.Role[]>([])
|
|
24
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
25
|
+
const [error, setError] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
// Fetch workspaces and roles
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const loadData = async () => {
|
|
30
|
+
try {
|
|
31
|
+
setIsLoading(true)
|
|
32
|
+
const [workspacesRes, rolesRes] = await Promise.all([
|
|
33
|
+
workspaceAPI.list(),
|
|
34
|
+
rbacAPI.getRoles()
|
|
35
|
+
])
|
|
36
|
+
setWorkspaces(workspacesRes.data)
|
|
37
|
+
setRoles(rolesRes.data)
|
|
38
|
+
|
|
39
|
+
// Set first workspace as current
|
|
40
|
+
if (workspacesRes.data.length > 0) {
|
|
41
|
+
setCurrentWorkspace(workspacesRes.data[0])
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
setError(err instanceof Error ? err.message : 'Failed to load workspaces')
|
|
45
|
+
} finally {
|
|
46
|
+
setIsLoading(false)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
loadData()
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
// Fetch members when workspace changes
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const loadMembers = async () => {
|
|
56
|
+
if (!currentWorkspace) {
|
|
57
|
+
setMembers([])
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const res = await workspaceAPI.getMembers(currentWorkspace.id)
|
|
63
|
+
setMembers(res.data)
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Failed to load members:', err)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
loadMembers()
|
|
70
|
+
}, [currentWorkspace])
|
|
71
|
+
|
|
72
|
+
const createWorkspace = useCallback(async (name: string, isPublic: boolean) => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await workspaceAPI.create(name, isPublic)
|
|
75
|
+
setWorkspaces([...workspaces, res.data])
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
79
|
+
}, [workspaces])
|
|
80
|
+
|
|
81
|
+
const addMember = useCallback(async (userId: string, roleName: string) => {
|
|
82
|
+
if (!currentWorkspace) throw new Error('No workspace selected')
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await workspaceAPI.addMember(currentWorkspace.id, userId, roleName)
|
|
86
|
+
// Refresh members
|
|
87
|
+
const res = await workspaceAPI.getMembers(currentWorkspace.id)
|
|
88
|
+
setMembers(res.data)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw err
|
|
91
|
+
}
|
|
92
|
+
}, [currentWorkspace])
|
|
93
|
+
|
|
94
|
+
const removeMember = useCallback(async (userId: string) => {
|
|
95
|
+
if (!currentWorkspace) throw new Error('No workspace selected')
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await workspaceAPI.removeMember(currentWorkspace.id, userId)
|
|
99
|
+
setMembers(members.filter(m => m.id !== userId))
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw err
|
|
102
|
+
}
|
|
103
|
+
}, [currentWorkspace, members])
|
|
104
|
+
|
|
105
|
+
const updateMember = useCallback(async (userId: string, roleName: string) => {
|
|
106
|
+
if (!currentWorkspace) throw new Error('No workspace selected')
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await workspaceAPI.updateMember(currentWorkspace.id, userId, roleName)
|
|
110
|
+
const res = await workspaceAPI.getMembers(currentWorkspace.id)
|
|
111
|
+
setMembers(res.data)
|
|
112
|
+
} catch (err) {
|
|
113
|
+
throw err
|
|
114
|
+
}
|
|
115
|
+
}, [currentWorkspace])
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
workspaces,
|
|
119
|
+
currentWorkspace,
|
|
120
|
+
members,
|
|
121
|
+
roles,
|
|
122
|
+
isLoading,
|
|
123
|
+
error,
|
|
124
|
+
setCurrentWorkspace,
|
|
125
|
+
createWorkspace,
|
|
126
|
+
addMember,
|
|
127
|
+
removeMember,
|
|
128
|
+
updateMember
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* Global Styles */
|
|
6
|
+
* {
|
|
7
|
+
@apply transition-colors duration-200;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
@apply bg-slate-50 text-slate-900;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Utility Classes */
|
|
15
|
+
.btn {
|
|
16
|
+
@apply px-4 py-2 rounded font-medium cursor-pointer transition-colors;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.btn-primary {
|
|
20
|
+
@apply bg-blue-600 text-white hover:bg-blue-700;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.btn-secondary {
|
|
24
|
+
@apply bg-slate-200 text-slate-900 hover:bg-slate-300;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.btn-sm {
|
|
28
|
+
@apply px-3 py-1 text-sm;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.card {
|
|
32
|
+
@apply bg-white rounded-lg border border-slate-200 p-4 shadow-sm;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.input {
|
|
36
|
+
@apply px-3 py-2 border border-slate-300 rounded font-medium focus:outline-none focus:border-blue-500;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.badge {
|
|
40
|
+
@apply inline-block px-2 py-1 text-sm font-medium rounded;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.badge-success {
|
|
44
|
+
@apply bg-green-100 text-green-800;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.badge-warning {
|
|
48
|
+
@apply bg-yellow-100 text-yellow-800;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.badge-danger {
|
|
52
|
+
@apply bg-red-100 text-red-800;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.loading-spinner {
|
|
56
|
+
@apply inline-block w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin;
|
|
57
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
4
|
+
import App from './App.tsx'
|
|
5
|
+
import './index.css'
|
|
6
|
+
|
|
7
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</React.StrictMode>,
|
|
13
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useSearchParams } from 'react-router-dom'
|
|
2
|
+
import { AnalyticsDashboard } from '../components'
|
|
3
|
+
|
|
4
|
+
export const DashboardPage = () => {
|
|
5
|
+
const [searchParams] = useSearchParams()
|
|
6
|
+
const workspaceId = searchParams.get('workspace')
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="p-6 max-w-7xl mx-auto">
|
|
10
|
+
<AnalyticsDashboard workspaceId={workspaceId} />
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useNavigate } from 'react-router-dom'
|
|
2
|
+
import { useWorkspace } from '../hooks'
|
|
3
|
+
import { BarChart3, Users, Zap, ArrowRight } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
export const HomePage = () => {
|
|
6
|
+
const navigate = useNavigate()
|
|
7
|
+
const { currentWorkspace } = useWorkspace()
|
|
8
|
+
|
|
9
|
+
const features = [
|
|
10
|
+
{
|
|
11
|
+
icon: BarChart3,
|
|
12
|
+
title: 'Analytics Dashboard',
|
|
13
|
+
description: 'Track task completion, decision quality, and agent performance in real-time.',
|
|
14
|
+
color: 'blue',
|
|
15
|
+
href: '/dashboard'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
icon: Users,
|
|
19
|
+
title: 'Workspace Management',
|
|
20
|
+
description: 'Manage team members and control access with role-based permissions.',
|
|
21
|
+
color: 'green',
|
|
22
|
+
href: '/workspaces'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
icon: Zap,
|
|
26
|
+
title: 'Integrations',
|
|
27
|
+
description: 'Connect with Slack, GitHub, and Jira to enhance your workflow.',
|
|
28
|
+
color: 'yellow',
|
|
29
|
+
href: '/integrations'
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const colorClasses = {
|
|
34
|
+
blue: 'bg-blue-50 text-blue-600 border-blue-200',
|
|
35
|
+
green: 'bg-green-50 text-green-600 border-green-200',
|
|
36
|
+
yellow: 'bg-yellow-50 text-yellow-600 border-yellow-200'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
|
|
41
|
+
{/* Hero Section */}
|
|
42
|
+
<div className="p-6 max-w-6xl mx-auto">
|
|
43
|
+
<div className="mb-12">
|
|
44
|
+
<h1 className="text-4xl font-bold text-slate-900 mb-4">
|
|
45
|
+
Welcome to Session Wrap
|
|
46
|
+
</h1>
|
|
47
|
+
<p className="text-xl text-slate-600 max-w-2xl">
|
|
48
|
+
Empower your team with real-time analytics, collaboration tools, and seamless integrations.
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Quick Stats */}
|
|
53
|
+
{currentWorkspace && (
|
|
54
|
+
<div className="grid grid-cols-3 gap-4 mb-12">
|
|
55
|
+
<div className="card">
|
|
56
|
+
<p className="text-sm text-slate-600">Current Workspace</p>
|
|
57
|
+
<p className="text-2xl font-bold text-slate-900 mt-2">{currentWorkspace.name}</p>
|
|
58
|
+
<p className="text-xs text-slate-500 mt-2">
|
|
59
|
+
{currentWorkspace.is_public ? '🌍 Public' : '🔒 Private'}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="card">
|
|
63
|
+
<p className="text-sm text-slate-600">Member Access</p>
|
|
64
|
+
<p className="text-2xl font-bold text-slate-900 mt-2">
|
|
65
|
+
{currentWorkspace.roles?.length || 0}
|
|
66
|
+
</p>
|
|
67
|
+
<p className="text-xs text-slate-500 mt-2">roles configured</p>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="card">
|
|
70
|
+
<p className="text-sm text-slate-600">Features</p>
|
|
71
|
+
<p className="text-2xl font-bold text-slate-900 mt-2">6+</p>
|
|
72
|
+
<p className="text-xs text-slate-500 mt-2">available modules</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{/* Features Grid */}
|
|
78
|
+
<div className="mb-12">
|
|
79
|
+
<h2 className="text-2xl font-bold text-slate-900 mb-6">Core Features</h2>
|
|
80
|
+
<div className="grid grid-cols-3 gap-6">
|
|
81
|
+
{features.map(({ icon: Icon, title, description, color, href }) => (
|
|
82
|
+
<button
|
|
83
|
+
key={href}
|
|
84
|
+
onClick={() => navigate(href)}
|
|
85
|
+
className={`card border-2 transition-all hover:shadow-lg cursor-pointer ${
|
|
86
|
+
colorClasses[color as keyof typeof colorClasses]
|
|
87
|
+
}`}
|
|
88
|
+
>
|
|
89
|
+
<div className="flex items-start justify-between mb-3">
|
|
90
|
+
<Icon size={32} />
|
|
91
|
+
<ArrowRight size={18} className="opacity-0 group-hover:opacity-100" />
|
|
92
|
+
</div>
|
|
93
|
+
<h3 className="text-lg font-semibold text-slate-900 text-left mb-2">
|
|
94
|
+
{title}
|
|
95
|
+
</h3>
|
|
96
|
+
<p className="text-sm text-slate-600 text-left">
|
|
97
|
+
{description}
|
|
98
|
+
</p>
|
|
99
|
+
</button>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Quick Links */}
|
|
105
|
+
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
|
106
|
+
<h2 className="text-lg font-semibold text-slate-900 mb-4">Quick Actions</h2>
|
|
107
|
+
<div className="grid grid-cols-2 gap-4">
|
|
108
|
+
<button
|
|
109
|
+
onClick={() => navigate('/workspaces')}
|
|
110
|
+
className="btn btn-primary text-left"
|
|
111
|
+
>
|
|
112
|
+
🏢 Create Workspace
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => navigate('/roles')}
|
|
116
|
+
className="btn btn-secondary text-left"
|
|
117
|
+
>
|
|
118
|
+
👥 Manage Members
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => navigate('/integrations')}
|
|
122
|
+
className="btn btn-secondary text-left"
|
|
123
|
+
>
|
|
124
|
+
⚡ Setup Integrations
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => navigate('/settings')}
|
|
128
|
+
className="btn btn-secondary text-left"
|
|
129
|
+
>
|
|
130
|
+
⚙️ Adjust Settings
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Getting Started Tips */}
|
|
136
|
+
<div className="mt-12 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
|
137
|
+
<h3 className="text-lg font-semibold text-blue-900 mb-3">Getting Started</h3>
|
|
138
|
+
<ul className="space-y-2 text-sm text-blue-800">
|
|
139
|
+
<li>
|
|
140
|
+
<span className="font-semibold">1. Create a Workspace</span> — Start by creating a workspace for your team
|
|
141
|
+
</li>
|
|
142
|
+
<li>
|
|
143
|
+
<span className="font-semibold">2. Add Members</span> — Invite team members and assign roles
|
|
144
|
+
</li>
|
|
145
|
+
<li>
|
|
146
|
+
<span className="font-semibold">3. Setup Integrations</span> — Connect with Slack, GitHub, or Jira
|
|
147
|
+
</li>
|
|
148
|
+
<li>
|
|
149
|
+
<span className="font-semibold">4. Monitor Analytics</span> — Track progress and performance
|
|
150
|
+
</li>
|
|
151
|
+
</ul>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useAuth } from '../hooks'
|
|
2
|
+
import { Save } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export const SettingsPage = () => {
|
|
5
|
+
const { user } = useAuth()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="p-6 max-w-4xl mx-auto">
|
|
9
|
+
<div className="mb-8">
|
|
10
|
+
<h1 className="text-2xl font-bold text-slate-900">Settings</h1>
|
|
11
|
+
<p className="text-slate-600 mt-2">Manage your preferences and account settings</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
{/* User Profile Section */}
|
|
15
|
+
<div className="card mb-6">
|
|
16
|
+
<h2 className="text-lg font-semibold mb-4">Account</h2>
|
|
17
|
+
<div className="space-y-4">
|
|
18
|
+
<div>
|
|
19
|
+
<label className="block text-sm font-medium text-slate-700 mb-2">GitHub Login</label>
|
|
20
|
+
<input
|
|
21
|
+
type="text"
|
|
22
|
+
value={user?.github_login || ''}
|
|
23
|
+
disabled
|
|
24
|
+
className="input w-full bg-slate-100"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{user?.email && (
|
|
29
|
+
<div>
|
|
30
|
+
<label className="block text-sm font-medium text-slate-700 mb-2">Email</label>
|
|
31
|
+
<input
|
|
32
|
+
type="email"
|
|
33
|
+
value={user.email}
|
|
34
|
+
disabled
|
|
35
|
+
className="input w-full bg-slate-100"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{user?.subscription && (
|
|
41
|
+
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
42
|
+
<p className="text-sm font-medium text-green-800">
|
|
43
|
+
✓ Subscription Active
|
|
44
|
+
</p>
|
|
45
|
+
<p className="text-xs text-green-700 mt-1">
|
|
46
|
+
Expires: {new Date(user.subscription.expires_at).toLocaleDateString()}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Preferences Section */}
|
|
54
|
+
<div className="card mb-6">
|
|
55
|
+
<h2 className="text-lg font-semibold mb-4">Preferences</h2>
|
|
56
|
+
<div className="space-y-4">
|
|
57
|
+
<div>
|
|
58
|
+
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
59
|
+
<input type="checkbox" defaultChecked className="mr-2" />
|
|
60
|
+
Email notifications
|
|
61
|
+
</label>
|
|
62
|
+
<p className="text-xs text-slate-600 ml-6">
|
|
63
|
+
Receive email updates for important workspace events
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div>
|
|
68
|
+
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
69
|
+
<input type="checkbox" defaultChecked className="mr-2" />
|
|
70
|
+
Analytics summaries
|
|
71
|
+
</label>
|
|
72
|
+
<p className="text-xs text-slate-600 ml-6">
|
|
73
|
+
Weekly analytics digest to your email
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div>
|
|
78
|
+
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
79
|
+
<input type="checkbox" className="mr-2" />
|
|
80
|
+
Slack notifications
|
|
81
|
+
</label>
|
|
82
|
+
<p className="text-xs text-slate-600 ml-6">
|
|
83
|
+
Send real-time alerts to Slack (requires Slack integration)
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Theme Section */}
|
|
90
|
+
<div className="card mb-6">
|
|
91
|
+
<h2 className="text-lg font-semibold mb-4">Theme</h2>
|
|
92
|
+
<div className="space-y-3">
|
|
93
|
+
<label className="flex items-center gap-3">
|
|
94
|
+
<input type="radio" name="theme" defaultChecked />
|
|
95
|
+
<span className="text-sm">Light (default)</span>
|
|
96
|
+
</label>
|
|
97
|
+
<label className="flex items-center gap-3">
|
|
98
|
+
<input type="radio" name="theme" />
|
|
99
|
+
<span className="text-sm">Dark</span>
|
|
100
|
+
</label>
|
|
101
|
+
<label className="flex items-center gap-3">
|
|
102
|
+
<input type="radio" name="theme" />
|
|
103
|
+
<span className="text-sm">System</span>
|
|
104
|
+
</label>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Action Buttons */}
|
|
109
|
+
<div className="flex gap-3">
|
|
110
|
+
<button className="btn btn-primary flex items-center gap-2">
|
|
111
|
+
<Save size={18} />
|
|
112
|
+
Save Settings
|
|
113
|
+
</button>
|
|
114
|
+
<button className="btn btn-secondary">Cancel</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { HomePage } from './HomePage'
|
|
2
|
+
export { DashboardPage } from './DashboardPage'
|
|
3
|
+
export { WorkspacesPage } from './WorkspacesPage'
|
|
4
|
+
export { RolesPage } from './RolesPage'
|
|
5
|
+
export { IntegrationsPage } from './IntegrationsPage'
|
|
6
|
+
export { SettingsPage } from './SettingsPage'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
// Mock import.meta.env
|
|
5
|
+
Object.defineProperty(import.meta, 'env', {
|
|
6
|
+
value: {
|
|
7
|
+
VITE_API_URL: 'http://localhost:3000',
|
|
8
|
+
VITE_JWT_STORAGE_KEY: 'auth_token',
|
|
9
|
+
VITE_ENV: 'test'
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Mock localStorage
|
|
14
|
+
const localStorageMock = {
|
|
15
|
+
getItem: vi.fn(),
|
|
16
|
+
setItem: vi.fn(),
|
|
17
|
+
removeItem: vi.fn(),
|
|
18
|
+
clear: vi.fn()
|
|
19
|
+
}
|
|
20
|
+
global.localStorage = localStorageMock as any
|
|
21
|
+
|
|
22
|
+
// Mock window.location.href
|
|
23
|
+
delete (window as any).location
|
|
24
|
+
window.location = { href: '' } as any
|
|
25
|
+
|
|
26
|
+
// Suppress console errors in tests
|
|
27
|
+
global.console = {
|
|
28
|
+
...console,
|
|
29
|
+
error: vi.fn(),
|
|
30
|
+
warn: vi.fn()
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ReactElement } from 'react'
|
|
2
|
+
import { render, RenderOptions } from '@testing-library/react'
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
4
|
+
|
|
5
|
+
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
|
|
6
|
+
return <BrowserRouter>{children}</BrowserRouter>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const customRender = (
|
|
10
|
+
ui: ReactElement,
|
|
11
|
+
options?: Omit<RenderOptions, 'wrapper'>
|
|
12
|
+
) => render(ui, { wrapper: AllTheProviders, ...options })
|
|
13
|
+
|
|
14
|
+
export * from '@testing-library/react'
|
|
15
|
+
export { customRender as render }
|