Skip to content

Commit abecb50

Browse files
committed
refactor: replace livewire search with alpine component
Livewire has too many issues that are never fixed. Examples include: - Trying to initialize the component before validating the arguments are even correct, which causes error 500s when a user enters letters into a numeric field - Not being able to handle clicking tickboxes quicker than responses are returned. - Requires unsafe inline JS enabled for the CSP. This is a proof of concept of how one might use alpine, alpine morph and blade fragments to replace livewire.
1 parent 074892c commit abecb50

File tree

9 files changed

+336
-253
lines changed

9 files changed

+336
-253
lines changed

app/Http/Controllers/Staff/ApplicationController.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Illuminate\Support\Facades\Mail;
2929
use Ramsey\Uuid\Uuid;
3030
use Exception;
31+
use Illuminate\Http\Request;
3132

3233
/**
3334
* @see \Tests\Todo\Feature\Http\Controllers\Staff\ApplicationControllerTest
@@ -37,9 +38,22 @@ class ApplicationController extends Controller
3738
/**
3839
* Display All Applications.
3940
*/
40-
public function index(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
41+
public function index(Request $request): \Illuminate\Contracts\View\Factory|\Illuminate\View\View|string
4142
{
42-
return view('Staff.application.index');
43+
return view('Staff.application.index', [
44+
'applications' => Application::query()
45+
->withoutGlobalScopes()
46+
->with([
47+
'user.group',
48+
'moderated.group',
49+
'imageProofs',
50+
'urlProofs'
51+
])
52+
->when($request->filled('email'), fn ($query) => $query->where('email', 'LIKE', '%'.$request->email.'%'))
53+
->when($request->filled('status'), fn ($query) => $query->where('status', '=', $request->status))
54+
->orderBy($request->sortField ?? 'created_at', $request->sortDirection ?? 'desc')
55+
->paginate($request->integer('perPage', 25)),
56+
])->fragmentIf($request->hasHeader('UNIT3D-Request'), 'application-search');
4357
}
4458

4559
/**
@@ -52,6 +66,21 @@ public function show(int $id): \Illuminate\Contracts\View\Factory|\Illuminate\Vi
5266
]);
5367
}
5468

69+
/**
70+
* Delete an application.
71+
*/
72+
public function destroy(Request $request, int $id): void
73+
{
74+
abort_unless($request->user()->group->is_modo, 403);
75+
76+
$application = Application::withoutGlobalScopes()->findOrFail($id);
77+
$application->urlProofs()->delete();
78+
$application->imageProofs()->delete();
79+
$application->delete();
80+
81+
$this->dispatch('success', type: 'success', message: 'Application has successfully been deleted!');
82+
}
83+
5584
/**
5685
* Approve A Application.
5786
*

app/Http/Livewire/ApplicationSearch.php

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

bun.lockb

348 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"build": "vite build"
66
},
77
"dependencies": {
8+
"@alpinejs/morph": "^3.15.0",
89
"ajv": "^8.17.1",
910
"alpinejs": "^3.14.8",
1011
"axios": "^1.7.9",

resources/js/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ import.meta.glob(['/public/img/pipes/**', '/resources/sass/vendor/webfonts/font-
3737

3838
// Livewire + AlpineJS
3939
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm.js';
40+
import morph from '@alpinejs/morph';
41+
42+
Alpine.plugin(morph);
4043

4144
// Custom AlpineJS Components
4245
import './components/alpine/chatbox';
@@ -47,6 +50,7 @@ import './components/alpine/dislikeButton';
4750
import './components/alpine/likeButton';
4851
import './components/alpine/livewireDialog';
4952
import './components/alpine/posterRow';
53+
import './components/alpine/searchPanel';
5054
import './components/alpine/smallBookmarkButton';
5155
import './components/alpine/tabs';
5256
import './components/alpine/toggle';
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
document.addEventListener('alpine:init', () => {
2+
Alpine.data('searchPanel', (defaultSortField = null, defaultSortDirection = 'desc') => ({
3+
sortField: defaultSortField,
4+
sortDirection: defaultSortDirection,
5+
page: 1,
6+
init() {
7+
this.$root.querySelectorAll('.data-table th:has([data-sort-by])').forEach((th) => {
8+
Alpine.bind(th, {
9+
'x-data'() {
10+
return {
11+
column: th.dataset.sortBy,
12+
};
13+
},
14+
'x-on:click'() {
15+
if (this.sortField === this.column && this.sortDirection === 'asc') {
16+
this.sortDirection = 'desc';
17+
} else {
18+
this.sortDirection = 'asc';
19+
}
20+
21+
this.sortField = this.column;
22+
23+
this.updatePanel();
24+
},
25+
'x-bind:role'() {
26+
return 'columnheader button';
27+
},
28+
});
29+
});
30+
Alpine.bind(this.$root.querySelector('.panel__actions'), {
31+
'x-on:input'(event) {
32+
this.updatePanel();
33+
},
34+
});
35+
this.$root
36+
.querySelectorAll('.pagination__link, .pagination__previous, .pagination__next')
37+
.forEach((link) => {
38+
Alpine.bind(link, {
39+
'x-on:click.prevent'() {
40+
this.page = new URL(this.$el.href).searchParams.get('page');
41+
42+
this.updatePanel();
43+
},
44+
});
45+
});
46+
},
47+
getFilters() {
48+
let form = this.$root.querySelector('.panel__actions');
49+
const formData = new FormData(form);
50+
const filters = new URLSearchParams(formData);
51+
52+
form.querySelectorAll('[name]').forEach((el) => {
53+
let defaultValue = null;
54+
55+
switch (true) {
56+
case el instanceof HTMLInputElement:
57+
case el instanceof HTMLTextAreaElement:
58+
defaultValue = el.defaultValue;
59+
break;
60+
case el instanceof HTMLSelectElement:
61+
defaultValue = Array.from(el.options).find(
62+
(option) => option.defaultSelected,
63+
)?.value;
64+
break;
65+
}
66+
67+
if (filters.get(el.name) === (defaultValue ?? '')) {
68+
filters.delete(el.name);
69+
}
70+
});
71+
72+
return filters;
73+
},
74+
updatePanel() {
75+
let params = this.getFilters();
76+
77+
if (this.sortField !== null && this.sortField !== defaultSortField) {
78+
params.append('sortField', this.sortField);
79+
}
80+
81+
if (this.sortField !== null && this.sortDirection !== defaultSortDirection) {
82+
params.append('sortDirection', this.sortDirection);
83+
}
84+
85+
if (this.page !== 1) {
86+
params.append('page', this.page);
87+
}
88+
89+
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?${params}`;
90+
window.history.pushState({ path: newUrl }, '', newUrl);
91+
92+
axios
93+
.get(newUrl, {
94+
headers: {
95+
'UNIT3D-Request': true,
96+
},
97+
})
98+
.then((newHtml) => Alpine.morph(this.$root, newHtml.data));
99+
},
100+
}));
101+
});

0 commit comments

Comments
 (0)