update tampilan landing page, add new modal, update table, new library for dependecies, update style tailwind, add type safe for ts, update nuxt config.

This commit is contained in:
Rynare 2025-04-26 00:26:10 +07:00
parent e60e47d28e
commit fd14f8a6c1
18 changed files with 588 additions and 9 deletions

View File

@ -1,3 +1,4 @@
<template> <template>
<NuxtPage /> <NuxtPage />
<NuxtUiNotifications />
</template> </template>

37
assets/css/main.tw.css Normal file
View File

@ -0,0 +1,37 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* assets/css/main.css */
.nuxtui-btn {
@apply
focus:outline-none
disabled:cursor-not-allowed
disabled:opacity-75
aria-disabled:cursor-not-allowed
aria-disabled:opacity-75
flex-shrink-0
font-medium
rounded-md
text-sm
gap-x-1.5
px-2.5
py-1.5
shadow-sm
text-white
dark:text-gray-900
bg-blue-500
hover:bg-blue-600
disabled:bg-blue-500
aria-disabled:bg-blue-500
dark:bg-blue-400
dark:hover:bg-blue-500
dark:disabled:bg-blue-400
dark:aria-disabled:bg-blue-400
focus-visible:outline
focus-visible:outline-2
focus-visible:outline-offset-2
focus-visible:outline-blue-500
dark:focus-visible:outline-blue-400
inline-flex
items-center;
}

View File

@ -0,0 +1,67 @@
<template>
<div>
<NuxtUiButton @click="modalShown = true" color="blue" label="Analyze" />
<NuxtUiModal v-model="modalShown">
<NuxtUiCard>
<div class="space-y-2 mb-4">
<h2 class="text-lg font-bold text-center">
Prediction Setup
</h2>
<NuxtUiAlert variant="soft" color="orange"
description="Please fill form honestly for better prediction." />
<NuxtUiDivider label="Form" />
<NuxtUiFormGroup label="Record type">
<NuxtUiSelectMenu v-model="model.recordPeriod" :options="recordOptions"
placeholder="Select period" />
</NuxtUiFormGroup>
<NuxtUiFormGroup label="Product">
<NuxtUiSelectMenu v-model="model.selectedProduct" :options="props.products"
placeholder="Select product to predict" />
</NuxtUiFormGroup>
<NuxtUiFormGroup label="Prediction type">
<NuxtUiSelectMenu v-model="model.predictionPeriod" :options="predictionOptions"
placeholder="Select prediction period" />
</NuxtUiFormGroup>
</div>
<div class="flex justify-end gap-2">
<NuxtUiButton label="Cancel" color="red" @click="modalShown = false" />
<NuxtUiButton @click="() => {
emit('prepared')
modalShown = false
}">Analyze Now!</NuxtUiButton>
</div>
</NuxtUiCard>
</NuxtUiModal>
</div>
</template>
<script lang="ts" setup>
import type { TDurationType, TModalMakePredictionModel, TModalMakePredictionProps } from '~/types/landing-page/demo/modalMakePrediction';
const modalShown = ref<boolean>(false)
const model = defineModel<TModalMakePredictionModel>({
default: {
recordPeriod: undefined,
selectedProduct: undefined,
predictionPeriod: undefined,
},
required: true
})
watch(() => model.value.recordPeriod, () => {
model.value.predictionPeriod = undefined
})
const props = withDefaults(defineProps<TModalMakePredictionProps>(), {
products: undefined
})
const emit = defineEmits(['prepared'])
const recordOptions: TDurationType[] = ['daily', 'weekly', 'monthly']
const predictionOptions = computed(() => {
return filterPeriods(recordOptions, model.value.recordPeriod).filter(v => v !== 'daily')
})
function filterPeriods(periods: TDurationType[], current?: TDurationType) {
return periods.filter(p => periods.indexOf(p) >= periods.indexOf(current || 'daily'));
}
</script>

View File

