Skip to content

Company info/stack registration #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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