Skip to content
102 changes: 102 additions & 0 deletions components/form/InputField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<template>
<label
class="form-label fw-semibold text-secondary text-uppercase mb-1"
:for="id"
>{{ label }}</label
>

<input
v-if="inputTypes.includes(inputType)"
:type="type"
class="form-control py-2 text-dark"
style="border-radius: 10px; font-size: 1rem"
:id="id"
:value="value"
:placeholder="placeholder"
@input="handleInput"
/>

<textarea
v-if="inputType === 'textarea'"
class="form-control py-2 text-dark"
style="border-radius: 10px; font-size: 1rem"
:id="id"
:value="value"
:placeholder="placeholder"
:type="type"
@input="handleInput"
rows="3"
></textarea>

<FormMultiSelect
v-if="inputType === 'multiselect'"
:suggestions="suggestions"
:modelValue="value"
:placeholder="placeholder"
@input="(value) => handleSelection(value)"
/>

<p v-if="error" class="text-danger">{{ error }}</p>
</template>
<script setup>
const inputTypes = ["text", "email", "password", "number", "tel", "file"];
const props = defineProps({
inputType: {
type: String,
default: "text",
},
id: {
type: String,
default: "",
},
value: {
type: [String, Number, Array],
default: "",
},
label: {
type: String,
default: "",
},
type: {
type: String,
default: "text",
validator: (value) =>
["text", "email", "password", "number", "tel", "file"].includes(value),
},
placeholder: {
type: String,
default: "",
},
required: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
error: {
type: String,
default: "",
},
suggestions: {
type: Array,
default: () => [],
},
selections: {
type: Array,
default: () => [],
},
});

const emit = defineEmits(["input"]);

// const inputValue = ref(props.value);
const handleInput = (event) => {
emit("input", event.target.value);
};

const handleSelection = (value) => {
emit("input", value);
};
</script>
204 changes: 204 additions & 0 deletions components/form/MultiSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<template>
<div class="position-relative">
<div
class="form-control d-flex flex-wrap gap-2 min-h-auto"
@click="focusInput"
>
<span
v-for="item in selectedItems"
:key="item"
class="badge bg-primary d-flex align-items-center gap-2"
>
{{ item }}
<button
type="button"
class="btn-close btn-close-white"
style="font-size: 0.5rem"
@click.stop="removeItem(item)"
aria-label="Remove"
></button>
</span>

<input
ref="inputRef"
type="text"
class="border-0 flex-grow-1 min-w-50"
style="outline: none; background: transparent"
v-model="searchQuery"
@input="handleInput"
@keydown.enter.prevent="handleEnter"
@keydown.down.prevent="handleArrowDown"
@keydown.up.prevent="handleArrowUp"
@keydown.backspace="handleBackspace"
@blur="handleBlur"
:placeholder="selectedItems.length ? '' : placeholder"
/>
</div>

<div
v-if="showSuggestions && (filteredSuggestions.length > 0 || searchQuery)"
class="position-absolute w-100 mt-1 shadow bg-white border rounded-2"
style="z-index: 3"
>
<template v-if="filteredSuggestions.length > 0">
<button
v-for="(suggestion, index) in filteredSuggestions"
:key="suggestion"
class="dropdown-item w-100 text-start"
:class="{ active: index === selectedIndex }"
@mousedown.prevent="selectSuggestion(suggestion)"
@mouseover="selectedIndex = index"
>
{{ suggestion }}
</button>
</template>

<button
v-if="searchQuery && !exactMatch"
class="dropdown-item w-100 text-start"
:class="{ active: selectedIndex === filteredSuggestions.length }"
@mousedown.prevent="addNewItem"
>
Add "{{ searchQuery }}"
</button>
</div>
</div>
</template>

<script setup>
const props = defineProps({
suggestions: {
type: Array,
default: () => [],
},
placeholder: {
type: String,
default: "Type to search or add new items...",
},
modelValue: {
type: Array,
default: () => [],
},
});

const emit = defineEmits(["update:modelValue", "add", "input"]);

