Skip to content

feat: add posts layout preference#100

Open
AlejandroAkbal wants to merge 1 commit intomainfrom
auto-triage/25-feature-option-to-change-list-layout-for-different-platform-e-g-pc
Open

feat: add posts layout preference#100
AlejandroAkbal wants to merge 1 commit intomainfrom
auto-triage/25-feature-option-to-change-list-layout-for-different-platform-e-g-pc

Conversation

@AlejandroAkbal
Copy link
Member

@AlejandroAkbal AlejandroAkbal commented Mar 14, 2026

Summary

  • add a persisted posts layout preference with list and grid options in settings
  • add an in-page toggle on the posts feed and keep list mode virtualized while grid mode uses manual load more
  • preserve existing empty and blocklist-aware states while avoiding grid-breaking promoted content placement

Testing

  • pnpm build (fails due to pre-existing assets/js/nuxt-image/imgproxy.provider.ts Buffer/browser build issue)

Summary by CodeRabbit

  • New Features
    • Users can now toggle between grid and list layouts for viewing posts
    • Posts layout preference is saved and persists across sessions
    • Added posts layout option to the settings page
    • Introduced "Load more" button for grid layout pagination
    • Improved empty state messaging with contextual details about blocked tags and hidden posts

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

Walkthrough

Introduces a posts layout feature enabling users to toggle between 'list' and 'grid' views. Adds a PostsLayout type and persisted state to useUserSettings composable, implements conditional rendering and pagination logic in the posts page, and adds a settings control to configure the default layout.

Changes

Cohort / File(s) Summary
User Settings State Management
composables/useUserSettings.ts
Added PostsLayout type union ('list' | 'grid'), introduced postsLayout reactive ref with localStorage persistence under key 'settings-postsLayout', and exposed it in composable's return object.
Posts Page Layout Implementation
pages/posts/[domain].vue
Integrated layout toggle functionality with computed property isGridPostsLayout and togglePostsLayout function. Implemented dual rendering paths: list layout preserves existing virtualized structure; grid layout introduces separate pagination with manual "Load more" controls. Added layout-aware pagination logic, conditional auto-pagination disabling for grid mode, and enhanced empty-state messaging to account for blocked tags and hidden posts.
Settings UI Control
pages/settings.vue
Added postsLayout binding from useUserSettings() composable and integrated SettingSelect component to allow users to configure default layout preference with 'list' and 'grid' options.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • chore: remove unnecessary param #72: Follows the same pattern of extending useUserSettings composable with a new user-setting (autoplayAnimatedMedia) and adding corresponding settings UI control in pages/settings.vue.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a user preference to select between list and grid layouts for posts, which is reflected across the composable, page component, and settings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auto-triage/25-feature-option-to-change-list-layout-for-different-platform-e-g-pc
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch auto-triage/25-feature-option-to-change-list-layout-for-different-platform-e-g-pc
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pages/posts/[domain].vue (1)

995-1023: ⚠️ Potential issue | 🔴 Critical

Add missing computed properties for isBlockedTagSelected and hasHiddenPosts.

