feat(backoffice): little adjustment in hackathon users management UI and API contract

This commit is contained in:
Hafid Nur
2025-12-02 18:11:34 +07:00
parent d927b95457
commit 2d80ed05db
4 changed files with 65 additions and 66 deletions
@@ -195,7 +195,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
<div className="border-b border-neutral-200 px-8 py-6 flex justify-between items-start">
<div className="flex items-center gap-4">
{/* Interactive User Avatar */}
<div className="relative group">
<div className="relative group ">
<div className="w-16 h-16 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden border-2 border-transparent group-hover:border-primary-300 transition-colors">
{formData.avatar ? (
<img
@@ -211,7 +211,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
{/* Avatar Hover Overlay */}
<button
onClick={() => setShowAvatarMenu(!showAvatarMenu)}
className="absolute inset-0 bg-black/40 rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
className="absolute inset-0 bg-neutral-400 cursor-pointer rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
>
<CameraOutlined className="text-white text-lg" />
</button>
@@ -221,7 +221,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
<div className="absolute top-full left-0 mt-2 bg-white rounded-lg shadow-lg border border-neutral-200 py-2 min-w-[140px] z-10">
<button
onClick={triggerFileUpload}
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-2"
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-2 cursor-pointer"
>
<UploadOutlined className="text-sm" />
{formData.avatar ? 'Change Photo' : 'Upload Photo'}
@@ -229,7 +229,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
{formData.avatar && (
<button
onClick={handleRemoveAvatar}
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 cursor-pointer"
>
<DeleteOutlined className="text-sm" />
Remove Photo
@@ -244,7 +244,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
{user ? 'Edit User Profile' : 'Create New User'}
</h2>
{user && (
<span className="px-3 py-1 bg-blue-100 text-blue-700 text-xs font-medium rounded-full">
<span className="px-3 py-1 bg-info-100 text-info-700 text-xs font-medium rounded-2xl">
Hover avatar to change
</span>
)}
@@ -265,7 +265,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
handleCancel();
}}
>
<CloseOutlined className="text-neutral-400 text-lg" />
<CloseOutlined className="text-neutral-400 text-lg cursor-pointer" />
</button>
</div>
@@ -397,7 +397,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
<button
onClick={() => handleInputChange('is_active', true)}
className={cn(
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 cursor-pointer',
formData.is_active
? 'bg-white text-success-700 shadow-sm ring-1 ring-success-200'
: 'text-neutral-600 hover:text-neutral-800'
@@ -418,7 +418,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
<button
onClick={() => handleInputChange('is_active', false)}
className={cn(
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 cursor-pointer',
!formData.is_active
? 'bg-white text-neutral-700 shadow-sm ring-1 ring-neutral-200'
: 'text-neutral-600 hover:text-neutral-800'
@@ -458,7 +458,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
formData.skills.map((skill, index) => (
<span
key={index}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium bg-blue-100 text-blue-800"
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-2xl text-sm font-medium bg-blue-100 text-blue-800"
>
{skill}
<button
@@ -467,9 +467,9 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
formData.skills.filter((_, i) => i !== index)
)
}
className="text-blue-600 hover:text-blue-800 ml-1"
className="text-blue-600 hover:text-blue-800 ml-1 cursor-pointer"
>
×
</button>
</span>
))
@@ -558,7 +558,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
{user && (
<button
onClick={() => setShowDeleteConfirm(true)}
className="text-red-600 hover:text-red-700 text-sm font-medium transition-colors"
className="text-red-600 hover:text-red-700 text-sm font-medium transition-colors cursor-pointer"
>
Delete Account
</button>
@@ -178,13 +178,13 @@ export const HackathonUsersPage: FC = (): ReactElement => {
{row.original.skills.slice(0, 2).map((skill, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 rounded-2xl text-xs font-medium bg-blue-100 text-blue-800"
className="inline-flex items-center px-2 py-1 rounded-2xl text-xs font-medium bg-success-100 text-success-800"
>
{skill.replace(' Developer', '').replace(' Engineer', '')}
</span>
))}
{row.original.skills.length > 2 && (
<span className="inline-flex items-center px-2 py-1 rounded-2xl text-xs font-medium bg-neutral-100 text-neutral-600">
<span className="inline-flex items-center px-2 py-1 rounded-2xl text-xs font-medium bg-success-200 text-success-700">
+{row.original.skills.length - 2}
</span>
)}
@@ -287,14 +287,14 @@ export const HackathonUsersPage: FC = (): ReactElement => {
{/* Filters and actions */}
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6">
<div className="flex flex-wrap gap-3 items-center justify-between">
{/* Left side - Filters */}
{/* Left side - Search & filters */}
<div className="flex flex-wrap gap-3 items-center">
{/* Global Search with Icon */}
{/* Search bar */}
<div className="relative">
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
<input
type="text"
className="border border-neutral-200 rounded-lg pl-10 pr-4 py-2.5 text-sm w-full sm:w-72 focus:border-primary-500 focus:outline-none"
className="border border-neutral-200 rounded-lg pl-10 pr-4 py-2.5 text-sm w-full sm:w-80 focus:border-primary-500 focus:outline-none"
placeholder="Search users by name or location..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
@@ -302,35 +302,41 @@ export const HackathonUsersPage: FC = (): ReactElement => {
</div>
{/* Status Filter */}
<select
className="border border-neutral-200 rounded-lg px-4 py-2.5 pr-10 text-sm w-full sm:w-36 focus:border-primary-500 focus:outline-none appearance-none bg-white"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<div className="relative">
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
<select
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-36 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
{/* Location Filter */}
<select
className="border border-neutral-200 rounded-lg px-4 py-2.5 pr-10 text-sm w-full sm:w-40 focus:border-primary-500 focus:outline-none appearance-none bg-white"
value={locationFilter}
onChange={(e) => setLocationFilter(e.target.value)}
>
<option value="all">All Locations</option>
{locations.map((location) => (
<option key={location} value={location}>
{location}
</option>
))}
</select>
<div className="relative">
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
<select
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-44 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
value={locationFilter}
onChange={(e) => setLocationFilter(e.target.value)}
>
<option value="all">All Locations</option>
{locations.map((location) => (
<option key={location} value={location}>
{location}
</option>
))}
</select>
</div>
{/* Skills Filter with Icon */}
<div className="relative">
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
<select
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-44 focus:border-primary-500 focus:outline-none appearance-none bg-white"
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-44 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
value=""
onChange={(e) => {
if (
@@ -357,9 +363,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
{/* Right side - Add User Button */}
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-500">
{filteredData.length} users found
</span>
<Button
variant="primary"
size="md"
@@ -381,26 +384,26 @@ export const HackathonUsersPage: FC = (): ReactElement => {
{/* Status filter badge */}
{statusFilter !== 'all' && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs">
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
Status: {statusFilter}
<button
onClick={() => setStatusFilter('all')}
className="text-blue-600 hover:text-blue-800"
className="text-info-600 hover:text-info-800 cursor-pointer"
>
×
</button>
</span>
)}
{/* Location filter badge */}
{locationFilter !== 'all' && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs">
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
Location: {locationFilter}
<button
onClick={() => setLocationFilter('all')}
className="text-green-600 hover:text-green-800"
className="text-green-600 hover:text-green-800 cursor-pointer"
>
×
</button>
</span>
)}
@@ -409,16 +412,16 @@ export const HackathonUsersPage: FC = (): ReactElement => {
{skillsFilter.map((skill) => (
<span
key={skill}
className="inline-flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-800 rounded-full text-xs"
className="inline-flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-800 rounded-2xl text-sm"
>
{skill.replace(' Developer', '').replace(' Engineer', '')}
<button
onClick={() =>
setSkillsFilter((prev) => prev.filter((s) => s !== skill))
}
className="text-purple-600 hover:text-purple-800"
className="text-purple-600 hover:text-purple-800 cursor-pointer"
>
×
</button>
</span>
))}
@@ -433,7 +436,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
setSkillsFilter([]);
setGlobalFilter('');
}}
className="text-xs text-neutral-600"
className="text-sm text-neutral-600"
>
Clear All
</Button>
@@ -445,8 +448,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
<div className="text-sm text-neutral-600">
Showing {Math.min(pageSize, filteredData.length)} of{' '}
{filteredData.length} users
{filteredData.length > pageSize &&
' (filtered results paginated below)'}
{filteredData.length > pageSize}
</div>
)}