Skip to content

Commit 5c46753

Browse files
authored
Merge pull request #20 from laravelcm/feature-forum
Feature forum
2 parents cb8df73 + a699da9 commit 5c46753

File tree

14 files changed

+365
-151
lines changed

14 files changed

+365
-151
lines changed

app/Filters/AbstractFilter.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace App\Filters;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
use Illuminate\Support\Arr;
7+
8+
abstract class AbstractFilter
9+
{
10+
abstract public function filter(Builder $builder, $value): Builder;
11+
12+
public function mappings(): array
13+
{
14+
return [];
15+
}
16+
17+
protected function resolveFilterValue($key)
18+
{
19+
return Arr::get($this->mappings(), $key);
20+
}
21+
}

app/Filters/AbstractFilters.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace App\Filters;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
use Illuminate\Http\Request;
7+
8+
abstract class AbstractFilters
9+
{
10+
protected Builder $builder;
11+
12+
protected array $filters = [];
13+
14+
public function __construct(public Request $request)
15+
{
16+
}
17+
18+
/**
19+
* Get all filters and make a new instance.
20+
*
21+
* @param Builder $builder
22+
* @return Builder
23+
*/
24+
public function filter(Builder $builder): Builder
25+
{
26+
foreach ($this->getFilters() as $filter => $value) {
27+
$this->resolverFilter($filter)->filter($builder, $value);
28+
}
29+
30+
return $builder;
31+
}
32+
33+
/**
34+
* Add Filters to current filter class.
35+
*
36+
* @param array $filters
37+
* @return $this
38+
*/
39+
public function add(array $filters): self
40+
{
41+
$this->filters = array_merge($this->filters, $filters);
42+
43+
return $this;
44+
}
45+
46+
/**
47+
* Get the Filter instance Class.
48+
*
49+
* @param $filter
50+
* @return mixed
51+
*/
52+
public function resolverFilter($filter)
53+
{
54+
return new $this->filters[$filter];
55+
}
56+
57+
/**
58+
* Fetch all relevant filters from the request.
59+
*
60+
* @return array
61+
*/
62+
public function getFilters(): array
63+
{
64+
return array_filter($this->request->only(array_keys($this->filters)));
65+
}
66+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Filters\Thread;
4+
5+
use App\Filters\AbstractFilter;
6+
use Illuminate\Database\Eloquent\Builder;
7+
8+
class SortByFilter extends AbstractFilter
9+
{
10+
public function mappings(): array
11+
{
12+
return [
13+
'recent' => 'recent',
14+
'resolved' => 'resolved',
15+
'unresolved' => 'unresolved',
16+
];
17+
}
18+
19+
public function filter(Builder $builder, $value): Builder
20+
{
21+
$value = $this->resolveFilterValue($value);
22+
23+
switch ($value) {
24+
case null:
25+
return $builder;
26+
case 'recent':
27+
return $builder->recent();
28+
case 'resolved':
29+
return $builder->resolved();
30+
case 'unresolved':
31+
return $builder->unresolved();
32+
}
33+
}
34+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Filters\Thread;
4+
5+
use App\Filters\AbstractFilters;
6+
7+
class ThreadFilters extends AbstractFilters
8+
{
9+
/**
10+
* Registered filters to operate upon.
11+
*
12+
* @var array
13+
*/
14+
protected array $filters = [
15+
'sortBy' => SortByFilter::class,
16+
];
17+
}

app/Http/Controllers/Forum/ThreadController.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Http\Controllers\Controller;
66
use App\Models\Channel;
77
use App\Models\Thread;
8+
use Illuminate\Http\Request;
89

910
class ThreadController extends Controller
1011
{
@@ -13,14 +14,24 @@ public function __construct()
1314
$this->middleware(['auth', 'verified'], ['only' => ['create']]);
1415
}
1516

16-
public function index()
17+
public function index(Request $request)
1718
{
18-
return view('forum.index', ['channel' => null]);
19+
$filter = getFilter('sortBy', ['recent', 'resolved', 'unresolved']);
20+
$threads = Thread::filter($request)->withviewscount()->paginate(10);
21+
22+
return view('forum.index', [
23+
'channel' => null,
24+
'threads' => $threads,
25+
'filter' => $filter,
26+
]);
1927
}
2028

21-
public function channel(Channel $channel)
29+
public function channel(Request $request, Channel $channel)
2230
{
23-
return view('forum.index', compact('channel'));
31+
$filter = getFilter('sortBy', ['recent', 'resolved', 'unresolved']);
32+
$threads = Thread::forChannel($channel)->filter($request)->withviewscount()->paginate(10);
33+
34+
return view('forum.index', compact('channel', 'threads', 'filter'));
2435
}
2536

2637
public function create()

app/Http/Livewire/Forum/Browse.php

Lines changed: 0 additions & 51 deletions
This file was deleted.

app/Models/Thread.php

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Contracts\ReplyInterface;
77
use App\Contracts\SubscribeInterface;
88
use App\Exceptions\CouldNotMarkReplyAsSolution;
9+
use App\Filters\Thread\ThreadFilters;
910
use App\Traits\HasAuthor;
1011
use App\Traits\HasReplies;
1112
use App\Traits\HasSlug;
@@ -16,9 +17,7 @@
1617
use CyrildeWit\EloquentViewable\Contracts\Viewable;
1718
use CyrildeWit\EloquentViewable\InteractsWithViews;
1819
use Exception;
19-
use Illuminate\Contracts\Pagination\Paginator;
2020
use Illuminate\Database\Eloquent\Builder;
21-
use Illuminate\Database\Eloquent\Collection;
2221
use Illuminate\Database\Eloquent\Factories\HasFactory;
2322
use Illuminate\Database\Eloquent\Model;
2423
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -171,26 +170,37 @@ public function unmarkSolution()
171170
$this->save();
172171
}
173172

174-
public function scopeResolved(Builder $query): Builder
173+
public function scopeForChannel(Builder $query, Channel $channel): Builder
175174
{
176-
return $query->whereNotNull('solution_reply_id');
175+
return $query->whereHas('channels', function ($query) use ($channel) {
176+
if ($channel->hasItems()) {
177+
$query->whereIn('channels.id', array_merge([$channel->id], $channel->items->modelKeys()));
178+
} else {
179+
$query->where('channels.slug', $channel->slug());
180+
}
181+
});
177182
}
178183

179-
public function scopeUnresolved(Builder $query): Builder
184+
public function scopeRecent(Builder $query): Builder
180185
{
181-
return $query->whereNull('solution_reply_id');
186+
return $query->feedQuery()->orderByDesc('last_posted_at');
182187
}
183188

184-
public function scopeForChannel(Builder $query, string $channel): Builder
189+
public function scopeResolved(Builder $query): Builder
185190
{
186-
return $query->whereHas('channels', function ($query) use ($channel) {
187-
$query->where('channels.slug', $channel);
188-
});
191+
return $query->feedQuery()
192+
->whereNotNull('solution_reply_id');
189193
}
190194

191-
public function scopeRecent(Builder $query): Builder
195+
public function scopeUnresolved(Builder $query): Builder
196+
{
197+
return $query->feedQuery()
198+
->whereNull('solution_reply_id');
199+
}
200+
201+
public function scopeFilter(Builder $builder, $request, array $filters = []): Builder
192202
{
193-
return self::feedQuery()->orderByDesc('created_at');
203+
return (new ThreadFilters($request))->add($filters)->filter($builder);
194204
}
195205

196206
public function delete()
@@ -214,37 +224,12 @@ public function toFeedItem(): FeedItem
214224
->authorName($this->user->name);
215225
}
216226

