138 lines
3.2 KiB
Vue
138 lines
3.2 KiB
Vue
<script setup lang="ts">
|
|
|
|
// types
|
|
|
|
type Props = {
|
|
status?: "success" | "error" | "idle";
|
|
modelValue: never[];
|
|
autofocus?: boolean;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
// props
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
status: "idle"
|
|
});
|
|
const { modelValue, disabled, status } = toRefs(props);
|
|
|
|
// state
|
|
|
|
const { $gsap: gsap } = useNuxtApp();
|
|
|
|
// emit
|
|
|
|
const emit = defineEmits(["complete", "update:modelValue"]);
|
|
|
|
// state
|
|
|
|
const currentOtpCode = ref([]);
|
|
|
|
// methods
|
|
|
|
const handleChange = () => {
|
|
emit("update:modelValue", currentOtpCode.value);
|
|
};
|
|
|
|
const handleComplete = () => {
|
|
emit("update:modelValue", currentOtpCode.value);
|
|
emit("complete", currentOtpCode.value);
|
|
};
|
|
|
|
const playStatusAnimation = () => {
|
|
|
|
const inputCount = 6;
|
|
const duration = 0.100;
|
|
|
|
let statusColor = {
|
|
border : "",
|
|
bg : ""
|
|
};
|
|
|
|
if (status.value === "success") {
|
|
statusColor.border = "var(--color-success-500)";
|
|
statusColor.bg = "var(--color-success-50)";
|
|
} else if (status.value === "error") {
|
|
statusColor.border = "var(--color-danger-500)";
|
|
statusColor.bg = "var(--color-danger-50)";
|
|
}
|
|
|
|
let index = 0;
|
|
const animate = (index: number) => {
|
|
setTimeout(() => {
|
|
gsap.to(`#otp-input-${index}`, {
|
|
borderColor: statusColor.border,
|
|
backgroundColor: statusColor.bg,
|
|
scale: 1.2,
|
|
duration: duration / 2
|
|
});
|
|
|
|
gsap.to(`#otp-input-${index}`, {
|
|
scale: 1,
|
|
duration: duration / 2,
|
|
delay: duration
|
|
});
|
|
|
|
setTimeout(() => {
|
|
gsap.to(`#otp-input-${index}`, {
|
|
borderColor: "black",
|
|
backgroundColor: "var(--color-slate-50)"
|
|
});
|
|
}, (inputCount + 1) * duration * 3000);
|
|
|
|
}, index * duration * 500);
|
|
};
|
|
|
|
while (index < 6) {
|
|
animate(index);
|
|
index++;
|
|
}
|
|
};
|
|
|
|
// watch
|
|
|
|
watch(() => modelValue.value, (value) => {
|
|
currentOtpCode.value = value;
|
|
});
|
|
|
|
watch(() => disabled.value, (value) => {
|
|
if (!value) {
|
|
const otpInputFirst = document.querySelector("#otp-input-0") as HTMLInputElement;
|
|
setTimeout(() => {
|
|
otpInputFirst.focus();
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
watch(() => status.value, (value) => {
|
|
if (value !== "idle") {
|
|
playStatusAnimation();
|
|
}
|
|
});
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<PinInputRoot
|
|
:disabled="disabled"
|
|
v-bind="$attrs"
|
|
type="number"
|
|
v-model="currentOtpCode"
|
|
placeholder="_"
|
|
class="flex gap-4 items-center justify-center mt-1"
|
|
@change="handleChange"
|
|
@complete="handleComplete"
|
|
otp
|
|
>
|
|
<PinInputInput
|
|
v-for="(id, index) in 6"
|
|
:id="`otp-input-${index}`"
|
|
:key="id"
|
|
:index="index"
|
|
:autofocus="autofocus ? index === 0 ? true : 'off' : 'off'"
|
|
class="disabled:text-slate-400 focus-within:border-black transition-all size-16 bg-slate-50 typo-label-lg rounded-lg text-center border-[1.5px] border-slate-200 outline-none"
|
|
/>
|
|
</PinInputRoot>
|
|
</div>
|
|
</template> |