Skip to content

Implement comments page task #58

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 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fd80478
feat(Views):Initialize comments page and add its route in router file
xGooddevilx May 10, 2025
0d165c6
feat:(Components):Add component page link to the sidebar menu component
xGooddevilx May 10, 2025
495a0eb
feat(Services):Implement comments services & its composable
xGooddevilx May 10, 2025
058a793
feat(View):Implement comments table & add status field to the data
xGooddevilx May 10, 2025
6f7b508
feat(Component):Initialize comments filter form
xGooddevilx May 10, 2025
2cf204c
feat(View):Implement client-side filtering for comments
xGooddevilx May 11, 2025
9945bf8
feat(View): Implement highlighting serach results for query
xGooddevilx May 11, 2025
3bdbac7
refactor(View): replace query matches with regex pattern
xGooddevilx May 11, 2025
1b30a22
feat(Services):Implement post service
xGooddevilx May 11, 2025
4cb8521
feat(View): Implement post modal
xGooddevilx May 11, 2025
ac38473
feat(Composables): Implement caching post logic
xGooddevilx May 11, 2025
78ea6fa
fix(Composable): resolve issue with not showing cached data in post f…
xGooddevilx May 11, 2025
927cf65
feat:Implement sync logic between tabs with localestorage
xGooddevilx May 11, 2025
2d54240
refactor(Composables): Implement cache until function and replace it …
xGooddevilx May 11, 2025
218b4ea
feat(View): Implement confirmatin modal
xGooddevilx May 11, 2025
3a5421e
feat(View): Implement changing comment status
xGooddevilx May 11, 2025
e5ec12f
feat(Views): Implement multi status changing
xGooddevilx May 11, 2025
240ea4c
feat:Initialized CustomToast component
xGooddevilx May 11, 2025
5adbca8
feat:Implement undo action via custom toast component
xGooddevilx May 12, 2025
da3fb2e
refactor(Views): Improve tracking selectedRows state and its related …
xGooddevilx May 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<template>
<RouterView />
<VToasts/>
<VToasts />
<CustomToast />
</template>

<script>
// Components
import VToasts from '@/components/VToasts.vue';
// Components
import VToasts from '@/components/VToasts.vue';
import CustomToast from './components/comments/CustomToast.vue';

export default {
name: 'App',
export default {
name: 'App',

components: { VToasts }
};
</script>
components: { VToasts, CustomToast }
};
</script>
3 changes: 1 addition & 2 deletions src/components/VToasts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
aria-live="assertive"
aria-atomic="true"
>
<div class="toast-header" v-html="item.title" v-if="!!item.title"></div>
<div class="d-flex justify-content-between align-items-center">
<div class="toast-body" v-html="item.body"></div>

<button
v-if="item.clearable"
type="button"
Expand All @@ -29,7 +29,6 @@