@ -75,7 +75,7 @@
</nav> </nav>
</div> </div>
<NuxtUiModal v-model="authModalIsOpen" prevent-close :ui="{ <NuxtUiModal v-model="authModalIsOpen" prevent-close :ui="{
width: 'w-full max-w-[400px]', width: 'w-full max-w-screen-mobile sm:max-w-screen-mobile',
}"> }">
<Auth @vue:mounted="authSection = 'login'"> <Auth @vue:mounted="authSection = 'login'">
<div class="flex justify-end"> <div class="flex justify-end">

View File

@ -1,6 +1,6 @@
<template> <template>
<section class="mt-20 pb-20 md:mt-32 md:pb-32 mx-auto min-h-screen flex gap-3 flex-col sm:flex-row"> <section class="mx-auto md:min-h-screen flex gap-3 flex-col md:flex-row md:py-10">
<div class="flex flex-col justify-center space-y-6 sm:w-[60%] py-10"> <div class="flex flex-col justify-center space-y-6 md:w-[60%] py-10 tablet:py-20">
<div class="space-y-2"> <div class="space-y-2">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl"> <h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
Predict the Future with ARIMA Predict the Future with ARIMA
@ -22,7 +22,7 @@
</NuxtUiButton> </NuxtUiButton>
</div> </div>
</div> </div>
<Placeholder class="aspect-video flex-grow bg-gray-500" /> <NuxtImg src="https://placehold.co/600x400/png" format="webp" class="grow" />
</section> </section>
</template> </template>
<style> <style>

View File