217-
public static function feed(int $limit = 20): Collection
218-
{
219-
return static::feedQuery()->limit($limit)->get();
220-
}
221-
222-
public static function feedPaginated(int $perPage = 20): Paginator
223-
{
224-
return static::feedQuery()->paginate($perPage);
225-
}
226-
227-
public static function feedByChannelPaginated(Channel $channel, int $perPage = 20): Paginator
228-
{
229-
return static::feedByChannelQuery($channel)
230-
->paginate($perPage);
231-
}
232-
233-
public static function feedByChannelQuery(Channel $channel): Builder
234-
{
235-
return static::feedQuery()
236-
->join('channel_thread', function ($join) {
237-
$join->on('threads.id', 'channel_thread.thread_id');
238-
})
239-
->where('channel_thread.channel_id', $channel->id);
240-
}
241-
242227
/**
243228
* This will order the threads by creation date and latest reply.
244229
*/
245-
public static function feedQuery(): Builder
230+
public function scopeFeedQuery(Builder $query): Builder
246231
{
247-
return static::with([
232+
return $query->with([
248233
'solutionReply',
249234
'replies',
250235
'reactions',
@@ -283,7 +268,7 @@ public static function resolutionTime()
283268

284269
public static function getFeedItems(): SupportCollection
285270
{
286-
return static::feedQuery()
271+
return static::with(['reactions'])->feedQuery()
287272
->paginate(static::FEED_PAGE_SIZE)
288273
->getCollection();
289274
}

app/helpers.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,12 @@ function canonical(string $route, array $params = []): string
7070
return route($route, $params);
7171
}
7272
}
73+
74+
if (! function_exists('getFilter')) {
75+
function getFilter(string $key, array $filters = [], string $default = 'recent'): string
76+
{
77+
$filter = (string) request($key);
78+
79+
return in_array($filter, $filters) ? $filter : $default;
80+
}
81+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<nav class="relative z-0 rounded-lg shadow flex divide-x divide-skin-base" aria-label="Tabs">
2+
<a
3+
href="{{ url(request()->url() . '?sortBy=recent') }}"
4+
aria-current="{{ $filter === 'recent' ? 'page' : 'false' }}"
5+
class="w-full {{ $filter === 'recent' ? 'text-skin-inverted': 'text-skin-base hover:text-skin-inverted' }} rounded-l-lg group relative min-w-0 flex-1 overflow-hidden bg-skin-card py-4 px-6 text-sm font-medium text-center hover:bg-skin-card-muted focus:z-10"
6+
>
7+
<span>Récent</span>
8+
<span aria-hidden="true" class="{{ $filter === 'recent' ? 'bg-skin-primary': 'bg-transparent' }} absolute inset-x-0 bottom-0 h-0.5"></span>
9+
</a>
10+
11+
<a
12+
href="{{ url(request()->url() . '?sortBy=resolved') }}"
13+
aria-current="{{ $filter === 'resolved' ? 'page' : 'false' }}"
14+
class="w-full {{ $filter === 'resolved' ? 'text-skin-inverted': 'text-skin-base hover:text-skin-inverted' }} group relative min-w-0 flex-1 overflow-hidden bg-skin-card py-4 px-6 text-sm font-medium text-center hover:bg-skin-card-muted focus:z-10"
15+
>
16+
<span>Résolu</span>
17+
<span aria-hidden="true" class="{{ $filter === 'resolved' ? 'bg-skin-primary': 'bg-transparent' }} absolute inset-x-0 bottom-0 h-0.5"></span>
18+
</a>
19+
20+
<a
21+
href="{{ url(request()->url() . '?sortBy=unresolved') }}"
22+
aria-current="{{ $filter === 'unresolved' ? 'page' : 'false' }}"
23+
class="w-full {{ $filter === 'unresolved' ? 'text-skin-inverted': 'text-skin-base hover:text-skin-inverted' }} rounded-r-lg group relative min-w-0 flex-1 overflow-hidden bg-skin-card py-4 px-6 text-sm font-medium text-center hover:bg-skin-card-muted focus:z-10"
24+
>
25+
<span>Non résolu</span>
26+
<span aria-hidden="true" class="{{ $filter === 'unresolved' ? 'bg-skin-primary': 'bg-transparent' }} absolute inset-x-0 bottom-0 h-0.5"></span>
27+
</a>
28+
</nav>

0 commit comments

Comments
 (0)