Refactor GroupsPage and ListDetailPage for improved loading and error handling

This commit is contained in:
mohamad 2025-06-05 00:46:23 +02:00
parent 6e79fbfa04
commit d8db5721f4
4 changed files with 928 additions and 713 deletions

View File

@ -3,13 +3,11 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/fe/public/favicon.ico" /> <!-- Or your favicon -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="mitlist pwa">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- PWA manifest and theme color will be injected by vite-plugin-pwa -->
<title>mitlist</title>
</head>

View File

@ -2,17 +2,26 @@
<main class="container page-padding">
<!-- <h1 class="mb-3">Your Groups</h1> -->
<div v-if="fetchError" class="alert alert-error mb-3" role="alert">
<!-- Initial Loading Spinner -->
<div v-if="isInitiallyLoading && groups.length === 0 && !fetchError" class="text-center my-5">
<p>{{ t('groupsPage.loadingText', 'Loading groups...') }}</p>
<span class="spinner-dots-lg" role="status"><span /><span /><span /></span>
</div>
<!-- Error Display -->
<div v-else-if="fetchError" class="alert alert-error mb-3" role="alert">
<div class="alert-content">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-alert-triangle" />
</svg>
{{ fetchError }}
</div>
<button type="button" class="btn btn-sm btn-danger" @click="fetchGroups">{{ t('groupsPage.retryButton') }}</button>
<button type="button" class="btn btn-sm btn-danger" @click="() => fetchGroups(true)">{{
t('groupsPage.retryButton') }}</button>
</div>
<div v-else-if="groups.length === 0" class="card empty-state-card">
<!-- Empty State: show if not initially loading, no error, and groups genuinely empty -->
<div v-else-if="!isInitiallyLoading && groups.length === 0" class="card empty-state-card">
<svg class="icon icon-lg" aria-hidden="true">
<use xlink:href="#icon-clipboard" />
</svg>
@ -26,7 +35,8 @@
</button>
</div>
<div v-else class="mb-3">
<!-- Groups List -->
<div v-else-if="groups.length > 0" class="mb-3">
<div class="neo-groups-grid">
<div v-for="group in groups" :key="group.id" class="neo-group-card" @click="selectGroup(group)">
<h1 class="neo-group-header">{{ group.name }}</h1>
@ -77,7 +87,8 @@
aria-labelledby="createGroupTitle">
<div class="modal-header">
<h3 id="createGroupTitle">{{ t('groupsPage.createDialog.title') }}</h3>
<button class="close-button" @click="closeCreateGroupDialog" :aria-label="t('groupsPage.createDialog.closeButtonLabel')">
<button class="close-button" @click="closeCreateGroupDialog"
:aria-label="t('groupsPage.createDialog.closeButtonLabel')">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-close" />
</svg>
@ -86,14 +97,16 @@
<form @submit.prevent="handleCreateGroup">
<div class="modal-body">
<div class="form-group">
<label for="newGroupNameInput" class="form-label">{{ t('groupsPage.createDialog.groupNameLabel') }}</label>
<label for="newGroupNameInput" class="form-label">{{ t('groupsPage.createDialog.groupNameLabel')
}}</label>
<input type="text" id="newGroupNameInput" v-model="newGroupName" class="form-input" required
ref="newGroupNameInputRef" />
<p v-if="createGroupFormError" class="form-error-text">{{ createGroupFormError }}</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-neutral" @click="closeCreateGroupDialog">{{ t('groupsPage.createDialog.cancelButton') }}</button>
<button type="button" class="btn btn-neutral" @click="closeCreateGroupDialog">{{
t('groupsPage.createDialog.cancelButton') }}</button>
<button type="submit" class="btn btn-primary ml-2" :disabled="creatingGroup">
<span v-if="creatingGroup" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
{{ t('groupsPage.createDialog.createButton') }}
@ -134,8 +147,8 @@ interface Group {
const router = useRouter();
const notificationStore = useNotificationStore();
const groups = ref<Group[]>([]);
const loading = ref(false);
const fetchError = ref<string | null>(null);
const isInitiallyLoading = ref(true); // Added for managing initial load state
const showCreateGroupDialog = ref(false);
const newGroupName = ref('');
@ -157,29 +170,51 @@ const cachedGroups = useStorage<Group[]>('cached-groups', []);
const cachedTimestamp = useStorage<number>('cached-groups-timestamp', 0);
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
// Load cached data immediately if available and not expired
const loadCachedData = () => {
const now = Date.now();
if (cachedGroups.value.length > 0 && (now - cachedTimestamp.value) < CACHE_DURATION) {
groups.value = cachedGroups.value;
// Attempt to initialize groups from valid cache
const now = Date.now();
if (cachedGroups.value && (now - cachedTimestamp.value) < CACHE_DURATION) {
if (cachedGroups.value.length > 0) {
groups.value = JSON.parse(JSON.stringify(cachedGroups.value)); // Deep copy for safety from potential proxy issues
isInitiallyLoading.value = false;
} else { // Valid cache, but it's empty
groups.value = []; // Ensure it's an empty array
isInitiallyLoading.value = false; // We know it's empty, not "loading"
}
};
}
// If cache is stale or not present, groups.value remains [], and isInitiallyLoading remains true.
// Fetch fresh data from API
const fetchGroups = async () => {
const fetchGroups = async (isRetryAttempt = false) => {
// If it's a retry triggered by user AND the list is currently empty, set loading to true to show spinner.
// Or, if it's the very first load (isInitiallyLoading is still true) AND list is empty (no cache hit).
if ((isRetryAttempt && groups.value.length === 0) || (isInitiallyLoading.value && groups.value.length === 0)) {
isInitiallyLoading.value = true;
}
// If groups.value has items (from cache), isInitiallyLoading is false, and this fetch acts as a background update.
fetchError.value = null; // Clear previous error before new attempt
try {
const response = await apiClient.get(API_ENDPOINTS.GROUPS.BASE);
groups.value = response.data;
const freshGroups = response.data as Group[];
groups.value = freshGroups;
// Update cache
cachedGroups.value = response.data;
cachedGroups.value = freshGroups;
cachedTimestamp.value = Date.now();
} catch (err) {
fetchError.value = err instanceof Error ? err.message : t('groupsPage.errors.fetchFailed');
// If we have cached data, keep showing it even if refresh failed
if (cachedGroups.value.length === 0) {
groups.value = [];
} catch (err: any) {
let message = t('groupsPage.errors.fetchFailed');
// Attempt to get a more specific error message from the API response
if (err.response && err.response.data && err.response.data.detail) {
message = err.response.data.detail;
} else if (err.message) {
message = err.message;
}
fetchError.value = message;
// If fetch fails, groups.value will retain its current state (either from cache or empty).
// The template will then show the error message.
} finally {
isInitiallyLoading.value = false; // Mark loading as complete for this attempt
}
};
@ -292,15 +327,14 @@ const onListCreated = (newList: any) => {
type: 'success'
});
// Optionally refresh the groups list to show the new list
fetchGroups();
fetchGroups(); // Refresh data, isRetryAttempt will be false
};
onMounted(async () => {
// Load cached data immediately
loadCachedData();
// Then fetch fresh data in background
await fetchGroups();
onMounted(() => {
// groups might have been populated from cache synchronously above.
// isInitiallyLoading reflects whether cache was used or if we need to show a spinner.
// Call fetchGroups to get fresh data or perform initial load if cache was missed.
fetchGroups();
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -47,8 +47,8 @@
<li class="neo-list-item new-item-input-container">
<label class="neo-checkbox-label">
<input type="checkbox" disabled />
<input type="text" class="neo-new-item-input" :placeholder="t('listsPage.addItemPlaceholder')" ref="newItemInputRefs"
:data-list-id="list.id" @keyup.enter="addNewItem(list, $event)"
<input type="text" class="neo-new-item-input" :placeholder="t('listsPage.addItemPlaceholder')"
ref="newItemInputRefs" :data-list-id="list.id" @keyup.enter="addNewItem(list, $event)"
@blur="handleNewItemBlur(list, $event)" @click.stop />
</label>
</li>
@ -465,7 +465,7 @@ onUnmounted(() => {
.page-padding {
padding: 1rem;
max-width: 1200px;
max-width: 1600px;
margin: 0 auto;
}