@ -0,0 +1,78 @@
<script setup lang="ts">
import { DatePicker as VCalendarDatePicker } from 'v-calendar'
// @ts-ignore
import type { DatePickerDate, DatePickerRangeObject } from 'v-calendar/dist/types/src/use/datePicker'
import 'v-calendar/dist/style.css'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: {
type: [Date, Object] as PropType<DatePickerDate | DatePickerRangeObject | null>,
default: null
},
calendarColumn: {
type: Number,
default: 2
}
})
const emit = defineEmits(['update:model-value', 'close'])
const date = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:model-value', value)
emit('close')
}
})
const attrs = {
'transparent': true,
'borderless': true,
'color': 'primary',
'is-dark': { selector: 'html', darkClass: 'dark' },
'first-day-of-week': 2
}
function onDayClick(_: any, event: MouseEvent): void {
const target = event.target as HTMLElement
target.blur()
}
</script>
<template>
<VCalendarDatePicker v-if="date && (date as DatePickerRangeObject)?.start && (date as DatePickerRangeObject)?.end"
v-model.range="date" :columns="props.calendarColumn" v-bind="{ ...attrs, ...$attrs }" @dayclick="onDayClick" />
<VCalendarDatePicker v-else v-model="date" v-bind="{ ...attrs, ...$attrs }" @dayclick="onDayClick" />
</template>
<style>
:root {
--vc-gray-50: rgb(var(--color-gray-50));
--vc-gray-100: rgb(var(--color-gray-100));
--vc-gray-200: rgb(var(--color-gray-200));
--vc-gray-300: rgb(var(--color-gray-300));
--vc-gray-400: rgb(var(--color-gray-400));
--vc-gray-500: rgb(var(--color-gray-500));
--vc-gray-600: rgb(var(--color-gray-600));
--vc-gray-700: rgb(var(--color-gray-700));
--vc-gray-800: rgb(var(--color-gray-800));
--vc-gray-900: rgb(var(--color-gray-900));
}
.vc-primary {
--vc-accent-50: rgb(var(--color-primary-50));
--vc-accent-100: rgb(var(--color-primary-100));
--vc-accent-200: rgb(var(--color-primary-200));
--vc-accent-300: rgb(var(--color-primary-300));
--vc-accent-400: rgb(var(--color-primary-400));
--vc-accent-500: rgb(var(--color-primary-500));
--vc-accent-600: rgb(var(--color-primary-600));
--vc-accent-700: rgb(var(--color-primary-700));
--vc-accent-800: rgb(var(--color-primary-800));
--vc-accent-900: rgb(var(--color-primary-900));
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div class="flex">
<NuxtUiFormGroup label="Type">
<NuxtUiSelectMenu v-model="valueFormat" :options="valueFormatOptions" placeholder="Select input type"
option-attribute="label" value-attribute="value" />
</NuxtUiFormGroup>
<NuxtUiFormGroup class="grow" label="Value" v-if="valueFormat === 'text'">
<NuxtUiInput />
</NuxtUiFormGroup>
<NuxtUiFormGroup class="grow" label="Value" v-else-if="valueFormat === 'number'">
<MyInputNumber />
</NuxtUiFormGroup>
<div v-else-if="valueFormat === 'date'">
<NuxtUiFormGroup class="grow" label="Value">
<MyInputNumber />
</NuxtUiFormGroup>
<NuxtUiFormGroup class="grow" label="Format">
<MyInputNumber />
</NuxtUiFormGroup>
</div>
</div>
</template>
<script lang="ts" setup>
type TValueFormat = 'date' | 'text' | 'number' | undefined
type TValueFormatOptions = {
value: TValueFormat,
label: string,
}
const props = defineProps<{
name: string
}>()
const valueFormat = ref<TValueFormat>('text')
const valueFormatOptions: TValueFormatOptions[] = [
{ value: 'date', label: 'Date' },
{ value: 'text', label: 'Text' },
{ value: 'number', label: 'Number' },
]
onMounted(() => {
if (props.name.includes('date')) {
valueFormat.value = 'date'
} else if (Number(props.name || NaN) >= 0) {
valueFormat.value = 'number'
} else {
valueFormat.value = 'text'
}
});
</script>

View File

@ -0,0 +1,3 @@
<template>
<NuxtUiInput />
</template>

View File

@ -0,0 +1,25 @@
<template>
<input type="text" v-model="model" ref="inputDOM" />
</template>
<script setup lang="ts">
const model = defineModel<string>()
const inputDOM = ref<HTMLInputElement | null>(null)
watch(model, (val, _, onCleanup) => {
if (!inputDOM.value)
return
const start = inputDOM.value.selectionStart
const end = (Number(inputDOM.value.selectionEnd) || 1) - 1
const filtered = val?.replace(/[0-9a-b]/g, '')
if (val !== filtered) {
model.value = filtered
nextTick(() => {
inputDOM.value?.setSelectionRange(start, end)
})
}
})
</script>

View File

@ -0,0 +1,4 @@
<template>
<div>
</div>
</template>

View File

@ -0,0 +1,38 @@
import type { TableColumn } from '#ui/types'
export function usePredictionTableColumn() {
const requiredColumn = ['date', 'product code', 'product name', 'sold(qty)']
const columnKeys = ref<string[]>([])
const columns: ComputedRef<TableColumn[]> = computed(() => {
if (columnKeys.value.length >= 1) {
return [{
key: 'actions',
sortable: true,
label: 'Actions'
}, ...columnKeys.value.map(v => ({
key: v,
sortable: true,
label: v
}) as TableColumn)]
} else {
return [{
key: 'actions',
sortable: true,
label: 'Actions'
}, ...requiredColumn.map(v => ({
key: v,
sortable: true,
label: v,
}))]
}
})
const missingColumn = computed(() => {
const currentColumn = columns.value.map(v => v.key)
return requiredColumn.filter(v => !currentColumn.includes(v))
})
return {
columnKeys,
columns,
missingColumn
}
}

View File

@ -0,0 +1,45 @@
import { sheetToJSON } from "~/utils/spreadsheet/sheetsToJSON"
export function useFileToJSON() {
const toast = useToast()
const file = ref<File | null>(null)
const status = ref<'idle' | 'loading' | 'error' | 'success'>('idle')
const result = ref<Record<string, any>[]>([])
const error = ref<Error | null>(null)
watch(file, async (newVal) => {
if (!newVal)
return
status.value = 'loading'
error.value = null
try {
const json = await sheetToJSON(newVal);
if (json) {
result.value = json as Record<string, any>[]
}
} catch (e: unknown) {
status.value = 'error'
if (e instanceof Error) {
error.value = e
}
toast.add({
title: 'Error',
icon: 'i-heroicons-x-circle',
color: 'red',
description: error.value?.message
})
} finally {
if (status.value !== 'error') {
status.value = 'success'
toast.add({
title: 'Success',
icon: 'i-heroicons-document-check',
color: 'green',
description: 'File Imported Successfully.'
})
}
}
})
return {
file, status, result, error
}
}

View File

@ -12,7 +12,7 @@ export default defineNuxtConfig({
}, },
compatibilityDate: '2024-11-01', compatibilityDate: '2024-11-01',
devtools: { enabled: true }, devtools: { enabled: true },
modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt'], modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt', 'dayjs-nuxt'],
ui: { ui: {
prefix: 'NuxtUi' prefix: 'NuxtUi'
}, },
@ -30,5 +30,8 @@ export default defineNuxtConfig({
image: { image: {
format: ['webp'], format: ['webp'],
quality: 80, quality: 80,
} },
css: [
'@/assets/css/main.tw.css'
]
}) })