const inputRef = ref(null);
const searchQuery = ref("");
const showSuggestions = ref(false);
const selectedIndex = ref(-1);
const selectedItems = ref([...props.modelValue]);

const filteredSuggestions = computed(() => {
if (!searchQuery.value)
return props.suggestions.filter(
(item) => !selectedItems.value.includes(item)
);

return props.suggestions.filter(
(suggestion) =>
suggestion.toLowerCase().includes(searchQuery.value.toLowerCase()) &&
!selectedItems.value.includes(suggestion)
);
});

const exactMatch = computed(() => {
return props.suggestions.some(
(suggestion) => suggestion.toLowerCase() === searchQuery.value.toLowerCase()
);
});

const focusInput = () => {
inputRef.value?.focus();
};

const handleInput = () => {
showSuggestions.value = true;
selectedIndex.value = -1;
};

const handleEnter = () => {
if (selectedIndex.value >= 0) {
if (selectedIndex.value < filteredSuggestions.value.length) {
selectSuggestion(filteredSuggestions.value[selectedIndex.value]);
} else {
addNewItem();
}
} else if (searchQuery.value && !exactMatch.value) {
addNewItem();
}
};

const handleArrowDown = () => {
const maxIndex =
filteredSuggestions.value.length + (exactMatch.value ? 0 : 1) - 1;
selectedIndex.value =
selectedIndex.value < maxIndex ? selectedIndex.value + 1 : -1;
};

const handleArrowUp = () => {
const maxIndex =
filteredSuggestions.value.length + (exactMatch.value ? 0 : 1) - 1;
selectedIndex.value =
selectedIndex.value > -1 ? selectedIndex.value - 1 : maxIndex;
};

const handleBackspace = () => {
if (!searchQuery.value && selectedItems.value.length > 0) {
removeItem(selectedItems.value[selectedItems.value.length - 1]);
}
};

const handleBlur = () => {
setTimeout(() => {
showSuggestions.value = false;
selectedIndex.value = -1;
searchQuery.value = "";
}, 200);
};

const selectSuggestion = (suggestion) => {
if (!selectedItems.value.includes(suggestion)) {
selectedItems.value = [...selectedItems.value, suggestion];
emit("input", selectedItems.value);
}
searchQuery.value = "";
showSuggestions.value = false;
};

const addNewItem = () => {
if (searchQuery.value && !selectedItems.value.includes(searchQuery.value)) {
const newItem = searchQuery.value.trim();
selectedItems.value = [...selectedItems.value, newItem];
emit("add", newItem);
emit("input", selectedItems.value);
searchQuery.value = "";
showSuggestions.value = false;
}
};

const removeItem = (item) => {
selectedItems.value = selectedItems.value.filter((i) => i !== item);
};
</script>

<style scoped>
.dropdown-item {
cursor: pointer;
padding: 0.5rem 1rem;
}

.dropdown-item:hover,
.dropdown-item.active {
background-color: #e9ecef;
color: #16181b;
}

.min-h-auto {
min-height: auto;
}

.min-w-50 {
min-width: 50px;
}
</style>
47 changes: 47 additions & 0 deletions components/form/Pagination.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center">
<template v-for="(step, index) in steps" :key="index">
<div class="d-flex align-items-center">
<div
:class="[
'rounded-circle d-flex align-items-center justify-content-center',
'border border-2',
'text-center',
currentStep > index
? 'bg-dark text-white'
: currentStep === index
? 'bg-secondary text-white'
: 'bg-light',
'position-relative',
]"
style="width: 40px; height: 40px"
>
{{ index + 1 }}
</div>
<span class="ms-2 text-uppercase">{{ step.title }}</span>
</div>
<div
v-if="index < steps.length - 1"
:class="['flex-grow-1 mx-4 my-auto', 'progress', 'position-relative']"
style="height: 2px"
>
<div
:class="[
'progress-bar',
currentStep > index ? 'bg-dark' : 'bg-secondary',
]"
:style="{ width: currentStep > index ? '100%' : '0%' }"
></div>
</div>
</template>
</div>
</div>
</template>

<script setup>
defineProps({
steps: Array,
currentStep: Number,
});
</script>
Loading