Skip to content

Commit b8e6e72

Browse files
committed
adds sl-helpers. improves validation and user feedback (closes #348)
1 parent e3ff283 commit b8e6e72

File tree

14 files changed

+365
-247
lines changed

14 files changed

+365
-247
lines changed

app/Providers/AppServiceProvider.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use App\Traits\MonitorJoinerApi;
1616
use Illuminate\Support\ServiceProvider;
1717
use Illuminate\Support\Facades\URL;
18+
use Illuminate\Support\Facades\Blade;
1819

1920
class AppServiceProvider extends ServiceProvider
2021
{
@@ -63,5 +64,39 @@ public function boot(): void
6364
if (config('app.env') === 'production') {
6465
URL::forceScheme('https');
6566
}
67+
68+
// Sets an attribute if the value is defined and removes the attribute if undefined.
69+
Blade::directive('wcSetAttribute', function ($arguments) {
70+
list($attribute, $condition) = explode(',', $arguments);
71+
$attribute = trim(str_replace(['"', "'"], '', $attribute));
72+
$condition = trim($condition);
73+
return "<?php echo {$condition} ? '{$attribute}' : '!{$attribute}' ?>";
74+
});
75+
76+
// Creates a model binding for sl-input
77+
Blade::directive('slInputModel', function ($arguments) {
78+
list($expression, $value) = explode(',', str_replace([' ', '"', "'"], '', $arguments));
79+
return "value=\"<?php echo {$value}; ?>\"
80+
x-on:sl-input=\"\$wire.\$set('{$expression}', \$event.target.value)\"
81+
x-on:sl-change=\"\$wire.\$set('{$expression}', \$event.target.value)\"";
82+
});
83+
84+
// Creates a model binding for sl-checkbox
85+
Blade::directive('slCheckboxModel', function ($arguments) {
86+
list($expression, $value) = explode(',', str_replace([' ', '"', "'"], '', $arguments));
87+
return "<?php echo {$value} ? 'checked' : '' ?> x-on:sl-change=\"\$wire.set('{$expression}', \$el.checked);\"";
88+
});
89+
90+
// Creates a model binding for sl-select including multiple select
91+
Blade::directive('slSelectModel', function ($arguments) {
92+
list($expression, $value) = explode(',', str_replace([' ', '"', "'"], '', $arguments));
93+
return "value=\"<?php echo is_array({$value}) ? implode(' ', {$value}) : {$value}; ?>\" x-on:sl-change=\"\$wire.set('{$expression}', \$el.value);\"";
94+
});
95+
96+
// Creates a model binding for sl-radio-group
97+
Blade::directive('slRadioGroupModel', function ($arguments) {
98+
list($expression, $value) = explode(',', str_replace([' ', '"', "'"], '', $arguments));
99+
return "value=\"<?php echo {$value}; ?>\" x-on:sl-change=\"\$wire.set('{$expression}', \$el.value);\"";
100+
});
66101
}
67102
}