114
package-lock.json generated
View File

@ -12,14 +12,18 @@
"@vueuse/core": "^13.0.0", "@vueuse/core": "^13.0.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^2.30.0",
"dayjs-nuxt": "^2.1.11",
"lucide-vue-next": "^0.485.0", "lucide-vue-next": "^0.485.0",
"nuxt": "^3.16.1", "nuxt": "^3.16.1",
"reka-ui": "^2.2.0", "reka-ui": "^2.2.0",
"shadcn-nuxt": "^1.0.3", "shadcn-nuxt": "^1.0.3",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.1.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^3.24.2" "zod": "^3.24.2"
} }
}, },
@ -422,6 +426,18 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
@ -2953,12 +2969,24 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"license": "MIT"
},
"node_modules/@types/parse-path": { "node_modules/@types/parse-path": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
"integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/resize-observer-browser": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.11.tgz",
"integrity": "sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==",
"license": "MIT"
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@ -4826,6 +4854,47 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/date-fns-tz": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.1.tgz",
"integrity": "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==",
"license": "MIT",
"peerDependencies": {
"date-fns": "2.x"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/dayjs-nuxt": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/dayjs-nuxt/-/dayjs-nuxt-2.1.11.tgz",
"integrity": "sha512-KDDNiET7KAKf6yzL3RaPWq5aV7ql9QTt5fIDYv+4eOegDmnEQGjwkKYADDystsKtPjt7QZerpVbhC96o3BIyqQ==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.7.4",
"dayjs": "^1.11.10"
}
},
"node_modules/db0": { "node_modules/db0": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/db0/-/db0-0.3.1.tgz", "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.1.tgz",
@ -8904,6 +8973,12 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/reka-ui": { "node_modules/reka-ui": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.2.0.tgz", "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.2.0.tgz",
@ -10852,6 +10927,24 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/v-calendar": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/v-calendar/-/v-calendar-3.1.2.tgz",
"integrity": "sha512-QDWrnp4PWCpzUblctgo4T558PrHgHzDtQnTeUNzKxfNf29FkCeFpwGd9bKjAqktaa2aJLcyRl45T5ln1ku34kg==",
"license": "MIT",
"dependencies": {
"@types/lodash": "^4.14.165",
"@types/resize-observer-browser": "^0.1.7",
"date-fns": "^2.16.1",
"date-fns-tz": "^2.0.0",
"lodash": "^4.17.20",
"vue-screen-utils": "^1.0.0-beta.13"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"vue": "^3.2.0"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -11221,6 +11314,15 @@
"vue": "^3.2.0" "vue": "^3.2.0"
} }
}, },
"node_modules/vue-screen-utils": {
"version": "1.0.0-beta.13",
"resolved": "https://registry.npmjs.org/vue-screen-utils/-/vue-screen-utils-1.0.0-beta.13.tgz",
"integrity": "sha512-EJ/8TANKhFj+LefDuOvZykwMr3rrLFPLNb++lNBqPOpVigT2ActRg6icH9RFQVm4nHwlHIHSGm5OY/Clar9yIg==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@ -11376,6 +11478,18 @@
} }
} }
}, },
"node_modules/xlsx": {
"version": "0.20.3",
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==",
"license": "Apache-2.0",
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xss": { "node_modules/xss": {
"version": "1.0.15", "version": "1.0.15",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz",

View File

@ -15,14 +15,18 @@
"@vueuse/core": "^13.0.0", "@vueuse/core": "^13.0.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^2.30.0",
"dayjs-nuxt": "^2.1.11",
"lucide-vue-next": "^0.485.0", "lucide-vue-next": "^0.485.0",
"nuxt": "^3.16.1", "nuxt": "^3.16.1",
"reka-ui": "^2.2.0", "reka-ui": "^2.2.0",
"shadcn-nuxt": "^1.0.3", "shadcn-nuxt": "^1.0.3",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.1.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^3.24.2" "zod": "^3.24.2"
} }
} }

