From 7ada83336f5f474b86bebeb88d1f396317810f0f Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:06:46 -0700 Subject: [PATCH] feat: add saved post folders --- components/pages/posts/post/PostSave.vue | 4 +- composables/usePocketbase.ts | 123 ++++++++++++++- pages/premium/saved-posts/[domain].vue | 182 ++++++++++++++++++++--- 3 files changed, 288 insertions(+), 21 deletions(-) diff --git a/components/pages/posts/post/PostSave.vue b/components/pages/posts/post/PostSave.vue index 645c91d1..8ec82059 100644 --- a/components/pages/posts/post/PostSave.vue +++ b/components/pages/posts/post/PostSave.vue @@ -12,7 +12,7 @@ const { $pocketBase } = useNuxtApp() - const { savedPostList } = usePocketbase() + const { savedPostList, removeSavedPostFolder } = usePocketbase() const { isPremium } = useUserData() const postInSavedList = computed(() => { @@ -60,6 +60,8 @@ const response = await $pocketBase.collection('posts').delete(postInSavedList.value.id) if (response === true) { + removeSavedPostFolder(postInSavedList.value.original_domain, postInSavedList.value.original_id) + savedPostList.value = savedPostList.value.filter((savedPost) => savedPost.id !== postInSavedList.value.id) } } diff --git a/composables/usePocketbase.ts b/composables/usePocketbase.ts index ec40bc5a..4fbcb4a5 100644 --- a/composables/usePocketbase.ts +++ b/composables/usePocketbase.ts @@ -1,5 +1,15 @@ import type { ISimplePocketbasePost } from '~/assets/js/pocketbase.dto' +type SavedPostFolderAssignments = Record + +function buildSavedPostFolderKey(originalDomain: string, originalId: number) { + return `${originalDomain}:${originalId}` +} + +function normalizeSavedPostFolderName(folderName: string) { + return folderName.trim().replace(/\s+/g, ' ') +} + export default function () { const { $pocketBase } = useNuxtApp() @@ -8,6 +18,106 @@ export default function () { const subscription_expires_at = useState('pocketbase-subscription_expires_at', () => null) const savedPostList = useLocalState('pocketbase-savedPostList', []) + const savedPostFolders = useLocalState('pocketbase-savedPostFolders', []) + const savedPostFolderAssignments = useLocalState( + 'pocketbase-savedPostFolderAssignments', + {} + ) + + function getSavedPostFolder(originalDomain: string, originalId: number) { + return savedPostFolderAssignments.value[buildSavedPostFolderKey(originalDomain, originalId)] ?? null + } + + function setSavedPostFolder(originalDomain: string, originalId: number, folderName: string) { + const normalizedFolderName = normalizeSavedPostFolderName(folderName) + + if (!normalizedFolderName) { + return false + } + + const existingFolder = + savedPostFolders.value.find((folder) => folder.toLowerCase() === normalizedFolderName.toLowerCase()) ?? null + + if (!existingFolder) { + return false + } + + savedPostFolderAssignments.value = { + ...savedPostFolderAssignments.value, + [buildSavedPostFolderKey(originalDomain, originalId)]: existingFolder + } + + return true + } + + function removeSavedPostFolder(originalDomain: string, originalId: number) { + const key = buildSavedPostFolderKey(originalDomain, originalId) + + if (!(key in savedPostFolderAssignments.value)) { + return + } + + const nextAssignments = { ...savedPostFolderAssignments.value } + + delete nextAssignments[key] + + savedPostFolderAssignments.value = nextAssignments + } + + function createSavedPostFolder(folderName: string) { + const normalizedFolderName = normalizeSavedPostFolderName(folderName) + + if (!normalizedFolderName) { + return false + } + + if (savedPostFolders.value.some((folder) => folder.toLowerCase() === normalizedFolderName.toLowerCase())) { + return false + } + + savedPostFolders.value = savedPostFolders.value.concat(normalizedFolderName) + + return true + } + + function deleteSavedPostFolder(folderName: string) { + const normalizedFolderName = normalizeSavedPostFolderName(folderName) + + const existingFolder = + savedPostFolders.value.find((folder) => folder.toLowerCase() === normalizedFolderName.toLowerCase()) ?? null + + if (!existingFolder) { + return + } + + savedPostFolders.value = savedPostFolders.value.filter((folder) => folder !== existingFolder) + + const nextAssignments = { ...savedPostFolderAssignments.value } + + Object.entries(nextAssignments).forEach(([key, folder]) => { + if (folder === existingFolder) { + delete nextAssignments[key] + } + }) + + savedPostFolderAssignments.value = nextAssignments + } + + function pruneSavedPostFolderAssignments() { + const savedPostKeys = new Set( + savedPostList.value.map((savedPost) => buildSavedPostFolderKey(savedPost.original_domain, savedPost.original_id)) + ) + + const nextAssignments = { ...savedPostFolderAssignments.value } + + Object.keys(nextAssignments).forEach((key) => { + if (!savedPostKeys.has(key)) { + delete nextAssignments[key] + } + }) + + savedPostFolderAssignments.value = nextAssignments + } if ($pocketBase.authStore.isValid) { // @@ -27,6 +137,8 @@ export default function () { requestKey: 'savedPostList' }) + + pruneSavedPostFolderAssignments() }) } } @@ -36,6 +148,15 @@ export default function () { license, subscription_expires_at, - savedPostList + savedPostList, + savedPostFolders, + savedPostFolderAssignments, + + getSavedPostFolder, + setSavedPostFolder, + removeSavedPostFolder, + + createSavedPostFolder, + deleteSavedPostFolder } } diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index f3fc62d4..47d2b447 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -24,7 +24,17 @@ const { toggle: toggleSearchMenu } = useSearchMenu() const { addUrlToPageHistory } = usePageHistory() - const { savedPostList } = usePocketbase() + const { + savedPostList, + savedPostFolders, + getSavedPostFolder, + setSavedPostFolder, + removeSavedPostFolder, + createSavedPostFolder, + deleteSavedPostFolder + } = usePocketbase() + + const selectedFolderFilter = ref(null) /** * URL @@ -83,9 +93,7 @@ return [] } - return tags - .split('|') - .map((tag) => new Tag({ name: tag }).toJSON()) + return tags.split('|').map((tag) => new Tag({ name: tag }).toJSON()) }) const selectedPage = computed(() => { @@ -303,6 +311,51 @@ window.location.reload() } + function onCreateFolderClick() { + const folderNamePrompt = prompt('Folder name') + + if (folderNamePrompt == null) { + return + } + + const wasFolderCreated = createSavedPostFolder(folderNamePrompt) + + if (!wasFolderCreated) { + toast.error('Invalid or duplicate folder name') + return + } + + const normalizedFolderName = folderNamePrompt.trim().replace(/\s+/g, ' ') + + selectedFolderFilter.value = normalizedFolderName + } + + function onDeleteSelectedFolderClick() { + if (!selectedFolderFilter.value) { + return + } + + if (!confirm(`Delete folder "${selectedFolderFilter.value}"?`)) { + return + } + + deleteSavedPostFolder(selectedFolderFilter.value) + selectedFolderFilter.value = null + } + + function onPostFolderChange(post: IPost, selectedFolderName: string) { + if (!selectedFolderName) { + removeSavedPostFolder(post.domain, post.id) + return + } + + const wasFolderAssigned = setSavedPostFolder(post.domain, post.id, selectedFolderName) + + if (!wasFolderAssigned) { + toast.error('Folder not found') + } + } + /** * Data fetching */ @@ -471,6 +524,24 @@ }) }) + const filteredRows = computed(() => { + if (!selectedFolderFilter.value) { + return allRows.value + } + + return allRows.value.filter((post) => getSavedPostFolder(post.domain, post.id) === selectedFolderFilter.value) + }) + + watch(savedPostFolders, (folders) => { + if (!selectedFolderFilter.value) { + return + } + + if (!folders.includes(selectedFolderFilter.value)) { + selectedFolderFilter.value = null + } + }) + const parentRef = ref(null) const parentOffsetRef = ref(0) @@ -479,8 +550,10 @@ }) const rowVirtualizerOptions = computed(() => { + const includeLoaderRow = hasNextPage.value + return { - count: hasNextPage.value ? allRows.value.length + 1 : allRows.value.length, + count: includeLoaderRow ? filteredRows.value.length + 1 : filteredRows.value.length, estimateSize: () => 600, @@ -511,11 +584,6 @@ return } - // Skip if there is no data - if (!allRows.value) { - return - } - const [lastItem] = [...virtualRows.value].reverse() if (!lastItem) { @@ -528,7 +596,7 @@ // AND there's no error (to prevent infinite retry loop) // THEN load next page automatically if ( - lastItem.index >= allRows.value.length - 1 && + lastItem.index >= filteredRows.value.length - 1 && hasNextPage.value && !isFetchingNextPage.value && !isError.value @@ -736,6 +804,49 @@ +
+
+ + + + + + + +
+ +

Assign a folder from each post card.

+
+
-