app/Traits/MonitorJoiner.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ trait MonitorJoiner
1717
* @return Monitor
1818
* @throws Exception If the monitor is not found or already joined.
1919
*/
20-
public function joinMonitor(string $monitorInput): Monitor
20+
public function join_monitor(string $monitorInput): Monitor
2121
{
2222
$monitorHash = Str::contains($monitorInput, 'join/')
2323
? Str::after($monitorInput, 'join/')

resources/views/components/layouts/app.blade.php

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@
88

99
@vite(['resources/css/app.css', 'resources/js/app.js'])
1010

11+
<script>
12+
// Add Livewire hook to preserve Web Component attributes
13+
Livewire.hook('morph.updating', ({ el, component, toEl }) => {
14+
// Check if element is a custom element
15+
if (!el.tagName.includes('-')) {
16+
return;
17+
}
18+
19+
// Store the original attributes
20+
let oldAttributes = Array.from(el.attributes)
21+
.reduce((attrs, attr) => {
22+
attrs[attr.name] = attr.value;
23+
return attrs;
24+
}, {});
25+
26+
// Restore all attributes that might have been removed by Livewire
27+
let currentAttributes = Array.from(toEl.attributes).map(attr => attr.name);
28+
Object.entries(oldAttributes).forEach(([name, value]) => {
29+
if (!name.startsWith('!') && !currentAttributes.includes(name)) {
30+
toEl.setAttribute(name, value);
31+
}
32+
});
33+
34+
// Remove attributes starting with '!' from the toEl
35+
Array.from(toEl.attributes).forEach(attr => {
36+
if (attr.name.startsWith('!')) {
37+
toEl.removeAttribute(attr.name.substring(1)); // Remove the corresponding actual attribute
38+
toEl.removeAttribute(attr.name); // Remove the attribute with the '!' prefix
39+
}
40+
});
41+
});
42+
</script>
43+
1144
</head>
1245
<body class="relative min-h-screen font-sans antialiased bg-base-200/50 dark:bg-base-200">
1346
<!-- QR Dialog -->
@@ -23,6 +56,24 @@
2356
<sl-button slot="footer" class="w-full">Close</sl-button>
2457
</sl-dialog>
2558

59+
<!-- Global Error Alert -->
60+
<div class="fixed right-4 bottom-4 z-50">
61+
<sl-alert id="global-error-alert-component" variant="danger" duration="5000" countdown="rtl" closable>
62+
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
63+
<strong class="alert-component-head"></strong><br>
64+
<span class="alert-component-content"></span>
65+
</sl-alert>
66+
</div>
67+
68+
<!-- Global Success Alert -->
69+
<div class="fixed right-4 bottom-4 z-50">
70+
<sl-alert id="global-success-alert-component" variant="success" duration="5000" countdown="rtl" closable>
71+
<sl-icon slot="icon" name="check-circle"></sl-icon>
72+
<strong class="alert-component-head"></strong><br>
73+
<span class="alert-component-content"></span>
74+
</sl-alert>
75+
</div>
76+
2677
<script>
2778
document.addEventListener('DOMContentLoaded', () => {
2879
const dialog = document.querySelector('#global-qr-dialog');
@@ -52,6 +103,62 @@ function updateQRSize() {
52103
closeButton.addEventListener('click', () => {
53104
dialog.hide();
54105
});
106+
107+
const errorComponent = document.querySelector('#global-error-alert-component');
108+
const successComponent = document.querySelector('#global-success-alert-component');
109+
110+
function getAlertContent(data) {
111+
let head = 'Error';
112+
let message = 'An error occurred';
113+
114+
if (data.detail) {
115+
head = data.detail.head || head;
116+
message = data.detail.message || message;
117+
} else if (typeof data === 'object') {
118+
// Try other possible structures
119+
if (data.head) head = data.head;
120+
if (data.message) message = data.message;
121+
// Check if it's an array
122+
if (Array.isArray(data) && data.length > 0) {
123+
if (data[0].detail) {
124+
head = data[0].detail.head || head;
125+
message = data[0].detail.message || message;
126+
} else {
127+
if (data[0].head) head = data[0].head;
128+
if (data[0].message) message = data[0].message;
129+
}
130+
}
131+
}
132+
133+
return {
134+
head,
135+
message
136+
}
137+
}
138+
139+
Livewire.on('show-error-alert', (data) => {
140+
console.log('Error alert event received:', data);
141+
142+
const componentHead = errorComponent.querySelector('.alert-component-head');
143+
const componentContent = errorComponent.querySelector('.alert-component-content');
144+
145+
const { head, message } = getAlertContent(data);
146+
componentHead.textContent = head;
147+
componentContent.textContent = message;
148+
errorComponent.show();
149+
});
150+
151+
Livewire.on('show-success-alert', (data) => {
152+
console.log('Success alert event received:', data);
153+
154+
const componentHead = successComponent.querySelector('.alert-component-head');
155+
const componentContent = successComponent.querySelector('.alert-component-content');
156+
157+
const { head, message } = getAlertContent(data);
158+
componentHead.textContent = head;
159+
componentContent.textContent = message;
160+
successComponent.show();
161+
});
55162
});
56163
</script>
57164

@@ -111,6 +218,16 @@ function updateQRSize() {
111218
background: rgba(0, 0, 0, 0.5);
112219
visibility: visible;
113220
}
221+
222+
sl-alert::part(base) {
223+
width: auto;
224+
min-width: max-content;
225+
overflow-wrap: break-word;
226+
}
227+
228+
sl-alert::part(message) {
229+
overflow-wrap: break-word;
230+
}
114231
</style>
115232

116233
<header class="sticky top-0 z-50">
@@ -121,7 +238,7 @@ function updateQRSize() {
121238
@endif
122239
</header>
123240

124-
<main class="min-h-screen px-8 pb-4">
241+
<main class="px-8 pb-4 min-h-screen">
125242
@if(request()->path() !== '/' &&
126243
request()->path() !== 'create-monitor' &&
127244
request()->path() !== 'create-open-source-monitor' &&

resources/views/livewire/auth/create-monitor.blade.php

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
use MonitorCreator;
1515
1616
public $notifications = [];
17-
public $create_monitor_error;
18-
public $error_head;
1917
public $project_url;
2018
public $pat_token;
2119
public $disable_pat_token = true;
@@ -65,17 +63,25 @@ public function create()
6563
6664
return redirect()->to('/monitors/' . $project->id);
6765
} catch (ValidationException $e) {
68-
$this->create_monitor_error = "Invalid input: " . implode(", ", $e->validator->errors()->all());
69-
$this->error_head = "Validation Failed!";
66+
$message = implode(", ", $e->validator->errors()->all());
67+
logger()->error('Create Monitor Error', ['errors' => $message]);
68+
69+
$this->dispatch('show-error-alert', [
70+
'head' => 'Create Monitor Error',
71+
'message' => $message
72+
]);
73+
return null;
7074
} catch (Exception $e) {
71-
$this->create_monitor_error = $e->getMessage();
72-
$this->error_head = "Something went wrong...";
75+
$message = $e->getMessage();
76+
logger()->error('Create Monitor Error', ['message' => $message]);
77+
78+
$this->dispatch('show-error-alert', [
79+
'head' => 'Create Monitor Error',
80+
'message' => 'Something unexpected happened!'
81+
]);
82+
return null;
7383
}
74-
}
7584
76-
public function on_create()
77-
{
78-
$this->create();
7985
if ($this->monitor != null) {
8086
CreateMonitor::dispatch($this->monitor);
8187
$latestMonitorLog = Monitor::whereId($this->monitor->id)
@@ -97,7 +103,7 @@ public function pollLogs()
97103
->latest()
98104
->take(20)
99105
->get()
100-
->reverse() // Reverse collection order
106+
->reverse()
101107
->toArray();
102108
$this->dispatch('updateLogs');
103109
}
@@ -110,52 +116,38 @@ public function switchTo()
110116
};
111117
?>
112118

113-
<div class="flex flex-col items-center w-full h-full bg-gray-100">
119+
<div class="flex relative flex-col items-center w-full h-full bg-gray-100">
114120
<div class="flex flex-col justify-center mt-10">
115-
<div class="flex flex-wrap gap-2 justify-center mt-16">
116-
<div>
117-
<div class="flex gap-1 items-center mb-2 sm:mt-10">
118-
<div class="w-[30px] h-[30px] rounded-full bg-primary-blue"></div>
119-
<div class="w-2 h-1 bg-primary-blue"></div>
120-
<div class="w-[30px] h-[30px] bg-primary-blue rounded-full"></div>
121-
</div>
121+
<div class="flex gap-1 items-center mt-16 mb-2 sm:mt-10">
122+
<div class="w-[30px] h-[30px] rounded-full bg-primary-blue"></div>
123+
<div class="w-2 h-1 bg-primary-blue"></div>
124+
<div class="w-[30px] h-[30px] bg-primary-blue rounded-full"></div>
125+
</div>
122126

123-
<div class="px-10 pt-8 pb-8 mx-auto w-96 max-w-full bg-white rounded-lg border border-border-color">
124-
<h1 class="mb-8 text-6xl uppercase font-koulen text-primary-blue">Create Monitor</h1>
125-
126-
<form wire:submit.prevent="create" class="flex flex-col gap-2">
127-
<sl-input size="medium" required wire:model.defer="project_url" placeholder="Your Project URL" type="text"></sl-input>
128-
<sl-input size="medium" wire:model.defer="pat_token" placeholder="Your PAT Token (Optional)" type="text"></sl-input>
129-
<sl-switch class="pt-1 text-secondary-grey" size="medium" wire:click="switchTo()">Open Source</sl-switch>
130-
131-
<div class="flex justify-between items-end mt-5">
132-
<a class="text-sm no-underline text-primary-blue hover:underline"
133-
href="{{ url('join') }}">
134-
Already have a monitor?
135-
</a>
136-
137-
<sl-button size="medium" wire:click="on_create">Create</sl-button>
138-
</div>
139-
</form>
140-
141-
@if($create_monitor_error)
142-
<sl-alert variant="danger" open closable class="mt-4">
143-
<sl-icon wire:ignore slot="icon" name="patch-exclamation"></sl-icon>
144-
<strong>{{ $error_head }}</strong><br/>
145-
{{ $create_monitor_error }}
146-
</sl-alert>
147-
@endif
148-
</div>
149-
</div>
127+
<div class="px-10 pt-8 pb-8 mx-auto w-96 max-w-full bg-white rounded-lg border border-border-color">
128+
<h1 class="mb-8 text-6xl uppercase font-koulen text-primary-blue">Create Monitor</h1>
150129

130+
<form wire:submit="create" class="flex flex-col gap-2">
131+
<sl-input size="medium" required wire:model.defer="project_url" placeholder="Your Project URL" type="text"></sl-input>
132+
<sl-input size="medium" wire:model.defer="pat_token" placeholder="Your PAT Token (Optional)" type="text"></sl-input>
133+
<sl-switch class="pt-1 text-secondary-grey" size="medium" wire:click="switchTo()">Open Source</sl-switch>
151134

152-
</div>
135+
<div class="flex justify-between items-end mt-5">
136+
<a class="text-sm no-underline text-primary-blue hover:underline"
137+
href="{{ url('join') }}">
138+
Already have a monitor?
139+
</a>
153140

154-
<script>
155-
window.addEventListener('updateLogs', function () {
156-
const logContainer = document.getElementById('log-container');
157-
logContainer.scrollTop = logContainer.scrollHeight; // Auto-scroll to latest log
158-
});
159-
</script>
141+
<sl-button size="medium" wire:ignore type="submit">Create</sl-button>
142+
</div>
143+
</form>
144+
</div>
160145
</div>
146+
147+
<script>
148+
window.addEventListener('updateLogs', function () {
149+
const logContainer = document.getElementById('log-container');
150+
logContainer.scrollTop = logContainer.scrollHeight;
151+
});
152+
</script>
161153
</div>

0 commit comments

Comments
 (0)