commit
24a5024e88
@ -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>
|
||||
|
||||
|
133
fe/package-lock.json
generated
133
fe/package-lock.json
generated
@ -18,8 +18,9 @@
|
||||
"motion": "^12.15.0",
|
||||
"pinia": "^3.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^12.0.0-alpha.2",
|
||||
"vue-i18n": "^9.9.1",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"workbox-background-sync": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -2585,11 +2586,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/bundle-utils/node_modules/@intlify/message-compiler": {
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.4.tgz",
|
||||
"integrity": "sha512-vtZCt7NqWhKEtHa3SD/322DlgP5uR9MqWxnE0y8Q0tjDs9H5Lxhss+b5wv8rmuXRoHKLESNgw9d+EN9ybBbj9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "9.14.4",
|
||||
"@intlify/shared": "9.14.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
|
||||
"integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.14.4",
|
||||
"source-map-js": "^1.0.2"
|
||||
@ -2601,54 +2618,10 @@
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/bundle-utils/node_modules/@intlify/shared": {
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.4.tgz",
|
||||
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "12.0.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-12.0.0-alpha.2.tgz",
|
||||
"integrity": "sha512-sPWvQ1Z4Wyw9Kp8xqjAk2sMOeZ4pO7p/NL3Eol8l9a7iPyMTuHyJ2DZVbOBG6zDnCupvLAqnRMAT1LAgvx0QRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "12.0.0-alpha.2",
|
||||
"@intlify/shared": "12.0.0-alpha.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "12.0.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-12.0.0-alpha.2.tgz",
|
||||
"integrity": "sha512-PD9C+oQbb7BF52hec0+vLnScaFkvnfX+R7zSbODYuRo/E2niAtGmHd0wPvEMsDhf9Z9b8f/qyDsVeZnD/ya9Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "12.0.0-alpha.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "12.0.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-12.0.0-alpha.2.tgz",
|
||||
"integrity": "sha512-P2DULVX9nz3y8zKNqLw9Es1aAgQ1JGC+kgpx5q7yLmrnAKkPR5MybQWoEhxanefNJgUY5ehsgo+GKif59SrncA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@ -2696,18 +2669,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/unplugin-vue-i18n/node_modules/@intlify/shared": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.4.tgz",
|
||||
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/unplugin-vue-i18n/node_modules/pathe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||
@ -2715,29 +2676,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-core": {
|
||||
"version": "12.0.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/vue-i18n-core/-/vue-i18n-core-12.0.0-alpha.2.tgz",
|
||||
"integrity": "sha512-y1NPEcPbD8xqWGiaEREkA9WxxWbxmd8IurN176w39MenZKEf5P20rYyk0w4r718+w/9jjm0m5zR1ed6uMaZT2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "12.0.0-alpha.2",
|
||||
"@intlify/shared": "12.0.0-alpha.2",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-core/node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@ -11211,6 +11149,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -12820,14 +12764,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "12.0.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-12.0.0-alpha.2.tgz",
|
||||
"integrity": "sha512-ZSaZrDV/PhD4hLVybo84bkaNRnkGDF7GpI0Fcmn9Yj+2Kq5C3nE7I5iRbo+DiHyRGrwZ/IV1VP3naFAhcNJGsg==",
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.4.tgz",
|
||||
"integrity": "sha512-B934C8yUyWLT0EMud3DySrwSUJI7ZNiWYsEEz2gknTthqKiG4dzWE/WSa8AzCuSQzwBEv4HtG1jZDhgzPfWSKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "12.0.0-alpha.2",
|
||||
"@intlify/shared": "12.0.0-alpha.2",
|
||||
"@intlify/vue-i18n-core": "12.0.0-alpha.2",
|
||||
"@intlify/core-base": "9.14.4",
|
||||
"@intlify/shared": "9.14.4",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -12894,6 +12837,18 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuedraggable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
|
||||
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sortablejs": "1.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
@ -31,6 +31,7 @@
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.9.1",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"workbox-background-sync": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -74,4 +75,4 @@
|
||||
"workbox-routing": "^7.3.0",
|
||||
"workbox-strategies": "^7.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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