- Add VideoProcessor service to PostHandler for frame-based video moderation - Implement multi-frame extraction and Azure OpenAI Vision analysis for video content - Enhance VideoStitchingService with filters, speed control, and text overlays - Add image upload dialogs for group avatar and banner in GroupCreationModal - Implement navigation placeholders for mentions, hashtags, and URLs in sojornRichText
1120 lines
28 KiB
Markdown
1120 lines
28 KiB
Markdown
# Profile Widgets System Documentation
|
|
|
|
## 🎨 Modular Profile Customization
|
|
|
|
**Version**: 3.0
|
|
**Status**: ✅ **COMPLETED**
|
|
**Last Updated**: February 17, 2026
|
|
|
|
---
|
|
|
|
## 🎯 Overview
|
|
|
|
The Profile Widgets system transforms user profiles from static displays into dynamic, personalized spaces. Inspired by MySpace but with modern design constraints, users can add, remove, and arrange various widgets to create unique profile expressions while maintaining platform consistency.
|
|
|
|
## 🏗️ Architecture
|
|
|
|
### System Components
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ Widget Engine │ │ Layout Manager │ │ Theme System │
|
|
│ │◄──►│ │◄──►│ │
|
|
│ • Widget Registry│ │ • Grid Layout │ │ • Color Schemes │
|
|
│ • Component Cache │ │ • Drag & Drop │ │ • Font Options │
|
|
│ • State Management│ │ • Persistence │ │ • Style Rules │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ Database │ │ User Interface │ │ Asset Storage │
|
|
│ │ │ │ │ │
|
|
│ • Profile Layout │ │ • Widget Renderer│ │ • Widget Assets │
|
|
│ • Widget Config │ │ • Drag Interface │ │ • Theme Assets │
|
|
│ • User Settings │ │ • Preview Mode │ │ • Custom Images │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 Widget Types
|
|
|
|
### Standard Fields (Always Present)
|
|
|
|
#### Profile Header
|
|
- **Avatar**: User profile image with upload capability
|
|
- **Display Name**: Editable display name
|
|
- **Handle**: Unique username (@handle)
|
|
- **Pronouns**: Optional pronoun field
|
|
- **Location**: Optional city-level location
|
|
|
|
### Widget Catalog
|
|
|
|
#### 📌 Pinned Posts Widget
|
|
Display up to 3 featured posts at the top of profile.
|
|
|
|
**Features:**
|
|
- Drag to reorder pinned posts
|
|
- Quick pin/unpin from post menu
|
|
- Automatic thumbnail generation
|
|
- Engagement stats display
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class PinnedPostsWidgetConfig {
|
|
final List<String> postIds;
|
|
final bool showEngagementStats;
|
|
final int maxPosts;
|
|
}
|
|
```
|
|
|
|
#### 🎵 Music Widget
|
|
Show currently listening or favorite music tracks.
|
|
|
|
**Features:**
|
|
- Spotify/Apple Music integration
|
|
- Manual track entry
|
|
- Album artwork display
|
|
- Play preview snippets
|
|
- Music sharing links
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class MusicWidgetConfig {
|
|
final String? currentTrack;
|
|
final String? artist;
|
|
final String? album;
|
|
final String? albumArt;
|
|
final String? spotifyUrl;
|
|
final String? appleMusicUrl;
|
|
final bool showCurrentlyListening;
|
|
}
|
|
```
|
|
|
|
#### 📸 Photo Grid Widget
|
|
Mini gallery of featured photos.
|
|
|
|
**Features:**
|
|
- 3-6 photo grid layout
|
|
- Tap to view full size
|
|
- Photo captions and dates
|
|
- Upload from device or library
|
|
- Photo organization
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class PhotoGridWidgetConfig {
|
|
final List<String> photoUrls;
|
|
final int columns;
|
|
final bool showCaptions;
|
|
final bool showDates;
|
|
final PhotoGridStyle style;
|
|
}
|
|
```
|
|
|
|
#### 🔗 Social Links Widget
|
|
Icon row for external social links.
|
|
|
|
**Features:**
|
|
- 20+ platform icons
|
|
- Custom link labels
|
|
- Verification badges
|
|
- Click tracking analytics
|
|
- Link preview on hover
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class SocialLinksWidgetConfig {
|
|
final List<SocialLink> links;
|
|
final SocialLinkStyle style;
|
|
final bool showVerificationBadges;
|
|
}
|
|
|
|
class SocialLink {
|
|
final SocialPlatform platform;
|
|
final String url;
|
|
final String? customLabel;
|
|
final bool isVerified;
|
|
}
|
|
```
|
|
|
|
#### 🏷️ Causes Widget
|
|
Tag-style badges for causes and interests.
|
|
|
|
**Features:**
|
|
- Pre-defined cause categories
|
|
- Custom cause creation
|
|
- Color-coded categories
|
|
- Click to explore similar users
|
|
- Cause-based recommendations
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class CausesWidgetConfig {
|
|
final List<CauseTag> causes;
|
|
final bool showDescription;
|
|
final CauseDisplayStyle style;
|
|
}
|
|
|
|
class CauseTag {
|
|
final String name;
|
|
final CauseCategory category;
|
|
final String? description;
|
|
final Color color;
|
|
}
|
|
```
|
|
|
|
#### 👥 Featured Friends Widget
|
|
Highlight 3-6 important connections.
|
|
|
|
**Features:**
|
|
- Friend selection from followers
|
|
- Mutual friends indicator
|
|
- Online status display
|
|
- Quick message action
|
|
- Relationship type labels
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class FeaturedFriendsWidgetConfig {
|
|
final List<String> friendIds;
|
|
final int maxFriends;
|
|
final bool showOnlineStatus;
|
|
final bool showMutualFriends;
|
|
final FriendDisplayStyle style;
|
|
}
|
|
```
|
|
|
|
#### 📊 Stats Widget
|
|
Display profile statistics and milestones.
|
|
|
|
**Features:**
|
|
- Post count, follower count
|
|
- Member since date
|
|
- Achievement badges
|
|
- Growth charts
|
|
- Privacy controls for sensitive stats
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class StatsWidgetConfig {
|
|
final bool showFollowerCount;
|
|
final bool showPostCount;
|
|
final bool showMemberSince;
|
|
final bool showAchievements;
|
|
final bool showGrowthChart;
|
|
final StatPrivacyLevel privacyLevel;
|
|
}
|
|
```
|
|
|
|
#### 💭 Quote Widget
|
|
Display a favorite quote or motto.
|
|
|
|
**Features:**
|
|
- Rich text formatting
|
|
- Quote attribution
|
|
- Background styling options
|
|
- Font customization
|
|
- Share quote feature
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class QuoteWidgetConfig {
|
|
final String text;
|
|
final String? attribution;
|
|
final QuoteStyle style;
|
|
final Color backgroundColor;
|
|
final Color textColor;
|
|
final String fontFamily;
|
|
}
|
|
```
|
|
|
|
#### 📍 Beacon Activity Widget
|
|
Show recent community contributions.
|
|
|
|
**Features:**
|
|
- Recent beacon posts
|
|
- Vouch/report statistics
|
|
- Community impact score
|
|
- Neighborhood focus
|
|
- Activity timeline
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class BeaconActivityWidgetConfig {
|
|
final int maxItems;
|
|
final bool showVouchCount;
|
|
final bool showReportCount;
|
|
final bool showNeighborhood;
|
|
final ActivityTimeframe timeframe;
|
|
}
|
|
```
|
|
|
|
#### 📝 Custom Text Widget
|
|
Markdown-rendered freeform content.
|
|
|
|
**Features:**
|
|
- Full Markdown support
|
|
- Custom styling options
|
|
- Link and media embedding
|
|
- Character limits
|
|
- Preview mode
|
|
|
|
**Configuration:**
|
|
```dart
|
|
class CustomTextWidgetConfig {
|
|
final String markdownContent;
|
|
final TextDisplayStyle style;
|
|
final bool allowHtml;
|
|
final int maxCharacters;
|
|
final bool showWordCount;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 Theming System
|
|
|
|
### Color Schemes
|
|
- **Default**: Platform blue and gray palette
|
|
- **Sunset**: Warm oranges and purples
|
|
- **Ocean**: Cool blues and teals
|
|
- **Forest**: Natural greens and browns
|
|
- **Monochrome**: Classic black and white
|
|
- **Neon**: Bright, vibrant colors
|
|
|
|
### Theme Options
|
|
```dart
|
|
class ProfileTheme {
|
|
final String name;
|
|
final Color primaryColor;
|
|
final Color secondaryColor;
|
|
final Color backgroundColor;
|
|
final Color textColor;
|
|
final Color accentColor;
|
|
final String fontFamily;
|
|
final bool darkMode;
|
|
}
|
|
```
|
|
|
|
### Customization Controls
|
|
- **Accent Color Picker**: Choose from palette or custom hex
|
|
- **Font Selection**: 5 font families with size options
|
|
- **Dark/Light Mode**: Independent of app theme
|
|
- **Banner Image**: Upload custom background
|
|
- **Widget Borders**: Show/hide widget borders
|
|
- **Shadow Effects**: Adjustable shadow intensity
|
|
|
|
---
|
|
|
|
## 📱 Implementation Details
|
|
|
|
### Frontend Components
|
|
|
|
#### Draggable Widget Grid
|
|
**File**: `sojorn_app/lib/widgets/profile/draggable_widget_grid.dart`
|
|
|
|
```dart
|
|
class DraggableWidgetGrid extends StatefulWidget {
|
|
final List<ProfileWidget> widgets;
|
|
final Function(List<ProfileWidget>)? onLayoutChanged;
|
|
final bool isEditing;
|
|
final ProfileTheme theme;
|
|
|
|
@override
|
|
_DraggableWidgetGridState createState() => _DraggableWidgetGridState();
|
|
}
|
|
|
|
class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
|
|
late List<ProfileWidget> _widgets;
|
|
final GlobalKey _gridKey = GlobalKey();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_widgets = List.from(widget.widgets);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
key: _gridKey,
|
|
child: ReorderableGridView.count(
|
|
crossAxisCount: 3,
|
|
mainAxisSpacing: 16,
|
|
crossAxisSpacing: 16,
|
|
onReorder: _onReorder,
|
|
children: _widgets.map((widget) => _buildWidget(widget)).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWidget(ProfileWidget widget) {
|
|
return ReorderableWidget(
|
|
key: ValueKey(widget.id),
|
|
reorderable: widget.isEditing,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: widget.theme.backgroundColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: widget.showBorder
|
|
? Border.all(color: widget.theme.primaryColor)
|
|
: null,
|
|
boxShadow: widget.showShadows
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: Offset(0, 4),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: WidgetRenderer(
|
|
widget: widget,
|
|
theme: widget.theme,
|
|
isEditing: widget.isEditing,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onReorder(int oldIndex, int newIndex) {
|
|
setState(() {
|
|
if (newIndex > oldIndex) {
|
|
newIndex -= 1;
|
|
}
|
|
final ProfileWidget widget = _widgets.removeAt(oldIndex);
|
|
_widgets.insert(newIndex, widget);
|
|
});
|
|
|
|
widget.onLayoutChanged?.call(_widgets);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Widget Renderer
|
|
**File**: `sojorn_app/lib/widgets/profile/widget_renderer.dart`
|
|
|
|
```dart
|
|
class WidgetRenderer extends StatelessWidget {
|
|
final ProfileWidget widget;
|
|
final ProfileTheme theme;
|
|
final bool isEditing;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
switch (widget.type) {
|
|
case WidgetType.pinnedPosts:
|
|
return PinnedPostsWidget(
|
|
config: widget.config as PinnedPostsWidgetConfig,
|
|
theme: theme,
|
|
isEditing: isEditing,
|
|
);
|
|
case WidgetType.music:
|
|
return MusicWidget(
|
|
config: widget.config as MusicWidgetConfig,
|
|
theme: theme,
|
|
isEditing: isEditing,
|
|
);
|
|
case WidgetType.photoGrid:
|
|
return PhotoGridWidget(
|
|
config: widget.config as PhotoGridWidgetConfig,
|
|
theme: theme,
|
|
isEditing: isEditing,
|
|
);
|
|
// ... other widget types
|
|
default:
|
|
return Container(
|
|
child: Text('Unknown widget type'),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Profile Editor
|
|
**File**: `sojorn_app/lib/screens/profile/profile_editor_screen.dart`
|
|
|
|
```dart
|
|
class ProfileEditorScreen extends ConsumerStatefulWidget {
|
|
@override
|
|
_ProfileEditorScreenState createState() => _ProfileEditorScreenState();
|
|
}
|
|
|
|
class _ProfileEditorScreenState extends ConsumerState<ProfileEditorScreen> {
|
|
List<ProfileWidget> _widgets = [];
|
|
ProfileTheme _theme = ProfileTheme.default();
|
|
bool _isEditing = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('Edit Profile'),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(_isEditing ? Icons.check : Icons.edit),
|
|
onPressed: _toggleEditing,
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.palette),
|
|
onPressed: _showThemeSelector,
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// Profile header
|
|
_buildProfileHeader(),
|
|
|
|
// Widget grid
|
|
Expanded(
|
|
child: DraggableWidgetGrid(
|
|
widgets: _widgets,
|
|
onLayoutChanged: _onLayoutChanged,
|
|
isEditing: _isEditing,
|
|
theme: _theme,
|
|
),
|
|
),
|
|
|
|
// Widget catalog (when editing)
|
|
if (_isEditing) _buildWidgetCatalog(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWidgetCatalog() {
|
|
return Container(
|
|
height: 120,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
children: WidgetType.values.map((type) =>
|
|
WidgetCatalogItem(
|
|
type: type,
|
|
onTap: () => _addWidget(type),
|
|
),
|
|
).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _addWidget(WidgetType type) {
|
|
final newWidget = ProfileWidget.create(type, _theme);
|
|
setState(() {
|
|
_widgets.add(newWidget);
|
|
});
|
|
_saveLayout();
|
|
}
|
|
|
|
void _saveLayout() {
|
|
final layout = ProfileLayout(
|
|
widgets: _widgets,
|
|
theme: _theme,
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
|
|
ref.read(profileServiceProvider).saveLayout(layout);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Backend Integration
|
|
|
|
#### Profile Layout Service
|
|
**File**: `go-backend/internal/services/profile_service.go`
|
|
|
|
```go
|
|
type ProfileService struct {
|
|
db *pgxpool.Pool
|
|
}
|
|
|
|
type ProfileLayout struct {
|
|
UserID string `json:"user_id"`
|
|
Widgets []ProfileWidget `json:"widgets"`
|
|
Theme ProfileTheme `json:"theme"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ProfileWidget struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Config map[string]interface{} `json:"config"`
|
|
Order int `json:"order"`
|
|
}
|
|
|
|
func (s *ProfileService) SaveLayout(ctx context.Context, userID string, layout ProfileLayout) error {
|
|
// Serialize layout to JSON
|
|
layoutJSON, err := json.Marshal(layout)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal layout: %w", err)
|
|
}
|
|
|
|
// Save to database
|
|
_, err = s.db.Exec(ctx, `
|
|
INSERT INTO profile_layouts (user_id, layout_data, updated_at)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (user_id)
|
|
DO UPDATE SET
|
|
layout_data = $2,
|
|
updated_at = $3
|
|
`, userID, layoutJSON, time.Now())
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *ProfileService) GetLayout(ctx context.Context, userID string) (*ProfileLayout, error) {
|
|
var layoutJSON []byte
|
|
err := s.db.QueryRow(ctx, `
|
|
SELECT layout_data
|
|
FROM profile_layouts
|
|
WHERE user_id = $1
|
|
`, userID).Scan(&layoutJSON)
|
|
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
// Return default layout
|
|
return s.getDefaultLayout(userID), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var layout ProfileLayout
|
|
err = json.Unmarshal(layoutJSON, &layout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal layout: %w", err)
|
|
}
|
|
|
|
return &layout, nil
|
|
}
|
|
|
|
func (s *ProfileService) getDefaultLayout(userID string) *ProfileLayout {
|
|
return &ProfileLayout{
|
|
UserID: userID,
|
|
Widgets: []ProfileWidget{
|
|
{
|
|
ID: "bio",
|
|
Type: "bio",
|
|
Config: map[string]interface{}{
|
|
"show_pronouns": true,
|
|
"show_location": true,
|
|
},
|
|
Order: 0,
|
|
},
|
|
{
|
|
ID: "social_links",
|
|
Type: "social_links",
|
|
Config: map[string]interface{}{
|
|
"style": "icons",
|
|
},
|
|
Order: 1,
|
|
},
|
|
{
|
|
ID: "pinned_posts",
|
|
Type: "pinned_posts",
|
|
Config: map[string]interface{}{
|
|
"max_posts": 3,
|
|
"show_engagement": true,
|
|
},
|
|
Order: 2,
|
|
},
|
|
},
|
|
Theme: ProfileTheme{
|
|
Name: "default",
|
|
PrimaryColor: "#1976D2",
|
|
DarkMode: false,
|
|
},
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🗂️ Data Models
|
|
|
|
### Profile Widget Model
|
|
```dart
|
|
class ProfileWidget {
|
|
final String id;
|
|
final WidgetType type;
|
|
final Map<String, dynamic> config;
|
|
final int order;
|
|
final bool isVisible;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
const ProfileWidget({
|
|
required this.id,
|
|
required this.type,
|
|
required this.config,
|
|
required this.order,
|
|
this.isVisible = true,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
factory ProfileWidget.create(WidgetType type, ProfileTheme theme) {
|
|
return ProfileWidget(
|
|
id: const Uuid().v4(),
|
|
type: type,
|
|
config: _getDefaultConfig(type),
|
|
order: 0,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
}
|
|
|
|
static Map<String, dynamic> _getDefaultConfig(WidgetType type) {
|
|
switch (type) {
|
|
case WidgetType.pinnedPosts:
|
|
return {
|
|
'max_posts': 3,
|
|
'show_engagement_stats': true,
|
|
'post_ids': <String>[],
|
|
};
|
|
case WidgetType.music:
|
|
return {
|
|
'show_currently_listening': true,
|
|
'current_track': null,
|
|
'artist': null,
|
|
'album': null,
|
|
};
|
|
case WidgetType.photoGrid:
|
|
return {
|
|
'columns': 3,
|
|
'show_captions': true,
|
|
'show_dates': false,
|
|
'photo_urls': <String>[],
|
|
};
|
|
// ... other widget types
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Profile Layout Model
|
|
```dart
|
|
class ProfileLayout {
|
|
final String userId;
|
|
final List<ProfileWidget> widgets;
|
|
final ProfileTheme theme;
|
|
final DateTime updatedAt;
|
|
|
|
const ProfileLayout({
|
|
required this.userId,
|
|
required this.widgets,
|
|
required this.theme,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
factory ProfileLayout.fromJson(Map<String, dynamic> json) {
|
|
return ProfileLayout(
|
|
userId: json['user_id'],
|
|
widgets: (json['widgets'] as List)
|
|
.map((w) => ProfileWidget.fromJson(w))
|
|
.toList(),
|
|
theme: ProfileTheme.fromJson(json['theme']),
|
|
updatedAt: DateTime.parse(json['updated_at']),
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'user_id': userId,
|
|
'widgets': widgets.map((w) => w.toJson()).toList(),
|
|
'theme': theme.toJson(),
|
|
'updated_at': updatedAt.toIso8601String(),
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🗄️ Database Schema
|
|
|
|
### Profile Layout Table
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS profile_layouts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
layout_data JSONB NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
UNIQUE(user_id)
|
|
);
|
|
|
|
-- Index for efficient querying
|
|
CREATE INDEX idx_profile_layouts_user_id ON profile_layouts(user_id);
|
|
CREATE INDEX idx_profile_layouts_updated_at ON profile_layouts(updated_at);
|
|
|
|
-- Widget assets table
|
|
CREATE TABLE IF NOT EXISTS widget_assets (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
widget_id VARCHAR(100) NOT NULL,
|
|
asset_type VARCHAR(50) NOT NULL, -- 'banner', 'avatar', 'custom'
|
|
asset_url TEXT NOT NULL,
|
|
file_name VARCHAR(255),
|
|
file_size INTEGER,
|
|
mime_type VARCHAR(100),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
INDEX idx_widget_assets_user_id ON widget_assets(user_id),
|
|
INDEX idx_widget_assets_widget_id ON widget_assets(widget_id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 Technical Implementation
|
|
|
|
### Widget Constraints
|
|
```dart
|
|
class WidgetConstraints {
|
|
static const double maxWidth = 400;
|
|
static const double maxHeight = 600;
|
|
static const double minWidth = 200;
|
|
static const double minHeight = 150;
|
|
|
|
static const Map<WidgetType, Size> defaultSizes = {
|
|
WidgetType.pinnedPosts: Size(400, 300),
|
|
WidgetType.music: Size(400, 200),
|
|
WidgetType.photoGrid: Size(400, 400),
|
|
WidgetType.socialLinks: Size(400, 100),
|
|
WidgetType.causes: Size(400, 150),
|
|
WidgetType.featuredFriends: Size(400, 250),
|
|
WidgetType.stats: Size(400, 200),
|
|
WidgetType.quote: Size(400, 150),
|
|
WidgetType.beaconActivity: Size(400, 300),
|
|
WidgetType.customText: Size(400, 250),
|
|
};
|
|
|
|
static Size getMaxSize(WidgetType type) {
|
|
return defaultSizes[type] ?? Size(maxWidth, maxHeight);
|
|
}
|
|
|
|
static Size getMinSize(WidgetType type) {
|
|
final defaultSize = defaultSizes[type] ?? Size(maxWidth, maxHeight);
|
|
return Size(minWidth, defaultSize.height * 0.6);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Performance Optimization
|
|
```dart
|
|
class WidgetCacheManager {
|
|
static final Map<String, Widget> _widgetCache = {};
|
|
static const Duration _cacheTimeout = Duration(minutes: 5);
|
|
|
|
static Widget? getCachedWidget(String widgetId) {
|
|
return _widgetCache[widgetId];
|
|
}
|
|
|
|
static void cacheWidget(String widgetId, Widget widget) {
|
|
_widgetCache[widgetId] = widget;
|
|
|
|
// Auto-remove after timeout
|
|
Timer(_cacheTimeout, () {
|
|
_widgetCache.remove(widgetId);
|
|
});
|
|
}
|
|
|
|
static void clearCache() {
|
|
_widgetCache.clear();
|
|
}
|
|
}
|
|
```
|
|
|
|
### State Management
|
|
```dart
|
|
class ProfileLayoutNotifier extends StateNotifier<ProfileLayout> {
|
|
ProfileLayoutNotifier(this._profileService) : super(ProfileLayout.empty());
|
|
|
|
final ProfileService _profileService;
|
|
|
|
Future<void> loadLayout(String userId) async {
|
|
try {
|
|
final layout = await _profileService.getLayout(userId);
|
|
state = layout;
|
|
} catch (e) {
|
|
// Handle error
|
|
state = ProfileLayout.defaultForUser(userId);
|
|
}
|
|
}
|
|
|
|
Future<void> saveLayout() async {
|
|
try {
|
|
await _profileService.saveLayout(state);
|
|
} catch (e) {
|
|
// Handle error
|
|
}
|
|
}
|
|
|
|
void addWidget(WidgetType type) {
|
|
final newWidget = ProfileWidget.create(type, state.theme);
|
|
state = state.copyWith(
|
|
widgets: [...state.widgets, newWidget],
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
saveLayout();
|
|
}
|
|
|
|
void removeWidget(String widgetId) {
|
|
state = state.copyWith(
|
|
widgets: state.widgets.where((w) => w.id != widgetId).toList(),
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
saveLayout();
|
|
}
|
|
|
|
void updateWidget(String widgetId, Map<String, dynamic> config) {
|
|
state = state.copyWith(
|
|
widgets: state.widgets.map((w) {
|
|
if (w.id == widgetId) {
|
|
return w.copyWith(
|
|
config: config,
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
}
|
|
return w;
|
|
}).toList(),
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
saveLayout();
|
|
}
|
|
|
|
void reorderWidgets(int oldIndex, int newIndex) {
|
|
final widgets = List.from(state.widgets);
|
|
if (newIndex > oldIndex) {
|
|
newIndex -= 1;
|
|
}
|
|
final widget = widgets.removeAt(oldIndex);
|
|
widgets.insert(newIndex, widget);
|
|
|
|
state = state.copyWith(
|
|
widgets: widgets,
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
saveLayout();
|
|
}
|
|
|
|
void updateTheme(ProfileTheme theme) {
|
|
state = state.copyWith(
|
|
theme: theme,
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
saveLayout();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📱 User Interface
|
|
|
|
### Widget Catalog
|
|
- **Visual Preview**: Thumbnail previews of each widget type
|
|
- **Drag & Drop**: Drag widgets from catalog to grid
|
|
- **Search**: Filter widget types by name or category
|
|
- **Categories**: Organized by function (social, content, media, etc.)
|
|
|
|
### Editing Interface
|
|
- **Live Preview**: See changes in real-time
|
|
- **Undo/Redo**: Revert changes if needed
|
|
- **Save Status**: Visual indication of save state
|
|
- **Help Tooltips**: Contextual help for each widget
|
|
|
|
### Theme Customization
|
|
- **Color Picker**: Visual color selection with hex input
|
|
- **Font Selector**: Preview fonts before applying
|
|
- **Banner Upload**: Drag and drop banner images
|
|
- **Reset Options**: Reset to default theme or layout
|
|
|
|
---
|
|
|
|
## 🔒 Security & Privacy
|
|
|
|
### Content Validation
|
|
- **XSS Prevention**: Sanitize all user-generated content
|
|
- **HTML Filtering**: Remove dangerous HTML tags and attributes
|
|
- **Link Validation**: Verify and sanitize external links
|
|
- **Image Upload**: Scan uploads for malicious content
|
|
|
|
### Privacy Controls
|
|
- **Widget Privacy**: Individual widget visibility settings
|
|
- **Data Minimization**: Only store necessary widget data
|
|
- **User Consent**: Clear consent for data collection
|
|
- **Access Control**: Proper authorization for profile access
|
|
|
|
### Content Moderation
|
|
- **Widget Content**: Moderate custom text and images
|
|
- **Link Safety**: Check external links for safety
|
|
- **User Reporting**: Report inappropriate profile content
|
|
- **Automated Filtering**: AI-powered content analysis
|
|
|
|
---
|
|
|
|
## 📊 Analytics & Metrics
|
|
|
|
### Widget Usage
|
|
- **Popular Widgets**: Track most used widget types
|
|
- **Customization Trends**: Analyze theme preferences
|
|
- **Engagement Metrics**: Measure widget interaction rates
|
|
- **User Behavior**: Track profile editing patterns
|
|
|
|
### Performance Metrics
|
|
- **Load Times**: Profile page load performance
|
|
- **Widget Rendering**: Individual widget render times
|
|
- **Cache Hit Rates**: Widget cache effectiveness
|
|
- **Error Rates**: Widget failure rates and types
|
|
|
|
---
|
|
|
|
## 🚀 Deployment
|
|
|
|
### Environment Configuration
|
|
```bash
|
|
# Widget system settings
|
|
PROFILE_WIDGETS_ENABLED=true
|
|
PROFILE_WIDGETS_MAX_PER_USER=10
|
|
PROFILE_WIDGETS_CACHE_TTL=300
|
|
|
|
# Asset storage
|
|
WIDGET_ASSETS_BUCKET=sojorn-widget-assets
|
|
WIDGET_ASSETS_MAX_SIZE=10485760 # 10MB
|
|
|
|
# Theme settings
|
|
PROFILE_THEMES_ENABLED=true
|
|
PROFILE_CUSTOM_THEMES=true
|
|
PROFILE_BANNER_MAX_SIZE=2097152 # 2MB
|
|
```
|
|
|
|
### Health Checks
|
|
```go
|
|
func (s *ProfileService) HealthCheck() HealthStatus {
|
|
// Check widget cache
|
|
if s.widgetCache == nil {
|
|
return HealthStatus{
|
|
Status: "degraded",
|
|
Message: "Widget cache not available",
|
|
}
|
|
}
|
|
|
|
// Check asset storage
|
|
if _, err := s.assetService.HealthCheck(); err != nil {
|
|
return HealthStatus{
|
|
Status: "degraded",
|
|
Message: "Asset storage not accessible",
|
|
}
|
|
}
|
|
|
|
return HealthStatus{
|
|
Status: "healthy",
|
|
Message: "Profile widgets system ready",
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### Widget Not Rendering
|
|
```dart
|
|
// Check widget configuration
|
|
if (widget.config.isEmpty) {
|
|
return ErrorWidget(
|
|
message: "Widget configuration is missing",
|
|
action: "Reset to default",
|
|
);
|
|
}
|
|
|
|
// Check theme compatibility
|
|
if (!theme.isCompatible(widget.type)) {
|
|
return ErrorWidget(
|
|
message: "Theme not compatible with widget",
|
|
action: "Use default theme",
|
|
);
|
|
}
|
|
```
|
|
|
|
#### Layout Not Saving
|
|
```dart
|
|
try {
|
|
await profileService.saveLayout(layout);
|
|
} catch (e) {
|
|
// Show user-friendly error
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Failed to save layout: $e')),
|
|
);
|
|
|
|
// Log detailed error
|
|
logger.error('Layout save failed', error: e);
|
|
}
|
|
```
|
|
|
|
#### Performance Issues
|
|
```dart
|
|
// Implement lazy loading for widgets
|
|
class LazyWidget extends StatelessWidget {
|
|
final WidgetType type;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder<Widget>(
|
|
future: _loadWidget(type),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasData) {
|
|
return snapshot.data!;
|
|
}
|
|
return CircularProgressIndicator();
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Future Enhancements
|
|
|
|
### Version 3.1 (Planned)
|
|
- **Widget Templates**: Pre-designed widget combinations
|
|
- **Collaborative Profiles**: Multiple users editing same profile
|
|
- **Advanced Analytics**: Detailed widget performance metrics
|
|
- **Widget Marketplace**: Community-created widgets
|
|
|
|
### Version 4.0 (Long-term)
|
|
- **AI Widget Suggestions**: AI-powered widget recommendations
|
|
- **Interactive Widgets**: Widgets with real-time data
|
|
- **3D Widgets**: 3D visualization widgets
|
|
- **Voice Commands**: Voice-controlled profile editing
|
|
|
|
---
|
|
|
|
## 📞 Support & Documentation
|
|
|
|
### User Guides
|
|
- **Getting Started**: Quick start guide for profile customization
|
|
- **Widget Catalog**: Complete widget reference and examples
|
|
- **Theme Guide**: Theme customization and design principles
|
|
- **Troubleshooting**: Common issues and solutions
|
|
|
|
### Developer Resources
|
|
- **Widget Development**: Guide to creating custom widgets
|
|
- **API Documentation**: Complete API reference
|
|
- **Design System**: UI/UX guidelines and components
|
|
- **Code Examples**: Sample implementations and patterns
|
|
|
|
---
|
|
|
|
**🎨 The Profile Widgets system provides users with powerful yet constrained customization options, allowing for personal expression while maintaining platform consistency and performance.**
|