View File

@ -1,7 +1,91 @@
<template> <template>
<NuxtLayout name="landing-page"> <NuxtLayout name="landing-page">
<div @dragenter.prevent @dragover.prevent @drop="e => {
if (convertStatus !== 'loading') {
handleDragFile(e)
}
}">
<NuxtUiCard>
<template #header>
<div class="mb-3 flex gap-2">
<div> <div>
demo page <label for="convert-file-input" class="nuxtui-btn">
Import
</label>
<input id="convert-file-input" type="file" hidden @input="handleFileInput" />
</div>
<LandingDemoModalMakePrediction v-model="modalMakePredictionModel" :products />
</div>
<div class="warning space-y-2">
<NuxtUiAlert v-for="(item, index) in missingColumn" :key="index"
icon="i-heroicons-exclamation-circle" color="orange" variant="subtle"
:description="`Column '${item}' is missing.`">
</NuxtUiAlert>
</div>
</template>
<template #default>
<NuxtUiTable :columns :loading="convertStatus === 'loading'" :rows="rowsData">
<template #actions-data="{ row }">
<NuxtUiButton icon="i-heroicons-ellipsis-vertical-solid" color="blue"
@click="console.log(row)" />
</template>
</NuxtUiTable>
</template>
<template #footer>
</template>
</NuxtUiCard>
{{ rowsData }}
<MyInputNumber v-model="a" />
</div> </div>
</NuxtLayout> </NuxtLayout>
</template> </template>
<script lang="ts" setup>
import type { TModalMakePredictionModel, TProduct } from '~/types/landing-page/demo/modalMakePrediction'
const a = ref()
const { file, result: convertResult, status: convertStatus } = useFileToJSON()
const { columnKeys, columns, missingColumn } = usePredictionTableColumn()
const rowsData = ref(convertResult.value)
const products = ref<TProduct[]>([])
watch(convertResult, newVal => {
try {
columnKeys.value = Object.keys(newVal[0])
rowsData.value = newVal
products.value = []
const { product_code, product_name } = newVal[0]
const hasRequiredKeys = !!product_code && !!product_name
newVal.forEach(v => {
if (hasRequiredKeys)
products.value.push({
product_code: v.product_code,
product_name: v.product_name
})
})
} catch (error) {
columnKeys.value = []
}
})
const modalMakePredictionModel = reactive<TModalMakePredictionModel>({
predictionPeriod: undefined,
recordPeriod: undefined,
selectedProduct: undefined
})
function handleDragFile(e: DragEvent) {
e.preventDefault();
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
file.value = files[0]
}
}
function handleFileInput(e: Event) {
const target = e.target as HTMLInputElement
if (target?.files && target.files.length > 0) {
const uploaded = target.files[0];
file.value = uploaded;
}
}
</script>

View File

@ -0,0 +1,13 @@
export type TDurationType = 'daily' | 'weekly' | 'monthly'
export type TModalMakePredictionModel = {
recordPeriod?: TDurationType,
selectedProduct?: string,
predictionPeriod?: TDurationType,
}
export type TProduct = {
product_code: string,
product_name: string
}
export type TModalMakePredictionProps = {
products?: TProduct[]
}

View File

@ -0,0 +1,13 @@
import * as XLSX from 'xlsx';
export async function sheetToJSON(file: File) {
try {
const fileBuffer = await file.arrayBuffer();
const workbook = XLSX.read(fileBuffer);
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const json = XLSX.utils.sheet_to_json(sheet)
return json
} catch (error: unknown) {
throw error
}
}