Skip to content

Profile Page ​

File Information ​

  • Path: app/pages/[brand_name]/profile.vue
  • Route: /{brand_name}/profile
  • Purpose: User profile management with profile editing, image upload, notification settings, and password change
  • Layout: collage-layout
  • Middleware: check-url

Overview ​

The Profile page allows users to view and manage their profile information. It provides comprehensive profile management features including profile image upload, name editing, notification preferences, and password change functionality.

Key Features ​

  1. Profile Display

    • User profile image (with avatar fallback)
    • User name and email display
    • Profile image upload
    • Image preview
  2. Profile Editing

    • Full name editing
    • Real-time validation
    • Form state management
    • Cancel functionality
  3. Notification Settings

    • Digital Asset Management email notification toggle
    • Tooltip with explanation
    • Preference persistence
  4. Password Change

    • Old password input
    • New password input
    • Confirm password input
    • Password validation
    • Form validation
  5. Form Validation

    • Name validation (required, format)
    • Password validation (required, minimum length, match)
    • Real-time error display
    • Submit button state management

Components Used ​

  • v-text-field - Vuetify text input components
  • v-textarea - Vuetify textarea component
  • v-file-input - Vuetify file input component
  • v-btn - Vuetify button component
  • v-switch - Vuetify switch component
  • v-progress-circular - Loading spinner
  • v-skeleton-loader - Skeleton loaders
  • v-tooltip - Tooltip component
  • AsyncIcon - Async icon component

Composables Used ​

  • useProfile() - Profile management (app/composables/core/pages/useProfile.ts)
  • useProfileApi() - Profile API (app/composables/api/useProfileApi.ts)
  • useAuthStore() - Authentication store
  • useSnackbar() - Snackbar notifications
  • useHelpers() - Helper functions

Data Properties ​

Reactive Data (from useProfile composable) ​

typescript
{
  name: string | null,                // User full name
  email: string | null,               // User email (read-only)
  logo_preview: string | null,        // Profile image URL
  ifNotImageChar: string,             // Avatar character if no image
  upload_profile: string | null,      // Uploaded image preview (data URL)
  upload_profile_file: File | null,    // Uploaded image file
  contentLoading: boolean,            // Initial content loading
  loading: boolean,                   // Profile save loading
  password_loading: boolean,          // Password change loading
  notification: boolean,              // Notification preference
  profileImageReload: number,         // Profile image reload key
  changePasswordForm: {                // Password change form
    old_pass: string,
    password: string,
    confirm_password: string
  },
  errors: {                           // Form errors
    name: string,
    changePasswordForm: {
      old_pass: string,
      password: string,
      confirm_password: string
    }
  },
  initData: {                         // Initial form data
    name: string,
    notification: boolean
  },
  user_id: number | null,             // User ID
  workspace_id: string                 // Workspace ID
}

Computed Properties ​

submitProfileDisabled ​

Determines if profile save button should be disabled.

typescript
computed(() => {
  if (loading.value || errors.name) return true
  
  const formName = (name.value ?? "").trim()
  const initName = (initData.name ?? "").trim()
  
  return (
    formName === initName &&
    initData.notification === notification.value &&
    !upload_profile.value
  )
})

Returns: boolean - True if form is unchanged or invalid

submitPasswordDisabled ​

Determines if password save button should be disabled.

typescript
computed(() => {
  const f = changePasswordForm
  
  if (
    !f.old_pass.trim() ||
    !f.password.trim() ||
    !f.confirm_password.trim()
  ) {
    return true
  }
  
  return Boolean(
    errors.changePasswordForm.old_pass ||
    errors.changePasswordForm.password ||
    errors.changePasswordForm.confirm_password ||
    password_loading.value
  )
})

Returns: boolean - True if form is invalid or loading

Methods ​

fetchUserProfile() ​

Fetches user profile data from auth store.

typescript
async function fetchUserProfile() {
  try {
    user_id.value = authUser.id
    name.value = authUser.name
    ifNotImageChar.value = authUser.name?.charAt(0)?.toUpperCase() ?? ""
    email.value = authUser.email
    logo_preview.value = authUser.profile_image
    notification.value = !!authUser.enable_notification
    
    Object.assign(initData, {
      name: authUser.name,
      notification: notification.value,
    })
  } catch (e) {
    snackbar.showError(getErrorMessage(e))
  }
}

