diff --git a/app/components/WorkspacePanel.tsx b/app/components/WorkspacePanel.tsx
index 49102da..fc2f176 100644
--- a/app/components/WorkspacePanel.tsx
+++ b/app/components/WorkspacePanel.tsx
@@ -4,7 +4,7 @@ import { Workspace, Repository } from '../types/workspace';
import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
export const WorkspacePanel: React.FC = () => {
- const {
+const {
workspaces,
currentWorkspace,
setCurrentWorkspace,
@@ -14,8 +14,19 @@ export const WorkspacePanel: React.FC = () => {
addRepository,
removeRepository,
updateRepositorySettings,
+ createGroup,
+ updateGroup,
+ deleteGroup,
+ getGroup,
} = useWorkspaces();
+ const [showNewGroupForm, setShowNewGroupForm] = useState(false);
+ const [newGroupName, setNewGroupName] = useState("");
+ const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
+ const [isRenamingGroup, setIsRenamingGroup] = useState(false);
+ const [renamingGroupId, setRenamingGroupId] = useState<string | null>(null);
+ const [editedGroupName, setEditedGroupName] = useState("");
+
const [showNewWorkspace, setShowNewWorkspace] = React.useState(false);
const [newWorkspaceName, setNewWorkspaceName] = React.useState('');
const [newWorkspaceVisibility, setNewWorkspaceVisibility] = React.useState<'private' | 'public' | 'team'>('private');
@@ -75,7 +86,55 @@ export const WorkspacePanel: React.FC = () => {
}
};
- const currentWorkspaceData = workspaces.find(w => w.id === currentWorkspace);
+const currentWorkspaceData = workspaces.find((w) =>w.id === currentWorkspace);
+
+ const handleCreateGroup = () =>{
+ if (newGroupName && currentWorkspace) {
+ createGroup(currentWorkspace, newGroupName);
+ setNewGroupName("");
+ setShowNewGroupForm(false);
+ }
+ };
+
+ const handleRenameGroup = (group: Group) =>{
+ setRenamingGroupId(group.id);
+ setEditedGroupName(group.name);
+ setIsRenamingGroup(true);
+ };
+
+ const handleSaveGroupName = (groupId: string) =>{
+ if (currentWorkspace && editedGroupName) {
+ updateGroup(currentWorkspace, groupId, { name: editedGroupName });
+ }
+ setIsRenamingGroup(false);
+ setRenamingGroupId(null);
+ setEditedGroupName("");
+ };
+
+ const handleDeleteGroup = (groupId: string) =>{
+ if (
+ currentWorkspace &&
+ confirm("Are you sure you want to delete this group?")
+ ) {
+ deleteGroup(currentWorkspace, groupId);
+ setSelectedGroup(null);
+ }
+ };
+
+ const handleSelectGroup = (groupId: string) =>{
+ setSelectedGroup(groupId);
+ };
+
+ const filteredRepositories = React.useMemo(() =>{
+ if (!currentWorkspaceData) return [];
+ if (!selectedGroup) return currentWorkspaceData.repositories;
+
+ const group = getGroup(currentWorkspaceData.id, selectedGroup);
+ if (!group) return [];
+
+ return currentWorkspaceData.repositories.filter((repo) =>group.repositoryIds.includes(repo.id)
+ );
+ }, [currentWorkspaceData, selectedGroup]);
return (
@@ -90,81 +149,195 @@ export const WorkspacePanel: React.FC = () => {
-
- {workspaces.map(workspace => (
-
setCurrentWorkspace(workspace.id)}
- >
-
-
-
-
{workspace.name}
-
- {workspace.settings?.visibility || 'private'}
-
-
- {workspace.description && (
-
{workspace.description}
- )}
- {workspace.settings?.collaborators?.length > 0 && (
-
- {workspace.settings.collaborators.length} collaborator(s)
-
- )}
-
-
-
-
-
-
-
- {workspace.repositories.length} repositories
-
-
-
- ))}
-
+ >
+ <div className="flex justify-between items-start mb-2">
+ <div>
+ <div className="flex items-center gap-2">
+ <h3 className="font-medium">{workspace.name}</h3>
+ <span
+ className={`text-xs px-2 py-0.5 rounded-full ${
+ workspace.settings?.visibility === "public"
+ ? "bg-green-600/20 text-green-300"
+ : workspace.settings?.visibility === "team"
+ ? "bg-blue-600/20 text-blue-300"
+ : "bg-gray-600/20 text-gray-300"
+ }`}
+ >
+ {workspace.settings?.visibility || "private"}
+ </span>
+ </div>
+ {workspace.description && (
+ <p className="text-sm text-gray-400">
+ {workspace.description}
+ </p>
+ )}
+ {workspace.settings?.collaborators?.length > 0 && (
+ <div className="text-xs text-gray-400 mt-1">
+ {workspace.settings.collaborators.length} collaborator(s)
+ </div>
+ )}
+ </div>
+ <div className="flex items-center gap-2">
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ updateWorkspace(workspace.id, {
+ name:
+ prompt("New workspace name:", workspace.name) ||
+ workspace.name,
+ });
+ }}
+ className="p-1 rounded hover:bg-[#444444] transition-colors"
+ >
+ <PencilIcon className="h-4 w-4" />
+ </button>
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ if (
+ confirm("Are you sure you want to delete this workspace?")
+ ) {
+ deleteWorkspace(workspace.id);
+ }
+ }}
+ className="p-1 rounded hover:bg-[#444444] transition-colors"
+ >
+ <TrashIcon className="h-4 w-4" />
+ </button>
+ </div>
+ </div>
+ <div className="flex items-center gap-2 text-sm text-gray-400">
+ <span>{workspace.repositories.length} repositories</span>
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ setCurrentWorkspace(workspace.id);
+ setShowCollaborators(true);
+ }}
+ className="px-2 py-1 rounded bg-[#444444] hover:bg-[#555555] transition-colors"
+ >
+ Manage Collaborators
+ </button>
+ </div>
+ </div>
+ {workspace.id === currentWorkspace && (
+ <div className="mt-4">
+ <div className="flex justify-between items-center mb-2">
+ <h4 className="text-lg font-medium">Groups</h4>
+ <button
+ onClick={() => setShowNewGroupForm(true)}
+ className="px-3 py-1.5 rounded text-sm bg-indigo-600 hover:bg-indigo-700 transition-colors flex items-center gap-2"
+ >
+ <PlusIcon className="h-4 w-4" />
+ New Group
+ </button>
+ </div>
+ <div className="space-y-2">
+ {workspace.groups.map((group) => (
+ <div
+ key={group.id}
+ className={`p-2 rounded-lg ${
+ selectedGroup === group.id
+ ? "bg-indigo-600/20 border border-indigo-500/30"
+ : "bg-[#444444] hover:bg-[#4A4A4A]"
+ } transition-colors cursor-pointer`}
+ onClick={() => handleSelectGroup(group.id)}
+ >
+ <div className="flex justify-between items-center">
+ <span className="font-medium">
+ {isRenamingGroup && renamingGroupId === group.id ? (
+ <input
+ type="text"
+ value={editedGroupName}
+ onChange={(e) =>
+ setEditedGroupName(e.target.value)
+ }
+ onBlur={() => handleSaveGroupName(group.id)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleSaveGroupName(group.id);
+ }
+ }}
+ className="px-2 py-1 rounded bg-[#333333] text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
+ autoFocus
+ />
+ ) : (
+ group.name
+ )}
+ </span>
+ <div className="flex items-center gap-2">
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ handleRenameGroup(group);
+ }}
+ className="p-1 rounded hover:bg-[#555555] transition-colors"
+ >
+ <PencilIcon className="h-4 w-4" />
+ </button>
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ handleDeleteGroup(group.id);
+ }}
+ className="p-1 rounded hover:bg-[#555555] transition-colors"
+ >
+ <TrashIcon className="h-4 w-4" />
+ </button>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ {/* New Group Form */}
+ {showNewGroupForm && (
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
+ <div className="bg-[#212121] p-6 rounded-lg w-full max-w-md mx-4">
+ <h2 className="text-xl font-semibold mb-4">
+ Create New Group
+ </h2>
+ <input
+ type="text"
+ value={newGroupName}
+ onChange={(e) => setNewGroupName(e.target.value)}
+ placeholder="Group name"
+ className="w-full p-2 rounded bg-[#333333] text-white mb-4"
+ />
+ <div className="flex justify-end gap-4">
+ <button
+ onClick={() => setShowNewGroupForm(false)}
+ className="px-4 py-2 rounded bg-[#444444] hover:bg-[#555555] transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleCreateGroup}
+ className="px-4 py-2 rounded bg-indigo-600 hover:bg-indigo-700 transition-colors"
+ >
+ Create
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ ))}
+ </div>
{currentWorkspaceData && (
diff --git a/app/hooks/useWorkspaces.ts b/app/hooks/useWorkspaces.ts
index 90d455a..14f5d77 100644
--- a/app/hooks/useWorkspaces.ts
+++ b/app/hooks/useWorkspaces.ts
@@ -4,39 +4,149 @@ import { Workspace, Repository, WorkspaceSettings } from '../types/workspace';
const STORAGE_KEY = 'workspaces';
export const useWorkspaces = () => {
- const [workspaces, setWorkspaces] = useState([]);
+const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
+ const [groups, setGroups] = useState<Group[]>([]);
const [currentWorkspace, setCurrentWorkspace] = useState(null);
- useEffect(() => {
+useEffect(() =>{
// Load workspaces from localStorage on mount
const storedWorkspaces = localStorage.getItem(STORAGE_KEY);
if (storedWorkspaces) {
const parsed = JSON.parse(storedWorkspaces);
setWorkspaces(parsed);
- if (parsed.length > 0) {
- setCurrentWorkspace(parsed[0].id);
+ if (parsed.length >0) {
+ setCurrentWorkspace(parsed.find((w: Workspace) =>w.isDefault)?.id || parsed[0].id);
+ } else {
+ // Create a default workspace if none exists
+ const defaultWorkspace: Workspace = {
+ id: crypto.randomUUID(),
+ name: 'Default Workspace',
+ description: 'Your default workspace',
+ repositories: [],
+ settings: {
+ visibility: 'private',
+ collaborators: [],
+ lastAccessed: new Date(),
+ },
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ isDefault: true,
+ };
+ setWorkspaces([defaultWorkspace]);
+ setCurrentWorkspace(defaultWorkspace.id);
}
+ } else {
+ // Create a default workspace if none exists
+ const defaultWorkspace: Workspace = {
+ id: crypto.randomUUID(),
+ name: 'Default Workspace',
+ description: 'Your default workspace',
+ repositories: [],
+ settings: {
+ visibility: 'private',
+ collaborators: [],
+ lastAccessed: new Date(),
+ },
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ isDefault: true,
+ };
+ setWorkspaces([defaultWorkspace]);
+ setCurrentWorkspace(defaultWorkspace.id);
}
}, []);
+const getWorkspace = (workspaceId: string): Workspace | undefined =>{
+ return workspaces.find((w) =>w.id === workspaceId);
+ };
+
+ const getGroup = (workspaceId: string, groupId: string): Group | undefined =>{
+ const workspace = getWorkspace(workspaceId);
+ if (!workspace) return undefined;
+ return workspace.groups.find((g) =>g.id === groupId);
+ };
+
+ const createGroup = (workspaceId: string, groupName: string): Group =>{
+ const newGroup: Group = {
+ id: crypto.randomUUID(),
+ name: groupName,
+ repositoryIds: [],
+ };
+
+ setWorkspaces(
+ workspaces.map((workspace) =>workspace.id === workspaceId
+ ? { ...workspace, groups: [...workspace.groups, newGroup] }
+ : workspace
+ )
+ );
+
+ return newGroup;
+ };
+
+ const updateGroup = (
+ workspaceId: string,
+ groupId: string,
+ updates: Partial<Group>
+ ) =>{
+ setWorkspaces(
+ workspaces.map((workspace) =>workspace.id === workspaceId
+ ? {
+ ...workspace,
+ groups: workspace.groups.map((group) =>group.id === groupId ? { ...group, ...updates } : group
+ ),
+ }
+ : workspace
+ )
+ );
+ };
+
+ const deleteGroup = (workspaceId: string, groupId: string) =>{
+ setWorkspaces(
+ workspaces.map((workspace) =>workspace.id === workspaceId
+ ? {
+ ...workspace,
+ groups: workspace.groups.filter((group) =>group.id !== groupId),
+ }
+ : workspace
+ )
+ );
+ };
+
useEffect(() => {
// Save workspaces to localStorage when they change
localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces));
}, [workspaces]);
- const createWorkspace = (workspace: Omit) => {
+const createWorkspace = (
+ workspace: Omit<Workspace, "id" | "createdAt" | "updatedAt" | "groups">
+ ) =>{
const newWorkspace: Workspace = {
...workspace,
id: crypto.randomUUID(),
createdAt: new Date(),
updatedAt: new Date(),
+ groups: [],
settings: {
...workspace.settings,
lastAccessed: new Date(),
- visibility: workspace.settings?.visibility || 'private'
+ visibility: workspace.settings?.visibility || "private",
},
};
+
+ const defaultGroup: Group = {
+ id: crypto.randomUUID(),
+ name: "Default Group",
+ repositoryIds: [],
+ };
+
+ newWorkspace.groups.push(defaultGroup);
+
+ if (workspaces.length === 0) {
+ newWorkspace.isDefault = true;
+ }
+
setWorkspaces([...workspaces, newWorkspace]);
+ setCurrentWorkspace(newWorkspace.id);
return newWorkspace;
};
@@ -48,37 +158,74 @@ export const useWorkspaces = () => {
));
};
- const deleteWorkspace = (id: string) => {
- setWorkspaces(workspaces.filter(workspace => workspace.id !== id));
+const deleteWorkspace = (id: string) =>{
+ const updatedWorkspaces = workspaces.filter(workspace =>workspace.id !== id);
+ setWorkspaces(updatedWorkspaces);
if (currentWorkspace === id) {
- setCurrentWorkspace(workspaces[0]?.id ?? null);
+ setCurrentWorkspace(
+ updatedWorkspaces.find(w =>w.isDefault)?.id ||
+ updatedWorkspaces[0]?.id ||
+ null
+ );
}
};
- const addRepository = (workspaceId: string, repository: Omit) => {
+const addRepository = (
+ workspaceId: string,
+ repository: Omit<Repository, "id" | "addedAt">,
+ groupId?: string
+ ) =>{
+ const workspace = getWorkspace(workspaceId);
+ if (!workspace) {
+ throw new Error(`Workspace not found: ${workspaceId}`);
+ }
+
const newRepository: Repository = {
...repository,
id: crypto.randomUUID(),
addedAt: new Date(),
+ groupId: groupId || workspace.groups[0]?.id, // Add to the specified group or the first group by default
};
updateWorkspace(workspaceId, {
- repositories: [
- ...(workspaces.find(w => w.id === workspaceId)?.repositories || []),
- newRepository
- ]
+ repositories: [...workspace.repositories, newRepository],
});
+ // Add the repository ID to the group's repositoryIds array
+ if (groupId) {
+ updateGroup(workspaceId, groupId, {
+ repositoryIds: [
+ ...(workspace.groups.find((g) =>g.id === groupId)?.repositoryIds ||
+ []),
+ newRepository.id,
+ ],
+ });
+ }
+
return newRepository;
};
- const removeRepository = (workspaceId: string, repositoryId: string) => {
- const workspace = workspaces.find(w => w.id === workspaceId);
- if (workspace) {
- updateWorkspace(workspaceId, {
- repositories: workspace.repositories.filter(r => r.id !== repositoryId)
- });
+const removeRepository = (workspaceId: string, repositoryId: string) =>{
+ const workspace = getWorkspace(workspaceId);
+ if (!workspace) return;
+
+ const repository = workspace.repositories.find((r) =>r.id === repositoryId);
+ if (!repository) return;
+
+ // Remove the repository from its group
+ if (repository.groupId) {
+ const group = getGroup(workspaceId, repository.groupId);
+ if (group) {
+ updateGroup(workspaceId, repository.groupId, {
+ repositoryIds: group.repositoryIds.filter((id) =>id !== repositoryId),
+ });
+ }
}
+
+ // Remove the repository from the workspace
+ updateWorkspace(workspaceId, {
+ repositories: workspace.repositories.filter((r) =>r.id !== repositoryId),
+ });
};
const updateRepositorySettings = (
@@ -105,7 +252,7 @@ export const useWorkspaces = () => {
}
};
- return {
+return {
workspaces,
currentWorkspace,
setCurrentWorkspace,
@@ -116,5 +263,9 @@ export const useWorkspaces = () => {
removeRepository,
updateRepositorySettings,
updateWorkspaceSettings,
+ createGroup,
+ updateGroup,
+ deleteGroup,
+ getGroup,
};
};
diff --git a/app/types/workspace.ts b/app/types/workspace.ts
index 44efb4f..28ea182 100644
--- a/app/types/workspace.ts
+++ b/app/types/workspace.ts
@@ -3,9 +3,17 @@ export interface Workspace {
name: string;
description?: string;
repositories: Repository[];
+ groups: Group[];
settings: WorkspaceSettings;
createdAt: Date;
updatedAt: Date;
+ isDefault?: boolean;
+}
+
+export interface Group {
+ id: string;
+ name: string;
+ repositoryIds: string[]; // Array to store IDs of repositories in the group
}
export interface Repository {
@@ -16,6 +24,7 @@ export interface Repository {
defaultBranch: string;
settings?: RepositorySettings;
addedAt: Date;
+ groupId?: string; // Optional group ID for the repository
}
export interface WorkspaceSettings {