Skip to content

Commit 8743d2e

Browse files
committed
Show latest news in dashboard
1 parent 6280995 commit 8743d2e

File tree

5 files changed

+133
-0
lines changed

5 files changed

+133
-0
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
},
2121
"require": {
2222
"php": "^8.2",
23+
"ext-simplexml": "*",
2324
"doctrine/dbal": "^3.6",
2425
"filament/filament": "^3.2.57",
2526
"filament/spatie-laravel-settings-plugin": "^3.2",
2627
"guzzlehttp/guzzle": "^7.8",
28+
"illuminate/cache": "^11.23.0",
2729
"illuminate/console": "^11.23.0",
2830
"illuminate/database": "^11.23.0",
2931
"illuminate/events": "^11.23.0",

config/cachet.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,9 @@
160160
*/
161161
'demo_mode' => env('CACHET_DEMO_MODE', false),
162162

163+
'feed' => [
164+
'uri' => env('CACHET_FEED_URI', 'https://blog.cachethq.io/rss'),
165+
'cache' => env('CACHET_FEED_CACHE', 3600),
166+
],
167+
163168
];

resources/lang/en/cachet.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
'keep_up_to_date' => 'Keep up to date with the latest news and releases by following the *Cachet blog*.',
88
'work_in_progress_text' => 'Cachet is under active development. Things are still subject to change.',
99
],
10+
'feed' => [
11+
'section_heading' => 'Latest Blog Posts',
12+
'empty' => 'No blog posts were found. Check *the blog* for further information.',
13+
'posted_at' => 'Posted :date'
14+
],
1015
'powered_by' => 'Powered by',
1116
'open_source_status_page' => 'The open-source status page.',
1217
'all_times_shown_in' => 'All times are shown in *:timezone*.',
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<x-filament::widget>
2+
<x-filament::section :heading="__('cachet::cachet.feed.section_heading')">
3+
<div class="relative">
4+
<ul role="list" class="gap-4 flex flex-col">
5+
@forelse ($items as $post)
6+
<li>
7+
<a class="flex items-center justify-between text-sm" href="{{ $post['link'] }}" target="_blank">
8+
<div class="overflow-hidden text-sm leading-6 text-gray-500 dark:text-gray-400">
9+
<h3 class="text-base font-medium text-gray-950 dark:text-white">{{ $post['title'] }}</h3>
10+
<time class="text-muted text-xs" datetime="{{ $post['date']->toW3cString() }}" title="{{ $post['date']->toDateTimeString() }}">
11+
{{ __('cachet::cachet.feed.posted_at', ['date' => $post['date']->diffForHumans()]) }}
12+
</time>
13+
<p class="break-words truncate">{{ $post['description'] }}</p>
14+
</div>
15+
<div class="">
16+
<x-heroicon-o-chevron-right class="w-5 h-5 text-gray-400" />
17+
</div>
18+
</a>
19+
</li>
20+
@empty
21+
<li class="text-center filament-tables-text-column">
22+
<p class="text-sm text-gray-500">{!! $noItems !!}</p>
23+
</li>
24+
@endforelse
25+
</ul>
26+
</div>
27+
</x-filament::section>
28+
</x-filament::widget>
29+

src/Filament/Widgets/Feed.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace Cachet\Filament\Widgets;
4+
5+
use Filament\Widgets\Concerns\CanPoll;
6+
use Filament\Widgets\Widget;
7+
use Illuminate\Support\Carbon;
8+
use Illuminate\Support\Facades\Blade;
9+
use Illuminate\Support\Facades\Cache;
10+
use Illuminate\Support\Str;
11+
use Illuminate\Support\Uri;
12+
use Throwable;
13+
14+
class Feed extends Widget
15+
{
16+
use CanPoll;
17+
18+
protected int|string|array $columnSpan = 'full';
19+
20+
protected static string $view = 'cachet::filament.widgets.feed';
21+
22+
protected static ?int $sort = 10;
23+
24+
protected function getViewData(): array
25+
{
26+
return [
27+
'items' => $this->getFeed(),
28+
'noItems' => Blade::render($this->getEmptyBlock()),
29+
];
30+
}
31+
32+
/**
33+
* Get the generated empty block text.
34+
*/
35+
public function getEmptyBlock(): string
36+
{
37+
return preg_replace(
38+
'/\*(.*?)\*/',
39+
'<x-filament::link href="'.config('cachet.feed.uri').'" target="_blank" rel="nofollow noopener">$1</x-filament::link>',
40+
__('cachet::cachet.feed.empty')
41+
);
42+
}
43+
44+
/**
45+
* Get the feed from the cache or fetch it fresh.
46+
*/
47+
protected function getFeed(): array
48+
{
49+
return Cache::flexible('cachet-feed', [
50+
60 * 15,
51+
60 * 60,
52+
], fn () => $this->fetchFeed(
53+
config('cachet.feed.uri')
54+
));
55+
}
56+
57+
/**
58+
* Fetch the data from the given RSS feed.
59+
*/
60+
protected function fetchFeed(string $uri, int $maxPosts = 5): array
61+
{
62+
try {
63+
$xml = simplexml_load_string(file_get_contents($uri));
64+
65+
$posts = [];
66+
67+
$feedItems = $xml->channel->item ?? $xml->entry ?? [];
68+
$feedIndex = 0;
69+
70+
foreach ($feedItems as $item) {
71+
if ($feedIndex >= $maxPosts) {
72+
break;
73+
}
74+
75+
$posts[] = [
76+
'title' => (string)($item->title ?? ''),
77+
'link' => Uri::of((string)($item->link ?? ''))->withQuery([
78+
'ref' => 'cachet-dashboard',
79+
]),
80+
'description' => Str::of($item->description ?? $item->summary ?? '')->limit(preserveWords: true),
81+
'date' => Carbon::parse((string)($item->pubDate ?? $item->updated ?? '')),
82+
];
83+
84+
$feedIndex++;
85+
}
86+
87+
return $posts;
88+
} catch (Throwable $e) {
89+
return [];
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)