PromptVault/resources/js/Components/PromptForm.vue

199 lines
6.8 KiB
Vue
Raw Normal View History

2026-10-26 20:12:16 +00:00
<script setup>
import { Link, useForm, usePage } from '@inertiajs/vue3';
import { computed, ref } from 'vue';
const props = defineProps({
initial: { type: Object, required: true },
suggestedTags: { type: Array, required: true },
method: { type: String, default: 'post' },
action: { type: String, required: true },
submitLabel: { type: String, default: 'Submit' },
cancelHref: { type: String, default: '/prompts' },
});
const page = usePage();
const PLACEHOLDER = '{{variable_name}}';
const form = useForm({
name: props.initial.name ?? '',
description: props.initial.description ?? '',
body: props.initial.body ?? '',
tags: (props.initial.tags ?? []).map((tag) => String(tag)),
});
const tagInput = ref('');
const errors = computed(() => page.props.errors ?? {});
const availableSuggestions = computed(() =>
props.suggestedTags.filter((tag) => !form.tags.includes(tag))
);
const addTag = (raw) => {
const value = String(raw ?? '').trim().toLowerCase();
if (value === '' || form.tags.includes(value)) {
return;
}
form.tags.push(value);
};
const removeTag = (index) => {
form.tags.splice(index, 1);
};
const onTagKeydown = (event) => {
if (event.key === 'Enter' || event.key === ',') {
event.preventDefault();
addTag(tagInput.value);
tagInput.value = '';
} else if (event.key === 'Backspace' && tagInput.value === '' && form.tags.length > 0) {
removeTag(form.tags.length - 1);
}
};
const submit = () => {
if (tagInput.value.trim() !== '') {
addTag(tagInput.value);
tagInput.value = '';
}
form.transform(() => ({
name: form.name,
description: form.description?.trim() === '' ? null : form.description.trim(),
body: form.body,
tags: [...form.tags],
}));
const submitter = props.method === 'put' ? form.put.bind(form) : form.post.bind(form);
submitter(props.action, {
preserveScroll: true,
onError: () => form.clearErrors(),
});
};
</script>
<template>
<form
class="space-y-4 rounded-lg border border-slate-200 bg-white p-5 shadow-sm"
@submit.prevent="submit"
>
<div>
<label for="prompt-name" class="mb-1 block text-sm font-medium text-slate-700">
Name
</label>
<input
id="prompt-name"
v-model="form.name"
type="text"
maxlength="120"
class="w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
/>
<p v-if="errors.name" class="mt-1 text-xs text-red-600">{{ errors.name }}</p>
</div>
<div>
<label
for="prompt-description"
class="mb-1 block text-sm font-medium text-slate-700"
>
Description <span class="font-normal text-slate-400">(optional)</span>
</label>
<textarea
id="prompt-description"
v-model="form.description"
rows="2"
maxlength="500"
class="w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
/>
<p v-if="errors.description" class="mt-1 text-xs text-red-600">
{{ errors.description }}
</p>
</div>
<div>
<label for="prompt-body" class="mb-1 block text-sm font-medium text-slate-700">
Prompt body
</label>
<textarea
id="prompt-body"
v-model="form.body"
rows="8"
spellcheck="false"
class="w-full rounded-md border border-slate-300 px-3 py-2 font-mono text-sm shadow-sm focus:border-slate-500 focus:outline-none"
/>
<p class="mt-1 text-xs text-slate-500">
Wrap placeholders like <code class="rounded bg-slate-100 px-1 py-0.5">{{ PLACEHOLDER }}</code>
</p>
<p v-if="errors.body" class="mt-1 text-xs text-red-600">{{ errors.body }}</p>
</div>
<div>
<label for="prompt-tags" class="mb-1 block text-sm font-medium text-slate-700">
Tags
</label>
<div
class="flex flex-wrap items-center gap-1.5 rounded-md border border-slate-300 px-2 py-1.5 shadow-sm focus-within:border-slate-500"
>
<span
v-for="(tag, index) in form.tags"
:key="tag"
class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700"
>
{{ tag }}
<button
type="button"
class="text-slate-400 hover:text-slate-700"
:aria-label="`Remove tag ${tag}`"
@click="removeTag(index)"
>
×
</button>
</span>
<input
id="prompt-tags"
v-model="tagInput"
type="text"
placeholder="Type a tag, press Enter…"
class="min-w-28 flex-1 border-0 bg-transparent p-0 text-sm focus:outline-none"
@keydown="onTagKeydown"
@blur="addTag(tagInput)"
/>
</div>
<p v-if="errors.tags" class="mt-1 text-xs text-red-600">
{{ Array.isArray(errors.tags) ? errors.tags[0] : errors.tags }}
</p>
<div v-if="availableSuggestions.length > 0" class="mt-2 flex flex-wrap gap-1.5">
<button
v-for="tag in availableSuggestions"
:key="tag"
type="button"
class="rounded-full border border-dashed border-slate-300 px-2 py-0.5 text-xs text-slate-500 hover:border-slate-400 hover:text-slate-700"
@click="addTag(tag)"
>
+ {{ tag }}
</button>
</div>
</div>
<div class="flex items-center justify-between border-t border-slate-100 pt-4">
<Link
:href="cancelHref"
class="text-sm text-slate-500 hover:text-slate-800 hover:underline"
>
Cancel
</Link>
<button
type="submit"
:disabled="form.processing"
class="rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
>
{{ form.processing ? 'Saving…' : submitLabel }}
</button>
</div>
</form>
</template>