merapikan koding

This commit is contained in:
2025-07-29 22:57:15 +07:00
parent f49a7e9fc7
commit ba5de3c7a7
19 changed files with 5327 additions and 3094 deletions
+54 -25
View File
@@ -94,8 +94,8 @@
</div>
</div>
<div class="flex items-center ltr:ml-auto mt-8">
<button type="submit" class="btn btn-success !py-1"><icon-save class="me-1" /> Simpan</button>
<button type="reset" class="btn btn-dark !py-1 ml-1"><icon-restore class="me-1" />Reset</button>
<button type="submit" class="btn btn-success !py-1" :disabled="isLoading"><icon-save class="me-1" /> Simpan</button>
<button type="reset" class="btn btn-dark !py-1 ml-1" @click="resetForm"><icon-restore class="me-1" />Reset</button>
</div>
</form>
</div>
@@ -106,27 +106,43 @@
<script lang="ts" setup>
import { useVuelidate } from '@vuelidate/core';
import { integer, minValue,required } from '@vuelidate/validators';
import { helpers, integer, minValue,required } from '@vuelidate/validators';
import Multiselect from '@suadelabs/vue3-multiselect';
import '@suadelabs/vue3-multiselect/dist/vue3-multiselect.css';
import '@/assets/css/file-upload-preview.css';
useHead({ title: 'Edit Skin Karakter' });
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: {
name: { required },
description: { required },
character: { required },
featured_image: { required },
featured_icon: { required },
featured_image: { imageType, maxFileSize },
featured_icon: { imageType, maxFileSize },
}
};
const router = useRouter();
const route = useRoute();
const config = useRuntimeConfig();
const isLoading = ref(false);
const featuredImageUploader = ref(null);
const featuredIconUploader = ref(null);
const params = reactive({
search: null,
current_page: 1,
@@ -147,29 +163,29 @@
}
);
const { data: form } = await useAsyncData('skins',
() => $fetch(`${config.public.apiBase}character/character-skins/${route.params.id}`, {})
const { data: form, refresh } = await useAsyncData('skin-get',
() => $fetch(`${config.public.apiBase}/character/character-skins/${route.params.id}/`, {})
);
const $validate = useVuelidate(rules, { form });
onMounted(async () => {
if (!form.value) return;
const fileupload = await import('file-upload-with-preview');
let FileUploadWithPreview = fileupload.default;
// single image upload
new FileUploadWithPreview('featuredImage', {
featuredImageUploader.value = new FileUploadWithPreview('featuredImage', {
images: {
baseImage: form.value.featured_image || '/assets/images/file-preview.svg',
baseImage: form.value?.featured_image || '/assets/images/file-preview.svg',
backgroundImage: '',
},
});
// single image upload
new FileUploadWithPreview('featuredIcon', {
featuredIconUploader.value = new FileUploadWithPreview('featuredIcon', {
images: {
baseImage: form.value.featured_icon || '/assets/images/file-preview.svg',
baseImage: form.value?.featured_icon || '/assets/images/file-preview.svg',
backgroundImage: '',
},
});
@@ -188,23 +204,25 @@
formData.append('character', form.value.character.id);
formData.append('description', form.value.description);
if (form.value.featured_image) {
if (form.value.featured_image && typeof form.value.featured_image !== 'string') {
formData.append('featured_image', form.value.featured_image);
}
if (form.value.featured_icon) {
if (form.value.featured_icon && typeof form.value.featured_icon !== 'string') {
formData.append('featured_icon', form.value.featured_icon);
}
await $fetch(`${config.public.apiBase}character/character-skins/${route.params.id}/`, {
isLoading.value = true;
await $fetch(`${config.public.apiBase}/character/character-skins/${route.params.id}/`, {
method: 'PUT',
body: formData
}).then(() => {
//redirect
router.push({ path: "/character/skins/list" });
})
.catch((error) => {
console.log(error);
})
.finally(() => {
isLoading.value = false;
});
}
@@ -219,20 +237,31 @@
return
}
form.value.featured_image = file
form.value.featured_image = file;
}
function handleIconChange(event: { target: { files: any[]; }; }) {
const file = event.target.files[0]
const file = event.target.files[0];
if (!file) return
if (!file) return;
const allowed = ['image/png', 'image/jpeg', 'image/jpg']
const allowed = ['image/png', 'image/jpeg', 'image/jpg'];
if (!allowed.includes(file.type)) {
alert('Hanya PNG yang diizinkan')
return
alert('Hanya PNG yang diizinkan');
return;
}
form.value.featured_icon = file
form.value.featured_icon = file;
}
const resetForm = () => {
refresh()
isSubmitted.value = false;
$validate.value.form.$reset();
featuredImageUploader?.value?.clearPreviewPanel();
featuredIconUploader?.value?.clearPreviewPanel();
};
</script>
+47 -16
View File
@@ -94,8 +94,8 @@
</div>
</div>
<div class="flex items-center ltr:ml-auto mt-8">
<button type="submit" class="btn btn-success !py-1"><icon-save class="me-1" /> Simpan</button>
<button type="reset" class="btn btn-dark !py-1 ml-1"><icon-restore class="me-1" />Reset</button>
<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>
@@ -107,7 +107,7 @@
<script lang="ts" setup>
import { reactive } from 'vue'
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { required, helpers } from '@vuelidate/validators';
import Multiselect from '@suadelabs/vue3-multiselect';
import '@suadelabs/vue3-multiselect/dist/vue3-multiselect.css';
import '@/assets/css/file-upload-preview.css';
@@ -116,19 +116,29 @@
const form = ref({
name: '',
decription: '',
character: '',
description: '',
character: null,
featured_image: '',
featured_icon: '',
});
const isSubmitted = ref(false);
const imageType = helpers.withMessage('Format gambar harus PNG atau JPG', (value) => {
if (!value) return false;
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 false;
return value.size <= 1048576; // 1MB dalam byte
});
const rules = {
form: {
name: { required },
description: { required },
character: { required },
featured_image: { required },
featured_icon: { required },
featured_image: { required, imageType, maxFileSize },
featured_icon: { required, imageType, maxFileSize },
}
};
const $validate = useVuelidate(rules, { form });
@@ -143,7 +153,7 @@
});
const { data: characters } = await useAsyncData('characters',
() => $fetch(`${config.public.apiBase}character/characters/`, {
() => $fetch(`${config.public.apiBase}/character/characters/`, {
params: {
page: params.current_page,
page_size: 50
@@ -152,21 +162,24 @@
watch: [params]
}
);
const isLoading = ref(false);
const featuredImageUploader = ref(null);
const featuredIconUploader = ref(null);
onMounted(async () => {
const fileupload = await import('file-upload-with-preview');
let FileUploadWithPreview = fileupload.default;
// single image upload
new FileUploadWithPreview('featuredImage', {
featuredImageUploader.value = new FileUploadWithPreview('featuredImage', {
images: {
baseImage: '/assets/images/file-preview.svg',
backgroundImage: '',
},
});
// single image upload
new FileUploadWithPreview('featuredIcon', {
featuredIconUploader.value = new FileUploadWithPreview('featuredIcon', {
images: {
baseImage: '/assets/images/file-preview.svg',
backgroundImage: '',
@@ -194,17 +207,18 @@
formData.append('featured_icon', form.value.featured_icon);
}
await $fetch(`${config.public.apiBase}character/character-skins/`, {
isLoading.value = true;
await $fetch(`${config.public.apiBase}/character/character-skins/`, {
method: 'POST',
body: formData
}).then(() => {
//redirect
router.push({ path: "/character/skins/list" });
})
.catch((error) => {
//assign response error data to state "errors"
console.log(error);
})
.finally(() => {
isLoading.value = false;
});
}
@@ -235,4 +249,21 @@
form.value.featured_icon = file
}
const resetForm = () => {
form.value = {
name: '',
description: '',
character: null,
featured_image: '',
featured_icon: '',
};
isSubmitted.value = false;
$validate.value.form.$reset();
featuredImageUploader?.value?.clearPreviewPanel();
featuredIconUploader?.value?.clearPreviewPanel();
};
</script>
+15 -9
View File
@@ -19,14 +19,14 @@
</NuxtLink>
</div>
<div class="mb-5">
<input v-model.lazy="params.search" type="text" class="form-input max-w-xs" placeholder="Cari..." @change="changeSearch"/>
<input v-model.lazy="params.search" type="text" class="form-input max-w-xs" placeholder="Cari...(tombol Enter untuk mencari)" @change="changeSearch"/>
</div>
<div class="mb-5">
<div class="datatable">
<vue3-datatable
:rows="skins?.results"
:rows="rows"
:columns="cols"
:totalRows="skins?.count"
:totalRows="totalRows"
:isServerMode=true
:page="params.current_page"
:pageSize="params.pagesize"
@@ -40,8 +40,8 @@
previousArrow='<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-4.5 h-4.5 rtl:rotate-180"> <path d="M15 5L9 12L15 19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>'
nextArrow='<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-4.5 h-4.5 rtl:rotate-180"> <path d="M9 5L15 12L9 19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>'
>
<template #id="data">
<div class="flex gap-1">
<template #actions="data">
<div class="flex justify-end gap-1">
<button type="button" class="btn btn-success !py-1" @click="viewData(data.value)">
<icon-edit class="me-1" />
Edit
@@ -72,8 +72,14 @@
ref([
{ field: 'name', title: 'Nama' },
{ field: 'character.name', title: 'Karakter' },
{ field: 'id', title: 'Aksi' },
{
field: 'actions',
title: 'Aksi',
headerClass: 'text-right',
cellClass: 'text-right',
sort: false,
width: '150px'
}
]) || [];
const params = reactive({
@@ -84,6 +90,8 @@
sort_direction: 'asc',
});
const rows = computed(() => skins.value?.results ?? []);
const totalRows = computed(() => skins.value?.count ?? 0);
const { data: skins } = await useAsyncData('skins',
() => {
return $fetch(`${config.public.apiBase}/character/character-skins/`, {
@@ -106,9 +114,7 @@
};
const changeSearch = (data: any) => {
console.log(data);
params.current_page = 1;
console.log(params);
};
const viewData = (data: any) => {