style : use modal component for forms based on Lesstime pattern
- Create reusable AppModal component (Teleport, backdrop blur, transitions) - Replace inline forms with modals on list and detail pages - Consistent with Lesstime TaskModal design (header, body scroll, footer) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ const router = useRouter()
|
||||
|
||||
const applications = ref<Application[]>([])
|
||||
const loading = ref(true)
|
||||
const showCreateForm = ref(false)
|
||||
const showCreateModal = ref(false)
|
||||
const createForm = ref<ApplicationWrite>({
|
||||
name: '',
|
||||
slug: '',
|
||||
@@ -26,12 +26,16 @@ async function loadApplications() {
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
createForm.value = { name: '', slug: '', registryImage: '', description: '', giteaUrl: '' }
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const created = await createApplication(createForm.value)
|
||||
showCreateForm.value = false
|
||||
createForm.value = { name: '', slug: '', registryImage: '', description: '', giteaUrl: '' }
|
||||
showCreateModal.value = false
|
||||
router.push(`/applications/${created.slug}`)
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
@@ -51,60 +55,12 @@ onMounted(loadApplications)
|
||||
<h1 class="text-2xl font-bold text-primary-500 sm:text-4xl">{{ t('applications.title') }}</h1>
|
||||
<button
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
||||
@click="showCreateForm = !showCreateForm"
|
||||
@click="openCreateModal"
|
||||
>
|
||||
+ {{ t('applications.addButton') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Create form -->
|
||||
<div v-if="showCreateForm" class="rounded-lg border border-neutral-200 bg-white p-6 mb-8">
|
||||
<form @submit.prevent="handleCreate" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-neutral-700 mb-1">{{ t('applications.form.name') }}</label>
|
||||
<MalioInputText
|
||||
v-model="createForm.name"
|
||||
@update:model-value="generateSlug"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-neutral-700 mb-1">{{ t('applications.form.slug') }}</label>
|
||||
<MalioInputText v-model="createForm.slug" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-neutral-700 mb-1">{{ t('applications.form.registryImage') }}</label>
|
||||
<MalioInputText v-model="createForm.registryImage" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-neutral-700 mb-1">{{ t('applications.form.giteaUrl') }}</label>
|
||||
<MalioInputText v-model="createForm.giteaUrl" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-neutral-700 mb-1">{{ t('applications.form.description') }}</label>
|
||||
<textarea
|
||||
v-model="createForm.description"
|
||||
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:ring-2 focus:ring-secondary-500/20"
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
|
||||
@click="showCreateForm = false"
|
||||
>
|
||||
{{ t('applications.form.cancel') }}
|
||||
</button>
|
||||
<MalioButton type="submit" :loading="isSubmitting">
|
||||
{{ t('applications.form.save') }}
|
||||
</MalioButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="grid gap-6 [grid-template-columns:repeat(auto-fill,minmax(280px,1fr))]">
|
||||
<div v-for="i in 3" :key="i" class="rounded-lg bg-tertiary-500 p-5 animate-pulse">
|
||||
@@ -147,5 +103,49 @@ onMounted(loadApplications)
|
||||
</p>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Create modal -->
|
||||
<AppModal
|
||||
v-model="showCreateModal"
|
||||
:submit-label="t('applications.form.save')"
|
||||
:cancel-label="t('applications.form.cancel')"
|
||||
:loading="isSubmitting"
|
||||
@submit="handleCreate"
|
||||
>
|
||||
<template #title>{{ t('applications.addButton') }}</template>
|
||||
|
||||
<form @submit.prevent="handleCreate" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
|
||||
<MalioInputText
|
||||
v-model="createForm.name"
|
||||
:label="t('applications.form.name')"
|
||||
@update:model-value="generateSlug"
|
||||
required
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="createForm.slug"
|
||||
:label="t('applications.form.slug')"
|
||||
required
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="createForm.registryImage"
|
||||
:label="t('applications.form.registryImage')"
|
||||
required
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="createForm.giteaUrl"
|
||||
:label="t('applications.form.giteaUrl')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-neutral-700">{{ t('applications.form.description') }}</label>
|
||||
<textarea
|
||||
v-model="createForm.description"
|
||||
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:ring-2 focus:ring-secondary-500/20"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</AppModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user