export default {
name: 'VToasts',

setup() {
let items = installToast();

Expand Down
63 changes: 63 additions & 0 deletions src/components/comments/CommentsFilterForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script>
import { useModelRef } from '@/composables/model.composable';
import VForm from '../form/VForm.vue';
import VInput from '../form/VInput.vue';
import { t } from '@/services/language.service';
import VSelect from '../form/VSelect.vue';
export default {
name: 'CommentsFilterForm',
setup() {


const filters = useModelRef('modelValue');

const statusItems = [
{
key: 'PENDING',
text: t('Pending')
},
{
key: 'REJECTED',
text: t('Rejected')
},
{
key: 'CONFIRMED',
text: t('Confirmed')
}
]

return {
filters,
statusItems
}
},
props: {
modelValue: {
type: Object,
required: true
}
},
components: {
VForm,
VInput,
VSelect
},
emits: ['update:modelValue'],

}
</script>

<template>
<VForm class="row">
<div class="col-3">
<VInput v-model="filters.searchQuery" :placeholder="$t('SearchInputPlaceholder')" id="searchQuery" />
</div>
<div class="col-3">
<VInput v-model="filters.email" :placeholder="$t('EmailPlaceholder')" />
</div>
<div class="col-2">
<VSelect v-model="filters.status" :items="statusItems" item-key="key" item-text="text"
:placeholder="$t('Filter by status')" clearable />
</div>
</VForm>
</template>
41 changes: 41 additions & 0 deletions src/components/comments/CustomToast.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<template>
<div class="toast-container bottom-0 end-0">
<div v-for="item of items" :key="item.id"
:class="`toast show align-items-center border-1 m-2 fw-medium text-bg-${item.theme}`" role="alert"
aria-live="assertive" aria-atomic="true">
<div class="toast-header" v-html="item.title" v-if="!!item.title"></div>
<div class="d-flex justify-content-between align-items-center">
<div class="toast-body">
<button @click="handleCallbackCall(item.id,item.callback)" class="btn btn-secondary">
Undo
</button>
</div>
<button v-if="item.clearable" type="button" class="btn-close p-3" aria-label="Close"
@click="hideToast(item.id)"></button>
</div>
</div>
</div>
</template>

<script>
import { installCustomToast, useCustomToast } from '@/composables/customToast.composable';

export default {
name: 'CustomToast',
setup() {
let items = installCustomToast();

const { hideToast } = useCustomToast();
const handleCallbackCall = (id,callback) => {
callback?.()
hideToast(id)
}

return {
items,
hideToast,
handleCallbackCall
};
},
}
</script>
20 changes: 13 additions & 7 deletions src/components/layout/side-menu/VSideMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@

{{ $t('Todos') }}
</VSideMenuItem>
<VSideMenuItem :to="{ name: 'Comments' }">
<template #icon>
<i class="bi-chat-dots-fill"></i>
</template>
{{ $t('Comments') }}
</VSideMenuItem>
</nav>
</aside>
</template>

<script>
// Components
import VSideMenuItem from '@/components/layout/side-menu/VSideMenuItem.vue';
// Components
import VSideMenuItem from '@/components/layout/side-menu/VSideMenuItem.vue';

export default {
name: 'VSideBar',
export default {
name: 'VSideBar',

components: {
VSideMenuItem
}
components: {
VSideMenuItem
}
}
</script>
16 changes: 16 additions & 0 deletions src/composables/cache.composable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import StorageService from "@/services/storage.service";

export default function useLocaleStorageCache() {
const getCache = (key) => {
return StorageService.get(key);
};
const clearCache = (key) => StorageService.delete(key);

const setCache = (key, value) => StorageService.set(key, value);

return {
getCache,
clearCache,
setCache,
};
}
43 changes: 43 additions & 0 deletions src/composables/comments.composable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { computed, ref } from "vue";
import { useLoading } from "./loading.composable";
import { keyBy } from "@/utils";
import CommentsService from "@/services/comments.service";

export const useFetchComments = () => {
const { isLoading, startLoading, endLoading } = useLoading();
const comments = ref([]);
const commentsKeyById = computed(() => keyBy(comments.value, "id"));

/**
* @param {AxiosRequestConfig} [config]
*/
const fetchComments = (config) => {
startLoading();
return CommentsService.getAll(config)
.then((response) => {
comments.value = response.data.map((item) => ({ ...item, status: "PENDING" }));
return response;
})
.finally(() => {
endLoading();
});
};

const changeStatus = (id, status) => {
const updatedItemIndex = comments.value.findIndex((comment) => comment.id === id);
comments.value[updatedItemIndex].status = status;

// comments.value = comments.value.map((comment) => {
// if (comment.id === id) return { ...comment, status };
// else return comment;
// });
};

return {
comments,
commentsIsLoading: isLoading,
commentsKeyById,
fetchComments,
changeStatus,
};
};
52 changes: 52 additions & 0 deletions src/composables/customToast.composable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ref, getCurrentInstance } from 'vue';

// Utils
import { getUniqueId } from '@/utils';

const items = ref([]);

export function installCustomToast() {
const instance = getCurrentInstance();
if (instance.type.name !== 'CustomToast') {
throw new Error('installToast should only be called in the Custom toast component');
}

return items;
}

export function useCustomToast() {
function showToast({ theme, duration = 5000, clearable = true , title, callback}) {
const id = getUniqueId();

const _timeOutId = setTimeout(function () {
hideToast(id);
}, duration);

items.value.push({
id,
duration,
theme,
clearable,
title,
callback,
_time_out_id: _timeOutId
});

return id;
}

function hideToast(id) {
const index = items.value.findIndex(function (item) {
return item.id === id;
});

clearTimeout(items.value[index]._time_out_id);

items.value.splice(index, 1);
}

return {
showToast,
hideToast
};
}
37 changes: 37 additions & 0 deletions src/composables/posts.composable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ref } from "vue";
import { useLoading } from "./loading.composable";
import PostsServices from "@/services/posts.services";
import useLocaleStorageCache from "./cache.composable";

export default function useFetchPost() {
const { endLoading, isLoading, startLoading } = useLoading();
const { getCache, setCache } = useLocaleStorageCache();

const post = ref(null);

async function fetchPost(id) {
const cachedPosts = getCache("cached-posts") || {};

if (cachedPosts[`post-${id}`]) {
post.value = cachedPosts[`post-${id}`];
return;
} else {
startLoading();
return PostsServices.getOneById(id)
.then((response) => {
post.value = response.data;
setCache('cached-posts', { ...cachedPosts, [`post-${id}`]: response.data })
return response;
})
.finally(() => {
endLoading();
});
}
}

return {
postIsLoading: isLoading,
post,
fetchPost,
};
}
12 changes: 12 additions & 0 deletions src/locales/en/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export default {
"Password": "Password",
"Submit": "Submit",
"Cancel": "Cancel",
"Comments": "Comments",
"Confirm":"Confirm",
"Rejected":"Rejected",
"Pending":"Pending",
"Dashboard": "Dashboard",
"Users": "Users",
"Authorization Error": "Authorization Error",
Expand All @@ -16,6 +20,8 @@ export default {
"Email": "Email",
"Loading": "Loading",
"Previous": "Previous",
"SearchInputPlaceholder":'Search...',
"Text":"Text",
"Next": "Next",
"From": "From",
"Size": "Size",
Expand All @@ -34,4 +40,10 @@ export default {
"Dark": "Dark",
"System": "System",
"Settings": "Settings",
"Actions":"Actions",
"EmailPlaceholder":"Email",
"Post":"Post",
"RejectAll":"Reject All",
"ConfirmAll":"Confirm All",
"Confirmed":"Confirmed"
};
Loading