The template references isBlockedTagSelected and hasHiddenPosts at lines 995, 1014, and 1017, but these variables are not defined anywhere in the component's script section. This will cause runtime errors. Define these as computed properties based on the existing blocklist logic:

  • isBlockedTagSelected: should evaluate whether any of the selected tags are in the blocklist
  • hasHiddenPosts: should track whether posts were filtered out due to blocklisted tags
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/posts/`[domain].vue around lines 995 - 1023, The template references
undefined computed properties isBlockedTagSelected and hasHiddenPosts causing
runtime errors; add them to the component's script as computed properties:
implement isBlockedTagSelected to return true if any selected tag (e.g.,
selectedTags or currentFilters) intersects the user's tag blocklist (use the
same blocklist variable/name already present in the component), and implement
hasHiddenPosts to reflect whether any posts were removed by that blocklist
(e.g., compare the full posts list length vs filtered list length or track
filteredOutCount used in the existing filter logic); wire both into the
component's computed section so the template bindings (isBlockedTagSelected,
hasHiddenPosts) resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@composables/useUserSettings.ts`:
- Around line 19-21: The postsLayout ref created via
useLocalStorage<PostsLayout>('settings-postsLayout', 'list', ...) can end up
with an invalid string if localStorage was tampered with; update useUserSettings
to validate the stored value against the allowed PostsLayout union (e.g.,
'list'|'grid') when initializing postsLayout and whenever it changes, and if the
value is not one of the allowed options, replace it with the default
('list')—implement this by adding a small validator/helper inside
useUserSettings that checks postsLayout (and/or useLocalStorage's initial read)
and assigns the default when invalid so downstream consumers of postsLayout
always receive a safe value.

In `@pages/posts/`[domain].vue:
- Around line 1121-1137: The grid template rendering allRows via the v-for in
the template v-else (the OL that instantiates PostComponent for each post) is
unvirtualized and can cause performance issues when many pages are loaded;
either document this limitation and add a UI warning/soft cap on how many pages
can be loaded in grid mode (e.g., disable "Load more" after N pages and show a
message) or implement virtualization for the grid (replace the v-for over
allRows with a virtual-scroller component such as vue-virtual-scroller /
VirtualList that renders PostComponent on demand); update the code managing
allRows/pagination and the UI around the grid (the OL + PostComponent usage and
the logic that appends to allRows) to enforce the cap or integrate the virtual
list so only visible items are mounted.

In `@pages/settings.vue`:
- Around line 16-20: postsLayoutOptions is hardcoded and duplicates the
PostsLayout type, risking divergence; replace the literal array with a derived
list from the PostsLayout type/enum (e.g., use Object.values(PostsLayout) or map
its keys) so options always reflect the type, and import/ensure PostsLayout is
available in pages/settings.vue; update any usage assuming strings if
PostsLayout is an enum so types line up with postsLayoutOptions.

---

Outside diff comments:
In `@pages/posts/`[domain].vue:
- Around line 995-1023: The template references undefined computed properties
isBlockedTagSelected and hasHiddenPosts causing runtime errors; add them to the
component's script as computed properties: implement isBlockedTagSelected to
return true if any selected tag (e.g., selectedTags or currentFilters)
intersects the user's tag blocklist (use the same blocklist variable/name
already present in the component), and implement hasHiddenPosts to reflect
whether any posts were removed by that blocklist (e.g., compare the full posts
list length vs filtered list length or track filteredOutCount used in the
existing filter logic); wire both into the component's computed section so the
template bindings (isBlockedTagSelected, hasHiddenPosts) resolve.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ef792c5-2bd4-4461-b021-b1e635a3dee7

📥 Commits

Reviewing files that changed from the base of the PR and between 5cc34fe and d547b28.

📒 Files selected for processing (3)
  • composables/useUserSettings.ts
  • pages/posts/[domain].vue
  • pages/settings.vue

