feat: add ErrorNotification component and API error handling in Nuisance form
Some checks failed
/ golint (push) Failing after 10s
Some checks failed
/ golint (push) Failing after 10s
- Add ts/rmo/components/ErrorNotification.vue — reusable error alert with dismiss support, accessible markup, and configurable message - Replace inline error div in Nuisance.vue with ErrorNotification - Add resp.ok check in doSubmit() so HTTP error responses are caught and the workflow stops instead of proceeding to /submitted - Fix typo: borwser → browser Issue: #8
This commit is contained in:
parent
9d2895bd94
commit
06e8de800a
2 changed files with 92 additions and 4 deletions
81
ts/rmo/components/ErrorNotification.vue
Normal file
81
ts/rmo/components/ErrorNotification.vue
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<style scoped>
|
||||||
|
.error-notification {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.error-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.error-text {
|
||||||
|
color: #721c24;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.error-dismiss {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #721c24;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.error-dismiss:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="visible" class="error-notification" role="alert">
|
||||||
|
<span class="error-icon" aria-hidden="true">✗</span>
|
||||||
|
<span class="error-text">{{ message }}</span>
|
||||||
|
<button
|
||||||
|
v-if="dismissible"
|
||||||
|
type="button"
|
||||||
|
class="error-dismiss"
|
||||||
|
aria-label="Dismiss error"
|
||||||
|
@click="dismiss"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
message?: string;
|
||||||
|
dismissible?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
message: "Something went wrong. Your request could not be completed. Please try again.",
|
||||||
|
dismissible: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
dismissed: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
visible.value = false;
|
||||||
|
emit("dismissed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, dismiss });
|
||||||
|
</script>
|
||||||
|
|
@ -444,9 +444,11 @@ select.tall {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-md-end mt-3 mt-md-0">
|
<div class="col-md-4 text-md-end mt-3 mt-md-0">
|
||||||
<div v-if="errorMessage" class="error-message">
|
<ErrorNotification
|
||||||
✗ {{ errorMessage }}
|
v-if="errorMessage"
|
||||||
</div>
|
:message="errorMessage"
|
||||||
|
@dismissed="errorMessage = ''"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary btn-lg"
|
class="btn btn-primary btn-lg"
|
||||||
|
|
@ -469,6 +471,7 @@ import AddressSuggestion from "@/components/AddressSuggestion.vue";
|
||||||
import ImageUpload, { Image } from "@/components/ImageUpload.vue";
|
import ImageUpload, { Image } from "@/components/ImageUpload.vue";
|
||||||
import MapLocator from "@/components/MapLocator.vue";
|
import MapLocator from "@/components/MapLocator.vue";
|
||||||
import AddressAndMapLocator from "@/rmo/components/AddressAndMapLocator.vue";
|
import AddressAndMapLocator from "@/rmo/components/AddressAndMapLocator.vue";
|
||||||
|
import ErrorNotification from "@/rmo/components/ErrorNotification.vue";
|
||||||
import { useStoreGeocode } from "@/store/geocode";
|
import { useStoreGeocode } from "@/store/geocode";
|
||||||
import { useStoreLocal } from "@/store/local";
|
import { useStoreLocal } from "@/store/local";
|
||||||
import { useStorePublicReport } from "@/store/publicreport";
|
import { useStorePublicReport } from "@/store/publicreport";
|
||||||
|
|
@ -529,8 +532,12 @@ async function doSubmit() {
|
||||||
const resp = await fetch("/api/rmo/nuisance", {
|
const resp = await fetch("/api/rmo/nuisance", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
// Don't set Content-Type, the borwser should do it
|
// Don't set Content-Type, the browser should do it
|
||||||
});
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
errorMessage.value = "Something went wrong. Your request could not be completed. Please try again.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data: PublicReport = (await resp.json()) as PublicReport;
|
const data: PublicReport = (await resp.json()) as PublicReport;
|
||||||
storePublicReport.add(data);
|
storePublicReport.add(data);
|
||||||
router.push("/submitted/" + data.public_id);
|
router.push("/submitted/" + data.public_id);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue