Refactor GroupsPage and ListDetailPage for improved loading and error handling
This commit is contained in:
parent
6e79fbfa04
commit
d8db5721f4
@ -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>
|
||||
|
||||
|
@ -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
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user