maj via codex
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="p-4 max-w-4xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-yellow">🔧 Outils</h1>
|
||||
<button @click="showForm = true" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">+ Ajouter</button>
|
||||
<button @click="openCreate" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">+ Ajouter</button>
|
||||
</div>
|
||||
|
||||
<div v-if="toolsStore.loading" class="text-text-muted text-sm">Chargement...</div>
|
||||
@@ -19,11 +19,28 @@
|
||||
</div>
|
||||
<span v-if="t.categorie" class="text-xs text-yellow bg-yellow/10 rounded-full px-2 py-0.5 w-fit">{{ t.categorie }}</span>
|
||||
<p v-if="t.description" class="text-text-muted text-xs">{{ t.description }}</p>
|
||||
<p v-if="t.boutique_nom || t.prix_achat != null" class="text-text-muted text-xs">
|
||||
<span v-if="t.boutique_nom">🛒 {{ t.boutique_nom }}</span>
|
||||
<span v-if="t.prix_achat != null"> · 💶 {{ t.prix_achat }} €</span>
|
||||
</p>
|
||||
<a v-if="t.boutique_url" :href="t.boutique_url" target="_blank" rel="noopener noreferrer"
|
||||
class="text-blue text-xs hover:underline truncate">🔗 Boutique</a>
|
||||
<a v-if="t.video_url" :href="t.video_url" target="_blank" rel="noopener noreferrer"
|
||||
class="text-aqua text-xs hover:underline truncate">🎬 Vidéo</a>
|
||||
<a v-if="t.notice_fichier_url" :href="t.notice_fichier_url" target="_blank" rel="noopener noreferrer"
|
||||
class="text-aqua text-xs hover:underline truncate">📄 Notice</a>
|
||||
|
||||
<div v-if="t.photo_url || t.video_url" class="mt-auto pt-2 space-y-2">
|
||||
<img v-if="t.photo_url" :src="t.photo_url" alt="photo outil"
|
||||
class="w-full h-28 object-cover rounded border border-bg-hard bg-bg" />
|
||||
<video v-if="t.video_url" :src="t.video_url" controls muted
|
||||
class="w-full h-36 object-cover rounded border border-bg-hard bg-bg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showForm" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm">
|
||||
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-sm border border-bg-soft">
|
||||
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft">
|
||||
<h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier l\'outil' : 'Nouvel outil' }}</h2>
|
||||
<form @submit.prevent="submitTool" class="flex flex-col gap-3">
|
||||
<input v-model="form.nom" placeholder="Nom de l'outil *" required
|
||||
@@ -35,11 +52,41 @@
|
||||
<option value="fourche">Fourche</option>
|
||||
<option value="griffe">Griffe/Grelinette</option>
|
||||
<option value="arrosage">Arrosage</option>
|
||||
<option value="taille">Taille</option>
|
||||
<option value="autre">Autre</option>
|
||||
</select>
|
||||
<option value="taille">Taille</option>
|
||||
<option value="autre">Autre</option>
|
||||
</select>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<input v-model="form.boutique_nom" placeholder="Nom boutique"
|
||||
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
|
||||
<input v-model.number="form.prix_achat" type="number" min="0" step="0.01" placeholder="Prix achat (€)"
|
||||
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
|
||||
</div>
|
||||
<input v-model="form.boutique_url" type="url" placeholder="URL boutique (https://...)"
|
||||
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
|
||||
<textarea v-model="form.description" placeholder="Description..."
|
||||
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow resize-none h-16" />
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Photo de l'outil</label>
|
||||
<input type="file" accept="image/*" @change="onPhotoSelected"
|
||||
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
|
||||
<img v-if="photoPreview" :src="photoPreview" alt="Prévisualisation photo"
|
||||
class="mt-2 w-full h-28 object-cover rounded border border-bg-hard bg-bg" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Vidéo de l'outil</label>
|
||||
<input type="file" accept="video/*" @change="onVideoSelected"
|
||||
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
|
||||
<video v-if="videoPreview" :src="videoPreview" controls muted
|
||||
class="mt-2 w-full h-36 object-cover rounded border border-bg-hard bg-bg" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Notice (fichier texte)</label>
|
||||
<input type="file" accept=".txt,.md,text/plain" @change="onNoticeSelected"
|
||||
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
|
||||
<div v-if="noticeFileName || form.notice_fichier_url" class="text-text-muted text-xs mt-1 truncate">
|
||||
{{ noticeFileName || fileNameFromUrl(form.notice_fichier_url || '') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" @click="closeForm" class="px-4 py-2 text-text-muted hover:text-text text-sm">Annuler</button>
|
||||
<button type="submit" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
|
||||
@@ -54,29 +101,152 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useToolsStore } from '@/stores/tools'
|
||||
import type { Tool } from '@/api/tools'
|
||||
|
||||
const toolsStore = useToolsStore()
|
||||
const showForm = ref(false)
|
||||
const editId = ref<number | null>(null)
|
||||
const form = reactive({ nom: '', categorie: '', description: '' })
|
||||
const photoFile = ref<File | null>(null)
|
||||
const videoFile = ref<File | null>(null)
|
||||
const noticeFile = ref<File | null>(null)
|
||||
const photoPreview = ref('')
|
||||
const videoPreview = ref('')
|
||||
const noticeFileName = ref('')
|
||||
const form = reactive({
|
||||
nom: '',
|
||||
categorie: '',
|
||||
description: '',
|
||||
boutique_nom: '',
|
||||
boutique_url: '',
|
||||
prix_achat: undefined as number | undefined,
|
||||
photo_url: '',
|
||||
video_url: '',
|
||||
notice_fichier_url: '',
|
||||
})
|
||||
|
||||
function startEdit(t: Tool) {
|
||||
editId.value = t.id!
|
||||
Object.assign(form, { nom: t.nom, categorie: t.categorie || '', description: t.description || '' })
|
||||
function fileNameFromUrl(url: string) {
|
||||
if (!url) return ''
|
||||
return url.split('/').pop() || url
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(form, {
|
||||
nom: '',
|
||||
categorie: '',
|
||||
description: '',
|
||||
boutique_nom: '',
|
||||
boutique_url: '',
|
||||
prix_achat: undefined,
|
||||
photo_url: '',
|
||||
video_url: '',
|
||||
notice_fichier_url: '',
|
||||
})
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editId.value = null
|
||||
resetForm()
|
||||
photoFile.value = null
|
||||
videoFile.value = null
|
||||
noticeFile.value = null
|
||||
photoPreview.value = ''
|
||||
videoPreview.value = ''
|
||||
noticeFileName.value = ''
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function closeForm() { showForm.value = false; editId.value = null }
|
||||
function onPhotoSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0] || null
|
||||
photoFile.value = file
|
||||
if (file) photoPreview.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
function onVideoSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0] || null
|
||||
videoFile.value = file
|
||||
if (file) videoPreview.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
function onNoticeSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0] || null
|
||||
noticeFile.value = file
|
||||
noticeFileName.value = file?.name || ''
|
||||
}
|
||||
|
||||
function startEdit(t: Tool) {
|
||||
editId.value = t.id!
|
||||
Object.assign(form, {
|
||||
nom: t.nom,
|
||||
categorie: t.categorie || '',
|
||||
description: t.description || '',
|
||||
boutique_nom: t.boutique_nom || '',
|
||||
boutique_url: t.boutique_url || '',
|
||||
prix_achat: t.prix_achat,
|
||||
photo_url: t.photo_url || '',
|
||||
video_url: t.video_url || '',
|
||||
notice_fichier_url: t.notice_fichier_url || '',
|
||||
})
|
||||
photoFile.value = null
|
||||
videoFile.value = null
|
||||
noticeFile.value = null
|
||||
photoPreview.value = t.photo_url || ''
|
||||
videoPreview.value = t.video_url || ''
|
||||
noticeFileName.value = fileNameFromUrl(t.notice_fichier_url || '')
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function closeForm() {
|
||||
showForm.value = false
|
||||
editId.value = null
|
||||
photoFile.value = null
|
||||
videoFile.value = null
|
||||
noticeFile.value = null
|
||||
photoPreview.value = ''
|
||||
videoPreview.value = ''
|
||||
noticeFileName.value = ''
|
||||
}
|
||||
|
||||
async function uploadFile(file: File): Promise<string> {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
const { data } = await axios.post('/api/upload', fd)
|
||||
return data.url as string
|
||||
}
|
||||
|
||||
async function submitTool() {
|
||||
if (editId.value) {
|
||||
await toolsStore.update(editId.value, { ...form })
|
||||
} else {
|
||||
await toolsStore.create({ ...form })
|
||||
let saved: Tool
|
||||
const payload: Partial<Tool> = {
|
||||
nom: form.nom,
|
||||
categorie: form.categorie || undefined,
|
||||
description: form.description || undefined,
|
||||
boutique_nom: form.boutique_nom || undefined,
|
||||
boutique_url: form.boutique_url || undefined,
|
||||
prix_achat: form.prix_achat,
|
||||
photo_url: form.photo_url || undefined,
|
||||
video_url: form.video_url || undefined,
|
||||
notice_fichier_url: form.notice_fichier_url || undefined,
|
||||
}
|
||||
Object.assign(form, { nom: '', categorie: '', description: '' })
|
||||
|
||||
if (editId.value) {
|
||||
saved = await toolsStore.update(editId.value, payload)
|
||||
} else {
|
||||
saved = await toolsStore.create(payload)
|
||||
}
|
||||
|
||||
if (saved.id && (photoFile.value || videoFile.value || noticeFile.value)) {
|
||||
const patch: Partial<Tool> = {}
|
||||
if (photoFile.value) patch.photo_url = await uploadFile(photoFile.value)
|
||||
if (videoFile.value) patch.video_url = await uploadFile(videoFile.value)
|
||||
if (noticeFile.value) patch.notice_fichier_url = await uploadFile(noticeFile.value)
|
||||
if (Object.keys(patch).length) await toolsStore.update(saved.id, patch)
|
||||
}
|
||||
|
||||
resetForm()
|
||||
closeForm()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user