Skip to content

Commit

Permalink
feat: implement add equipment page and enhance equipment state manage…
Browse files Browse the repository at this point in the history
…ment

Fixes #145
  • Loading branch information
Perdolique committed Nov 6, 2024
1 parent fed01ec commit 8e6797d
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 340 deletions.
4 changes: 2 additions & 2 deletions app/components/PerdSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@
<script lang="ts" setup>
import type { SelectHTMLAttributes } from 'vue';
interface Option {
export interface SelectOption {
readonly value: string;
readonly label: string;
}
interface Props {
readonly options: readonly Option[];
readonly options: readonly SelectOption[];
readonly placeholder?: string;
readonly required?: SelectHTMLAttributes['required'];
readonly disabled?: SelectHTMLAttributes['disabled'];
Expand Down
269 changes: 269 additions & 0 deletions app/components/equipment/AddEquipmentForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
<template>
<EmptyState
v-if="hasError"
icon="streamline-emojis:face-screaming-in-fear"
>
Can't load required data
</EmptyState>

<form
v-else
:class="$style.form"
:disabled="isSubmitting"
@submit.prevent="onSubmit"
>
<div :class="$style.inputs">
<ImageUpload :class="$style.image" />

<PerdInput
required
autocomplete="off"
label="Name"
placeholder="Ultralight Tent"
:class="$style.name"
v-model.trim="name"
/>

<PerdTextArea
label="Description"
placeholder="A lightweight tent for backpacking."
v-model.trim="description"
:class="$style.description"
/>

<PerdInput
required
autocomplete="off"
label="Weight"
placeholder="1488"
inputmode="numeric"
pattern="\d*"
:class="$style.weight"
v-model.trim="weight"
/>

<PerdSelect
required
placeholder="Equipment type"
:options="typeOptions"
:class="$style.type"
v-model="selectedType"
/>

<PerdSelect
required
placeholder="Equipment group"
:options="groupOptions"
:class="$style.group"
v-model="selectedGroup"
/>
</div>

<hr :class="$style.divider">

<PerdButton
type="submit"
:class="$style.button"
>
Add equipment
</PerdButton>
</form>
</template>

<script lang="ts" setup>
import { FetchError } from 'ofetch';
import ImageUpload from '~/components/ImageUpload.vue';
import PerdButton from '~/components/PerdButton.vue';
import PerdInput from '~/components/PerdInput.vue';
import PerdSelect from '~/components/PerdSelect.vue';
import PerdTextArea from '~/components/PerdTextArea.vue';
import EmptyState from '~/components/EmptyState.vue';
const name = ref('')
const description = ref('')
const weight = ref('')
const selectedType = ref('')
const selectedGroup = ref('')
const isSubmitting = ref(false)
const { addToast } = useToaster()
const { groups, fetchGroups, hasError: hasGroupsError } = useEquipmentGroupsState()
const { types, fetchTypes, hasError: hasTypesError } = useEquipmentTypesState()
const groupOptions = computed(() => {
return groups.value.map((group) => ({
value: group.id.toString(),
label: group.name
}))
})
const typeOptions = computed(() => {
return types.value.map((type) => ({
value: type.id.toString(),
label: type.name
}))
})
const hasError = computed(() => hasGroupsError.value || hasTypesError.value)
await Promise.all([fetchGroups(), fetchTypes()])
function resetForm() {
name.value = ''
description.value = ''
weight.value = ''
selectedType.value = ''
selectedGroup.value = ''
}
async function onSubmit() {
if (isSubmitting.value) {
return
}
try {
isSubmitting.value = true
const descriptionValue = description.value === '' ? undefined : description.value
await $fetch('/api/equipment', {
method: 'POST',
body: {
name: name.value,
description: descriptionValue,
weight: parseInt(weight.value),
typeId: parseInt(selectedType.value),
groupId: parseInt(selectedGroup.value)
}
})
addToast({
title: 'Equipment added 🎉',
message: 'The equipment has been successfully added.'
})
resetForm()
} catch (error) {
if (error instanceof FetchError) {
addToast({
title: 'Failed to add equipment 🥲',
message: error.data.message
})
}
} finally {
isSubmitting.value = false
}
}
</script>

<style lang="scss" module>
.form {
display: grid;
row-gap: var(--spacing-16);
@include mobileLarge() {
row-gap: var(--spacing-32);
}
}
.inputs {
display: grid;
gap: var(--spacing-16);
grid-template-areas:
"image"
"name"
"description"
"weight"
"type"
"group";
@include mobileLarge() {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr auto;
grid-template-areas:
"image name name"
"image description description"
"weight type group";
}
@include tablet() {
grid-template-columns: repeat(4, 1fr);
grid-template-rows: auto 1fr;
grid-template-areas:
"image name name name"
"image description description description"
"image weight type group"
}
@include laptop() {
grid-template-columns: auto 1fr 1fr;
grid-template-rows: auto auto auto auto;
grid-template-areas:
"image name description"
"image weight description"
"image type description"
"image group description";
}
}
.image {
grid-area: image;
aspect-ratio: 1 / 1;
min-width: 200px;
justify-self: center;
@include mobileLarge() {
width: 100%;
}
@include laptop() {
max-width: 200px;
width: 100%;
justify-self: end;
}
}
.name {
grid-area: name;
}
.description {
grid-area: description;
height: 100%;
min-height: 100px;
}
.weight {
grid-area: weight;
}
.type {
grid-area: type;
}
.group {
grid-area: group;
}
.divider {
display: none;
@include mobileLarge() {
display: block;
height: 1px;
border: none;
background-color: var(--accent-200);
margin: 0 var(--spacing-16);
}
}
.button {
width: 100%;
@include mobileLarge() {
max-width: 300px;
justify-self: center;
}
}
</style>
1 change: 0 additions & 1 deletion app/components/layout/PageContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
display: grid;
gap: var(--spacing-16);
justify-content: space-between;
align-items: center;
@include mobileLarge {
grid-template-columns: 1fr auto;
Expand Down
27 changes: 0 additions & 27 deletions app/composables/use-equipment-groups-data.ts

This file was deleted.

39 changes: 39 additions & 0 deletions app/composables/use-equipment-groups-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
interface EquipmentGroup {
readonly id: number
readonly name: string
}

export default function useEquipmentGroupsState() {
const groupsState = useState<EquipmentGroup[]>('equipmentGroups', () => [])
const hasError = useState('equipmentGroupsError', () => false)

const sortedGroups = computed(
() => groupsState.value.sort((groupA, groupB) => groupA.name.localeCompare(groupB.name))
)


async function fetchGroups() {
const { data, error } = await useFetch('/api/equipment/groups')

if (data.value !== undefined) {
groupsState.value = data.value
}

if (error.value !== undefined) {
hasError.value = true
} else {
hasError.value = false
}
}

function addGroup(group: EquipmentGroup) {
groupsState.value.push(group)
}

return {
addGroup,
fetchGroups,
hasError,
groups: sortedGroups
}
}
27 changes: 0 additions & 27 deletions app/composables/use-equipment-types-data.ts

This file was deleted.

Loading

0 comments on commit 8e6797d

Please sign in to comment.