freekake_webapp/pages/content/contents/[id].vue
2025-07-31 13:56:11 +07:00

468 lines
24 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<ul class="flex space-x-2 rtl:space-x-reverse">
<li>
<a href="javascript:;" class="text-primary hover:underline">Konten</a>
</li>
<li class="before:mr-2 before:content-['/'] rtl:before:ml-2">
<NuxtLink to="/content/contents/list" class="text-primary hover:underline">Daftar Konten</NuxtLink>
</li>
<li class="before:mr-2 before:content-['/'] rtl:before:ml-2">
<span>Edit Konten</span>
</li>
</ul>
<div class="grid grid-cols-1 gap-6 pt-5">
<div class="panel">
<div class="mb-5 flex items-center justify-between">
<h5 class="text-lg font-semibold dark:text-white-light">Edit Konten</h5>
<NuxtLink to="/content/contents/list" class="dark:text-white-light btn btn-dark !py-1">
<icon-arrow-backward class="me-1" />
Daftar
</NuxtLink>
</div>
<div class="mb-5">
<form class="space-y-5" @submit.prevent="submitForm()">
<div class="grid grid-cols-1 gap-4 md:grid-cols-4 gap-2">
<div :class="{ 'has-error': $validate.form.title.$error, 'has-success': isSubmitted && !$validate.form.title.$error }">
<label for="title">Judul</label>
<input id="title" type="text" class="form-input" v-model="form.title" />
<template v-if="isSubmitted && $validate.form.title.$error">
<p class="text-danger mt-1">Judul konten harus diisi</p>
</template>
</div>
<div :class="{ 'has-error': $validate.form.slug.$error, 'has-success': isSubmitted && !$validate.form.slug.$error }">
<label for="slug">Slug</label>
<input id="slug" type="text" class="form-input" v-model="form.slug" />
<template v-if="isSubmitted && $validate.form.slug.$error">
<p class="text-danger mt-1">Slug konten harus diisi</p>
</template>
</div>
<div :class="{ 'has-error': $validate.form.status.$error, 'has-success': isSubmitted && !$validate.form.status.$error }">
<label for="status">Status</label>
<multiselect id="status"
v-model="form.status"
:options="statuses?.results"
class="custom-multiselect"
:searchable="true"
placeholder="Pilih status konten"
selected-label=""
select-label=""
deselect-label=""
label="label"
track-by="value"
></multiselect>
<template v-if="isSubmitted && $validate.form.status.$error">
<p class="text-danger mt-1">Jenis status harus diisi</p>
</template>
</div>
<div v-if="form.status?.value === 'published'" :class="{ 'has-error': $validate.form.posted_at.$error, 'has-success': isSubmitted && !$validate.form.posted_at.$error }">
<label for="posted_at">Tanggal Publikasi</label>
<flat-pickr id="posted_at" v-model="form.posted_at" class="form-input" :config="basic"></flat-pickr>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4 gap-2">
<div :class="{ 'has-error': $validate.form.theme.$error, 'has-success': isSubmitted && !$validate.form.theme.$error }">
<label for="theme">Tema</label>
<multiselect id="theme"
v-model="form.theme"
:options="themes?.results"
class="custom-multiselect"
:searchable="true"
placeholder="Pilih tema konten"
selected-label=""
select-label=""
deselect-label=""
label="label"
track-by="value"
></multiselect>
<template v-if="isSubmitted && $validate.form.theme.$error">
<p class="text-danger mt-1">Tema konten harus diisi</p>
</template>
</div>
<div :class="{ 'has-error': $validate.form.topic.$error, 'has-success': isSubmitted && !$validate.form.topic.$error }">
<label for="topic">Topik</label>
<multiselect v-if="topics" id="topic"
v-model="form.topic"
:options="topics?.results"
class="custom-multiselect"
:searchable="true"
placeholder="Pilih topik konten"
selected-label=""
select-label=""
deselect-label=""
label="label"
track-by="value"
></multiselect>
<template v-if="isSubmitted && $validate.form.topic.$error">
<p class="text-danger mt-1">Topik konten harus diisi</p>
</template>
</div>
<div :class="{ 'has-error': $validate.form.format.$error, 'has-success': isSubmitted && !$validate.form.format.$error }">
<label for="format">Format</label>
<multiselect id="format"
v-model="form.format"
:options="formats?.results"
class="custom-multiselect"
:searchable="true"
placeholder="Pilih format konten"
selected-label=""
select-label=""
deselect-label=""
label="label"
track-by="value"
></multiselect>
<template v-if="isSubmitted && $validate.form.format.$error">
<p class="text-danger mt-1">Format konten harus diisi</p>
</template>
</div>
<div :class="{ 'has-error': $validate.form.type.$error, 'has-success': isSubmitted && !$validate.form.type.$error }">
<label for="type">Jenis Konten</label>
<multiselect id="theme"
v-model="form.type"
:options="types?.results"
class="custom-multiselect"
:searchable="true"
placeholder="Pilih jenis konten"
selected-label=""
select-label=""
deselect-label=""
label="label"
track-by="value"
></multiselect>
<template v-if="isSubmitted && $validate.form.type.$error">
<p class="text-danger mt-1">Jenis konten harus diisi</p>
</template>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-1 gap-2">
<div :class="{ 'has-error': $validate.form.description.$error, 'has-success': isSubmitted && !$validate.form.description.$error }">
<label for="description">Deskripsi Singkat Konten</label>
<textarea id="description" rows="3" class="form-textarea" v-model="form.description"></textarea>
<template v-if="isSubmitted && $validate.form.description.$error">
<p class="text-danger mt-1">Deskripsi konten harus diisi</p>
</template>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-1 gap-2">
<div :class="{ 'has-error': $validate.form.content.$error, 'has-success': isSubmitted && !$validate.form.content.$error }">
<label for="content">Detail Konten</label>
<div style="min-height: 300px">
<quillEditor v-if="form.content" id="content" ref="content" v-model:value="form.content" :options="quilloptions" style="min-height: 200px; height: 100%;" contentType="html"></quillEditor>
</div>
<template v-if="isSubmitted && $validate.form.content.$error">
<p class="text-danger mt-1">Detail konten harus diisi</p>
</template>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 gap-2">
<div>
<label for="data">Data Konten (ditulis dalam format JSON)</label>
<textarea id="data" rows="3" class="form-textarea" v-model="form.data"></textarea>
</div>
<div>
<label for="grade">Kelas</label>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 gap-2">
<label class="inline-flex cursor-pointer" v-for="n in 6" :key="n">
<input type="checkbox" class="form-checkbox" v-model="form.grades" :value="n"/>
<span class="text-white-dark">Kelas {{ n }}</span>
</label>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4 gap-2">
<div :class="{ 'has-error': $validate.form.point.$error, 'has-success': isSubmitted && !$validate.form.point.$error }">
<label for="point">Poin</label>
<input id="point" type="number" class="form-input" v-model="form.point" />
<template v-if="isSubmitted && $validate.form.point.$error">
<p class="text-danger mt-1">Poin konten harus diisi dan lebih dari 0</p>
</template>
</div>
<div :class="{ 'has-error': $validate.form.coin.$error, 'has-success': isSubmitted && !$validate.form.coin.$error }">
<label for="coin">Koin</label>
<input id="coin" type="number" class="form-input" v-model="form.coin" />
<template v-if="isSubmitted && $validate.form.coin.$error">
<p class="text-danger mt-1">Koin konten harus diisi dan lebih dari 0</p>
</template>
</div>
<div>
<label for="color">Warna Latar Belakang</label>
<input id="color" class="form-input" v-model="form.color" />
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 gap-2 ">
<div class="custom-file-container" data-upload-id="featuredImage"
:class="{ 'has-error': $validate.form.featured_image.$error, 'has-success': isSubmitted && !$validate.form.featured_image.$error }">
<div class="label-container">
<label for="featured_image">Gambar </label> <a href="javascript:;" class="custom-file-container__image-clear" title="Clear Image">×</a>
</div>
<label class="custom-file-container__custom-file" >
<input id="featured_image" type="file" class="custom-file-container__custom-file__custom-file-input"
accept="image/png" @change="handleImageChange" />
<input type="hidden" name="MAX_FILE_SIZE" value="10485760" />
<span class="custom-file-container__custom-file__custom-file-control ltr:pr-20 rtl:pl-20"></span>
</label>
<template v-if="isSubmitted && $validate.form.featured_image.$error">
<p class="text-danger mt-1">Gambar tema konten harus diisi</p>
</template>
<div class="custom-file-container__image-preview"></div>
</div>
</div>
<div class="flex items-center ltr:ml-auto mt-8">
<button type="submit" class="btn btn-success !py-1" :disabled="isLoading"><icon-save class="me-1" /> {{ isLoading ? 'Menyimpan...' : 'Simpan' }}</button>
<button type="button" class="btn btn-dark !py-1 ml-1" @click="resetForm"><icon-restore class="me-1" />Reset</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useVuelidate } from '@vuelidate/core';
import { helpers, integer, minValue,required, requiredIf } from '@vuelidate/validators';
import Multiselect from '@suadelabs/vue3-multiselect';
import '@suadelabs/vue3-multiselect/dist/vue3-multiselect.css';
import '@/assets/css/file-upload-preview.css';
import 'flatpickr/dist/flatpickr.css';
import flatPickr from 'vue-flatpickr-component';
import 'vue3-quill/lib/vue3-quill.css';
useHead({ title: 'Edit Konten' });
const imageType = helpers.withMessage('Format gambar harus PNG atau JPG', (value) => {
if (!value) return true;
if (typeof value === 'string') return true;
return ['image/png', 'image/jpeg', 'image/jpg'].includes(value.type);
});
const maxFileSize = helpers.withMessage('Ukuran gambar tidak boleh lebih dari 1MB', (value) => {
if (!value) return true;
if (typeof value === 'string') return true;
return value.size <= 1048576; // 1MB dalam byte
});
const isSubmitted = ref(false);
const rules = {
form: {
title: { required },
slug: { required },
theme: { required },
topic: { required },
format: { required },
description: { required },
content: { required },
grades: { required },
point: { required, integer, minValue: minValue(0) },
coin: { required, integer, minValue: minValue(0) },
featured_image: { imageType, maxFileSize },
status: { required },
type: { required },
posted_at: {
required: requiredIf(() => form.value.status?.value === 'published'),
},
}
};
const router = useRouter();
const route = useRoute();
const config = useRuntimeConfig();
const { $api } = useNuxtApp();
const quilloptions = ref({});
const basic: any = ref({
dateFormat: 'Y-m-d',
position: 'auto left',
minDate: "today",
});
const { data: form, refresh} = await useAsyncData('content-get',
async () => {
const content = await $api(`/content/contents/${route.params.id}/`, {});
return {
...content,
data: JSON.stringify(content.data),
};
});
const $validate = useVuelidate(rules, { form });
const { data: formats } = await useAsyncData('formats',
() => $api(`/content/formats/`)
);
const { data: themes } = await useAsyncData('themes',
() => $api(`/content/themes/`)
);
// const { data: topics, refresh: refreshTopics } = await useAsyncData('topics',
// () => $fetch(`${config.public.apiBase}/content/topics/${form.value.theme?.value}/`), {
// watch: [() => form.value.theme?.value],
// }
// );
const { data: types } = await useAsyncData('types',
() => $api(`/content/types/`)
);
const { data: statuses } = await useAsyncData('statuses',
() => $api(`/content/statuses/`)
);
const topics = ref({ results: [] });
const fetchTopics = async () => {
if (!form.value.theme?.value) return;
try {
const res = await $api(`/content/topics/${form.value.theme.value}/`);
topics.value = res;
// mapping kembali topik jika perlu
if (typeof form.value.topic === 'string') {
form.value.topic = res.results.find(t => t.value === form.value.topic) ?? null;
}
} catch (error) {
console.error('Gagal mengambil topik:', error);
}
};
watchEffect(() => {
if (form.value && formats.value?.results?.length && typeof form.value.format === 'string') {
form.value.format = formats.value.results.find(f => f.value === form.value.format) ?? null;
}
if (form.value && themes.value?.results?.length && typeof form.value.theme === 'string') {
form.value.theme = themes.value.results.find(t => t.value === form.value.theme) ?? null;
}
if (form.value && topics.value?.results?.length && typeof form.value.topic === 'string') {
form.value.topic = topics.value.results.find(t => t.value === form.value.topic) ?? null;
}
if (form.value && types.value?.results?.length && typeof form.value.type === 'string') {
form.value.type = types.value.results.find(t => t.value === form.value.type) ?? null;
}
if (form.value && statuses.value?.results?.length && typeof form.value.status === 'string') {
form.value.status = statuses.value.results.find(s => s.value === form.value.status) ?? null;
}
});
watchEffect(async () => {
if (!form.value.theme || typeof form.value.theme === 'string') {
if (themes.value?.results?.length && typeof form.value.theme === 'string') {
form.value.theme = themes.value.results.find(t => t.value === form.value.theme) ?? null;
}
}
if (form.value.theme?.value) {
await fetchTopics();
}
});
const isLoading = ref(false);
const featuredImageUploader = ref(null);
onMounted(async () => {
const fileupload = await import('file-upload-with-preview');
let FileUploadWithPreview = fileupload.default;
featuredImageUploader.value = new FileUploadWithPreview('featuredImage', {
images: {
baseImage: form.value.featured_image ? form.value.featured_image : '/assets/images/file-preview.svg',
backgroundImage: '',
},
});
if (!form.value.status && statuses?.value?.results?.length) {
form.value.status = statuses.value.results.find(s => s.value === 'draft');
}
if (!form.value.type && types?.value?.results?.length) {
form.value.type = types.value.results.find(t => t.value === 'content');
}
});
const submitForm = async () => {
isSubmitted.value = true;
$validate.value.form.$touch();
if ($validate.value.form.$invalid) {
return false;
}
const formData = new FormData();
formData.append('title', form.value.title);
formData.append('slug', form.value.slug);
formData.append('format', form.value.format.value);
formData.append('topic', form.value.topic.value);
formData.append('theme', form.value.theme.value);
formData.append('description', form.value.description);
formData.append('content', form.value.content);
formData.append('data', JSON.stringify(form.value.data));
formData.append('grades', JSON.stringify(form.value.grades));
formData.append('point', form.value.point);
formData.append('coin', form.value.coin);
formData.append('color', form.value.color);
formData.append('type', form.value.type.value);
formData.append('status', form.value.status.value);
if (form.value.status?.value === 'published' && form.value.posted_at) {
formData.append('posted_at', form.value.posted_at);
}
if (form.value.status?.value === 'archived') {
const today = new Date().toISOString().slice(0, 10);
formData.append('archived_at', today);
}
if (form.value.featured_image && typeof form.value.featured_image !== 'string') {
formData.append('featured_image', form.value.featured_image);
}
isLoading.value = true;
await $api(`/content/contents/${route.params.id}/`, {
method: 'PUT',
body: formData
}).then(() => {
router.push({ path: "/content/contents/list" });
})
.catch((error) => {
console.log(error);
})
.finally(() => {
isLoading.value = false;
});
}
function handleImageChange(event: { target: { files: any[]; }; }) {
const file = event.target.files[0];
if (!file) return;
const allowed = ['image/png', 'image/jpeg', 'image/jpg'];
if (!allowed.includes(file.type)) {
alert('Hanya PNG atau JPG yang diizinkan');
return;
}
form.value.featured_image = file;
}
const resetForm = () => {
refresh();
if (!form.value.status && statuses?.value?.results?.length) {
form.value.status = statuses.value.results.find(s => s.value === 'draft');
}
if (!form.value.type && types?.value?.results?.length) {
form.value.type = types.value.results.find(t => t.value === 'content');
}
isSubmitted.value = false;
$validate.value.form.$reset();
featuredImageUploader?.value?.clearPreviewPanel();
}
</script>