Skip to content

Commit c45352e

Browse files
Merge pull request #62 from nashtech-garage/feat/remove-from-ws
Feat/remove from ws
2 parents 729a16f + 418f363 commit c45352e

File tree

9 files changed

+477
-100
lines changed

9 files changed

+477
-100
lines changed

backend/app/repositories/WorkspaceRepository.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,14 @@ class WorkspaceRepository @Inject()(
160160
(for {
161161
uw <- userWorkspaces if uw.workspaceId === workspaceId
162162
u <- userTables if u.id === uw.userId
163-
w <- workspaces if w.id === workspaceId && !w.isDeleted
164163
} yield (u, uw)).result.map { rows =>
165164
rows.map { case (user, userWorkspace) =>
166165
UserWorkspaceDTO(
167166
userId = user.id.getOrElse(0),
168-
name = user.name,
169-
email = user.email,
170-
role = userWorkspace.role.toString,
171-
joinedAt = Some(userWorkspace.joinedAt)
167+
name = Option(user.name).getOrElse(""),
168+
email = Option(user.email).getOrElse(""),
169+
role = Option(userWorkspace.role.toString).getOrElse("")
170+
// joinedAt = Option(userWorkspace.joinedAt)
172171
)
173172
}
174173
}

backend/conf/application.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ play.mailer {
126126
port = ${?MAIL_PORT}
127127
tls = yes
128128
ssl = no
129+
user = "358c8a28e1dbff"
129130
user = ${?MAIL_USER}
131+
password = "225cc91d69eb94"
130132
password = ${?MAIL_PASSWORD}
131133
}
132134

frontend/src/index.css

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,22 @@
6868
box-sizing: border-box;
6969
}
7070

71+
/* Smooth theme transitions - increased from 0.01s to 0.3s */
7172
* {
72-
transition: background-color 0.01s ease, color 0.01s ease, border-color 0.01s ease;
73+
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
74+
}
75+
76+
/* Respect user's motion preferences for accessibility */
77+
@media (prefers-reduced-motion: reduce) {
78+
* {
79+
transition: none !important;
80+
animation: none !important;
81+
}
7382
}
7483

7584
/* Scrollbar Track */
7685
::-webkit-scrollbar {
7786
width: 6px;
78-
/* or height for horizontal scrollbar */
7987
}
8088

8189
/* Scrollbar Track Background */
@@ -90,7 +98,6 @@
9098
background-color: var(--scrollbar-thumb-bg);
9199
border-radius: 10px;
92100
border: 2px solid transparent;
93-
/* space around thumb */
94101
}
95102

96103
/* On hover */
@@ -100,7 +107,6 @@
100107

101108
body {
102109
scrollbar-gutter: stable;
103-
/* optional: prevent layout shift */
104110
}
105111

106112
@theme inline {

frontend/src/pages/workspace/WorkspaceCollaborators.tsx

Lines changed: 105 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Guest, JoinRequest, Member } from "@/types/collaboration";
2-
import { UserPlus } from "lucide-react";
2+
import { UserPlus, ChevronLeft, ChevronRight, Users, UserCheck, UserPlus2 } from "lucide-react";
33
import { useState } from "react";
44
import MembersTab from "./member/MembersTab";
55
import GuestsTab from "./member/GuestsTab";
@@ -9,13 +9,13 @@ import InviteModal from "./member/InviteModal";
99
type TabType = 'members' | 'guests' | 'requests';
1010

