-

+
+ {onBannerUrlChange && (
+
+
+ onBannerUrlChange(e.target.value)}
+ className="max-w-md"
+ />
+
+ )}
);
}
diff --git a/apps/web/src/components/settings/profile/edit-profile/types.ts b/apps/web/src/components/settings/profile/edit-profile/types.ts
index 68aa4f8e8..5aacd2688 100644
--- a/apps/web/src/components/settings/profile/edit-profile/types.ts
+++ b/apps/web/src/components/settings/profile/edit-profile/types.ts
@@ -3,6 +3,7 @@ export interface EditProfileFormData {
first_name: string;
last_name: string;
bio: string;
+ banner_url: string;
location: string;
gender: string;
birthdate: string;
diff --git a/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts b/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts
index 6819bb274..076c0fc5c 100644
--- a/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts
+++ b/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts
@@ -11,6 +11,7 @@ const initialFormData: EditProfileFormData = {
first_name: '',
last_name: '',
bio: '',
+ banner_url: '',
location: '',
gender: 'Prefer not to say',
birthdate: '',
@@ -38,12 +39,15 @@ export function useEditProfile() {
first_name: p.first_name || '',
last_name: p.last_name || '',
bio: p.bio || '',
+ banner_url: p.banner_url || '',
location: p.location || '',
gender: p.gender || 'Prefer not to say',
birthdate: p.birthdate || '',
});
- if (p.avatar) setAvatar(p.avatar);
- if (p.banner) setBanner(p.banner);
+ if (p.avatar_url) setAvatar(p.avatar_url);
+ if (p.banner_url) {
+ setBanner(p.banner_url);
+ }
} catch (e) {
logger.error('Failed to load profile settings', {
error: e instanceof Error ? e.message : String(e),
diff --git a/apps/web/src/features/profile/pages/user-profile-page/UserProfilePage.tsx b/apps/web/src/features/profile/pages/user-profile-page/UserProfilePage.tsx
index 80489814a..58072c442 100644
--- a/apps/web/src/features/profile/pages/user-profile-page/UserProfilePage.tsx
+++ b/apps/web/src/features/profile/pages/user-profile-page/UserProfilePage.tsx
@@ -45,7 +45,7 @@ export function UserProfilePage() {
return (
-
+
- {/* Base gradient layer */}
-
+ {bannerUrl ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+ {/* Base gradient layer */}
+
{/* Radial highlight at center-top */}
@@ -29,6 +43,8 @@ export function UserProfilePageHero() {
className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-background to-transparent"
aria-hidden
/>
+ >
+ )}
);
}
diff --git a/apps/web/src/features/profile/services/profileService.ts b/apps/web/src/features/profile/services/profileService.ts
index ae51e5de3..1597df52a 100644
--- a/apps/web/src/features/profile/services/profileService.ts
+++ b/apps/web/src/features/profile/services/profileService.ts
@@ -6,6 +6,7 @@ export interface UserProfile {
first_name: string | null;
last_name: string | null;
avatar_url: string | null;
+ banner_url?: string | null;
bio: string | null;
location: string | null;
birthdate: string | null;
@@ -35,6 +36,7 @@ export interface UpdateProfileRequest {
last_name?: string;
username?: string;
bio?: string;
+ banner_url?: string;
location?: string;
birthdate?: string;
gender?: string;
diff --git a/veza-backend-api/internal/handlers/profile_handler.go b/veza-backend-api/internal/handlers/profile_handler.go
index 2d49ea7df..dd70b9d8a 100644
--- a/veza-backend-api/internal/handlers/profile_handler.go
+++ b/veza-backend-api/internal/handlers/profile_handler.go
@@ -510,6 +510,7 @@ type UpdateProfileRequest struct {
Birthdate string `json:"birthdate" binding:"omitempty,datetime=2006-01-02" validate:"omitempty,datetime=2006-01-02"`
Gender string `json:"gender" binding:"omitempty,oneof=Male Female Other 'Prefer not to say'" validate:"omitempty,oneof=Male Female Other 'Prefer not to say'"`
SocialLinks map[string]interface{} `json:"social_links" binding:"omitempty"`
+ BannerURL string `json:"banner_url" binding:"omitempty,max=2048"`
}
// UpdateProfile updates a user profile
@@ -636,6 +637,10 @@ func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
SocialLinks: req.SocialLinks,
}
+ if req.BannerURL != "" {
+ serviceReq.BannerURL = &req.BannerURL
+ }
+
if req.Birthdate != "" {
birthdate, _ := time.Parse("2006-01-02", req.Birthdate)
birthdateStr := birthdate.Format("2006-01-02")
diff --git a/veza-backend-api/internal/models/user.go b/veza-backend-api/internal/models/user.go
index 61132b7df..c25c04c31 100644
--- a/veza-backend-api/internal/models/user.go
+++ b/veza-backend-api/internal/models/user.go
@@ -20,6 +20,7 @@ type User struct {
FirstName string `gorm:"size:100" json:"first_name" db:"first_name"`
LastName string `gorm:"size:100" json:"last_name" db:"last_name"`
Avatar string `gorm:"type:text" json:"avatar" db:"avatar"`
+ BannerURL string `gorm:"type:text" json:"banner_url" db:"banner_url"`
Bio string `gorm:"type:text" json:"bio" db:"bio"`
Location string `gorm:"size:100" json:"location" db:"location"`
Birthdate *time.Time `json:"birthdate" db:"birthdate"`
diff --git a/veza-backend-api/internal/services/user_service.go b/veza-backend-api/internal/services/user_service.go
index 4a8afc131..ef2927092 100644
--- a/veza-backend-api/internal/services/user_service.go
+++ b/veza-backend-api/internal/services/user_service.go
@@ -61,6 +61,7 @@ type Profile struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarURL *string `json:"avatar_url"`
+ BannerURL *string `json:"banner_url"`
Bio *string `json:"bio"`
Location *string `json:"location"`
Birthdate *string `json:"birthdate"`
@@ -297,6 +298,9 @@ func (s *UserService) UpdateProfile(userID uuid.UUID, req types.UpdateProfileReq
socialLinksJSON, _ := json.Marshal(req.SocialLinks)
updates["social_links"] = string(socialLinksJSON)
}
+ if req.BannerURL != nil {
+ updates["banner_url"] = *req.BannerURL
+ }
// Apply updates to user object
if firstname, ok := updates["first_name"].(string); ok {
@@ -331,6 +335,9 @@ func (s *UserService) UpdateProfile(userID uuid.UUID, req types.UpdateProfileReq
if socialLinks, ok := updates["social_links"].(string); ok {
user.SocialLinks = socialLinks
}
+ if bannerURL, ok := updates["banner_url"].(string); ok {
+ user.BannerURL = bannerURL
+ }
// Save changes
err = s.userRepo.Update(user)
@@ -349,6 +356,11 @@ func (s *UserService) userToProfile(user *models.User) *Profile {
avatarURL = &user.Avatar
}
+ var bannerURL *string
+ if user.BannerURL != "" {
+ bannerURL = &user.BannerURL
+ }
+
var bio *string
if user.Bio != "" {
bio = &user.Bio
@@ -382,6 +394,7 @@ func (s *UserService) userToProfile(user *models.User) *Profile {
FirstName: user.FirstName,
LastName: user.LastName,
AvatarURL: avatarURL,
+ BannerURL: bannerURL,
Bio: bio,
Location: location,
Birthdate: birthdate,
diff --git a/veza-backend-api/internal/types/user.go b/veza-backend-api/internal/types/user.go
index 35388d6fe..9087744b3 100644
--- a/veza-backend-api/internal/types/user.go
+++ b/veza-backend-api/internal/types/user.go
@@ -13,6 +13,7 @@ type UpdateProfileRequest struct {
Gender *string `json:"gender"`
Timezone *string `json:"timezone"`
SocialLinks map[string]interface{} `json:"social_links"`
+ BannerURL *string `json:"banner_url"`
WebsiteURL *string `json:"website_url"`
ProfilePrivacy *string `json:"profile_privacy"`
}
diff --git a/veza-backend-api/migrations/083_add_user_banner.sql b/veza-backend-api/migrations/083_add_user_banner.sql
new file mode 100644
index 000000000..dbbcd9f76
--- /dev/null
+++ b/veza-backend-api/migrations/083_add_user_banner.sql
@@ -0,0 +1,6 @@
+-- Migration: Add user profile banner
+-- Description: Adds banner_url column for customizable profile banner image
+
+ALTER TABLE users ADD COLUMN IF NOT EXISTS banner_url TEXT DEFAULT '';
+
+COMMENT ON COLUMN users.banner_url IS 'URL of user profile banner image';