Flow:

  1. Gets user data from auth store
  2. Sets form fields
  3. Sets avatar character
  4. Sets initial data for comparison
  5. Handles errors

handleSubmit() ​

Handles profile form submission.

typescript
async function handleSubmit() {
  validateName()
  if (submitProfileDisabled.value) return
  
  loading.value = true
  try {
    const formData = new FormData()
    formData.append("id", String(user_id.value))
    formData.append("instance_id", String(authUser.default_instance_id))
    
    if (upload_profile_file.value) {
      formData.append("profile_image", upload_profile_file.value)
    }
    formData.append("name", name.value)
    formData.append("phone", "")
    formData.append("enable_notification", notification.value ? "1" : "0")
    
    const response = await updateUserProfile(formData)
    
    snackbar.showSuccess(response?.message)
    
    if (name.value !== initData.name) {
      (nuxtApp as any).$mittBus.emit("profile-name-updated", name.value)
    }
    
    Object.assign(initData, {
      name: name.value,
      notification: notification.value,
    })
    
    if (upload_profile.value) {
      const newLogo =
        response?.data?.user?.profile_image || upload_profile.value
      // Add cache-busting query param
      const cacheBustedLogo =
        newLogo && typeof newLogo === "string"
          ? `${newLogo}${newLogo.includes("?") ? "&" : "?"}t=${Date.now()}`
          : newLogo
      logo_preview.value = cacheBustedLogo as string | null
      upload_profile.value = null
      upload_profile_file.value = null
      profileImageReload.value++
      (nuxtApp as any).$mittBus.emit(
        "profile-image-updated",
        logo_preview.value
      )
    }
  } catch (e) {
    console.log(e)
    snackbar.showError(getErrorMessage(e))
  } finally {
    loading.value = false
  }
}

Flow:

  1. Validates name
  2. Checks if form is valid
  3. Builds FormData with profile data
  4. Appends image file if uploaded
  5. Calls update API
  6. Updates initial data
  7. Updates profile image with cache-busting
  8. Emits events for header updates
  9. Handles errors

handleCancel() ​

Cancels profile changes and resets form.

typescript
function handleCancel() {
  upload_profile.value = null
  upload_profile_file.value = null
  name.value = initData.name
  notification.value = initData.notification
}

changeProfile(payload, fileInput) ​

Handles profile image upload.

typescript
function changeProfile(payload: any, fileInput: Ref) {
  const file = payload?.target?.files[0] || payload
  const resetFile = () => {
    // Clear file input
    const inputEl =
      fileInput.value?.$el?.querySelector('input[type="file"]') ??
      fileInput.value?.$el?.querySelector("input")
    if (inputEl) {
      inputEl.value = ""
    }
    upload_profile_file.value = null
    upload_profile.value = null
    profileImageReload.value++
  }
  
  if (!file) {
    resetFile()
    return
  }
  
  try {
    // Validate file type
    if (!file.type.includes("image")) {
      snackbar.showError("Please choose an image file")
      resetFile()
      return
    }
    
    // Validate file size (4MB max)
    const maxSize = 4 * 1024 * 1024
    if (file.size > maxSize) {
      snackbar.showError("File size should not be more than 4MB")
      resetFile()
      return
    }
    
    upload_profile_file.value = file as File
    const reader = new FileReader()
    reader.onload = (e) => {
      upload_profile.value = e.target?.result as string
    }
    reader.readAsDataURL(file)
  } catch (error) {
    console.log("changeProfile error", error)
    upload_profile.value = null
    upload_profile_file.value = null
    resetFile()
  }
  profileImageReload.value++
}

Validation:

  • File must be an image
  • File size must be ≤ 4MB
  • Creates data URL preview

validateName() ​

Validates full name field.

typescript
function validateName() {
  errors.name = validateFullName(name.value)
}

Uses: validateFullName helper from useHelpers()

validatePassword(value, key) ​

Validates password fields.

typescript
function validatePassword(
  value: string,
  key: keyof typeof changePasswordForm
) {
  if (!key) return
  
  if (!value.trim()) {
    errors.changePasswordForm[key] = "This field is required"
  } else if (value.trim().length < 6 && key !== "confirm_password") {
    errors.changePasswordForm[key] =
      "Password should have at least 6 characters!"
  } else if (
    (key === "confirm_password" ||
      changePasswordForm.confirm_password.trim().length > 0) &&
    changePasswordForm.password.trim() !==
    changePasswordForm.confirm_password.trim()
  ) {
    errors.changePasswordForm.confirm_password =
      "Both passwords must be exactly same!"
    if (value.trim().length >= 6 && key === "password") {
      errors.changePasswordForm.password = ""
    }
  } else {
    errors.changePasswordForm[key] = ""
  }
}

