Refactor GroupsPage and ListDetailPage for improved loading and error handling
This commit is contained in:
parent
6e79fbfa04
commit
d8db5721f4
@ -3,13 +3,11 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="mitlist pwa">
|
<meta name="description" content="mitlist pwa">
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="msapplication-tap-highlight" content="no">
|
<meta name="msapplication-tap-highlight" content="no">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<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>
|
<title>mitlist</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -2,17 +2,26 @@
|
|||||||
<main class="container page-padding">
|
<main class="container page-padding">
|
||||||
<!-- <h1 class="mb-3">Your Groups</h1> -->
|
<!-- <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">
|
<div class="alert-content">
|
||||||
<svg class="icon" aria-hidden="true">
|
<svg class="icon" aria-hidden="true">
|
||||||
<use xlink:href="#icon-alert-triangle" />
|
<use xlink:href="#icon-alert-triangle" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ fetchError }}
|
{{ fetchError }}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<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">
|
<svg class="icon icon-lg" aria-hidden="true">
|
||||||
<use xlink:href="#icon-clipboard" />
|
<use xlink:href="#icon-clipboard" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -26,7 +35,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 class="neo-groups-grid">
|
||||||
<div v-for="group in groups" :key="group.id" class="neo-group-card" @click="selectGroup(group)">
|
<div v-for="group in groups" :key="group.id" class="neo-group-card" @click="selectGroup(group)">
|
||||||
<h1 class="neo-group-header">{{ group.name }}</h1>
|
<h1 class="neo-group-header">{{ group.name }}</h1>
|
||||||
@ -77,7 +87,8 @@
|
|||||||
aria-labelledby="createGroupTitle">
|
aria-labelledby="createGroupTitle">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="createGroupTitle">{{ t('groupsPage.createDialog.title') }}</h3>
|
<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">
|
<svg class="icon" aria-hidden="true">
|
||||||
<use xlink:href="#icon-close" />
|
<use xlink:href="#icon-close" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -86,14 +97,16 @@
|
|||||||
<form @submit.prevent="handleCreateGroup">
|
<form @submit.prevent="handleCreateGroup">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<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
|
<input type="text" id="newGroupNameInput" v-model="newGroupName" class="form-input" required
|
||||||
ref="newGroupNameInputRef" />
|
ref="newGroupNameInputRef" />
|
||||||
<p v-if="createGroupFormError" class="form-error-text">{{ createGroupFormError }}</p>
|
<p v-if="createGroupFormError" class="form-error-text">{{ createGroupFormError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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">
|
<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>
|
<span v-if="creatingGroup" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
|
||||||
{{ t('groupsPage.createDialog.createButton') }}
|
{{ t('groupsPage.createDialog.createButton') }}
|
||||||
@ -134,8 +147,8 @@ interface Group {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
const groups = ref<Group[]>([]);
|
const groups = ref<Group[]>([]);
|
||||||
const loading = ref(false);
|
|
||||||
const fetchError = ref<string | null>(null);
|
const fetchError = ref<string | null>(null);
|
||||||
|
const isInitiallyLoading = ref(true); // Added for managing initial load state
|
||||||
|
|
||||||
const showCreateGroupDialog = ref(false);
|
const showCreateGroupDialog = ref(false);
|
||||||
const newGroupName = ref('');
|
const newGroupName = ref('');
|
||||||
@ -157,29 +170,51 @@ const cachedGroups = useStorage<Group[]>('cached-groups', []);
|
|||||||
const cachedTimestamp = useStorage<number>('cached-groups-timestamp', 0);
|
const cachedTimestamp = useStorage<number>('cached-groups-timestamp', 0);
|
||||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||||
|
|
||||||
// Load cached data immediately if available and not expired
|
// Attempt to initialize groups from valid cache
|
||||||
const loadCachedData = () => {
|
const now = Date.now();
|
||||||
const now = Date.now();
|
if (cachedGroups.value && (now - cachedTimestamp.value) < CACHE_DURATION) {
|
||||||
if (cachedGroups.value.length > 0 && (now - cachedTimestamp.value) < CACHE_DURATION) {
|
if (cachedGroups.value.length > 0) {
|
||||||
groups.value = cachedGroups.value;
|
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
|
// 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 {
|
try {
|
||||||
const response = await apiClient.get(API_ENDPOINTS.GROUPS.BASE);
|
const response = await apiClient.get(API_ENDPOINTS.GROUPS.BASE);
|
||||||
groups.value = response.data;
|
const freshGroups = response.data as Group[];
|
||||||
|
groups.value = freshGroups;
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
cachedGroups.value = response.data;
|
cachedGroups.value = freshGroups;
|
||||||
cachedTimestamp.value = Date.now();
|
cachedTimestamp.value = Date.now();
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
fetchError.value = err instanceof Error ? err.message : t('groupsPage.errors.fetchFailed');
|
let message = t('groupsPage.errors.fetchFailed');
|
||||||
// If we have cached data, keep showing it even if refresh failed
|
// Attempt to get a more specific error message from the API response
|
||||||
if (cachedGroups.value.length === 0) {
|
if (err.response && err.response.data && err.response.data.detail) {
|
||||||
groups.value = [];
|
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'
|
type: 'success'
|
||||||
});
|
});
|
||||||
// Optionally refresh the groups list to show the new list
|
// Optionally refresh the groups list to show the new list
|
||||||
fetchGroups();
|
fetchGroups(); // Refresh data, isRetryAttempt will be false
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
// Load cached data immediately
|
// groups might have been populated from cache synchronously above.
|
||||||
loadCachedData();
|
// 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.
|
||||||
// Then fetch fresh data in background
|
fetchGroups();
|
||||||
await fetchGroups();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -47,8 +47,8 @@
|
|||||||
<li class="neo-list-item new-item-input-container">
|
<li class="neo-list-item new-item-input-container">
|
||||||
<label class="neo-checkbox-label">
|
<label class="neo-checkbox-label">
|
||||||
<input type="checkbox" disabled />
|
<input type="checkbox" disabled />
|
||||||
<input type="text" class="neo-new-item-input" :placeholder="t('listsPage.addItemPlaceholder')" ref="newItemInputRefs"
|
<input type="text" class="neo-new-item-input" :placeholder="t('listsPage.addItemPlaceholder')"
|
||||||
:data-list-id="list.id" @keyup.enter="addNewItem(list, $event)"
|
ref="newItemInputRefs" :data-list-id="list.id" @keyup.enter="addNewItem(list, $event)"
|
||||||
@blur="handleNewItemBlur(list, $event)" @click.stop />
|
@blur="handleNewItemBlur(list, $event)" @click.stop />
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
@ -465,7 +465,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.page-padding {
|
.page-padding {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-width: 1200px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user