Skip to content

Commit

Permalink
feat(brands): add EditBrandForm component and implement brand creatio…
Browse files Browse the repository at this point in the history
…n and editing functionality

Fixes #247
  • Loading branch information
Perdolique committed Nov 30, 2024
1 parent 34db907 commit 0d7e3f8
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 6 deletions.
8 changes: 4 additions & 4 deletions app/components/PerdInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
</label>

<button
v-if="clearable"
:class="[$style.clearAction, { visible: hasValue }]"
@click="clearValue"
type="button"
Expand All @@ -36,21 +37,20 @@
interface Props {
readonly label: string;
readonly clearable?: boolean;
readonly autocomplete?: InputHTMLAttributes['autocomplete'];
readonly autofocus?: InputHTMLAttributes['autofocus'];
readonly inputmode?: InputHTMLAttributes['inputmode'];
readonly pattern?: InputHTMLAttributes['pattern'];
readonly placeholder?: InputHTMLAttributes['placeholder'];
readonly required?: InputHTMLAttributes['required'];
readonly type?: 'text';
readonly type?: InputHTMLAttributes['type'];
readonly disabled?: InputHTMLAttributes['disabled'];
}
type Emits = (event: 'clear') => void;
const {
type = 'text'
} = defineProps<Props>();
defineProps<Props>();
const model = defineModel<string>({
required: true
Expand Down
95 changes: 95 additions & 0 deletions app/components/brands/EditBrandForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<form
:class="$style.form"
:disabled="submitting"
@submit.prevent="onSubmit"
>
<div :class="$style.inputs">
<PerdInput
required
autofocus
autocomplete="off"
label="Name"
placeholder="The West Pace"
v-model.trim="name"
/>

<PerdInput
label="Website URL"
placeholder="https://perd.pages.dev"
type="url"
v-model.trim="websiteUrl"
/>
</div>

<div :class="$style.buttons">
<PerdButton
secondary
type="button"
:disabled="submitting"
@click="onCancel"
>
Cancel
</PerdButton>

<PerdButton
type="submit"
:loading="submitting"
>
{{ saveButtonText }}
</PerdButton>
</div>
</form>
</template>

<script lang="ts" setup>
import PerdButton from '~/components/PerdButton.vue'
import PerdInput from '~/components/PerdInput.vue'
interface Props {
readonly submitting: boolean;
readonly saveButtonText: string;
}
type Emits = (event: 'submit') => void;
defineProps<Props>()
const emit = defineEmits<Emits>()
const router = useRouter()
const name = defineModel<string>('name', {
required: true
})
const websiteUrl = defineModel<string>('websiteUrl', {
required: true
})
function onSubmit() {
emit('submit')
}
function onCancel() {
router.back()
}
</script>

<style module>
.form {
display: grid;
row-gap: var(--spacing-32);
}
.inputs {
display: grid;
gap: var(--spacing-16);
}
.buttons {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
column-gap: var(--spacing-16);
}
</style>
1 change: 1 addition & 0 deletions app/components/equipment/SearchInput.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<PerdInput
clearable
v-model.trim="model"
label="Search"
placeholder="Name, description, etc."
Expand Down
5 changes: 4 additions & 1 deletion app/components/layout/PageContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
{{ pageTitle }}
</PerdHeading>

<div :class="$style.actions">
<div
v-if="$slots.actions"
:class="$style.actions"
>
<slot name="actions" />
</div>
</div>
Expand Down
65 changes: 65 additions & 0 deletions app/pages/brands/add.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

<template>
<PageContent page-title="Add new brand">
<EditBrandForm
v-model:name="name"
v-model:website-url="websiteUrl"
:submitting="isSubmitting"
save-button-text="Add brand"
@submit="onSubmit"
/>
</PageContent>
</template>

<script lang="ts" setup>
import PageContent from '~/components/layout/PageContent.vue'
import EditBrandForm from '~/components/brands/EditBrandForm.vue'
definePageMeta({
layout: 'page',
middleware: ['admin']
})
const name = ref('')
const websiteUrl = ref('')
const isSubmitting = ref(false)
const { addToast } = useToaster()
const { showErrorToast } = useApiErrorToast()
function resetForm() {
name.value = ''
websiteUrl.value = ''
}
async function onSubmit() {
if (isSubmitting.value) {
return
}
try {
isSubmitting.value = true
const websiteUrlValue = websiteUrl.value === '' ? undefined : websiteUrl.value
await $fetch('/api/brands', {
method: 'post',
body: {
name: name.value,
websiteUrl: websiteUrlValue
}
})
addToast({
title: 'Brand added 🎉',
message: 'The brand has been successfully added'
})
resetForm()
} catch (error) {
showErrorToast(error, 'Failed to add brand 🥲')
} finally {
isSubmitting.value = false
}
}
</script>
92 changes: 92 additions & 0 deletions app/pages/brands/details/[brandId]/edit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@

<template>
<PageContent page-title="Edit brand">
<EmptyState
v-if="error"
:icon="errorIcon"
>
{{ errorText }}
</EmptyState>

<EditBrandForm
v-else
v-model:name="name"
v-model:website-url="websiteUrl"
:submitting="isSubmitting"
save-button-text="Save changes"
@submit="onSubmit"
/>
</PageContent>
</template>

<script lang="ts" setup>
import PageContent from '~/components/layout/PageContent.vue'
import EmptyState from '~/components/EmptyState.vue'
import EditBrandForm from '~/components/brands/EditBrandForm.vue'
definePageMeta({
layout: 'page',
middleware: ['admin']
})
const route = useRoute()
const router = useRouter()
const brandId = route.params.brandId?.toString() ?? ''
const name = ref('')
const websiteUrl = ref('')
const isSubmitting = ref(false)
const { addToast } = useToaster()
const { showErrorToast } = useApiErrorToast()
const { data, error } = await useFetch(`/api/brands/${brandId}`)
if (data.value) {
name.value = data.value.name
websiteUrl.value = data.value.websiteUrl ?? ''
}
const errorIcon = computed(() => {
if (error.value?.statusCode === 404) {
return 'streamline-emojis:man-shrugging-1'
}
return 'streamline-emojis:face-screaming-in-fear'
})
const errorText = computed(() => {
if (error.value?.statusCode === 404) {
return `Brand with ID ${brandId} not found`
}
return 'Something went wrong'
})
async function onSubmit() {
if (isSubmitting.value) {
return
}
try {
isSubmitting.value = true
await $fetch(`/api/brands/${brandId}`, {
method: 'patch',
body: {
name: name.value,
websiteUrl: websiteUrl.value || undefined
}
})
addToast({
title: 'Brand updated 🎉',
message: 'The brand has been successfully updated'
})
router.push(`/brands/details/${brandId}`)
} catch (error) {
showErrorToast(error, 'Failed to update brand 🥲')
} finally {
isSubmitting.value = false
}
}
</script>
3 changes: 2 additions & 1 deletion app/pages/brands/details/[brandId]/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<PageContent :page-title="brandName">
<template
v-if="user.isAdmin"
v-if="hasActions"
#actions
>
<PerdMenu>
Expand Down Expand Up @@ -110,6 +110,7 @@
const isDeleting = ref(false)
const isDeleteDialogOpened = ref(false)
const { data, error } = await useFetch(`/api/brands/${brandId}`)
const hasActions = computed(() => user.value.isAdmin && error.value === undefined)
brandName.value = data.value?.name ?? '¯\\_(ツ)_/¯'
itemsCount.value = data.value?.equipmentCount ?? 0
Expand Down
62 changes: 62 additions & 0 deletions server/api/brands/[brandId].patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { eq } from 'drizzle-orm'
import * as v from 'valibot'
import { limits } from '~~/constants'

const paramsSchema = v.object({
brandId: stringToIntegerValidator
})

const bodySchema = v.object({
name: v.pipe(
v.string(),
v.nonEmpty(),
v.maxLength(limits.maxBrandNameLength)
),

websiteUrl: v.optional(
v.pipe(
v.string(),
v.url(),
)
)
})

function validateParams(params: unknown) {
return v.parse(paramsSchema, params)
}

function validateBody(body: unknown) {
return v.parse(bodySchema, body)
}

export default defineEventHandler(async (event) => {
const { db } = event.context
const { brandId } = await getValidatedRouterParams(event, validateParams)
const { name, websiteUrl } = await readValidatedBody(event, validateBody)

await validateAdmin(event)

const [updated] = await db
.update(tables.brands)
.set({
name,
websiteUrl
})
.where(
eq(tables.brands.id, brandId)
)
.returning({
id: tables.brands.id,
name: tables.brands.name,
websiteUrl: tables.brands.websiteUrl
})

if (updated === undefined) {
throw createError({
statusCode: 404,
message: `Brand with ID ${brandId} not found`
})
}

return updated
})
Loading

0 comments on commit 0d7e3f8

Please sign in to comment.