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:
parent
e60e47d28e
commit
fd14f8a6c1
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -75,7 +75,7 @@
|
|||
</nav>
|
||||
</div>
|
||||
<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'">
|
||||
<div class="flex justify-end">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<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">
|
||||
<div class="flex flex-col justify-center space-y-6 sm:w-[60%] py-10">
|
||||
<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 md:w-[60%] py-10 tablet:py-20">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||
Predict the Future with ARIMA
|
||||
|
@ -22,7 +22,7 @@
|
|||
</NuxtUiButton>
|
||||
</div>
|
||||
</div>
|
||||
<Placeholder class="aspect-video flex-grow bg-gray-500" />
|
||||
<NuxtImg src="https://placehold.co/600x400/png" format="webp" class="grow" />
|
||||
</section>
|
||||
</template>
|
||||
<style>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<NuxtUiInput />
|
||||
</template>
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
|||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ export default defineNuxtConfig({
|
|||
},
|
||||
compatibilityDate: '2024-11-01',
|
||||
devtools: { enabled: true },
|
||||
modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt'],
|
||||
modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt', 'dayjs-nuxt'],
|
||||
ui: {
|
||||
prefix: 'NuxtUi'
|
||||
},
|
||||
|
@ -30,5 +30,8 @@ export default defineNuxtConfig({
|
|||
image: {
|
||||
format: ['webp'],
|
||||
quality: 80,
|
||||
}
|
||||
})
|
||||
},
|
||||
css: [
|
||||
'@/assets/css/main.tw.css'
|
||||
]
|
||||
})
|
||||
|
|
|
@ -12,14 +12,18 @@
|
|||
"@vueuse/core": "^13.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs-nuxt": "^2.1.11",
|
||||
"lucide-vue-next": "^0.485.0",
|
||||
"nuxt": "^3.16.1",
|
||||
"reka-ui": "^2.2.0",
|
||||
"shadcn-nuxt": "^1.0.3",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"v-calendar": "^3.1.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
},
|
||||
|
@ -422,6 +426,18 @@
|
|||
"@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": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
||||
|
@ -2953,12 +2969,24 @@
|
|||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"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": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
||||
"integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==",
|
||||
"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": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
|
@ -4826,6 +4854,47 @@
|
|||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"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": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/db0/-/db0-0.3.1.tgz",
|
||||
|
@ -8904,6 +8973,12 @@
|
|||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.2.0.tgz",
|
||||
|
@ -10852,6 +10927,24 @@
|
|||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
@ -11221,6 +11314,15 @@
|
|||
"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": {
|
||||
"version": "3.0.1",
|
||||
"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": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz",
|
||||
|
|
|
@ -15,14 +15,18 @@
|
|||
"@vueuse/core": "^13.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs-nuxt": "^2.1.11",
|
||||
"lucide-vue-next": "^0.485.0",
|
||||
"nuxt": "^3.16.1",
|
||||
"reka-ui": "^2.2.0",
|
||||
"shadcn-nuxt": "^1.0.3",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"v-calendar": "^3.1.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,91 @@
|
|||
<template>
|
||||
<NuxtLayout name="landing-page">
|
||||
<div>
|
||||
demo page
|
||||
<div @dragenter.prevent @dragover.prevent @drop="e => {
|
||||
if (convertStatus !== 'loading') {
|
||||
handleDragFile(e)
|
||||
}
|
||||
}">
|
||||
<NuxtUiCard>
|
||||
<template #header>
|
||||
<div class="mb-3 flex gap-2">
|
||||
<div>
|
||||
<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>
|
||||
</NuxtLayout>
|
||||
</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>
|
||||
|
|
|
@ -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[]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue