fetchChangelogData(); if (! $data || ! isset($data['entries'])) { return collect(); } return collect($data['entries']) ->filter(fn ($entry) => $this->validateEntryData($entry)) ->map(function ($entry) { $entry['published_at'] = Carbon::parse($entry['published_at']); $entry['content_html'] = $this->parseMarkdown($entry['content']); return (object) $entry; }) ->filter(fn ($entry) => $entry->published_at <= now()) ->sortBy('published_at') ->reverse() ->values(); } // Load entries from recent months for performance $availableMonths = $this->getAvailableMonths(); $monthsToLoad = $availableMonths->take($recentMonths); return $monthsToLoad ->flatMap(fn ($month) => $this->getEntriesForMonth($month)) ->sortBy('published_at') ->reverse() ->values(); } public function getAllEntries(): Collection { $availableMonths = $this->getAvailableMonths(); return $availableMonths ->flatMap(fn ($month) => $this->getEntriesForMonth($month)) ->sortBy('published_at') ->reverse() ->values(); } public function getEntriesForUser(User $user): Collection { $entries = $this->getEntries(); $readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id); return $entries->map(function ($entry) use ($readIdentifiers) { $entry->is_read = in_array($entry->tag_name, $readIdentifiers); return $entry; })->sortBy([ ['is_read', 'asc'], // unread first ['published_at', 'desc'], // then by date ])->values(); } public function getUnreadCountForUser(User $user): int { if (isDev()) { $entries = $this->getEntries(); $readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id); return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count(); } else { return Cache::remember( 'user_unread_changelog_count_'.$user->id, now()->addHour(), function () use ($user) { $entries = $this->getEntries(); $readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id); return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count(); } ); } } public function getAvailableMonths(): Collection { $pattern = base_path('changelogs/*.json'); $files = glob($pattern); if ($files === false) { return collect(); } return collect($files) ->map(fn ($file) => basename($file, '.json')) ->filter(fn ($name) => preg_match('/^\d{4}-\d{2}$/', $name)) ->sort() ->reverse() ->values(); } public function getEntriesForMonth(string $month): Collection { $path = base_path("changelogs/{$month}.json"); if (! file_exists($path)) { return collect(); } $content = file_get_contents($path); if ($content === false) { Log::error("Failed to read changelog file: {$month}.json"); return collect(); } $data = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { Log::error("Invalid JSON in {$month}.json: ".json_last_error_msg()); return collect(); } if (! isset($data['entries']) || ! is_array($data['entries'])) { return collect(); } return collect($data['entries']) ->filter(fn ($entry) => $this->validateEntryData($entry)) ->map(function ($entry) { $entry['published_at'] = Carbon::parse($entry['published_at']); $entry['content_html'] = $this->parseMarkdown($entry['content']); return (object) $entry; }) ->filter(fn ($entry) => $entry->published_at <= now()) ->sortBy('published_at') ->reverse() ->values(); } private function fetchChangelogData(): ?array { // Legacy support for old changelog.json $path = base_path('changelog.json'); if (file_exists($path)) { $content = file_get_contents($path); if ($content === false) { Log::error('Failed to read changelog.json file'); return null; } $data = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { Log::error('Invalid JSON in changelog.json: '.json_last_error_msg()); return null; } return $data; } // New monthly structure - combine all months $allEntries = []; foreach ($this->getAvailableMonths() as $month) { $monthEntries = $this->getEntriesForMonth($month); foreach ($monthEntries as $entry) { $allEntries[] = (array) $entry; } } return ['entries' => $allEntries]; } public function markAsReadForUser(string $version, User $user): void { UserChangelogRead::markAsRead($user->id, $version); Cache::forget('user_unread_changelog_count_'.$user->id); } public function markAllAsReadForUser(User $user): void { $entries = $this->getEntries(); foreach ($entries as $entry) { UserChangelogRead::markAsRead($user->id, $entry->tag_name); } Cache::forget('user_unread_changelog_count_'.$user->id); } private function validateEntryData(array $data): bool { $required = ['tag_name', 'title', 'content', 'published_at']; foreach ($required as $field) { if (! isset($data[$field]) || empty($data[$field])) { return false; } } return true; } public function clearAllReadStatus(): array { try { $count = UserChangelogRead::count(); UserChangelogRead::truncate(); // Clear all user caches $this->clearAllUserCaches(); return [ 'success' => true, 'message' => "Successfully cleared {$count} read status records", ]; } catch (\Exception $e) { Log::error('Failed to clear read status: '.$e->getMessage()); return [ 'success' => false, 'message' => 'Failed to clear read status: '.$e->getMessage(), ]; } } private function clearAllUserCaches(): void { $users = User::select('id')->get(); foreach ($users as $user) { Cache::forget('user_unread_changelog_count_'.$user->id); } } private function parseMarkdown(string $content): string { $renderer = app(MarkdownRenderer::class); $html = $renderer->toHtml($content); // Apply custom Tailwind CSS classes for dark mode compatibility $html = $this->applyCustomStyling($html); return $html; } private function applyCustomStyling(string $html): string { // Headers $html = preg_replace('/]*>/', '

', $html); $html = preg_replace('/]*>/', '

', $html); $html = preg_replace('/]*>/', '

', $html); // Paragraphs $html = preg_replace('/]*>/', '

', $html); // Lists $html = preg_replace('/]*>/', '

    ', $html); $html = preg_replace('/]*>/', '
      ', $html); $html = preg_replace('/]*>/', '
    1. ', $html); // Code blocks and inline code $html = preg_replace('/]*>/', '
      ', $html);
              $html = preg_replace('/]*>/', '', $html);
      
              // Links - Apply styling to existing markdown links
              $html = preg_replace('/]*)>/', '', $html);
      
              // Convert plain URLs to clickable links (that aren't already in  tags)
              $html = preg_replace('/(?)(?"]+)(?![^<]*<\/a>)/', '$1', $html);
      
              // Strong/bold text
              $html = preg_replace('/]*>/', '', $html);
      
              // Emphasis/italic text
              $html = preg_replace('/]*>/', '', $html);
      
              return $html;
          }
      }