1111
const sampleMembers: Member[] = [
12-
{ id: '1', name: 'Ha.NguyenVan', username: '@hanguyenvan9', lastActive: 'November 2025', boardCount: 1, isAdmin: true },
13-
{ id: '2', name: 'Long.LeVanQuoc', username: '@longlevanquoc1', lastActive: 'October 2025', boardCount: 1, isAdmin: true },
14-
{ id: '3', name: 'Lâm Nhất Nguyên', username: '@nguyenlamnhat', lastActive: 'November 2025', boardCount: 2, isAdmin: true },
15-
{ id: '4', name: 'Nhien Tran Duc', username: '@nhientranduc', lastActive: 'November 2025', boardCount: 1, isAdmin: true },
16-
{ id: '5', name: 'Hiển Lương', username: '@tranhienluong2003', lastActive: 'November 2025', boardCount: 4, isAdmin: true },
17-
{ id: '6', name: 'Vu Tran', username: '@vutran308', lastActive: 'November 2025', boardCount: 3, isAdmin: true },
18-
{ id: '7', name: 'NguyenHuuTuan', username: '@tuungnhu12', lastActive: 'September 2025', boardCount: 1, isAdmin: true },
12+
{ userId: '1', name: 'Ha.NguyenVan', username: '@hanguyenvan9', lastActive: 'November 2025', boardCount: 1, isAdmin: true },
13+
{ userId: '2', name: 'Long.LeVanQuoc', username: '@longlevanquoc1', lastActive: 'October 2025', boardCount: 1, isAdmin: true },
14+
{ userId: '3', name: 'Lâm Nhất Nguyên', username: '@nguyenlamnhat', lastActive: 'November 2025', boardCount: 2, isAdmin: true },
15+
{ userId: '4', name: 'Nhien Tran Duc', username: '@nhientranduc', lastActive: 'November 2025', boardCount: 1, isAdmin: true },
16+
{ userId: '5', name: 'Hiển Lương', username: '@tranhienluong2003', lastActive: 'November 2025', boardCount: 4, isAdmin: true },
17+
{ userId: '6', name: 'Vu Tran', username: '@vutran308', lastActive: 'November 2025', boardCount: 3, isAdmin: true },
18+
{ userId: '7', name: 'NguyenHuuTuan', username: '@tuungnhu12', lastActive: 'September 2025', boardCount: 1, isAdmin: true },
1919
];
2020

