From f195bba03d1beb336ede847dcae23e7bb6a64ef9 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Thu, 23 Apr 2026 20:53:23 +0330 Subject: [PATCH] refactor(auth, layouts): enhance UI components for improved aesthetics and usability - Updated SignIn and Profile components with new background styles and layout adjustments for a more modern look. - Refined button styles in GoogleSigninButton and SigninWithPassword for better user interaction feedback. - Enhanced InputGroup and Select components with improved styling and error handling for a more consistent user experience. - Adjusted Sidebar and Header components for better responsiveness and visual appeal. - Implemented spotlight effect in UserInfo dropdown for a more engaging user interface. --- Filestore Frontend Guide.md | 276 ++++++++++++++++++ src/app/auth/sign-in/page.tsx | 6 +- src/app/profile/page.tsx | 89 +++--- src/components/Auth/GoogleSigninButton.tsx | 2 +- src/components/Auth/Signin/index.tsx | 16 +- src/components/Auth/SigninWithPassword.tsx | 29 +- .../FormElements/InputGroup/index.tsx | 16 +- .../FormElements/InputGroup/tag-input.tsx | 17 +- .../FormElements/InputGroup/text-area.tsx | 13 +- src/components/FormElements/select.tsx | 13 +- src/components/Layouts/conditional-layout.tsx | 2 +- src/components/Layouts/header/index.tsx | 4 +- .../Layouts/header/notification/index.tsx | 10 +- .../Layouts/header/theme-toggle/index.tsx | 61 +++- .../Layouts/header/user-info/index.tsx | 51 +++- src/components/Layouts/showcase-section.tsx | 6 +- src/components/Layouts/sidebar/index.tsx | 14 +- src/components/Layouts/sidebar/menu-item.tsx | 10 +- src/components/Tables/admins/index.tsx | 5 +- .../Tables/admins/useAdminsPresenter.ts | 1 - src/components/Tables/cms/index.tsx | 5 +- .../companies/company-categories/index.tsx | 5 +- src/components/Tables/companies/index.tsx | 11 +- src/components/Tables/contact-us/index.tsx | 7 +- src/components/Tables/customers/index.tsx | 5 +- src/components/Tables/inquiries/index.tsx | 17 +- src/components/Tables/invoice-table.tsx | 5 +- .../Tables/notification-history/index.tsx | 43 +-- .../Tables/tenders/TenderListFilters.tsx | 30 +- src/components/Tables/tenders/index.tsx | 5 +- src/components/Tables/top-channels/index.tsx | 98 +++---- .../Tables/top-channels/skeleton.tsx | 5 +- src/components/Tables/top-products/index.tsx | 13 +- .../Tables/top-products/skeleton.tsx | 7 +- src/components/forms/admins/CreateAdmin.tsx | 41 ++- .../forms/admins/useCreateAdminPresenter.ts | 153 +++++++++- src/components/ui-elements/button.tsx | 65 ++++- src/components/ui/FileImageUploadCard.tsx | 147 ++++++++++ src/components/ui/FormFooter.tsx | 35 ++- src/components/ui/ListHeader.tsx | 38 ++- src/components/ui/ListWrapper.tsx | 15 + src/components/ui/TableSkeleton.tsx | 11 +- src/components/ui/dropdown.tsx | 13 +- src/components/ui/modal.tsx | 28 +- src/components/ui/pagination.tsx | 14 +- src/components/ui/skeleton.tsx | 3 +- src/components/ui/table.tsx | 25 +- src/lib/api/endpoints.ts | 4 + src/lib/api/services/user-services.ts | 40 +++ src/utils/shared.ts | 35 +++ 50 files changed, 1245 insertions(+), 319 deletions(-) create mode 100644 Filestore Frontend Guide.md create mode 100644 src/components/ui/FileImageUploadCard.tsx create mode 100644 src/components/ui/ListWrapper.tsx diff --git a/Filestore Frontend Guide.md b/Filestore Frontend Guide.md new file mode 100644 index 0000000..6568093 --- /dev/null +++ b/Filestore Frontend Guide.md @@ -0,0 +1,276 @@ +# Frontend Guide: File Upload + Profile Image APIs + +This is a simple reference for frontend integration with file upload, file retrieval, and setting profile image. + +## Base URLs + +- Admin APIs: `/admin/v1` +- Customer/Public APIs: `/api/v1` + +All protected endpoints require: + +- `Authorization: Bearer ` + +--- + +## 1) Login (Admin) + +Use this when you want to update admin user profile image. + +### Request + +- `POST /admin/v1/profile/login` +- `Content-Type: application/json` + +```json +{ + "username": "admin", + "password": "Admin1234" +} +``` + +### Response (success) + +```json +{ + "success": true, + "message": "Login successful", + "data": { + "user": { + "id": "65bc1234567890abcdef1234", + "profile_image": null + }, + "access_token": "", + "refresh_token": "", + "expires_at": 1776783486 + } +} +``` + +--- + +## 2) Upload File (GridFS) + +Use multipart form upload. + +### Admin route + +- `POST /admin/v1/files/upload` + +### Customer route + +- `POST /api/v1/files/upload` + +### Form fields + +- `file` (required, File) +- `category` (optional, string), example: `profile_images` +- `tags` (optional, repeatable), example: `user`, `profile` +- `description` (optional, string) + +### Response (success) + +```json +{ + "file_id": "69e7899c2053abd3fa5433e0", + "filename": "avatar.png", + "content_type": "image/png", + "size": 123456, + "message": "File uploaded successfully" +} +``` + +--- + +## 3) Get File Info + +- `GET /admin/v1/files/{file_id}/info` +- `GET /api/v1/files/{file_id}/info` + +### Response (success) + +```json +{ + "file_id": "69e7899c2053abd3fa5433e0", + "filename": "avatar.png", + "content_type": "image/png", + "size": 123456, + "uploaded_by": "65bc1234567890abcdef1234", + "uploaded_at": "2026-04-21T14:28:44.787Z", + "category": "profile_images", + "tags": ["user", "profile"], + "description": "sample profile photo" +} +``` + +--- + +## 4) Download File + +- `GET /admin/v1/files/{file_id}/download` +- `GET /api/v1/files/{file_id}/download` + +Returns file binary stream. + +Useful for image URL: + +- `http://localhost:8080/admin/v1/files//download` +- or `http://localhost:8080/api/v1/files//download` + +--- + +## 5) Set Admin User Profile Image + +Upload does not update user profile automatically. +After upload, call profile update endpoint with the download URL. + +### Request + +- `PUT /admin/v1/profile` +- `Content-Type: application/json` + +```json +{ + "profile_image": "http://localhost:8080/admin/v1/files/69e7899c2053abd3fa5433e0/download" +} +``` + +### Response + +- Success: standard success envelope with updated user data +- Failure example: + +```json +{ + "success": false, + "message": "Bad Request", + "error": { + "code": "BAD_REQUEST", + "message": "failed to update user" + } +} +``` + +--- + +## 6) Verify Profile + +- `GET /admin/v1/profile` + +Check: + +- `data.profile_image` is now populated with file URL. + +--- + +## Common Errors + +- `401 Unauthorized`: missing/expired token +- `413 Request Entity Too Large`: file too big +- `400 Unsupported file type` or validation error +- `404 File not found`: wrong `file_id` + +--- + +## Suggested Frontend Flow (Admin) + +1. Login -> save `access_token` +2. Upload image -> get `file_id` +3. Build URL `.../admin/v1/files/{file_id}/download` +4. Update profile with `profile_image` URL +5. Refresh profile screen via `GET /admin/v1/profile` + +--- + +## Customer: Company Documents Upload + +Customers can upload company documents using customer auth token. + +### Upload endpoint + +- `POST /api/v1/files/upload` + +### Recommended form-data for company docs + +- `file`: actual document file +- `category`: `company_documents` +- `tags`: repeatable. Suggested: + - `company` + - `document` + - `` (example: `tax_certificate`, `registration`, `license`) +- `description`: human-readable note, example: `Company tax certificate 2026` + +### Example curl + +```bash +CUSTOMER_TOKEN="" + +curl -X POST "http://localhost:8080/api/v1/files/upload" \ + -H "Authorization: Bearer ${CUSTOMER_TOKEN}" \ + -F "file=@/absolute/path/to/company-doc.pdf" \ + -F "category=company_documents" \ + -F "tags=company" \ + -F "tags=document" \ + -F "tags=tax_certificate" \ + -F "description=Company tax certificate 2026" +``` + +### Response + +```json +{ + "file_id": "69e8abcd2053abd3fa5433e1", + "filename": "company-doc.pdf", + "content_type": "application/pdf", + "size": 245632, + "message": "File uploaded successfully" +} +``` + +### Fetch company docs list + +Use list with category filter: + +- `GET /api/v1/files?category=company_documents&limit=50&skip=0` + +You can also filter by tags: + +- `GET /api/v1/files?category=company_documents&tags=tax_certificate` + +### Download company doc + +- `GET /api/v1/files/{file_id}/download` + +### Important current behavior + +- Upload works for customers and stores `uploaded_by` as `customer_id`. +- Company now supports `document_file_ids` for mapping GridFS files. + +### Attach uploaded documents to company + +Use admin company update endpoint after collecting uploaded `file_id` values: + +- `PUT /admin/v1/companies/{company_id}` + +Request body includes regular company fields plus `document_file_ids`: + +```json +{ + "name": "Opplens", + "type": "private", + "registration_number": "1234567890", + "tax_id": "1234567890", + "industry": "Technology", + "document_file_ids": [ + "69e8abcd2053abd3fa5433e1", + "69e8abcd2053abd3fa5433e2" + ] +} +``` + +`document_file_ids` is also returned in: + +- `GET /admin/v1/companies/{id}` +- `GET /api/v1/companies` (customer "my company" profile) + + diff --git a/src/app/auth/sign-in/page.tsx b/src/app/auth/sign-in/page.tsx index 6439105..21f9006 100644 --- a/src/app/auth/sign-in/page.tsx +++ b/src/app/auth/sign-in/page.tsx @@ -7,8 +7,10 @@ export const metadata: Metadata = { export default function SignIn() { return ( -
-
+
+
+
+
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 9af0bb2..e3da265 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -14,17 +14,20 @@ export default function Page() { const hasProfileImage = data?.profile_image && !imageError; return ( -
-
+
+
+
+
Cover +
-
+
{hasProfileImage ? ( setImageError(true)} @@ -42,47 +45,63 @@ export default function Page() {
-

+

{data?.full_name}

-

+

@{data?.username}

-
+

User Information

-
-

Email

-

{data?.email}

+
+

+ Email +

+

+ {data?.email} +

-
-

Phone

-

+

+

+ Phone +

+

{data?.phone ? formatPhoneNumber(data?.phone) : "-"}

-
-

Department

-

+

+

+ Department +

+

{data?.department}

-
-

Position

-

{data?.position}

+
+

+ Position +

+

+ {data?.position} +

-
-

Role

-

+

+

+ Role +

+

{data?.role}

-
-

Status

+
+

+ Status +

{data?.status ?? "-"}
@@ -90,13 +109,15 @@ export default function Page() {
-
+

Account Details

-
-

Verified

+
+

+ Verified +

{data?.is_verified ? ( @@ -107,17 +128,19 @@ export default function Page() {
-
-

Last Login

-

+

+

+ Last Login +

+

{unixToDate({ unix: data?.last_login_at ?? 0, hasTime: true })}

-
-

+

+

Member Since

-

+

{unixToDate({ unix: data?.created_at ?? 0, hasTime: true })}

diff --git a/src/components/Auth/GoogleSigninButton.tsx b/src/components/Auth/GoogleSigninButton.tsx index 8b4526d..04315b8 100644 --- a/src/components/Auth/GoogleSigninButton.tsx +++ b/src/components/Auth/GoogleSigninButton.tsx @@ -2,7 +2,7 @@ import { GoogleIcon } from "@/assets/icons"; export default function GoogleSigninButton({ text }: { text: string }) { return ( - diff --git a/src/components/Auth/Signin/index.tsx b/src/components/Auth/Signin/index.tsx index 36f8120..53280cc 100644 --- a/src/components/Auth/Signin/index.tsx +++ b/src/components/Auth/Signin/index.tsx @@ -4,13 +4,21 @@ import SigninWithPassword from "../SigninWithPassword"; export default function Signin() { return ( - <>
-
- +
+
+ +
+
+

+ Welcome back +

+

+ Sign in to continue to your dashboard +

+
- ); } diff --git a/src/components/Auth/SigninWithPassword.tsx b/src/components/Auth/SigninWithPassword.tsx index 0b22163..eb4848a 100644 --- a/src/components/Auth/SigninWithPassword.tsx +++ b/src/components/Auth/SigninWithPassword.tsx @@ -12,6 +12,7 @@ import { useState } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; import InputGroup from "../FormElements/InputGroup"; import { ThemeToggleSwitch } from "../Layouts/header/theme-toggle"; +import { Button } from "../ui-elements/button"; export default function SigninWithPassword() { const isMutating = useIsMutating(); @@ -29,7 +30,7 @@ export default function SigninWithPassword() { }; return (
-
+
{ setPasswordFieldInputType( passwordFieldInputType === "password" ? "text" : "password", @@ -74,20 +75,24 @@ export default function SigninWithPassword() { />
-
+ ) : ( + "Sign in" + ) + } + variant="primary" + shape="rounded" + size="small" type="submit" disabled={!!isMutating} className={cn( - "flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary p-4 font-medium text-white transition hover:bg-opacity-90", - isMutating && "cursor-not-allowed bg-primary/50", + "w-full rounded-xl bg-gradient-to-r from-primary to-primary/85 p-4 font-semibold shadow-theme-xs transition-all duration-300 hover:-translate-y-0.5 hover:opacity-95", + isMutating && "cursor-not-allowed opacity-70 hover:translate-y-0", )} - > - {isMutating ? ( -
- ) : ( - "Sign in" - )} - + />
); diff --git a/src/components/FormElements/InputGroup/index.tsx b/src/components/FormElements/InputGroup/index.tsx index fc3dc7d..206f06d 100644 --- a/src/components/FormElements/InputGroup/index.tsx +++ b/src/components/FormElements/InputGroup/index.tsx @@ -16,6 +16,7 @@ type InputGroupProps = { name: Path; label: string; type: HTMLInputTypeAttribute; + accept?: string; placeholder?: string; className?: string; fileStyleVariant?: "style1" | "style2"; @@ -38,6 +39,7 @@ const InputGroup = ({ label, type, placeholder, + accept, required, disabled, active, @@ -60,7 +62,7 @@ const InputGroup = ({
); diff --git a/src/components/FormElements/InputGroup/tag-input.tsx b/src/components/FormElements/InputGroup/tag-input.tsx index ade2004..039d2a8 100644 --- a/src/components/FormElements/InputGroup/tag-input.tsx +++ b/src/components/FormElements/InputGroup/tag-input.tsx @@ -112,7 +112,7 @@ const TagInput = ({