feat(backoffice): little adjustment in hackathon users management UI and API contract
This commit is contained in:
+11
-11
@@ -211,7 +211,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
{/* Avatar Hover Overlay */}
|
{/* Avatar Hover Overlay */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAvatarMenu(!showAvatarMenu)}
|
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" />
|
<CameraOutlined className="text-white text-lg" />
|
||||||
</button>
|
</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">
|
<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
|
<button
|
||||||
onClick={triggerFileUpload}
|
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" />
|
<UploadOutlined className="text-sm" />
|
||||||
{formData.avatar ? 'Change Photo' : 'Upload Photo'}
|
{formData.avatar ? 'Change Photo' : 'Upload Photo'}
|
||||||
@@ -229,7 +229,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
{formData.avatar && (
|
{formData.avatar && (
|
||||||
<button
|
<button
|
||||||
onClick={handleRemoveAvatar}
|
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" />
|
<DeleteOutlined className="text-sm" />
|
||||||
Remove Photo
|
Remove Photo
|
||||||
@@ -244,7 +244,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
{user ? 'Edit User Profile' : 'Create New User'}
|
{user ? 'Edit User Profile' : 'Create New User'}
|
||||||
</h2>
|
</h2>
|
||||||
{user && (
|
{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
|
Hover avatar to change
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -265,7 +265,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
handleCancel();
|
handleCancel();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseOutlined className="text-neutral-400 text-lg" />
|
<CloseOutlined className="text-neutral-400 text-lg cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleInputChange('is_active', true)}
|
onClick={() => handleInputChange('is_active', true)}
|
||||||
className={cn(
|
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
|
formData.is_active
|
||||||
? 'bg-white text-success-700 shadow-sm ring-1 ring-success-200'
|
? 'bg-white text-success-700 shadow-sm ring-1 ring-success-200'
|
||||||
: 'text-neutral-600 hover:text-neutral-800'
|
: 'text-neutral-600 hover:text-neutral-800'
|
||||||
@@ -418,7 +418,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleInputChange('is_active', false)}
|
onClick={() => handleInputChange('is_active', false)}
|
||||||
className={cn(
|
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
|
!formData.is_active
|
||||||
? 'bg-white text-neutral-700 shadow-sm ring-1 ring-neutral-200'
|
? 'bg-white text-neutral-700 shadow-sm ring-1 ring-neutral-200'
|
||||||
: 'text-neutral-600 hover:text-neutral-800'
|
: 'text-neutral-600 hover:text-neutral-800'
|
||||||
@@ -458,7 +458,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
formData.skills.map((skill, index) => (
|
formData.skills.map((skill, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
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}
|
{skill}
|
||||||
<button
|
<button
|
||||||
@@ -467,9 +467,9 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
formData.skills.filter((_, i) => i !== index)
|
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>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
@@ -558,7 +558,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
|||||||
{user && (
|
{user && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
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
|
Delete Account
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -178,13 +178,13 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
{row.original.skills.slice(0, 2).map((skill, index) => (
|
{row.original.skills.slice(0, 2).map((skill, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
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', '')}
|
{skill.replace(' Developer', '').replace(' Engineer', '')}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{row.original.skills.length > 2 && (
|
{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}
|
+{row.original.skills.length - 2}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -287,14 +287,14 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
{/* Filters and actions */}
|
{/* Filters and actions */}
|
||||||
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6">
|
<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">
|
<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">
|
<div className="flex flex-wrap gap-3 items-center">
|
||||||
{/* Global Search with Icon */}
|
{/* Search bar */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
|
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
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..."
|
placeholder="Search users by name or location..."
|
||||||
value={globalFilter}
|
value={globalFilter}
|
||||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||||
@@ -302,8 +302,10 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Filter */}
|
{/* Status Filter */}
|
||||||
|
<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
|
<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"
|
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}
|
value={statusFilter}
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
>
|
>
|
||||||
@@ -311,10 +313,13 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
<option value="active">Active</option>
|
<option value="active">Active</option>
|
||||||
<option value="inactive">Inactive</option>
|
<option value="inactive">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Location Filter */}
|
{/* Location Filter */}
|
||||||
|
<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
|
<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"
|
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}
|
value={locationFilter}
|
||||||
onChange={(e) => setLocationFilter(e.target.value)}
|
onChange={(e) => setLocationFilter(e.target.value)}
|
||||||
>
|
>
|
||||||
@@ -325,12 +330,13 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Skills Filter with Icon */}
|
{/* Skills Filter with Icon */}
|
||||||
<div className="relative">
|
<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" />
|
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
||||||
<select
|
<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=""
|
value=""
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (
|
if (
|
||||||
@@ -357,9 +363,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Right side - Add User Button */}
|
{/* Right side - Add User Button */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm text-neutral-500">
|
|
||||||
{filteredData.length} users found
|
|
||||||
</span>
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -381,26 +384,26 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Status filter badge */}
|
{/* Status filter badge */}
|
||||||
{statusFilter !== 'all' && (
|
{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}
|
Status: {statusFilter}
|
||||||
<button
|
<button
|
||||||
onClick={() => setStatusFilter('all')}
|
onClick={() => setStatusFilter('all')}
|
||||||
className="text-blue-600 hover:text-blue-800"
|
className="text-info-600 hover:text-info-800 cursor-pointer"
|
||||||
>
|
>
|
||||||
×
|
✕
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Location filter badge */}
|
{/* Location filter badge */}
|
||||||
{locationFilter !== 'all' && (
|
{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}
|
Location: {locationFilter}
|
||||||
<button
|
<button
|
||||||
onClick={() => setLocationFilter('all')}
|
onClick={() => setLocationFilter('all')}
|
||||||
className="text-green-600 hover:text-green-800"
|
className="text-green-600 hover:text-green-800 cursor-pointer"
|
||||||
>
|
>
|
||||||
×
|
✕
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -409,16 +412,16 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
{skillsFilter.map((skill) => (
|
{skillsFilter.map((skill) => (
|
||||||
<span
|
<span
|
||||||
key={skill}
|
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', '')}
|
{skill.replace(' Developer', '').replace(' Engineer', '')}
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSkillsFilter((prev) => prev.filter((s) => s !== skill))
|
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>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -433,7 +436,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
setSkillsFilter([]);
|
setSkillsFilter([]);
|
||||||
setGlobalFilter('');
|
setGlobalFilter('');
|
||||||
}}
|
}}
|
||||||
className="text-xs text-neutral-600"
|
className="text-sm text-neutral-600"
|
||||||
>
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</Button>
|
</Button>
|
||||||
@@ -445,8 +448,7 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
|||||||
<div className="text-sm text-neutral-600">
|
<div className="text-sm text-neutral-600">
|
||||||
Showing {Math.min(pageSize, filteredData.length)} of{' '}
|
Showing {Math.min(pageSize, filteredData.length)} of{' '}
|
||||||
{filteredData.length} users
|
{filteredData.length} users
|
||||||
{filteredData.length > pageSize &&
|
{filteredData.length > pageSize}
|
||||||
' (filtered results paginated below)'}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ Retrieve paginated list of hackathon participants with filtering, searching, and
|
|||||||
| Parameter | Type | Required | Default | Description |
|
| Parameter | Type | Required | Default | Description |
|
||||||
| ------------ | ------- | -------- | ------------ | ------------------------------------------------------------- |
|
| ------------ | ------- | -------- | ------------ | ------------------------------------------------------------- |
|
||||||
| `page` | integer | No | 1 | Page number (1-based) |
|
| `page` | integer | No | 1 | Page number (1-based) |
|
||||||
| `limit` | integer | No | 20 | Items per page (1-100) |
|
| `limit` | integer | No | 10 | Items per page (1-100) |
|
||||||
| `search` | string | No | - | Search by name or location (case-insensitive) |
|
| `search` | string | No | - | Search by name or location (case-insensitive) |
|
||||||
| `status` | string | No | `all` | Filter by status: `all`, `active`, `inactive` |
|
| `status` | string | No | `all` | Filter by status: `all`, `active`, `inactive` |
|
||||||
| `location` | string | No | `all` | Filter by location or `all` |
|
| `location` | string | No | `all` | Filter by location or `all` |
|
||||||
@@ -95,7 +95,7 @@ GET /api/v1/admin/users?sort_by=fullname&sort_order=asc
|
|||||||
"bio": "Passionate developer with 5+ years experience", // Optional
|
"bio": "Passionate developer with 5+ years experience", // Optional
|
||||||
"location": "Jakarta",
|
"location": "Jakarta",
|
||||||
"is_active": true,
|
"is_active": true,
|
||||||
"skills": ["Frontend Developer", "UI/UX Designer"],
|
"skills": ["Frontend Developer", "UI/UX Designer"], // Optional
|
||||||
"created_at": "2024-11-15T08:30:00Z",
|
"created_at": "2024-11-15T08:30:00Z",
|
||||||
"updated_at": "2024-11-30T14:22:00Z"
|
"updated_at": "2024-11-30T14:22:00Z"
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ GET /api/v1/admin/users?sort_by=fullname&sort_order=asc
|
|||||||
| ------------------------- | ------- | ------------------------------ |
|
| ------------------------- | ------- | ------------------------------ |
|
||||||
| `data.users[].id` | string | UUID format |
|
| `data.users[].id` | string | UUID format |
|
||||||
| `data.users[].avatar` | string | URL, nullable |
|
| `data.users[].avatar` | string | URL, nullable |
|
||||||
| `data.users[].fullname` | string | Required, 1-100 chars |
|
| `data.users[].fullname` | string | Required |
|
||||||
| `data.users[].bio` | string | Optional, max 500 chars |
|
| `data.users[].bio` | string | Optional, max 500 chars |
|
||||||
| `data.users[].location` | string | Required, from predefined list |
|
| `data.users[].location` | string | Required, from predefined list |
|
||||||
| `data.users[].is_active` | boolean | Account status |
|
| `data.users[].is_active` | boolean | Account status |
|
||||||
@@ -195,7 +195,7 @@ Create a new user account in the hackathon system.
|
|||||||
"location": "Jakarta", // Required, from predefined list
|
"location": "Jakarta", // Required, from predefined list
|
||||||
"is_active": true, // Required, boolean
|
"is_active": true, // Required, boolean
|
||||||
"skills": ["Backend Developer"], // Optional, array of valid skills
|
"skills": ["Backend Developer"], // Optional, array of valid skills
|
||||||
"avatar": "data:image/..." // Optional, base64 or URL
|
"avatar": "https://example.com/images/..." // Optional, URL
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ Content-Type: application/json
|
|||||||
"location": "Jakarta",
|
"location": "Jakarta",
|
||||||
"is_active": true,
|
"is_active": true,
|
||||||
"skills": ["Backend Developer", "Data Scientist"],
|
"skills": ["Backend Developer", "Data Scientist"],
|
||||||
"avatar": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
|
"avatar": "https://example.com/images/..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ Update an existing user's profile information.
|
|||||||
"location": "Bandung", // Optional, from predefined list
|
"location": "Bandung", // Optional, from predefined list
|
||||||
"is_active": false, // Optional, boolean
|
"is_active": false, // Optional, boolean
|
||||||
"skills": ["Full Stack Developer"], // Optional, array of valid skills
|
"skills": ["Full Stack Developer"], // Optional, array of valid skills
|
||||||
"avatar": "data:image/..." // Optional, base64/URL, null to remove
|
"avatar": "https://example.com/images/..." // Optional, URL, null to remove
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ Content-Type: application/json
|
|||||||
|
|
||||||
### Purpose
|
### Purpose
|
||||||
|
|
||||||
Permanently delete a user account from the hackathon system.
|
(Soft) Delete a user account from the hackathon system.
|
||||||
|
|
||||||
### Authentication & Authorization
|
### Authentication & Authorization
|
||||||
|
|
||||||
@@ -387,7 +387,6 @@ DELETE /api/v1/admin/users/550e8400-e29b-41d4-a716-446655440000
|
|||||||
- **Max file size**: 5MB
|
- **Max file size**: 5MB
|
||||||
- **Recommended dimensions**: 400x400px
|
- **Recommended dimensions**: 400x400px
|
||||||
- **Storage**: Uploaded avatars are processed and stored with generated URLs
|
- **Storage**: Uploaded avatars are processed and stored with generated URLs
|
||||||
- **Base64 support**: Accept `data:image/*;base64,` format in requests
|
|
||||||
- **URL response**: Always return publicly accessible HTTPS URLs
|
- **URL response**: Always return publicly accessible HTTPS URLs
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -395,4 +394,4 @@ DELETE /api/v1/admin/users/550e8400-e29b-41d4-a716-446655440000
|
|||||||
**Revision History**
|
**Revision History**
|
||||||
|
|
||||||
- v1.0.0 (2025-11-30): Initial contract drafted.
|
- v1.0.0 (2025-11-30): Initial contract drafted.
|
||||||
- v2.0.0 (2025-12-01): Added comprehensive user management endpoints with filtering, pagination, CRUD operations, and avatar handling.
|
- v2.0.0 (2025-12-01): Added user management endpoints with filtering, pagination, CRUD operations, and avatar handling.
|
||||||
|
|||||||
@@ -119,9 +119,7 @@ export const DataTable = <T extends RowData>({
|
|||||||
<span className="ml-2 text-xs text-gray-500">
|
<span className="ml-2 text-xs text-gray-500">
|
||||||
{header.column.getIsSorted() === 'asc' && '▲'}
|
{header.column.getIsSorted() === 'asc' && '▲'}
|
||||||
{header.column.getIsSorted() === 'desc' && '▼'}
|
{header.column.getIsSorted() === 'desc' && '▼'}
|
||||||
{!header.column.getIsSorted() && (
|
{!header.column.getIsSorted() && <span>⇅</span>}
|
||||||
<span className="opacity-50">⇅</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
Reference in New Issue
Block a user