2121
const sampleGuests: Guest[] = [];
@@ -25,54 +25,133 @@ const sampleRequests: JoinRequest[] = [];
2525
const WorkspaceCollaborators: React.FC = () => {
2626
const [activeTab, setActiveTab] = useState<TabType>('members');
2727
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
28+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
2829
const [members] = useState<Member[]>(sampleMembers);
2930
const [guests] = useState<Guest[]>(sampleGuests);
3031
const [requests] = useState<JoinRequest[]>(sampleRequests);
3132

3233
const totalCollaborators = members.length + guests.length;
3334

35+
const getTabIcon = (tab: TabType) => {
36+
switch (tab) {
37+
case 'members': return <Users size={20} />;
38+
case 'guests': return <UserCheck size={20} />;
39+
case 'requests': return <UserPlus2 size={20} />;
40+
}
41+
};
42+
43+
const getTabLabel = (tab: TabType, count: number) => {
44+
switch (tab) {
45+
case 'members': return `Workspace members (${count})`;
46+
case 'guests': return `Guests (${count})`;
47+
case 'requests': return `Join requests (${count})`;
48+
}
49+
};
50+
3451
return (
35-
<div className="h-full bg-[#1F1F21] text-white p-8">
36-
<div className="w-full">
37-
<div className="flex justify-between items-center mb-6">
38-
<h1 className="text-xl font-bold">
39-
Collaborators <span className="text-gray-400">{totalCollaborators} / 10</span>
52+
<div className="h-full bg-background text-foreground p-8 overflow-auto">
53+
<div className="w-full max-w-7xl mx-auto">
54+
<div className="flex justify-between items-center mb-6 animate-in fade-in slide-in-from-top-4 duration-500">
55+
<h1 className="text-xl font-bold text-foreground">
56+
Collaborators <span className="text-muted-foreground">{totalCollaborators} / 10</span>
4057
</h1>
4158
<button
4259
onClick={() => setIsInviteModalOpen(true)}
43-
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex items-center gap-2"
60+
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex items-center gap-2
61+
transition-all duration-200
62+
hover:scale-105 hover:shadow-lg hover:shadow-blue-500/50
63+
active:scale-95
64+
group"
4465
>
45-
<UserPlus size={18} />
66+
<UserPlus size={18} className="group-hover:scale-110 transition-transform duration-200" />
4667
Invite Workspace members
4768
</button>
4869
</div>
4970

5071
<div className="flex gap-6">
51-
<div className="w-64 space-y-2">
72+
{/* Collapsible Sidebar */}
73+
<div
74+
className={`relative space-y-2 animate-in fade-in slide-in-from-left-4 duration-500 delay-100
75+
${isSidebarCollapsed ? 'w-16' : 'w-64'}
76+
transition-all duration-500 ease-in-out`}
77+
>
78+
{/* Collapse Toggle Button */}
79+
<button
80+
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
81+
className="absolute -right-3 top-0 z-10 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-1.5
82+
transition-all duration-300 hover:scale-110 hover:rotate-180 shadow-lg active:scale-95"
83+
title={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
84+
>
85+
{isSidebarCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
86+
</button>
87+
88+
{/* Tab Buttons */}
5289
<button
5390
onClick={() => setActiveTab('members')}
54-
className={`cursor-pointer w-full text-left px-4 py-2 rounded ${activeTab === 'members' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-neutral-800'
55-
}`}
91+
className={`relative w-full ${isSidebarCollapsed ? 'px-2 justify-center' : 'px-4 justify-start'} text-left py-2 rounded overflow-hidden flex items-center gap-3
92+
transition-all duration-500 ease-in-out
93+
${activeTab === 'members'
94+
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/50'
95+
: 'text-muted-foreground hover:bg-muted hover:text-foreground hover:scale-105'
96+
}
97+
`}
98+
title={isSidebarCollapsed ? getTabLabel('members', members.length) : undefined}
5699
>
57-
Workspace members ({members.length})
100+
{activeTab === 'members' && (
101+
<div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-blue-500 animate-in fade-in duration-300"></div>
102+
)}
103+
<span className="relative z-10 transition-transform duration-300">{getTabIcon('members')}</span>
104+
<span className={`relative z-10 whitespace-nowrap transition-all duration-500 ease-in-out
105+
${isSidebarCollapsed ? 'opacity-0 w-0 overflow-hidden' : 'opacity-100'}`}>
106+
Workspace members ({members.length})
107+
</span>
58108
</button>
109+
59110
<button
60111
onClick={() => setActiveTab('guests')}
61-
className={`cursor-pointer w-full text-left px-4 py-2 rounded ${activeTab === 'guests' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-neutral-800'
62-
}`}
112+
className={`relative w-full ${isSidebarCollapsed ? 'px-2 justify-center' : 'px-4 justify-start'} text-left py-2 rounded overflow-hidden flex items-center gap-3
113+
transition-all duration-500 ease-in-out
114+
${activeTab === 'guests'
115+
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/50'
116+
: 'text-muted-foreground hover:bg-muted hover:text-foreground hover:scale-105'
117+
}
118+
`}
119+
title={isSidebarCollapsed ? getTabLabel('guests', guests.length) : undefined}
63120
>
64-
Guests ({guests.length})
121+
{activeTab === 'guests' && (
122+
<div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-blue-500 animate-in fade-in duration-300"></div>
123+
)}
124+
<span className="relative z-10 transition-transform duration-300">{getTabIcon('guests')}</span>
125+
<span className={`relative z-10 whitespace-nowrap transition-all duration-500 ease-in-out
126+
${isSidebarCollapsed ? 'opacity-0 w-0 overflow-hidden' : 'opacity-100'}`}>
127+
Guests ({guests.length})
128+
</span>
65129
</button>
130+
66131
<button
67132
onClick={() => setActiveTab('requests')}
68-
className={`cursor-pointer w-full text-left px-4 py-2 rounded ${activeTab === 'requests' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-neutral-800'
69-
}`}
133+
className={`relative w-full ${isSidebarCollapsed ? 'px-2 justify-center' : 'px-4 justify-start'} text-left py-2 rounded overflow-hidden flex items-center gap-3
134+
transition-all duration-500 ease-in-out
135+
${activeTab === 'requests'
136+
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/50'
137+
: 'text-muted-foreground hover:bg-muted hover:text-foreground hover:scale-105'
138+
}
139+
`}
140+
title={isSidebarCollapsed ? getTabLabel('requests', requests.length) : undefined}
70141
>
71-
Join requests ({requests.length})
142+
{activeTab === 'requests' && (
143+
<div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-blue-500 animate-in fade-in duration-300"></div>
144+
)}
145+
<span className="relative z-10 transition-transform duration-300">{getTabIcon('requests')}</span>
146+
<span className={`relative z-10 whitespace-nowrap transition-all duration-500 ease-in-out
147+
${isSidebarCollapsed ? 'opacity-0 w-0 overflow-hidden' : 'opacity-100'}`}>
148+
Join requests ({requests.length})
149+
</span>
72150
</button>
73151
</div>
74152

75-
<div className="flex-1">
153+
{/* Main Content */}
154+
<div className="flex-1 min-w-0">
76155
{activeTab === 'members' && <MembersTab members={members} />}
77156
{activeTab === 'guests' && <GuestsTab guests={guests} />}
78157
{activeTab === 'requests' && <JoinRequestsTab requests={requests} />}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { X } from "lucide-react";
2+
3+
interface ConfirmationModalProps {
4+
isOpen: boolean;
5+
onClose: () => void;
6+
onConfirm: () => void;
7+
title: string;
8+
message: string;
9+
confirmText?: string;
10+
cancelText?: string;
11+
isLoading?: boolean;
12+
}
13+
14+
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
15+
isOpen,
16+
onClose,
17+
onConfirm,
18+
title,
19+
message,
20+
confirmText = "Confirm",
21+
cancelText = "Cancel",
22+
isLoading = false
23+
}) => {
24+
if (!isOpen) return null;
25+
26+
return (
27+
<div
28+
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 animate-in fade-in duration-200"
29+
onClick={(e) => {
30+
if (e.target === e.currentTarget && !isLoading) {
31+
onClose();
32+
}
33+
}}
34+
>
35+
<div className="bg-neutral-800 rounded-lg w-full max-w-md p-6 animate-in zoom-in-95 slide-in-from-bottom-4 duration-300">
36+
<div className="flex justify-between items-center mb-4">
37+
<h2 className="text-white text-lg font-semibold">{title}</h2>
38+
<button
39+
onClick={onClose}
40+
className="text-gray-400 hover:text-white transition-all duration-200 hover:rotate-90 hover:scale-110"
41+
disabled={isLoading}
42+
>
43+
<X size={20} />
44+
</button>
45+
</div>
46+
47+
<p className="text-gray-300 mb-6">{message}</p>
48+
49+
<div className="flex justify-end gap-3">
50+
<button
51+
onClick={onClose}
52+
disabled={isLoading}
53+
className="px-4 py-2 bg-neutral-700 hover:bg-neutral-600 text-white rounded
54+
transition-all duration-200
55+
hover:scale-105 hover:shadow-lg
56+
active:scale-95
57+
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
58+
>
59+
{cancelText}
60+
</button>
61+
<button
62+
onClick={onConfirm}
63+
disabled={isLoading}
64+
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded
65+
transition-all duration-200
66+
hover:scale-105 hover:shadow-lg hover:shadow-red-500/50
67+
active:scale-95
68+
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
69+
>
70+
{isLoading ? (
71+
<span className="flex items-center gap-2">
72+
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
73+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"></circle>
74+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
75+
</svg>
76+
Removing...
77+
</span>
78+
) : confirmText}
79+
</button>
80+
</div>
81+
</div>
82+
</div>
83+
);
84+
};
85+
86+
export default ConfirmationModal;

0 commit comments

Comments
 (0)