Comment on lines +19 to +21
postsLayout = useLocalStorage<PostsLayout>('settings-postsLayout', 'list', {
writeDefaults: false
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating stored value against allowed options.

If localStorage contains an invalid value (e.g., user manually edited it), the ref will hold that invalid value. This could cause unexpected behavior in components that switch on postsLayout.

🛡️ Optional defensive validation
+const validLayouts: PostsLayout[] = ['list', 'grid']
+
 if (import.meta.client) {
   // ... other settings ...
-  postsLayout = useLocalStorage<PostsLayout>('settings-postsLayout', 'list', {
-    writeDefaults: false
-  })
+  const storedLayout = useLocalStorage<PostsLayout>('settings-postsLayout', 'list', {
+    writeDefaults: false
+  })
+  // Reset to default if invalid value stored
+  if (!validLayouts.includes(storedLayout.value)) {
+    storedLayout.value = 'list'
+  }
+  postsLayout = storedLayout
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useUserSettings.ts` around lines 19 - 21, The postsLayout ref
created via useLocalStorage<PostsLayout>('settings-postsLayout', 'list', ...)
can end up with an invalid string if localStorage was tampered with; update
useUserSettings to validate the stored value against the allowed PostsLayout
union (e.g., 'list'|'grid') when initializing postsLayout and whenever it
changes, and if the value is not one of the allowed options, replace it with the
default ('list')—implement this by adding a small validator/helper inside
useUserSettings that checks postsLayout (and/or useLocalStorage's initial read)
and assigns the default when invalid so downstream consumers of postsLayout
always receive a safe value.

Comment on lines +1121 to 1137
<template v-else>
<ol class="grid grid-cols-2 gap-4 sm:grid-cols-3">
<li
v-for="(post, postIndex) in allRows"
:key="selectedBooru.domain + '-' + post.id"
>
<PostComponent
:post="post"
:postIndex="postIndex"
:selectedTags="selectedTags"
@addTag="onPostAddTag"
@openTagInNewTab="onPostOpenTagInNewTab"
@setTag="onPostSetTag"
/>

</li>
</ol>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Grid layout does not virtualize, which may impact performance with many posts.

The grid renders all posts in allRows without virtualization. With manual "Load more", this is acceptable, but users loading many pages may experience degraded performance.

Consider documenting this as a known limitation or adding a warning/limit on how many pages can be loaded in grid mode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/posts/`[domain].vue around lines 1121 - 1137, The grid template
rendering allRows via the v-for in the template v-else (the OL that instantiates
PostComponent for each post) is unvirtualized and can cause performance issues
when many pages are loaded; either document this limitation and add a UI
warning/soft cap on how many pages can be loaded in grid mode (e.g., disable
"Load more" after N pages and show a message) or implement virtualization for
the grid (replace the v-for over allRows with a virtual-scroller component such
as vue-virtual-scroller / VirtualList that renders PostComponent on demand);
update the code managing allRows/pagination and the UI around the grid (the OL +
PostComponent usage and the logic that appends to allRows) to enforce the cap or
integrate the virtual list so only visible items are mounted.

Comment on lines +16 to +20
const { postFullSizeImages, postsPerPage, postsLayout, autoplayAnimatedMedia, blockAiGeneratedImages } =
useUserSettings()
const { isPremium } = useUserData()
const { selectedList, selectedBlockList, defaultBlockList, customBlockList, resetCustomBlockList } = useBlockLists()
const postsLayoutOptions = ['list', 'grid']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider deriving options from the PostsLayout type.

The postsLayoutOptions array duplicates the values from PostsLayout type. If the type changes, this array could fall out of sync.

♻️ Suggested improvement for type safety
+import type { PostsLayout } from '~/composables/useUserSettings'
+
 const { postFullSizeImages, postsPerPage, postsLayout, autoplayAnimatedMedia, blockAiGeneratedImages } =
   useUserSettings()
 const { isPremium } = useUserData()
 const { selectedList, selectedBlockList, defaultBlockList, customBlockList, resetCustomBlockList } = useBlockLists()
-const postsLayoutOptions = ['list', 'grid']
+const postsLayoutOptions: PostsLayout[] = ['list', 'grid']
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { postFullSizeImages, postsPerPage, postsLayout, autoplayAnimatedMedia, blockAiGeneratedImages } =
useUserSettings()
const { isPremium } = useUserData()
const { selectedList, selectedBlockList, defaultBlockList, customBlockList, resetCustomBlockList } = useBlockLists()
const postsLayoutOptions = ['list', 'grid']
import type { PostsLayout } from '~/composables/useUserSettings'
const { postFullSizeImages, postsPerPage, postsLayout, autoplayAnimatedMedia, blockAiGeneratedImages } =
useUserSettings()
const { isPremium } = useUserData()
const { selectedList, selectedBlockList, defaultBlockList, customBlockList, resetCustomBlockList } = useBlockLists()
const postsLayoutOptions: PostsLayout[] = ['list', 'grid']
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/settings.vue` around lines 16 - 20, postsLayoutOptions is hardcoded and
duplicates the PostsLayout type, risking divergence; replace the literal array
with a derived list from the PostsLayout type/enum (e.g., use
Object.values(PostsLayout) or map its keys) so options always reflect the type,
and import/ensure PostsLayout is available in pages/settings.vue; update any
usage assuming strings if PostsLayout is an enum so types line up with
postsLayoutOptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant