initial: bootstrap from BukidBountyApp base
This commit is contained in:
116
resources/js/Components/Core/SearchableTable.vue
Normal file
116
resources/js/Components/Core/SearchableTable.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div id="tableContainer">
|
||||
<table :id="tableId" class="display">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(header, i) in computedHeaders" :key="i">{{ header }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, rowIdx) in filteredData"
|
||||
:key="rowIdx"
|
||||
@click="$emit('row-click', row, rowIdx)"
|
||||
class="searchable-row"
|
||||
>
|
||||
<td v-for="(header, colIdx) in computedHeaders" :key="colIdx">
|
||||
{{ isObjectData ? (row[header] ?? '') : (row[colIdx] ?? '') }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Simple pagination -->
|
||||
<div v-if="totalPages > 1" class="table-pagination mt-3 d-flex justify-content-between align-items-center">
|
||||
<button class="btn btn-sm btn-default" :disabled="currentPage <= 1" @click="currentPage--">
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {{ currentPage }} of {{ totalPages }}</span>
|
||||
<button class="btn btn-sm btn-default" :disabled="currentPage >= totalPages" @click="currentPage++">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* Array of objects or array of arrays
|
||||
*/
|
||||
data: { type: Array, required: true },
|
||||
headers: { type: Array, default: () => [] },
|
||||
tableId: { type: String, default: 'dynamicTable' },
|
||||
pageLength: { type: Number, default: 10 },
|
||||
defaultSort: { type: Object, default: () => ({ column: 0, direction: 'asc' }) },
|
||||
defaultSearch: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['row-click'])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const searchTerm = ref(props.defaultSearch)
|
||||
|
||||
const isObjectData = computed(() =>
|
||||
props.data.length > 0 &&
|
||||
props.data[0] !== null &&
|
||||
typeof props.data[0] === 'object' &&
|
||||
!Array.isArray(props.data[0])
|
||||
)
|
||||
|
||||
const computedHeaders = computed(() => {
|
||||
if (props.headers.length > 0) return props.headers
|
||||
if (isObjectData.value) return Object.keys(props.data[0])
|
||||
return []
|
||||
})
|
||||
|
||||
const sortedData = computed(() => {
|
||||
const dataCopy = [...props.data]
|
||||
const { column, direction } = props.defaultSort
|
||||
|
||||
dataCopy.sort((a, b) => {
|
||||
const valA = isObjectData.value ? a[computedHeaders.value[column]] : a[column]
|
||||
const valB = isObjectData.value ? b[computedHeaders.value[column]] : b[column]
|
||||
|
||||
if (valA < valB) return direction === 'asc' ? -1 : 1
|
||||
if (valA > valB) return direction === 'asc' ? 1 : -1
|
||||
return 0
|
||||
})
|
||||
|
||||
return dataCopy
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let result = sortedData.value
|
||||
|
||||
if (searchTerm.value) {
|
||||
const q = searchTerm.value.toLowerCase()
|
||||
result = result.filter(row => {
|
||||
const text = isObjectData.value
|
||||
? Object.values(row).join(' ')
|
||||
: row.join(' ')
|
||||
return text.toLowerCase().includes(q)
|
||||
})
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const start = (currentPage.value - 1) * props.pageLength
|
||||
return result.slice(start, start + props.pageLength)
|
||||
})
|
||||
|
||||
const totalPages = computed(() => {
|
||||
const total = sortedData.value.length
|
||||
return Math.max(1, Math.ceil(total / props.pageLength))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.searchable-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
.searchable-row:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user