Validation Rules:

  • All fields required
  • Password minimum 6 characters
  • Confirm password must match password
  • Real-time validation

changeUserPassword() ​

Handles password change form submission.

typescript
async function changeUserPassword() {
  validatePassword(changePasswordForm.old_pass, "old_pass")
  validatePassword(changePasswordForm.password, "password")
  validatePassword(changePasswordForm.confirm_password, "confirm_password")
  
  if (submitPasswordDisabled.value) return
  
  password_loading.value = true
  try {
    const response = await changeUserPasswordApi({
      old_password: changePasswordForm.old_pass,
      password: changePasswordForm.password,
    })
    
    snackbar.showSuccess(response?.message)
    
    resetErrors()
    changePasswordForm.old_pass = ""
    changePasswordForm.password = ""
    changePasswordForm.confirm_password = ""
  } catch (e) {
    snackbar.showError(getErrorMessage(e))
  } finally {
    password_loading.value = false
  }
}

Flow:

  1. Validates all password fields
  2. Checks if form is valid
  3. Calls password change API
  4. Shows success message
  5. Resets form and errors
  6. Handles errors

resetErrors() ​

Resets all form errors.

typescript
function resetErrors() {
  errors.name = ""
  errors.changePasswordForm = {
    old_pass: "",
    password: "",
    confirm_password: "",
  }
}

User Interactions ​

  1. Profile Image

    • Click to upload new image
    • Image preview before upload
    • Validation (image type, 4MB max)
  2. Profile Editing

    • Edit full name
    • Real-time validation
    • Save changes
    • Cancel changes (resets form)
  3. Notification Settings

    • Toggle notification preference
    • Tooltip with explanation
    • Preference saved with profile
  4. Password Change

    • Enter old password
    • Enter new password
    • Confirm new password
    • Real-time validation
    • Submit password change

Real-time Features ​

  • Profile image updates in header via event bus
  • Name updates in header via event bus
  • Changes reflected immediately after save

Styling ​

  • Uses custom CSS from assets/scss/custom.scss
  • Form layout styles
  • Input field styles with validation states
  • Button styles with loading states
  • Image upload area styling
  • Profile image preview styling

Watchers ​

None - uses event handlers (@input, @blur) for validation.

Lifecycle Hooks ​

onMounted ​

Initializes profile data.

typescript
onMounted(async () => {
  await fetchUserProfile()
  contentLoading.value = false
})

API Endpoints ​

Update User Profile ​

  • Endpoint: PUT /user/profile (via updateUserProfile())
  • Request: FormData
    typescript
    {
      id: number,
      instance_id: number,
      profile_image?: File,
      name: string,
      phone: string,
      enable_notification: '0' | '1'
    }
  • Response:
    typescript
    {
      message: string,
      data: {
        user: {
          profile_image: string,
          name: string,
          enable_notification: boolean
        }
      }
    }

Change User Password ​

  • Endpoint: POST /user/change-password (via changeUserPasswordApi())
  • Request Body:
    typescript
    {
      old_password: string,
      password: string
    }
  • Response:
    typescript
    {
      message: string
    }

User Flow ​

  1. User navigates to profile page
  2. Page fetches user profile data from auth store
  3. User can:
    • Upload profile image (validates type and size)
    • Edit full name (with validation)
    • Toggle notification preference
    • Save profile changes
    • Cancel changes (resets form)
    • Change password (with validation)
  4. Form validation occurs in real-time
  5. Success/error messages displayed
  6. Profile image updates in header via event bus
  7. Name updates in header via event bus

Performance Optimizations ​

  1. Image Preview: Uses FileReader for client-side preview
  2. Cache Busting: Adds timestamp to image URL to force refresh
  3. Form State: Tracks initial state to detect changes
  4. Validation: Real-time validation prevents unnecessary API calls

Notes for Development ​

  • Profile image upload limited to 4MB
  • Only image files accepted for profile image
  • Name validation uses validateFullName helper
  • Password minimum length is 6 characters
  • Profile changes emit events for header updates
  • Image reload key forces component re-render
  • Form state compared to initial data for submit button
  • Cancel resets form to initial state