, * featured_modules: array, * featured_content: array, * draft_behavior: string, * default_landing_page: string, * widget_visibility: array, * widget_order: array, * card_density: string, * scheduling_timezone: string|null, * activity_last_read_at: string|null * } */ public function forUser(User $user): array { $record = DashboardPreference::query()->find($user->id); $stored = is_array($record?->studio_preferences) ? $record->studio_preferences : []; return [ 'default_content_view' => $this->normalizeView((string) ($stored['default_content_view'] ?? 'grid')), 'analytics_range_days' => $this->normalizeRange((int) ($stored['analytics_range_days'] ?? 30)), 'dashboard_shortcuts' => DashboardPreference::pinnedSpacesForUser($user), 'featured_modules' => $this->normalizeModules($stored['featured_modules'] ?? []), 'featured_content' => $this->normalizeFeaturedContent($stored['featured_content'] ?? []), 'draft_behavior' => $this->normalizeDraftBehavior((string) ($stored['draft_behavior'] ?? 'resume-last')), 'default_landing_page' => $this->normalizeLandingPage((string) ($stored['default_landing_page'] ?? 'overview')), 'widget_visibility' => $this->normalizeWidgetVisibility($stored['widget_visibility'] ?? []), 'widget_order' => $this->normalizeWidgetOrder($stored['widget_order'] ?? []), 'card_density' => $this->normalizeDensity((string) ($stored['card_density'] ?? 'comfortable')), 'scheduling_timezone' => $this->normalizeTimezone($stored['scheduling_timezone'] ?? null), 'activity_last_read_at' => $this->normalizeActivityLastReadAt($stored['activity_last_read_at'] ?? null), ]; } /** * @param array $attributes * @return array{ * default_content_view: string, * analytics_range_days: int, * dashboard_shortcuts: array, * featured_modules: array, * featured_content: array, * draft_behavior: string, * default_landing_page: string, * widget_visibility: array, * widget_order: array, * card_density: string, * scheduling_timezone: string|null, * activity_last_read_at: string|null * } */ public function update(User $user, array $attributes): array { $current = $this->forUser($user); $payload = [ 'default_content_view' => array_key_exists('default_content_view', $attributes) ? $this->normalizeView((string) $attributes['default_content_view']) : $current['default_content_view'], 'analytics_range_days' => array_key_exists('analytics_range_days', $attributes) ? $this->normalizeRange((int) $attributes['analytics_range_days']) : $current['analytics_range_days'], 'featured_modules' => array_key_exists('featured_modules', $attributes) ? $this->normalizeModules($attributes['featured_modules']) : $current['featured_modules'], 'featured_content' => array_key_exists('featured_content', $attributes) ? $this->normalizeFeaturedContent($attributes['featured_content']) : $current['featured_content'], 'draft_behavior' => array_key_exists('draft_behavior', $attributes) ? $this->normalizeDraftBehavior((string) $attributes['draft_behavior']) : $current['draft_behavior'], 'default_landing_page' => array_key_exists('default_landing_page', $attributes) ? $this->normalizeLandingPage((string) $attributes['default_landing_page']) : $current['default_landing_page'], 'widget_visibility' => array_key_exists('widget_visibility', $attributes) ? $this->normalizeWidgetVisibility($attributes['widget_visibility']) : $current['widget_visibility'], 'widget_order' => array_key_exists('widget_order', $attributes) ? $this->normalizeWidgetOrder($attributes['widget_order']) : $current['widget_order'], 'card_density' => array_key_exists('card_density', $attributes) ? $this->normalizeDensity((string) $attributes['card_density']) : $current['card_density'], 'scheduling_timezone' => array_key_exists('scheduling_timezone', $attributes) ? $this->normalizeTimezone($attributes['scheduling_timezone']) : $current['scheduling_timezone'], 'activity_last_read_at' => array_key_exists('activity_last_read_at', $attributes) ? $this->normalizeActivityLastReadAt($attributes['activity_last_read_at']) : $current['activity_last_read_at'], ]; $record = DashboardPreference::query()->firstOrNew(['user_id' => $user->id]); $record->pinned_spaces = array_key_exists('dashboard_shortcuts', $attributes) ? DashboardPreference::sanitizePinnedSpaces(is_array($attributes['dashboard_shortcuts']) ? $attributes['dashboard_shortcuts'] : []) : $current['dashboard_shortcuts']; $record->studio_preferences = $payload; $record->save(); return $this->forUser($user); } private function normalizeView(string $view): string { return in_array($view, ['grid', 'list'], true) ? $view : 'grid'; } private function normalizeRange(int $days): int { return in_array($days, [7, 14, 30, 60, 90], true) ? $days : 30; } /** * @param mixed $modules * @return array */ private function normalizeModules(mixed $modules): array { $allowed = ['artworks', 'cards', 'collections', 'stories']; return collect(is_array($modules) ? $modules : []) ->map(fn ($module): string => (string) $module) ->filter(fn (string $module): bool => in_array($module, $allowed, true)) ->unique() ->values() ->all(); } /** * @param mixed $featuredContent * @return array */ private function normalizeFeaturedContent(mixed $featuredContent): array { $allowed = ['artworks', 'cards', 'collections', 'stories']; return collect(is_array($featuredContent) ? $featuredContent : []) ->mapWithKeys(function ($id, $module) use ($allowed): array { $moduleKey = (string) $module; $normalizedId = (int) $id; if (! in_array($moduleKey, $allowed, true) || $normalizedId < 1) { return []; } return [$moduleKey => $normalizedId]; }) ->all(); } private function normalizeDraftBehavior(string $value): string { return in_array($value, ['resume-last', 'open-drafts', 'focus-published'], true) ? $value : 'resume-last'; } private function normalizeLandingPage(string $value): string { return in_array($value, ['overview', 'content', 'drafts', 'scheduled', 'analytics', 'activity', 'calendar', 'inbox', 'search', 'growth', 'challenges', 'preferences'], true) ? $value : 'overview'; } private function normalizeDensity(string $value): string { return in_array($value, ['compact', 'comfortable'], true) ? $value : 'comfortable'; } private function normalizeTimezone(mixed $value): ?string { if (! is_string($value)) { return null; } $value = trim($value); return $value !== '' ? $value : null; } private function normalizeActivityLastReadAt(mixed $value): ?string { if (! is_string($value)) { return null; } $value = trim($value); return $value !== '' ? $value : null; } /** * @param mixed $value * @return array */ private function normalizeWidgetVisibility(mixed $value): array { $defaults = collect($this->allowedWidgets())->mapWithKeys(fn (string $widget): array => [$widget => true]); return $defaults->merge( collect(is_array($value) ? $value : []) ->filter(fn ($enabled, $widget): bool => in_array((string) $widget, $this->allowedWidgets(), true)) ->map(fn ($enabled): bool => (bool) $enabled) )->all(); } /** * @param mixed $value * @return array */ private function normalizeWidgetOrder(mixed $value): array { $requested = collect(is_array($value) ? $value : []) ->map(fn ($widget): string => (string) $widget) ->filter(fn (string $widget): bool => in_array($widget, $this->allowedWidgets(), true)) ->unique() ->values(); return $requested ->concat(collect($this->allowedWidgets())->reject(fn (string $widget): bool => $requested->contains($widget))) ->values() ->all(); } /** * @return array */ private function allowedWidgets(): array { return [ 'quick_stats', 'continue_working', 'scheduled_items', 'recent_activity', 'top_performers', 'draft_reminders', 'module_summaries', 'growth_hints', 'active_challenges', 'creator_health', 'featured_status', 'comments_snapshot', 'stale_drafts', ]; } }