Skip to content

implement and add test todos api service and make improvement in todo and list inputs visualizations #54

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 10 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
VITE_API_BASE_URL=https://jsonplaceholder.typicode.com
VITE_API_TODO_URL=http://localhost:8000
VITE_API_TIMEOUT=10000
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
VITE_API_BASE_URL=https://jsonplaceholder.typicode.com
VITE_API_TODO_URL=http://localhost:8000
VITE_API_TIMEOUT=10000
27 changes: 27 additions & 0 deletions data/todos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"statusCards": [
{
"title": "active",
"id": "1"
},
{
"title": "inProgress",
"id": "2"
},
{
"title": "done",
"id": "3"
}
],
"todos": [
{
"statusCardId": "2",
"status": "inProgress",
"id": "c9d5",
"title": "an active todo",
"dueDate": "2025-04-06T08:37:00.000Z",
"createdAt": "2025-04-06T08:38:15.586Z",
"updatedAt": "2025-04-06T08:38:15.586Z"
}
]
}
845 changes: 845 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"server": "json-server --watch data/todos.json --port 8000"
},
"dependencies": {
"axios": "^1.3.2",
"bootstrap": "^5.3.1",
"bootstrap-icons": "^1.11.2",
"json-server": "^1.0.0-beta.3",
"pasoonate": "^1.2.5",
"pinia": "^2.0.28",
"vue": "^3.3.4",
Expand Down
8 changes: 8 additions & 0 deletions src/components/layout/side-menu/VSideMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@

{{ $t('Todos') }}
</VSideMenuItem>

<VSideMenuItem :to="{ name: 'Todos-Test' }">
<template #icon>
<i class="bi bi-check2-square"></i>
</template>

{{ $t('Todos Test') }}
</VSideMenuItem>
</nav>
</aside>
</template>
Expand Down
273 changes: 189 additions & 84 deletions src/components/pagination/VPagination.vue
Original file line number Diff line number Diff line change
@@ -1,108 +1,213 @@
<template>
<div class="d-flex align-items-center justify-content-center py-2">
<div class="d-flex align-items-center justify-content-center py-2">
<button
class="arrow bg-primary-subtle d-flex align-items-center justify-content-center"
@click="goPrev"
:disabled="modelValue === 1"
>
<i class="bi bi-arrow-left"></i>
</button>
<button
class="btn btn-sm btn-link"
@click="goPrev"
:disabled="modelValue === 1"
>{{ $t('Previous') }}</button>

<input
:value="modelValue"
@input="updateValue($event.target.value)"
type="tel"
class="form-control form-control-sm text-center ms-2"
style="width: 50px;"
v-for="number in visiblePageNumbers"
:key="number"
class="page-number bg-primary-subtle d-flex align-items-center justify-content-center"
@click="goToPage(number)"
:class="{ 'active': modelValue === number }"
>

<span class="small text-muted mx-2">{{ $t('From') }} {{ count }}</span>

<button
class="btn btn-sm btn-link"
@click="goNext"
:disabled="modelValue === count"
>{{ $t('Next') }}</button>

<span class="small text-muted ms-5 me-2">{{ $t('Size') }}:</span>

<select
:value="size"
@input="updateSize($event.target.value)"
class="form-select form-select-sm"
style="width: 70px;"
>
<option
v-for="option in sizeOptions"
:key="option"
>{{ option }}</option>
</select>
</div>
{{ number }}
</button>

<button
class="arrow bg-primary-subtle d-flex align-items-center justify-content-center border-0"
@click="goNext"
:disabled="modelValue === count"
>
<i class="bi bi-arrow-right"></i>
</button>

<span class="small text-muted ms-5 me-2">{{ $t('Size') }}:</span>

<select
:value="size"
@input="updateSize($event.target.value)"
class="form-select form-select-sm"
style="width: 70px;"
>
<option
v-for="option in sizeOptions"
:key="option"
:value="option"
>{{ option }}</option>
</select>
</div>
</template>

<script>
import { computed } from 'vue';

export default {
name: 'VPagination',

props: {
modelValue: {
type: Number,
required: true
},
total: {
type: Number,
required: true
},
size: {
type: Number,
default: 10
},
sizeOptions: {
type: Array,
default: () => [5, 10, 50, 100]
}
},
import { computed, ref, watch, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';

emits: ['update:modelValue', 'update:size'],
export default {
name: 'VPagination',

setup(props, context) {
const count = computed(() => Math.ceil(props.total / props.size));
props: {
modelValue: {
type: Number,
required: true
},
total: {
type: Number,
required: true
},
numbersCount: {
type: Number,
default: 5
},
size: {
type: Number,
default: 10
},
sizeOptions: {
type: Array,
default: () => [5, 10, 50, 100]
}
},

function updateValue(value) {
value = Number(value);
emits: ['update:modelValue', 'update:size'],

if (value < 1) {
value = 1;
}
setup(props, context) {
const route = useRoute();
const router = useRouter();

if (value > count.value) {
value = count.value;
}
const count = computed(() => Math.ceil(props.total / props.size));
const currentInSightNumbers = ref([1, props.numbersCount]);

context.emit('update:modelValue', value);
}

function goNext() {
updateValue(props.modelValue + 1);
const visiblePageNumbers = computed(() => {
if (count.value <= props.numbersCount) {
return Array.from({ length: count.value }, (_, i) => i + 1);
} else {
const [start, end] = currentInSightNumbers.value;
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
});

// watches count and if it changes (by changing size) currentInSightNumbers re-fetches. So the visible numbers change
watch(count, () => {
currentInSightNumbers.value = [1, props.numbersCount];
}, { immediate: true });

watch(() => props.modelValue, (newPage) => {
if (count.value <= props.numbersCount) return;
const [start, end] = currentInSightNumbers.value;

if (newPage < start) {
// newPage - props.numbersCount - 1 : fetches the end number going backward (before the current visible start number)
currentInSightNumbers.value = [newPage, newPage + props.numbersCount - 1];
} else if (newPage > end) {
// newPage - props.numbersCount + 1 : fetches the start number going forward (after the current visible end number)
currentInSightNumbers.value = [newPage - props.numbersCount + 1, newPage];
}
});

function goPrev() {
updateValue(props.modelValue - 1);
function updateValue(value) {
value = Number(value);

if (value < 1) {
value = 1;
}

function updateSize(value) {
context.emit('update:size', Number(value));
if (value > count.value) {
value = count.value;
}

return {
count,
context.emit('update:modelValue', value);
router.push({ query: { ...route.query, page: value } });
}

function goNext() {
updateValue(props.modelValue + 1);
}

function goPrev() {
updateValue(props.modelValue - 1);
}

updateValue,
goPrev,
goNext,
function goToPage(page) {
updateValue(page);
}

updateSize
};
function updateSize(value) {
context.emit('update:size', Number(value));
context.emit('update:modelValue', 1);
router.push({ query: { ...route.query, items_per_page: value, page: 1 } });
}

onMounted(() => {
const currentPageBySize = +route.query.page > +count.value ? 1 : +route.query.page;
const pageFromUrl = currentPageBySize;
const sizeFromUrl = Number(route.query.items_per_page) || props.size;

context.emit('update:modelValue', pageFromUrl);
context.emit('update:size', sizeFromUrl);

if (pageFromUrl > count.value) {
// start: 1, end: 5
currentInSightNumbers.value = [1, props.numbersCount];
} else {
const start = Math.max(1, pageFromUrl - Math.floor(props.numbersCount / 2));
const end = Math.min(count.value, start + props.numbersCount - 1);
currentInSightNumbers.value = [start, end];
}
});

return {
count,
visiblePageNumbers,
goToPage,
updateValue,
goPrev,
goNext,
updateSize
};
}
}
</script>

<style lang="scss">
.change-page {
border: none;
border-right: 1px solid #cccccc88;
width: 1.5rem;
aspect-ratio: 1/1;
transition: background-color, color, .08s ease;

&:disabled {
background-color: #cfe2ff66 !important;
pointer-events: none;
}

&:hover {
background-color: #a8c7f5 !important;
}

&:active {
background-color: #699be6 !important;
color: #fff;
}

&.active {
background-color: #699be6 !important;
color: #fff;
}
}

.arrow {
@extend .change-page
}

.page-number {
@extend .change-page;


}
</style>
Loading