skillverse 0.1.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/.prettierrc +10 -0
- package/README.md +369 -0
- package/client/README.md +73 -0
- package/client/eslint.config.js +23 -0
- package/client/index.html +13 -0
- package/client/package.json +41 -0
- package/client/postcss.config.js +6 -0
- package/client/public/vite.svg +1 -0
- package/client/src/App.css +42 -0
- package/client/src/App.tsx +26 -0
- package/client/src/assets/react.svg +1 -0
- package/client/src/components/AddSkillDialog.tsx +249 -0
- package/client/src/components/Layout.tsx +134 -0
- package/client/src/components/LinkWorkspaceDialog.tsx +196 -0
- package/client/src/components/LoadingSpinner.tsx +57 -0
- package/client/src/components/SkillCard.tsx +269 -0
- package/client/src/components/Toast.tsx +44 -0
- package/client/src/components/Tooltip.tsx +132 -0
- package/client/src/index.css +168 -0
- package/client/src/lib/api.ts +196 -0
- package/client/src/main.tsx +10 -0
- package/client/src/pages/Dashboard.tsx +209 -0
- package/client/src/pages/Marketplace.tsx +282 -0
- package/client/src/pages/Settings.tsx +136 -0
- package/client/src/pages/SkillLibrary.tsx +163 -0
- package/client/src/pages/Workspaces.tsx +662 -0
- package/client/src/stores/appStore.ts +222 -0
- package/client/tailwind.config.js +82 -0
- package/client/tsconfig.app.json +28 -0
- package/client/tsconfig.json +7 -0
- package/client/tsconfig.node.json +26 -0
- package/client/vite.config.ts +26 -0
- package/package.json +34 -0
- package/registry/.env.example +5 -0
- package/registry/Dockerfile +42 -0
- package/registry/docker-compose.yml +33 -0
- package/registry/package.json +37 -0
- package/registry/prisma/schema.prisma +59 -0
- package/registry/src/index.ts +34 -0
- package/registry/src/lib/db.ts +3 -0
- package/registry/src/middleware/errorHandler.ts +35 -0
- package/registry/src/routes/auth.ts +152 -0
- package/registry/src/routes/skills.ts +295 -0
- package/registry/tsconfig.json +23 -0
- package/server/.env.example +11 -0
- package/server/package.json +60 -0
- package/server/prisma/schema.prisma +73 -0
- package/server/public/assets/index-BsYtpZSa.css +1 -0
- package/server/public/assets/index-Dfr_6UV8.js +20 -0
- package/server/public/index.html +14 -0
- package/server/public/vite.svg +1 -0
- package/server/src/bin.ts +428 -0
- package/server/src/config.ts +39 -0
- package/server/src/index.ts +112 -0
- package/server/src/lib/db.ts +14 -0
- package/server/src/middleware/errorHandler.ts +40 -0
- package/server/src/middleware/logger.ts +12 -0
- package/server/src/routes/dashboard.ts +102 -0
- package/server/src/routes/marketplace.ts +273 -0
- package/server/src/routes/skills.ts +294 -0
- package/server/src/routes/workspaces.ts +168 -0
- package/server/src/services/bundleService.ts +123 -0
- package/server/src/services/skillService.ts +722 -0
- package/server/src/services/workspaceService.ts +521 -0
- package/server/src/verify-sync.ts +91 -0
- package/server/tsconfig.json +19 -0
- package/server/tsup.config.ts +18 -0
- package/shared/package.json +21 -0
- package/shared/pnpm-lock.yaml +24 -0
- package/shared/src/index.ts +169 -0
- package/shared/tsconfig.json +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import type {
|
|
3
|
+
Skill,
|
|
4
|
+
Workspace,
|
|
5
|
+
MarketplaceSkill,
|
|
6
|
+
DashboardStats,
|
|
7
|
+
ApiResponse,
|
|
8
|
+
PaginatedResponse,
|
|
9
|
+
CreateSkillFromGitDto,
|
|
10
|
+
CreateWorkspaceDto,
|
|
11
|
+
UpdateSkillDto,
|
|
12
|
+
UpdateWorkspaceDto,
|
|
13
|
+
PublishSkillDto,
|
|
14
|
+
SkillWorkspace,
|
|
15
|
+
UpdateCheckResponse,
|
|
16
|
+
} from '@skillverse/shared';
|
|
17
|
+
|
|
18
|
+
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
|
|
19
|
+
|
|
20
|
+
const api = axios.create({
|
|
21
|
+
baseURL: API_BASE_URL,
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Response interceptor for error handling
|
|
28
|
+
api.interceptors.response.use(
|
|
29
|
+
(response) => response,
|
|
30
|
+
(error) => {
|
|
31
|
+
const message = error.response?.data?.error || error.message || 'An error occurred';
|
|
32
|
+
console.error('API Error:', message);
|
|
33
|
+
return Promise.reject(new Error(message));
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Skills API
|
|
38
|
+
export const skillsApi = {
|
|
39
|
+
getAll: async (): Promise<Skill[]> => {
|
|
40
|
+
const { data } = await api.get<ApiResponse<Skill[]>>('/skills');
|
|
41
|
+
return data.data || [];
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
getById: async (id: string): Promise<Skill> => {
|
|
45
|
+
const { data } = await api.get<ApiResponse<Skill>>(`/skills/${id}`);
|
|
46
|
+
return data.data!;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
createFromGit: async (dto: CreateSkillFromGitDto): Promise<Skill> => {
|
|
50
|
+
const { data } = await api.post<ApiResponse<Skill>>('/skills/from-git', dto);
|
|
51
|
+
return data.data!;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
createFromLocal: async (name: string, file: File, description?: string): Promise<Skill> => {
|
|
55
|
+
const formData = new FormData();
|
|
56
|
+
formData.append('name', name);
|
|
57
|
+
formData.append('file', file);
|
|
58
|
+
if (description) formData.append('description', description);
|
|
59
|
+
|
|
60
|
+
const { data } = await api.post<ApiResponse<Skill>>('/skills/from-local', formData, {
|
|
61
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
62
|
+
});
|
|
63
|
+
return data.data!;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
update: async (id: string, dto: UpdateSkillDto): Promise<Skill> => {
|
|
67
|
+
const { data } = await api.put<ApiResponse<Skill>>(`/skills/${id}`, dto);
|
|
68
|
+
return data.data!;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
delete: async (id: string, removeFiles = true): Promise<void> => {
|
|
72
|
+
await api.delete(`/skills/${id}?removeFiles=${removeFiles}`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
linkToWorkspace: async (skillId: string, workspaceId: string): Promise<SkillWorkspace> => {
|
|
76
|
+
const { data } = await api.post<ApiResponse<SkillWorkspace>>(`/skills/${skillId}/link`, {
|
|
77
|
+
workspaceId,
|
|
78
|
+
});
|
|
79
|
+
return data.data!;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
unlinkFromWorkspace: async (skillId: string, workspaceId: string): Promise<void> => {
|
|
83
|
+
await api.delete(`/skills/${skillId}/unlink/${workspaceId}`);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
checkUpdate: async (id: string): Promise<UpdateCheckResponse> => {
|
|
87
|
+
const { data } = await api.get<ApiResponse<UpdateCheckResponse>>(`/skills/${id}/check-update`);
|
|
88
|
+
return data.data!;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
upgrade: async (id: string): Promise<Skill> => {
|
|
92
|
+
const { data } = await api.post<ApiResponse<Skill>>(`/skills/${id}/upgrade`);
|
|
93
|
+
return data.data!;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
checkUpdates: async (ids?: string[]): Promise<Record<string, UpdateCheckResponse>> => {
|
|
97
|
+
const { data } = await api.post<ApiResponse<Record<string, UpdateCheckResponse>>>('/skills/check-updates', { ids });
|
|
98
|
+
return data.data!;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
getSkillMd: async (id: string): Promise<{ exists: boolean; content: string | null }> => {
|
|
102
|
+
const { data } = await api.get<ApiResponse<{ exists: boolean; content: string | null }>>(`/skills/${id}/skill-md`);
|
|
103
|
+
return data.data!;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Workspaces API
|
|
108
|
+
export const workspacesApi = {
|
|
109
|
+
getAll: async (): Promise<Workspace[]> => {
|
|
110
|
+
const { data } = await api.get<ApiResponse<Workspace[]>>('/workspaces');
|
|
111
|
+
return data.data || [];
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
getById: async (id: string): Promise<Workspace> => {
|
|
115
|
+
const { data } = await api.get<ApiResponse<Workspace>>(`/workspaces/${id}`);
|
|
116
|
+
return data.data!;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
create: async (dto: CreateWorkspaceDto): Promise<Workspace> => {
|
|
120
|
+
const { data } = await api.post<ApiResponse<Workspace>>('/workspaces', dto);
|
|
121
|
+
return data.data!;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
update: async (id: string, dto: UpdateWorkspaceDto): Promise<Workspace> => {
|
|
125
|
+
const { data } = await api.put<ApiResponse<Workspace>>(`/workspaces/${id}`, dto);
|
|
126
|
+
return data.data!;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
delete: async (id: string): Promise<void> => {
|
|
130
|
+
await api.delete(`/workspaces/${id}`);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
detectSkills: async (id: string): Promise<{ name: string; hasSkillMd: boolean; path: string }[]> => {
|
|
134
|
+
const { data } = await api.post<ApiResponse<{ name: string; hasSkillMd: boolean; path: string }[]>>(`/workspaces/${id}/detect-skills`);
|
|
135
|
+
return data.data || [];
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
migrateSkills: async (id: string, skillNames: string[]): Promise<{ success: boolean; migrated: string[]; errors: string[] }> => {
|
|
139
|
+
const { data } = await api.post<ApiResponse<{ success: boolean; migrated: string[]; errors: string[] }>>(`/workspaces/${id}/migrate-skills`, { skillNames });
|
|
140
|
+
return data.data!;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Marketplace API
|
|
145
|
+
export const marketplaceApi = {
|
|
146
|
+
getSkills: async (
|
|
147
|
+
page = 1,
|
|
148
|
+
pageSize = 20,
|
|
149
|
+
search?: string
|
|
150
|
+
): Promise<PaginatedResponse<MarketplaceSkill>> => {
|
|
151
|
+
const params = new URLSearchParams({
|
|
152
|
+
page: page.toString(),
|
|
153
|
+
pageSize: pageSize.toString(),
|
|
154
|
+
});
|
|
155
|
+
if (search) params.append('search', search);
|
|
156
|
+
|
|
157
|
+
const { data } = await api.get<ApiResponse<PaginatedResponse<MarketplaceSkill>>>(
|
|
158
|
+
`/marketplace/skills?${params}`
|
|
159
|
+
);
|
|
160
|
+
return data.data!;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
getSkillById: async (id: string): Promise<MarketplaceSkill> => {
|
|
164
|
+
const { data } = await api.get<ApiResponse<MarketplaceSkill>>(`/marketplace/skills/${id}`);
|
|
165
|
+
return data.data!;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
publish: async (dto: PublishSkillDto): Promise<MarketplaceSkill> => {
|
|
169
|
+
const { data } = await api.post<ApiResponse<MarketplaceSkill>>('/marketplace/publish', dto);
|
|
170
|
+
return data.data!;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
install: async (id: string): Promise<Skill> => {
|
|
174
|
+
const { data } = await api.post<ApiResponse<Skill>>(`/marketplace/install/${id}`);
|
|
175
|
+
return data.data!;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
unpublish: async (skillId: string): Promise<void> => {
|
|
179
|
+
await api.delete(`/marketplace/unpublish/${skillId}`);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Dashboard API
|
|
184
|
+
export const dashboardApi = {
|
|
185
|
+
getStats: async (): Promise<DashboardStats> => {
|
|
186
|
+
const { data } = await api.get<ApiResponse<DashboardStats>>('/dashboard/stats');
|
|
187
|
+
return data.data!;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
getActivity: async (): Promise<any[]> => {
|
|
191
|
+
const { data } = await api.get<ApiResponse<any[]>>('/dashboard/activity');
|
|
192
|
+
return data.data || [];
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export default api;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useAppStore } from '../stores/appStore';
|
|
3
|
+
import { Package, FolderOpen, Link2, Store, TrendingUp, Clock } from 'lucide-react';
|
|
4
|
+
import { LoadingPage } from '../components/LoadingSpinner';
|
|
5
|
+
import Tooltip from '../components/Tooltip';
|
|
6
|
+
import { clsx } from 'clsx';
|
|
7
|
+
|
|
8
|
+
export default function Dashboard() {
|
|
9
|
+
const { stats, statsLoading, fetchStats } = useAppStore();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
fetchStats();
|
|
13
|
+
}, [fetchStats]);
|
|
14
|
+
|
|
15
|
+
if (statsLoading && !stats) {
|
|
16
|
+
return <LoadingPage />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const statCards = [
|
|
20
|
+
{
|
|
21
|
+
label: 'Total Skills',
|
|
22
|
+
value: stats?.totalSkills || 0,
|
|
23
|
+
icon: Package,
|
|
24
|
+
color: 'from-primary-500 to-primary-600',
|
|
25
|
+
bgColor: 'bg-primary-500/10',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'Workspaces',
|
|
29
|
+
value: stats?.totalWorkspaces || 0,
|
|
30
|
+
icon: FolderOpen,
|
|
31
|
+
color: 'from-accent-500 to-accent-600',
|
|
32
|
+
bgColor: 'bg-accent-500/10',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'Active Links',
|
|
36
|
+
value: stats?.totalLinks || 0,
|
|
37
|
+
icon: Link2,
|
|
38
|
+
color: 'from-green-500 to-green-600',
|
|
39
|
+
bgColor: 'bg-green-500/10',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'Published',
|
|
43
|
+
value: stats?.marketplaceSkills || 0,
|
|
44
|
+
icon: Store,
|
|
45
|
+
color: 'from-orange-500 to-orange-600',
|
|
46
|
+
bgColor: 'bg-orange-500/10',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="space-y-6 sm:space-y-8 animate-fade-in">
|
|
52
|
+
{/* Header */}
|
|
53
|
+
<div>
|
|
54
|
+
<h1 className="text-2xl sm:text-3xl font-bold text-dark-100">Dashboard</h1>
|
|
55
|
+
<p className="text-dark-400 mt-1 text-sm sm:text-base text-justified">
|
|
56
|
+
Overview of your skill management system
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Stats Grid - Responsive */}
|
|
61
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-6">
|
|
62
|
+
{statCards.map((stat) => (
|
|
63
|
+
<div
|
|
64
|
+
key={stat.label}
|
|
65
|
+
className="card hover:scale-[1.02] transition-transform p-4 sm:p-6"
|
|
66
|
+
>
|
|
67
|
+
<div className="flex items-start justify-between">
|
|
68
|
+
<div className="min-w-0 flex-1">
|
|
69
|
+
<p className="text-xs sm:text-sm text-dark-400 truncate">{stat.label}</p>
|
|
70
|
+
<p className="text-xl sm:text-3xl font-bold text-dark-100 mt-1">
|
|
71
|
+
{stat.value}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div
|
|
75
|
+
className={clsx(
|
|
76
|
+
'w-10 h-10 sm:w-12 sm:h-12 rounded-xl flex items-center justify-center shrink-0 ml-2',
|
|
77
|
+
stat.bgColor
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<stat.icon
|
|
81
|
+
className={clsx(
|
|
82
|
+
'w-5 h-5 sm:w-6 sm:h-6 bg-gradient-to-r bg-clip-text',
|
|
83
|
+
stat.color.replace('from-', 'text-').split(' ')[0]
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="mt-3 sm:mt-4 flex items-center gap-1 text-xs text-dark-500">
|
|
89
|
+
<TrendingUp className="w-3.5 h-3.5 text-green-400" />
|
|
90
|
+
<span className="text-green-400">Active</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Recent Skills */}
|
|
97
|
+
<div className="card">
|
|
98
|
+
<div className="flex items-center justify-between mb-4 sm:mb-6">
|
|
99
|
+
<h2 className="text-base sm:text-lg font-semibold text-dark-100">Recent Skills</h2>
|
|
100
|
+
<Clock className="w-5 h-5 text-dark-400" />
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{stats?.recentSkills && stats.recentSkills.length > 0 ? (
|
|
104
|
+
<div className="space-y-2 sm:space-y-3">
|
|
105
|
+
{stats.recentSkills.map((skill: any) => (
|
|
106
|
+
<div
|
|
107
|
+
key={skill.id}
|
|
108
|
+
className="flex items-center justify-between p-3 sm:p-4 rounded-xl bg-dark-750 hover:bg-dark-700 transition-colors gap-3"
|
|
109
|
+
>
|
|
110
|
+
<div className="flex items-center gap-3 sm:gap-4 min-w-0 flex-1">
|
|
111
|
+
<div
|
|
112
|
+
className={clsx(
|
|
113
|
+
'w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center shrink-0',
|
|
114
|
+
skill.source === 'git'
|
|
115
|
+
? 'bg-primary-500/10 text-primary-400'
|
|
116
|
+
: 'bg-accent-500/10 text-accent-400'
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<Package className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
120
|
+
</div>
|
|
121
|
+
<div className="min-w-0 flex-1">
|
|
122
|
+
<Tooltip content={skill.name} position="top">
|
|
123
|
+
<p className="font-medium text-dark-100 text-sm sm:text-base truncate max-w-[200px] sm:max-w-none">
|
|
124
|
+
{skill.name}
|
|
125
|
+
</p>
|
|
126
|
+
</Tooltip>
|
|
127
|
+
<p className="text-xs sm:text-sm text-dark-500">
|
|
128
|
+
{skill.source === 'git' ? 'Git Repository' : 'Local Upload'}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="text-right shrink-0">
|
|
133
|
+
<p className="text-xs sm:text-sm text-dark-400">
|
|
134
|
+
{new Date(skill.installDate).toLocaleDateString()}
|
|
135
|
+
</p>
|
|
136
|
+
<p className="text-xs text-dark-500">
|
|
137
|
+
{skill.linkedWorkspaces?.length || 0} linked
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
<div className="text-center py-6 sm:py-8">
|
|
145
|
+
<Package className="w-10 h-10 sm:w-12 sm:h-12 mx-auto text-dark-600 mb-3" />
|
|
146
|
+
<p className="text-dark-400 text-sm sm:text-base">No skills yet</p>
|
|
147
|
+
<p className="text-xs sm:text-sm text-dark-500 mt-1">
|
|
148
|
+
Add your first skill to get started
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Quick Actions - Responsive */}
|
|
155
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
156
|
+
<a
|
|
157
|
+
href="/skills"
|
|
158
|
+
className="card group cursor-pointer hover:border-primary-500/50 transition-all"
|
|
159
|
+
>
|
|
160
|
+
<div className="flex items-center gap-3 sm:gap-4">
|
|
161
|
+
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-primary-500/10 flex items-center justify-center group-hover:scale-110 transition-transform shrink-0">
|
|
162
|
+
<Package className="w-5 h-5 sm:w-6 sm:h-6 text-primary-400" />
|
|
163
|
+
</div>
|
|
164
|
+
<div className="min-w-0 flex-1">
|
|
165
|
+
<p className="font-medium text-dark-100 group-hover:text-primary-400 transition-colors text-sm sm:text-base">
|
|
166
|
+
Manage Skills
|
|
167
|
+
</p>
|
|
168
|
+
<p className="text-xs sm:text-sm text-dark-500 truncate">View and organize your skills</p>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</a>
|
|
172
|
+
|
|
173
|
+
<a
|
|
174
|
+
href="/workspaces"
|
|
175
|
+
className="card group cursor-pointer hover:border-accent-500/50 transition-all"
|
|
176
|
+
>
|
|
177
|
+
<div className="flex items-center gap-3 sm:gap-4">
|
|
178
|
+
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-accent-500/10 flex items-center justify-center group-hover:scale-110 transition-transform shrink-0">
|
|
179
|
+
<FolderOpen className="w-5 h-5 sm:w-6 sm:h-6 text-accent-400" />
|
|
180
|
+
</div>
|
|
181
|
+
<div className="min-w-0 flex-1">
|
|
182
|
+
<p className="font-medium text-dark-100 group-hover:text-accent-400 transition-colors text-sm sm:text-base">
|
|
183
|
+
Workspaces
|
|
184
|
+
</p>
|
|
185
|
+
<p className="text-xs sm:text-sm text-dark-500 truncate">Configure your workspaces</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</a>
|
|
189
|
+
|
|
190
|
+
<a
|
|
191
|
+
href="/marketplace"
|
|
192
|
+
className="card group cursor-pointer hover:border-orange-500/50 transition-all sm:col-span-2 lg:col-span-1"
|
|
193
|
+
>
|
|
194
|
+
<div className="flex items-center gap-3 sm:gap-4">
|
|
195
|
+
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-orange-500/10 flex items-center justify-center group-hover:scale-110 transition-transform shrink-0">
|
|
196
|
+
<Store className="w-5 h-5 sm:w-6 sm:h-6 text-orange-400" />
|
|
197
|
+
</div>
|
|
198
|
+
<div className="min-w-0 flex-1">
|
|
199
|
+
<p className="font-medium text-dark-100 group-hover:text-orange-400 transition-colors text-sm sm:text-base">
|
|
200
|
+
Marketplace
|
|
201
|
+
</p>
|
|
202
|
+
<p className="text-xs sm:text-sm text-dark-500 truncate">Browse and share skills</p>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</a>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|