Repository: samuellealb/bluelist
Pull request #14: Enhances Follows View with List Suggestions
Author: login: samuellealb
Created: 2025-04-24T20:48:19Z | Merged: 2025-04-24T21:11:19Z

Improves the user experience by integrating list suggestions directly into the follows view.

- Removes the separate "Suggestions" view and adds a "Suggest Lists" button to the "Follows" view.
- Implements a loading state and disables buttons during suggestion processing.
- Allows users to easily add follows to suggested lists via chips.
- Adds pagination and refreshing of follows data.
- Updates the pagination component to show the correct type label.

Relates to #6


Diff:
diff --git a/src/assets/styles/action-buttons.css b/src/assets/styles/action-buttons.css
index 00aac83..44100ef 100644
--- a/src/assets/styles/action-buttons.css
+++ b/src/assets/styles/action-buttons.css
@@ -5,6 +5,10 @@
   font-family: var(--font-mono);
 }
 
+.buttons-panel--disabled {
+  cursor: not-allowed;
+}
+
 .action-button {
   border: none;
   border-radius: var(--border-radius);
@@ -50,3 +54,9 @@
   color: var(--primary-color);
   border-radius: 0;
 }
+
+.action-button--disabled {
+  background-color: transparent;
+  color: var(--disabled-color);
+  pointer-events: none;
+}
diff --git a/src/assets/styles/data-card.css b/src/assets/styles/data-card.css
index f2112f5..3d62a9f 100644
--- a/src/assets/styles/data-card.css
+++ b/src/assets/styles/data-card.css
@@ -21,6 +21,26 @@
   background-color: var(--primary-color);
 }
 
+.data-card--loading::before {
+  pointer-events: none;
+}
+.data-card--loading::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: var(--card-bg-transparent-80);
+  cursor: not-allowed;
+}
+
+.data-card--loading:hover {
+  border: none;
+  box-shadow: none;
+  transform: none;
+}
+
 .data-card:hover {
   transform: translateY(-2px);
   box-shadow: 0 4px 8px var(--card-shadow);
@@ -134,23 +154,3 @@
 .data-card--follow .data-card__info {
   flex: 1;
 }
-
-/* Suggestion specific styles */
-.data-card--suggestion .data-card__header {
-  display: flex;
-  align-items: flex-start;
-}
-
-.data-card--suggestion .data-card__avatar {
-  width: 48px;
-  height: 48px;
-  border-radius: 0;
-  background-color: var(--card-color);
-  margin-right: 16px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-size: 1.2rem;
-  border: 1px solid var(--primary-color);
-  color: var(--primary-color);
-}
diff --git a/src/assets/styles/data-display.css b/src/assets/styles/data-display.css
index 58e88c0..856a682 100644
--- a/src/assets/styles/data-display.css
+++ b/src/assets/styles/data-display.css
@@ -84,14 +84,16 @@
   margin-bottom: 1.5rem;
 }
 
+/* Common button styles - apply to all buttons */
 .data-display__refresh-button,
 .data-display__accept-all-button,
-.data-display__toggle-all-button {
-  border: none;
-  border-radius: var(--border-radius);
+.data-display__toggle-all-button,
+.data-display__suggestions-button {
+  border: 1px solid var(--primary-color);
+  border-radius: 0;
   font-weight: 500;
   cursor: pointer;
-  transition: var(--transition);
+  transition: background-color 0.2s, transform 0.1s;
   font-size: 0.9rem;
   text-align: center;
   font-family: var(--font-mono);
@@ -100,36 +102,43 @@
   padding: 0.5rem 0.75rem;
   display: flex;
   align-items: center;
-  position: relative;
-  border: 1px solid var(--primary-color);
-  border-radius: 0;
-  transition: background-color 0.2s, transform 0.1s;
 }
 
 .data-display__refresh-button:hover,
 .data-display__accept-all-button:hover,
-.data-display__toggle-all-button:hover {
+.data-display__toggle-all-button:hover,
+.data-display__suggestions-button:hover {
   background-color: var(--primary-transparent-30);
   transform: translateY(-1px);
 }
 
 .data-display__refresh-button:active,
 .data-display__accept-all-button:active,
-.data-display__toggle-all-button:active {
+.data-display__toggle-all-button:active,
+.data-display__suggestions-button:active {
   transform: translateY(1px);
 }
 
 .data-display__refresh-button:disabled,
 .data-display__accept-all-button:disabled,
-.data-display__toggle-all-button:disabled {
+.data-display__toggle-all-button:disabled,
+.data-display__suggestions-button:disabled {
   cursor: not-allowed;
   opacity: 0.6;
   transform: none;
 }
 
+.data-display__suggestions-button--processing {
+  background-color: var(--primary-transparent-20);
+  color: var(--text-light);
+  cursor: not-allowed;
+}
+
+/* Common icon styles - apply to all button icons */
 .data-display__refresh-icon,
 .data-display__accept-all-icon,
-.data-display__toggle-all-icon {
+.data-display__toggle-all-icon,
+.data-display__suggestions-icon {
   margin-right: 0.5rem;
   font-size: 1rem;
   color: var(--accent-color);
@@ -152,25 +161,6 @@
   border-left-color: #c62828;
 }
 
-.data-display__refresh-button:hover {
-  background-color: var(--primary-transparent-20);
-}
-
-.data-display__refresh-button:active {
-  transform: translateY(1px);
-}
-
-.data-display__refresh-button:disabled {
-  background-color: var(--text-light-transparent-20);
-  color: var(--text-light);
-  border-color: var(--text-light);
-  cursor: not-allowed;
-}
-
-.data-display__refresh-icon {
-  font-size: 1.1rem;
-}
-
 .data-display__refresh-text {
   font-family: var(--font-mono);
 }
diff --git a/src/components/ActionButton.vue b/src/components/ActionButton.vue
index b739994..2f4a55e 100644
--- a/src/components/ActionButton.vue
+++ b/src/components/ActionButton.vue
@@ -1,11 +1,17 @@
 <template>
-  <button class="action-button" @click="onClick">
+  <button
+    class="action-button"
+    :class="state.isProcessingSuggestions ? 'action-button--processing' : ''"
+    :disabled="state.isProcessingSuggestions"
+    @click="onClick"
+  >
     <span class="action-button__icon">{{ icon }}</span>
     <span class="action-button__label">{{ label }}</span>
   </button>
 </template>
 
 <script setup lang="ts">
+import { state } from '~/src/store';
 defineOptions({
   name: 'ActionButton',
 });
diff --git a/src/components/ButtonsPanel.vue b/src/components/ButtonsPanel.vue
index a0fa8fa..fe13a2f 100644
--- a/src/components/ButtonsPanel.vue
+++ b/src/components/ButtonsPanel.vue
@@ -1,19 +1,20 @@
 <template>
-  <div class="buttons-panel">
-    <ActionButton icon="[L]" label="Feed" @click="displayFeed" />
-    <ActionButton icon="{#}" label="Lists" @click="displayLists" />
-    <ActionButton icon="(U)" label="Follows" @click="displayFollows" />
-    <ActionButton icon="<*>" label="Suggestions" @click="displaySuggestions" />
+  <div
+    class="buttons-panel"
+    :class="{ 'buttons-panel--disabled': state.isProcessingSuggestions }"
+  >
+    <ActionButton icon="[#]" label="Lists" @click="displayLists" />
+    <ActionButton icon="[o]" label="Follows" @click="displayFollows" />
+    <ActionButton icon="[≡]" label="Feed" @click="displayFeed" />
   </div>
 </template>
 
 <script setup lang="ts">
 import '~/src/assets/styles/action-buttons.css';
 import { getTimeline, getLists, getFollows } from '~/src/lib/bsky';
-import { curateUserLists } from '~/src/lib/openai';
 import { state } from '~/src/store';
 import ActionButton from '~/src/components/ActionButton.vue';
-import type { DataObject } from '~/src/types';
+import type { DataObject } from '~/src/types/index';
 
 defineOptions({
   name: 'ButtonsPanel',
@@ -100,27 +101,9 @@ const displayFollows = async (forceRefresh = false, page?: number) => {
   }
 };
 
-const displaySuggestions = async (forceRefresh = false) => {
-  try {
-    if (!forceRefresh && state.suggestionsJSON) {
-      state.displayData = JSON.parse(state.suggestionsJSON);
-      return;
-    }
-
-    setLoading();
-    const result = await curateUserLists();
-    state.displayData = result.displayData;
-    state.suggestionsJSON = result.suggestionsJSON;
-  } catch (error) {
-    setError(error as Error);
-    console.error('Error in displaySuggestions:', error);
-  }
-};
-
 defineExpose({
   displayFeed,
   displayLists,
   displayFollows,
-  displaySuggestions,
 });
 </script>
diff --git a/src/components/Dashboard.vue b/src/components/Dashboard.vue
index 3cc7be0..00714dc 100644
--- a/src/components/Dashboard.vue
+++ b/src/components/Dashboard.vue
@@ -9,7 +9,6 @@
 
     <div v-else class="dashboard">
       <div class="dashboard__actions-panel">
-        <h2>Actions [/]</h2>
         <ButtonsPanel ref="buttonsPanelRef" />
       </div>
 
@@ -33,6 +32,33 @@ defineOptions({
 
 const buttonsPanelRef = ref<InstanceType<typeof ButtonsPanel> | null>(null);
 
+// Function to safely load lists with a force refresh
+const loadListsView = async () => {
+  // Use nextTick to ensure DOM is updated and components are available
+  await nextTick();
+
+  // Small delay to ensure component is fully initialized
+  setTimeout(() => {
+    if (state.isLoggedIn && buttonsPanelRef.value) {
+      buttonsPanelRef.value.displayLists(true); // Force refresh
+    }
+  }, 100);
+};
+
+onMounted(() => {
+  loadListsView();
+});
+
+// Watch for login state changes to display Lists when user logs in
+watch(
+  () => state.isLoggedIn,
+  (isLoggedIn) => {
+    if (isLoggedIn) {
+      loadListsView();
+    }
+  }
+);
+
 const handleRefresh = async (type: string, page?: number) => {
   if (!buttonsPanelRef.value) return;
 
@@ -50,9 +76,6 @@ const handleRefresh = async (type: string, page?: number) => {
       await buttonsPanelRef.value.displayFollows(forceRefresh, page);
       break;
     }
-    case 'suggestions':
-      await buttonsPanelRef.value.displaySuggestions(true);
-      break;
     default:
       console.error('Unknown data type for refresh:', type);
   }
diff --git a/src/components/DataCard.vue b/src/components/DataCard.vue
index d153fcd..147ea3e 100644
--- a/src/components/DataCard.vue
+++ b/src/components/DataCard.vue
@@ -5,7 +5,7 @@
       'data-card--timeline': item.type === 'timeline',
       'data-card--list': item.type === 'lists',
       'data-card--follow': item.type === 'follows',
-      'data-card--suggestion': item.type === 'suggestions',
+      'data-card--loading': state.isProcessingSuggestions,
     }"
   >
     <!-- Timeline Item -->
@@ -49,36 +49,27 @@
         <p class="data-card__subtitle">@{{ followItem.handle }}</p>
         <p v-if="followItem.description">{{ followItem.description }}</p>
       </div>
-    </div>
 
-    <!-- Suggestion Item -->
-    <div v-else-if="item.type === 'suggestions' && suggestionItem">
-      <div class="data-card__header">
-        <div class="data-card__avatar">[o]</div>
-        <div>
-          <h3 class="data-card__title">{{ suggestionItem.name }}</h3>
-          <p v-if="suggestionItem.description" class="data-card__subtitle">
-            {{ suggestionItem.description }}
-          </p>
-        </div>
+      <!-- Footer for follows with list chips -->
+      <div class="data-card__footer">
+        <ListChips
+          v-if="followItem"
+          ref="followListChipsRef"
+          :profile-did="followItem.did"
+          :profile-name="followItem.name || followItem.handle"
+          :title="'Add to Lists'"
+          :show-no-lists-message="false"
+          :hide-warning-but-show-lists="true"
+          @add-to-list="handleAddToList"
+          @update:enabled-lists="updateFollowEnabledLists"
+        />
       </div>
-
-      <ListChips
-        v-if="suggestionItem"
-        ref="listChipsRef"
-        :profile-did="suggestionItem.did"
-        :profile-name="suggestionItem.name"
-        :lists="suggestionItem.suggestedLists"
-        :title="'Suggested Lists'"
-        :show-no-lists-message="true"
-        @add-to-list="handleAddToList"
-        @update:enabled-lists="updateEnabledLists"
-      />
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
+import { state } from '../store';
 import '~/src/assets/styles/data-card.css';
 import ListChips from '~/src/components/ListChips.vue';
 import type {
@@ -86,8 +77,7 @@ import type {
   TimelineItem,
   ListItem,
   FollowItem,
-  SuggestionItem,
-} from '~/src/types';
+} from '~/src/types/index';
 
 defineOptions({
   name: 'DataCard',
@@ -128,13 +118,6 @@ const followItem = computed(() => {
   return null;
 });
 
-const suggestionItem = computed(() => {
-  if (props.item.type === 'suggestions' && props.item.data[props.index]) {
-    return props.item.data[props.index] as SuggestionItem;
-  }
-  return null;
-});
-
 const formatDate = (dateString: string): string => {
   const date = new Date(dateString);
   return new Intl.DateTimeFormat('en-US', {
@@ -145,8 +128,8 @@ const formatDate = (dateString: string): string => {
   }).format(date);
 };
 
-const listChipsRef = ref<InstanceType<typeof ListChips> | null>(null);
-const enabledLists = ref<Record<string, boolean>>({});
+const followListChipsRef = ref<InstanceType<typeof ListChips> | null>(null);
+const followEnabledLists = ref<Record<string, boolean>>({});
 
 /**
  * Relay the add-to-list event from ListChips
@@ -160,23 +143,26 @@ const handleAddToList = (
 };
 
 /**
- * Update the enabledLists state from ListChips
+ * Update the followEnabledLists state from ListChips for follows view
  */
-const updateEnabledLists = (lists: Record<string, boolean>) => {
-  enabledLists.value = lists;
+const updateFollowEnabledLists = (lists: Record<string, boolean>) => {
+  followEnabledLists.value = lists;
 };
 
 /**
- * Toggle all list suggestions - delegate to ListChips component
+ * Toggle all list suggestions for follows - delegate to follow ListChips component
  */
-const toggleAllLists = (enable?: boolean, invertEach: boolean = false) => {
-  if (listChipsRef.value) {
-    listChipsRef.value.toggleAllLists(enable, invertEach);
+const toggleFollowAllLists = (
+  enable?: boolean,
+  invertEach: boolean = false
+) => {
+  if (followListChipsRef.value) {
+    followListChipsRef.value.toggleAllLists(enable, invertEach);
   }
 };
 
 defineExpose({
-  enabledLists,
-  toggleAllLists,
+  followEnabledLists,
+  toggleFollowAllLists,
 });
 </script>
diff --git a/src/components/DataDisplay.vue b/src/components/DataDisplay.vue
index baa508e..baaecff 100644
--- a/src/components/DataDisplay.vue
+++ b/src/components/DataDisplay.vue
@@ -21,23 +21,23 @@
     <div v-else-if="dataObject" class="data-display__container">
       <div class="data-display__header">
         <h2>{{ getDataTitle(dataObject.type) }}</h2>
-        <button
-          class="data-display__refresh-button"
-          title="Refresh data from API"
-          :disabled="isLoading || isAcceptingAll"
-          @click="handleRefresh"
-        >
-          <span class="data-display__refresh-icon">[R]</span>
-          <span class="data-display__refresh-text">Refresh</span>
-        </button>
+        <div class="data-display__header-buttons">
+          <button
+            class="data-display__refresh-button"
+            title="Refresh data from API"
+            :disabled="
+              isLoading || isAcceptingAll || state.isProcessingSuggestions
+            "
+            @click="handleRefresh"
+          >
+            <span class="data-display__refresh-icon">[R]</span>
+            <span class="data-display__refresh-text">Refresh</span>
+          </button>
+        </div>
       </div>
 
       <Pagination
-        v-if="
-          (dataObject.type === 'follows' || dataObject.type === 'lists') &&
-          dataObject.data &&
-          dataObject.data.length > 0
-        "
+        v-if="hasData"
         :current-page="
           dataObject.type === 'follows'
             ? state.follows.currentPage
@@ -48,6 +48,7 @@
             ? state.follows.prefetchedPages
             : state.lists.prefetchedPages
         "
+        :data-type="dataObject.type"
         :is-loading="isLoading"
         :has-more-pages="!!dataObject.pagination?.hasMorePages"
         :total-items="dataObject.pagination?.totalPrefetched"
@@ -56,30 +57,52 @@
       />
 
       <div
-        v-if="dataObject.type === 'suggestions'"
+        v-if="dataObject.type === 'follows'"
         class="data-display__action-buttons"
       >
+        <button
+          class="data-display__suggestions-button"
+          title="Get suggestions for your follows"
+          :class="{
+            'data-display__suggestions-button--processing':
+              state.isProcessingSuggestions,
+          }"
+          :disabled="
+            isLoading || isAcceptingAll || state.isProcessingSuggestions
+          "
+          @click="handleSuggestions"
+        >
+          <span class="data-display__suggestions-icon">[*]</span>
+          <span class="data-display__suggestions-text">{{
+            state.isProcessingSuggestions ? 'Processing' : 'Suggest Lists'
+          }}</span>
+        </button>
         <button
           class="data-display__toggle-all-button"
-          title="Toggle all suggestions between enabled and disabled states"
-          :disabled="isLoading || isAcceptingAll"
-          @click="handleToggleAll"
+          title="Toggle all list options between enabled and disabled states"
+          :disabled="
+            isLoading || isAcceptingAll || state.isProcessingSuggestions
+          "
+          @click="handleToggleFollowsLists"
         >
           <span class="data-display__toggle-all-icon">[↕]</span>
           <span class="data-display__toggle-all-text"> Toggle All </span>
         </button>
         <button
           class="data-display__accept-all-button"
-          title="Accept all enabled suggestions"
-          :disabled="isLoading || isAcceptingAll"
-          @click="handleAcceptAll"
+          title="Add all follows to their enabled lists"
+          :disabled="
+            isLoading || isAcceptingAll || state.isProcessingSuggestions
+          "
+          @click="handleAcceptFollowsLists"
         >
           <span class="data-display__accept-all-icon">[+]</span>
           <span class="data-display__accept-all-text">
-            {{ isAcceptingAll ? 'Processing...' : 'Accept All' }}
+            {{ isAcceptingAll ? 'Processing...' : 'Apply All' }}
           </span>
         </button>
       </div>
+
       <div
         v-if="acceptAllResult || detailedResults.length > 0"
         class="data-display__feedback-container"
@@ -135,7 +158,7 @@
         />
 
         <Pagination
-          v-if="dataObject.type === 'follows' || dataObject.type === 'lists'"
+          v-if="hasData"
           :current-page="
             dataObject.type === 'follows'
               ? state.follows.currentPage
@@ -146,6 +169,7 @@
               ? state.follows.prefetchedPages
               : state.lists.prefetchedPages
           "
+          :data-type="dataObject.type"
           :is-loading="isLoading"
           :has-more-pages="!!dataObject.pagination?.hasMorePages"
           :total-items="dataObject.pagination?.totalPrefetched"
@@ -184,8 +208,14 @@ import { state } from '~/src/store';
 import '~/src/assets/styles/data-display.css';
 import DataCard from '~/src/components/DataCard.vue';
 import Pagination from '~/src/components/Pagination.vue';
-import type { DataObject, SuggestionItem, ListItem } from '~/src/types';
+import type {
+  DataObject,
+  ListItem,
+  FollowItem,
+  SuggestionItem,
+} from '~/src/types/index';
 import { addUserToList } from '~/src/lib/bsky';
+import { curateUserLists } from '~/src/lib/openai';
 import type { ComponentPublicInstance } from 'vue';
 
 defineOptions({
@@ -207,6 +237,16 @@ const dataObject = computed<DataObject | null>(() => {
   return null;
 });
 
+const hasData = computed(() => {
+  return (
+    dataObject.value &&
+    (dataObject.value.type === 'follows' ||
+      dataObject.value.type === 'lists') &&
+    dataObject.value.data &&
+    dataObject.value.data.length > 0
+  );
+});
+
 const isLoading = computed(() => {
   return props.data?.type === 'loading';
 });
@@ -240,7 +280,22 @@ const acceptAllResult = ref('');
 const acceptAllError = ref(false);
 const detailedResults = ref<DetailedResult[]>([]);
 
-const handleAcceptAll = async () => {
+/**
+ * Toggle all list options across all follow profile cards
+ */
+const handleToggleFollowsLists = () => {
+  if (!dataObject.value || !dataCardRefs.value.length) return;
+
+  for (const cardRef of dataCardRefs.value) {
+    if (!cardRef) continue;
+    cardRef.toggleFollowAllLists(undefined, true);
+  }
+};
+
+/**
+ * Accept all enabled list assignments for follows
+ */
+const handleAcceptFollowsLists = async () => {
   if (!dataObject.value || !dataCardRefs.value.length || isAcceptingAll.value)
     return;
 
@@ -256,109 +311,50 @@ const handleAcceptAll = async () => {
   for (const cardRef of dataCardRefs.value) {
     if (!cardRef) continue;
 
-    const suggestionItem = dataObject.value.data[
+    const followItem = dataObject.value.data[
       cardRef.$props.index
-    ] as SuggestionItem;
-    if (!suggestionItem) continue;
+    ] as FollowItem;
+    if (!followItem) continue;
 
-    if (
-      suggestionItem.suggestedLists &&
-      suggestionItem.suggestedLists.length > 0
-    ) {
-      for (const list of suggestionItem.suggestedLists) {
-        const key = `${suggestionItem.did}-${list.uri}`;
-
-        if (cardRef.enabledLists[key]) {
-          try {
-            const result = await addUserToList(suggestionItem.did, list.uri);
-            const isDuplicate = result.includes('already in this list');
-
-            if (isDuplicate) {
-              duplicateCount++;
-            } else {
-              successCount++;
-            }
-
-            detailedResults.value.push({
-              profileName: suggestionItem.name,
-              profileDid: suggestionItem.did,
-              listName: list.name,
-              listUri: list.uri,
-              success: true,
-              message: result,
-              isDuplicate: isDuplicate,
-            });
-          } catch (error) {
-            errorCount++;
-            detailedResults.value.push({
-              profileName: suggestionItem.name,
-              profileDid: suggestionItem.did,
-              listName: list.name,
-              listUri: list.uri,
-              success: false,
-              message: (error as Error).message,
-              isDuplicate: false,
-            });
-            console.error('Error adding to list during Accept All:', error);
-          }
-        }
-      }
-    } else {
-      const profilePrefix = `${suggestionItem.did}-`;
-
-      for (const key in cardRef.enabledLists) {
-        if (key.startsWith(profilePrefix) && cardRef.enabledLists[key]) {
-          const listUri = key.substring(profilePrefix.length);
-
-          try {
-            let listName = 'Unknown List';
-            if (state.listsJSON) {
-              try {
-                const listsData = JSON.parse(state.listsJSON);
-                if (listsData.data && Array.isArray(listsData.data)) {
-                  const foundList = listsData.data.find(
-                    (list: ListItem) => list.uri === listUri
-                  );
-                  if (foundList) {
-                    listName = foundList.name;
-                  }
-                }
-              } catch (error) {
-                console.error('Error finding list name:', error);
-              }
-            }
-
-            const result = await addUserToList(suggestionItem.did, listUri);
-            const isDuplicate = result.includes('already in this list');
-
-            if (isDuplicate) {
-              duplicateCount++;
-            } else {
-              successCount++;
-            }
-
-            detailedResults.value.push({
-              profileName: suggestionItem.name,
-              profileDid: suggestionItem.did,
-              listName: listName,
-              listUri: listUri,
-              success: true,
-              message: result,
-              isDuplicate: isDuplicate,
-            });
-          } catch (error) {
-            errorCount++;
-            detailedResults.value.push({
-              profileName: suggestionItem.name,
-              profileDid: suggestionItem.did,
-              listName: 'Unknown List',
-              listUri: listUri,
-              success: false,
-              message: (error as Error).message,
-              isDuplicate: false,
-            });
-            console.error('Error adding to list during Accept All:', error);
+    const profilePrefix = `${followItem.did}-`;
+
+    for (const key in cardRef.followEnabledLists) {
+      if (key.startsWith(profilePrefix) && cardRef.followEnabledLists[key]) {
+        const listUri = key.substring(profilePrefix.length);
+
+        try {
+          const listName = findListNameByUri(listUri);
+
+          const result = await addUserToList(followItem.did, listUri);
+          const isDuplicate = result.includes('already in this list');
+
+          if (isDuplicate) {
+            duplicateCount++;
+          } else {
+            successCount++;
           }
+
+          detailedResults.value.push({
+            profileName: followItem.name || followItem.handle,
+            profileDid: followItem.did,
+            listName: listName,
+            listUri: listUri,
+            success: true,
+            message: result,
+            isDuplicate: isDuplicate,
+          });
+        } catch (error) {
+          errorCount++;
+          detailedResults.value.push({
+            profileName: followItem.name || followItem.handle,
+            profileDid: followItem.did,
+            listName: 'Unknown List',
+            listUri: listUri,
+            success: false,
+            message: (error as Error).message,
+            isDuplicate: false,
+          });
+          console.error('Error adding follow to list:', error);
         }
       }
     }
@@ -367,40 +363,27 @@ const handleAcceptAll = async () => {
   if ((successCount > 0 || duplicateCount > 0) && errorCount === 0) {
     acceptAllResult.value = `Successfully processed ${
       successCount + duplicateCount
-    } suggestions (${successCount} added, ${duplicateCount} already in lists)`;
+    } list assignments (${successCount} added, ${duplicateCount} already in lists)`;
     acceptAllError.value = false;
   } else if ((successCount > 0 || duplicateCount > 0) && errorCount > 0) {
     acceptAllResult.value = `Processed ${
       successCount + duplicateCount
-    } suggestions with ${errorCount} errors (${successCount} added, ${duplicateCount} already in lists)`;
+    } list assignments with ${errorCount} errors (${successCount} added, ${duplicateCount} already in lists)`;
     acceptAllError.value = true;
   } else if (successCount === 0 && duplicateCount === 0 && errorCount > 0) {
-    acceptAllResult.value = `Failed to add any suggestions. ${errorCount} errors occurred.`;
+    acceptAllResult.value = `Failed to add any profiles to lists. ${errorCount} errors occurred.`;
     acceptAllError.value = true;
   } else if (successCount === 0 && duplicateCount > 0 && errorCount === 0) {
-    acceptAllResult.value = `All ${duplicateCount} selected profiles are already in their suggested lists.`;
+    acceptAllResult.value = `All ${duplicateCount} selected profiles are already in their selected lists.`;
     acceptAllError.value = false;
   } else {
-    acceptAllResult.value = 'No suggestions were processed';
+    acceptAllResult.value = 'No list assignments were processed';
     acceptAllError.value = false;
   }
 
   isAcceptingAll.value = false;
 };
 
-/**
- * Toggle all suggestions across all profile cards
- */
-const handleToggleAll = () => {
-  if (!dataObject.value || !dataCardRefs.value.length) return;
-
-  // Call toggleAllLists with invertEach=true for each card to toggle individual suggestions
-  for (const cardRef of dataCardRefs.value) {
-    if (!cardRef) continue;
-    cardRef.toggleAllLists(undefined, true);
-  }
-};
-
 const handleRefresh = () => {
   if (dataObject.value) {
     emit('refresh', dataObject.value.type);
@@ -417,8 +400,6 @@ const getDataTitle = (type: string): string => {
       return 'Your Lists';
     case 'follows':
       return 'Your Follows';
-    case 'suggestions':
-      return 'Your Suggested Lists';
     default:
       return 'Data';
   }
@@ -455,4 +436,124 @@ watch(
     }
   }
 );
+
+const handleSuggestions = async () => {
+  if (!dataObject.value || dataObject.value.type !== 'follows') return;
+
+  state.isProcessingSuggestions = true;
+
+  try {
+    const { suggestionsJSON } = await curateUserLists();
+
+    if (!suggestionsJSON) {
+      throw new Error('Failed to get suggestions');
+    }
+
+    const suggestionsData = JSON.parse(suggestionsJSON);
+
+    applySuggestionsToFollows(suggestionsData);
+
+    acceptAllResult.value =
+      'Successfully applied list suggestions to current follows';
+    acceptAllError.value = false;
+  } catch (error) {
+    console.error('Error getting suggestions:', error);
+    acceptAllResult.value = `Error getting suggestions: ${
+      (error as Error).message
+    }`;
+    acceptAllError.value = true;
+  } finally {
+    state.isProcessingSuggestions = false;
+  }
+};
+
+/**
+ * Apply suggestions to the follows view by toggling the relevant list chips
+ * @param suggestionsData The suggestions data containing recommended lists for profiles
+ */
+const applySuggestionsToFollows = (suggestionsData: {
+  data: SuggestionItem[];
+}) => {
+  if (!dataCardRefs.value.length || !dataObject.value) return;
+
+  const suggestionsMap = suggestionsData.data.reduce(
+    (map: Record<string, SuggestionItem>, item: SuggestionItem) => {
+      map[item.did] = item;
+      return map;
+    },
+    {}
+  );
+
+  for (const cardRef of dataCardRefs.value) {
+    if (!cardRef) continue;
+
+    const followItem = dataObject.value.data[
+      cardRef.$props.index
+    ] as FollowItem;
+    if (!followItem) continue;
+
+    const matchingSuggestion = suggestionsMap[followItem.did];
+
+    if (
+      matchingSuggestion &&
+      matchingSuggestion.suggestedLists &&
+      matchingSuggestion.suggestedLists.length > 0
+    ) {
+      if (!cardRef.followEnabledLists) continue;
+
+      for (const list of matchingSuggestion.suggestedLists) {
+        const key = `${followItem.did}-${list.uri}`;
+
+        if (typeof cardRef.followEnabledLists[key] !== 'undefined') {
+          cardRef.followEnabledLists[key] = true;
+        }
+      }
+    }
+  }
+};
+
+/**
+ * Helper function to find a list name based on URI
+ * Uses multiple sources to ensure we find the correct name
+ */
+const findListNameByUri = (listUri: string): string => {
+  if (state.lists.allLists && state.lists.allLists.length > 0) {
+    const foundList = state.lists.allLists.find(
+      (list: ListItem) => list.uri === listUri
+    );
+    if (foundList) {
+      return foundList.name;
+    }
+  }
+
+  if (state.listsJSON) {
+    try {
+      const listsData = JSON.parse(state.listsJSON);
+      const listMap = new Map<string, string>();
+      if (listsData.data && Array.isArray(listsData.data)) {
+        for (const list of listsData.data) {
+          listMap.set(list.uri, list.name);
+        }
+
+        const foundName = listMap.get(listUri);
+        if (foundName) {
+          return foundName;
+        }
+      }
+    } catch (error) {
+      console.error('Error parsing lists JSON:', error);
+    }
+  }
+
+  try {
+    const uriParts = listUri.split('/');
+    if (uriParts.length > 0) {
+      return `List ${uriParts[uriParts.length - 1]}`;
+    }
+  } catch (error) {
+    console.error('Error extracting list name from URI:', error);
+  }
+
+  return 'Unknown List';
+};
 </script>
diff --git a/src/components/ListChips.vue b/src/components/ListChips.vue
index b3df83f..9ec3706 100644
--- a/src/components/ListChips.vue
+++ b/src/components/ListChips.vue
@@ -74,6 +74,42 @@
         </div>
       </div>
     </div>
+    <div
+      v-else-if="availableLists.length > 0 && hideWarningButShowLists"
+      class="list-chips"
+    >
+      <h4 class="list-chips__title">{{ title }}:</h4>
+      <div class="list-chips__buttons">
+        <button
+          v-for="(list, idx) in availableLists"
+          :key="idx"
+          class="list-chips__button"
+          :class="{
+            'list-chips__button--disabled':
+              !enabledLists[`${profileDid}-${list.uri}`],
+          }"
+          :title="list.description"
+          :disabled="loading"
+          @click="handleAddToList(profileDid, list.uri, list.name)"
+        >
+          <span
+            class="list-chips__checkbox"
+            @click.stop="toggleListEnabled(profileDid, list.uri)"
+          />
+          <span class="list-chips__name">
+            {{ list.name }}
+          </span>
+          <span v-if="activeList === list.uri && loading">...</span>
+        </button>
+      </div>
+      <div
+        v-if="actionMessage"
+        class="list-chips__action-message"
+        :class="{ 'list-chips__action-message--error': actionError }"
+      >
+        {{ actionMessage }}
+      </div>
+    </div>
   </div>
 </template>
 
@@ -81,7 +117,7 @@
 import '~/src/assets/styles/list-chips.css';
 import { addUserToList } from '~/src/lib/bsky';
 import { state } from '~/src/store';
-import type { SuggestedList, ListItem } from '~/src/types';
+import type { SuggestedList, ListItem } from '~/src/types/index';
 
 defineOptions({
   name: 'ListChips',
@@ -93,6 +129,7 @@ const props = defineProps<{
   lists?: SuggestedList[];
   title?: string;
   showNoListsMessage?: boolean;
+  hideWarningButShowLists?: boolean;
 }>();
 
 const emit = defineEmits<{
diff --git a/src/components/Pagination.vue b/src/components/Pagination.vue
index e84aada..1466d19 100644
--- a/src/components/Pagination.vue
+++ b/src/components/Pagination.vue
@@ -3,13 +3,15 @@
     <span class="pagination__info">
       Page {{ currentPage }} of {{ totalPages }}
       <span v-if="totalItems" class="pagination__details">
-        ({{ totalItems }} users loaded)
+        ({{ totalItems }} {{ typeLabel }} loaded)
       </span>
     </span>
     <div class="pagination__buttons">
       <button
         class="pagination__button"
-        :disabled="currentPage === 1 || isLoading"
+        :disabled="
+          currentPage === 1 || isLoading || state.isProcessingSuggestions
+        "
         title="First page"
         @click="handlePageChange(1)"
       >
@@ -17,7 +19,9 @@
       </button>
       <button
         class="pagination__button"
-        :disabled="currentPage === 1 || isLoading"
+        :disabled="
+          currentPage === 1 || isLoading || state.isProcessingSuggestions
+        "
         title="Previous page"
         @click="handlePageChange(currentPage - 1)"
       >
@@ -25,7 +29,7 @@
       </button>
       <button
         class="pagination__button"
-        :disabled="!hasMorePages || isLoading"
+        :disabled="!hasMorePages || isLoading || state.isProcessingSuggestions"
         title="Next page"
         @click="handlePageChange(currentPage + 1)"
       >
@@ -33,7 +37,7 @@
       </button>
       <button
         class="pagination__button"
-        :disabled="isLastPage || isLoading"
+        :disabled="isLastPage || isLoading || state.isProcessingSuggestions"
         title="Skip to last loaded page"
         @click="handlePageChange(lastPage)"
       >
@@ -59,6 +63,7 @@ const props = defineProps<{
   hasMorePages: boolean;
   totalItems?: number;
   isTop?: boolean;
+  dataType?: string;
 }>();
 
 const emit = defineEmits<{
@@ -68,20 +73,16 @@ const emit = defineEmits<{
 const lastPage = computed(() => {
   if (props.currentPage === undefined) return 1;
 
-  // Calculate last page based on the view type (follows or lists)
   if (state.follows.currentPage === props.currentPage) {
-    // This is the follows view
     if (!state.follows.allFollows.length) return 1;
     return Math.ceil(
       state.follows.allFollows.length / state.follows.itemsPerPage
     );
   } else if (state.lists.currentPage === props.currentPage) {
-    // This is the lists view
     if (!state.lists.allLists.length) return 1;
     return Math.ceil(state.lists.allLists.length / state.lists.itemsPerPage);
   }
 
-  // Default fallback
   return props.totalPages || 1;
 });
 
@@ -89,6 +90,10 @@ const isLastPage = computed(() => {
   return props.currentPage >= lastPage.value || !props.hasMorePages;
 });
 
+const typeLabel = computed(() => {
+  return props.dataType === 'follows' ? 'profiles' : 'lists';
+});
+
 const handlePageChange = (newPage: number) => {
   emit('pageChange', newPage);
 };
diff --git a/src/lib/bsky.ts b/src/lib/bsky.ts
index 6193061..fbcb269 100644
--- a/src/lib/bsky.ts
+++ b/src/lib/bsky.ts
@@ -1,11 +1,13 @@
 import { state } from '~/src/store';
 import { AtpAgent } from '@atproto/api';
-import type { DataObject } from '~/src/types';
+import type { DataObject } from '~/src/types/index';
 
 /**
  * Logs in a user to Bluesky using their credentials
- * @param identifier Email address of the user
- * @param password User's password
+ * @param identifier - Email address of the user
+ * @param password - User's password
+ * @returns {Promise<void>} - A Promise that resolves when login completes
+ * @throws {Error} - Throws when rate limit is exceeded or other login errors occur
  */
 export async function loginUser(
   identifier: string,
@@ -52,6 +54,8 @@ export async function loginUser(
 
 /**
  * Checks for an existing login session and restores it
+ * @returns {void} - No return value
+ * @throws {Error} - If session data cannot be parsed
  */
 export function checkLoginSession(): void {
   const storedData = localStorage.getItem('loginData');
@@ -79,7 +83,8 @@ export function checkLoginSession(): void {
 
 /**
  * Fetches the user's timeline from Bluesky
- * @returns Formatted timeline data and raw JSON
+ * @returns {Promise<{displayData: DataObject, timelineJSON: string}>} - Object containing formatted timeline data and raw JSON
+ * @throws {Error} - When user is not logged in, token has expired, or there's an error fetching the feed
  */
 export const getTimeline = async (): Promise<{
   displayData: DataObject;
@@ -128,10 +133,11 @@ export const getTimeline = async (): Promise<{
 
 /**
  * Fetches the user's lists from Bluesky with pagination and prefetching support
- * @param page Optional page number to fetch (defaults to current page in state)
- * @param refresh Whether to refresh and start from the first page
- * @param prefetchOnly If true, only prefetch data without updating display
- * @returns Formatted lists data and raw JSON
+ * @param {number} [page] - Optional page number to fetch (defaults to current page in state)
+ * @param {boolean} [refresh=false] - Whether to refresh and start from the first page
+ * @param {boolean} [prefetchOnly=false] - If true, only prefetch data without updating display
+ * @returns {Promise<{displayData: DataObject, listsJSON: string}>} - Object containing formatted lists data and raw JSON
+ * @throws {Error} - When user is not logged in, token has expired, or already fetching data
  */
 export const getLists = async (
   page?: number,
@@ -238,10 +244,11 @@ export const getLists = async (
 
 /**
  * Fetches the user's follows from Bluesky with pagination and prefetching support
- * @param page Optional page number to fetch (defaults to current page in state)
- * @param refresh Whether to refresh and start from the first page
- * @param prefetchOnly If true, only prefetch data without updating display
- * @returns Formatted follows data and raw JSON
+ * @param {number} [page] - Optional page number to fetch (defaults to current page in state)
+ * @param {boolean} [refresh=false] - Whether to refresh and start from the first page
+ * @param {boolean} [prefetchOnly=false] - If true, only prefetch data without updating display
+ * @returns {Promise<{displayData: DataObject, usersJSON: string}>} - Object containing formatted follows data and raw JSON
+ * @throws {Error} - When user is not logged in, token has expired, or already fetching data
  */
 export const getFollows = async (
   page?: number,
@@ -346,8 +353,9 @@ export const getFollows = async (
 
 /**
  * Helper function to fetch a single batch of follows from the API
- * @param cursor Optional cursor for pagination
- * @returns Object containing follows data and next cursor
+ * @param {string|null} cursor - Optional cursor for pagination
+ * @returns {Promise<{follows: Array, cursor: string|null}>} - Object containing follows data and next cursor
+ * @throws {Error} - When API request fails
  */
 const fetchFollowsBatch = async (cursor: string | null) => {
   const apiParams: { actor: string; limit: number; cursor?: string } = {
@@ -376,8 +384,10 @@ const fetchFollowsBatch = async (cursor: string | null) => {
 
 /**
  * Get the data for the current page from the prefetched follows
- * @param page The page number to get
- * @returns Formatted follows data and JSON for the requested page
+ * @param {number} page - The page number to get
+ * @returns {Object} - Object containing formatted follows data and JSON for the requested page
+ * @returns {DataObject} - displayData Formatted follows data
+ * @returns {string} - usersJSON JSON string representation of the formatted data
  */
 const getCurrentPageData = (page: number) => {
   const startIndex = (page - 1) * state.follows.itemsPerPage;
@@ -410,13 +420,14 @@ const getCurrentPageData = (page: number) => {
 
 /**
  * Helper function to fetch a single batch of lists from the API
- * @param cursor Optional cursor for pagination
- * @returns Object containing lists data and next cursor
+ * @param {string|null} cursor - Optional cursor for pagination
+ * @returns {Promise<{lists: Array, cursor: string|null}>} - Object containing lists data and next cursor
+ * @throws {Error} - When API request fails
  */
 const fetchListsBatch = async (cursor: string | null) => {
   const apiParams: { actor: string; limit: number; cursor?: string } = {
     actor: state.did,
-    limit: 50, // We fetch more at once but still paginate in the UI
+    limit: 50,
   };
 
   if (cursor) {
@@ -439,14 +450,20 @@ const fetchListsBatch = async (cursor: string | null) => {
 
 /**
  * Get the data for the current page from the prefetched lists
- * @param page The page number to get
- * @returns Formatted lists data and JSON for the requested page
+ * @param {number} page - The page number to get
+ * @returns {Object} - Object containing formatted lists data and JSON for the requested page
+ * @returns {DataObject} - displayData Formatted lists data
+ * @returns {string} - listsJSON JSON string representation of the formatted data
  */
 const getCurrentListsPageData = (page: number) => {
   const startIndex = (page - 1) * state.lists.itemsPerPage;
   const endIndex = startIndex + state.lists.itemsPerPage;
   const pageLists = state.lists.allLists.slice(startIndex, endIndex);
 
+  state.lists.prefetchedPages = Math.ceil(
+    state.lists.allLists.length / state.lists.itemsPerPage
+  );
+
   const listsData = {
     type: 'lists',
     data: pageLists,
@@ -471,6 +488,7 @@ const getCurrentListsPageData = (page: number) => {
 
 /**
  * Helper function to handle expired sessions
+ * @returns {void} - No return value
  */
 const handleSessionExpired = (): void => {
   state.formInfo = 'Session expired. Please login again.';
@@ -480,9 +498,10 @@ const handleSessionExpired = (): void => {
 
 /**
  * Adds a user to a specified list
- * @param userDid The DID of the user to add
- * @param listUri The URI of the list to add the user to
- * @returns A success message if the operation succeeds
+ * @param {string} userDid - The DID of the user to add
+ * @param {string} listUri - The URI of the list to add the user to
+ * @returns {Promise<string>} - A success message if the operation succeeds
+ * @throws {Error} - When user is not logged in, session expired, or API request fails
  */
 export const addUserToList = async (
   userDid: string,
@@ -530,8 +549,9 @@ export const addUserToList = async (
 
 /**
  * Adds multiple users to their respective lists in batch
- * @param usersToLists Array of objects containing a user DID and the lists they should be added to
- * @returns An array of results for each operation
+ * @param {Array<{profileDid: string, lists: Array<{uri: string, name: string}>}>} usersToLists - Array of objects containing user DIDs and lists to add them to
+ * @returns {Promise<Array<{profileDid: string, listUri: string, listName: string, success: boolean, message: string}>>} - An array of results for each operation
+ * @throws {Error} - When user is not logged in
  */
 export const addUsersToLists = async (
   usersToLists: Array<{
diff --git a/src/lib/openai.ts b/src/lib/openai.ts
index 437fd8c..15531be 100644
--- a/src/lib/openai.ts
+++ b/src/lib/openai.ts
@@ -11,7 +11,7 @@ import type {
   DataObject,
   ListItem,
   FollowItem,
-} from '~/src/types';
+} from '~/src/types/index';
 
 /**
  * Calls the List Curator API endpoint to process users and lists.
@@ -39,13 +39,10 @@ const callOpenAiAPI = async (users: string, lists: string) => {
 
 /**
  * Processes user follows and lists to suggest list curation options using OpenAI
- * @param {string} users - String representation of users to be processed
- * @param {string} lists - String representation of lists to be processed
- * @returns {Promise<ApiResponse>} - The processed response from the OpenAI API
+ * @returns {Promise<Object>} - Object containing the suggestions data
  * @throws {Error} - If the API call fails or if parsing the response fails
  */
 export const curateUserLists = async (): Promise<{
-  displayData: DataObject;
   suggestionsJSON: string;
 }> => {
   if (!state.agent || !state.isLoggedIn) {
@@ -137,13 +134,10 @@ export const curateUserLists = async (): Promise<{
     );
 
     const suggestionsData = {
-      type: 'suggestions',
       data: transformedSuggestions,
-      suggestions: transformedSuggestions,
     };
 
     return {
-      displayData: suggestionsData as DataObject,
       suggestionsJSON: JSON.stringify(suggestionsData),
     };
   } catch (error) {
diff --git a/src/store/index.ts b/src/store/index.ts
index 4287ed9..266d2b6 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,6 +1,6 @@
 import { reactive } from 'vue';
 import { AtpAgent } from '@atproto/api';
-import type { DataObject, FollowItem, ListItem } from '~/src/types';
+import type { DataObject, FollowItem, ListItem } from '~/src/types/index';
 
 export const state = reactive({
   formInfo: '',
@@ -14,7 +14,6 @@ export const state = reactive({
   usersJSON: '',
   listsJSON: '',
   timelineJSON: '',
-  suggestionsJSON: '',
   follows: {
     currentPage: 1,
     itemsPerPage: 20,
@@ -26,11 +25,12 @@ export const state = reactive({
   },
   lists: {
     currentPage: 1,
-    itemsPerPage: 5, // 5 items per page as specified in the requirements
+    itemsPerPage: 5,
     cursor: null as string | null,
     hasMorePages: false,
     allLists: [] as ListItem[],
     prefetchedPages: 0,
     isFetching: false,
   },
+  isProcessingSuggestions: false,
 });
diff --git a/src/types.d.ts b/src/types/index.ts
similarity index 87%
rename from src/types.d.ts
rename to src/types/index.ts
index 40f5059..0862093 100644
--- a/src/types.d.ts
+++ b/src/types/index.ts
@@ -1,8 +1,3 @@
-export interface GraphEntity {
-  name: string;
-  description?: string;
-}
-
 export interface Author {
   did: string;
   handle: string;
@@ -44,17 +39,22 @@ export interface SuggestionItem {
 }
 
 export interface DataObject {
-  type: 'timeline' | 'lists' | 'follows' | 'suggestions' | 'error' | 'loading';
+  type: 'timeline' | 'lists' | 'follows' | 'error' | 'loading';
   data:
     | TimelineItem[]
     | ListItem[]
     | FollowItem[]
     | SuggestionItem[]
     | { message: string }[];
-  suggestions?: SuggestionItem[];
   pagination?: { totalPrefetched: number; hasMorePages: boolean };
 }
 
+// API response related interfaces
+export interface ApiResponseList {
+  name: string;
+  uri: string;
+}
+
 export interface ApiResponseItem {
   name: string;
   did: string;
@@ -62,16 +62,12 @@ export interface ApiResponseItem {
   lists?: ApiResponseList[];
 }
 
-export interface ApiResponseList {
-  name: string;
-  uri: string;
-}
-
 export interface ApiResponse {
   data: ApiResponseItem[];
   error?: string;
 }
 
+// Simplified data structures used for API requests
 export interface SimplifiedUser {
   name: string;
   description: string;


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @samuellealb, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request enhances the follows view by integrating list suggestions directly into the follows view, removing the separate "Suggestions" view and adding a "Suggest Lists" button to the "Follows" view. It implements a loading state and disables buttons during suggestion processing, allowing users to easily add follows to suggested lists via chips. Pagination and refreshing of follows data are also added, and the pagination component is updated to show the correct type label.

### Highlights
* **Follows View Enhancement**: Integrates list suggestions directly into the follows view, removing the separate "Suggestions" view.
* **Loading State and Button Disabling**: Implements a loading state and disables buttons during suggestion processing to improve user experience.
* **List Chips Integration**: Allows users to easily add follows to suggested lists via chips.
* **Pagination and Refreshing**: Adds pagination and refreshing of follows data to handle large datasets.
* **Pagination Component Update**: Updates the pagination component to show the correct type label.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **src/assets/styles/action-buttons.css**
  * Adds `.buttons-panel--disabled` class to disable the buttons panel.
  * Adds `.action-button--disabled` class to disable individual action buttons.
* **src/assets/styles/data-card.css**
  * Adds `.data-card--loading` styles to indicate loading state.
  * Removes suggestion-specific styles as the suggestion view is being integrated into the follows view.
* **src/assets/styles/data-display.css**
  * Adds styles for the suggestions button (`.data-display__suggestions-button`) and its processing state (`.data-display__suggestions-button--processing`).
  * Generalizes button styles to apply to all buttons, including the new suggestions button.
  * Removes specific styles for the refresh button that are now covered by the generalized button styles.
* **src/components/ActionButton.vue**
  * Disables the action button based on the `state.isProcessingSuggestions` flag, using the `.action-button--disabled` class.
* **src/components/ButtonsPanel.vue**
  * Removes the Suggestions button.
  * Reorders the buttons.
  * Disables the entire panel when `state.isProcessingSuggestions` is true, using the `.buttons-panel--disabled` class.
  * Removes the `displaySuggestions` function and its export.
* **src/components/Dashboard.vue**
  * Removes the "Actions" heading.
  * Adds logic to load the Lists view on mount and when the user logs in.
* **src/components/DataCard.vue**
  * Removes the suggestion item display logic.
  * Adds ListChips component to the follow item display.
  * Adds a loading class based on `state.isProcessingSuggestions`.
  * Updates imports to use the index file.
* **src/components/DataDisplay.vue**
  * Adds a "Suggest Lists" button to the follows view.
  * Adds logic to handle the "Suggest Lists" button click, including calling the OpenAI API and applying suggestions.
  * Adds logic to toggle all list options and accept all list assignments for follows.
  * Removes suggestion-specific code.
  * Adds logic to find list names by URI.
  * Adds a computed property `hasData` to simplify conditional rendering.
  * Updates imports to use the index file.
* **src/components/ListChips.vue**
  * Adds a `hideWarningButShowLists` prop to allow displaying lists even when no lists are available.
  * Updates imports to use the index file.
* **src/components/Pagination.vue**
  * Disables pagination buttons during suggestion processing.
  * Adds a `dataType` prop to determine the type label (profiles or lists).
  * Updates imports to use the index file.
* **src/lib/bsky.ts**
  * Updates jsdoc comments.
  * Updates imports to use the index file.
* **src/lib/openai.ts**
  * Removes the `displayData` return value from `curateUserLists`.
  * Updates imports to use the index file.
* **src/store/index.ts**
  * Removes `suggestionsJSON` from the state.
  * Adds `isProcessingSuggestions` to the state.
  * Updates imports to use the index file.
* **src/types/index.ts**
  * Removes the `suggestions` property from the `DataObject` interface.
  * Removes the `GraphEntity` interface.
  * Updates imports to use the index file.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A button's click, a list's grace,
> Suggestions bloom in digital space.
> Follows align,
> Lists intertwine,
> A curated feed, a better place.


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T20:48:28Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request enhances the follows view by integrating list suggestions, improving the user experience. The changes include removing the separate 'Suggestions' view, adding a 'Suggest Lists' button, implementing a loading state, and allowing users to easily add follows to suggested lists via chips. Pagination and refreshing of follows data are also updated. Overall, the changes seem well-structured and address the intended improvements.

### Summary of Findings
* **Missing Error Handling**: The `applySuggestionsToFollows` function in `DataDisplay.vue` does not handle the case where `matchingSuggestion.suggestedLists` is undefined or null, which could lead to errors.  A check should be added to ensure `matchingSuggestion.suggestedLists` exists before iterating over it.
* **Potential Performance Issue**: The `findListNameByUri` function in `DataDisplay.vue` iterates through `state.lists.allLists` and `listsData.data` on every call. This could become a performance bottleneck if the lists are very large. Consider optimizing this function by creating a map of list URIs to list names when the data is loaded.
* **Lack of Input Validation**: The `addUserToList` function in `bsky.ts` does not validate the `userDid` and `listUri` parameters.  Adding validation can prevent unexpected errors and improve security.

### Merge Readiness
The pull request introduces significant improvements to the follows view. However, there are a few issues that should be addressed before merging, particularly the missing error handling in `applySuggestionsToFollows` and the potential performance issue in `findListNameByUri`. Addressing these issues will improve the robustness and efficiency of the code. I am unable to directly approve this pull request, and recommend that others review and approve this code before merging.
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-24T20:48:39Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request enhances the follows view by integrating list suggestions, improving the user experience. The changes include removing the separate "Suggestions" view, adding a "Suggest Lists" button, implementing a loading state, and allowing users to add follows to suggested lists via chips. Pagination and refreshing of follows data are also updated. Overall, the changes seem well-structured and address the intended improvements.

### Summary of Findings
* **Missing Error Handling**: The `applySuggestionsToFollows` function in `DataDisplay.vue` directly accesses `cardRef.followEnabledLists[key]` without checking if `cardRef` or `cardRef.followEnabledLists` is defined. This could lead to runtime errors if `cardRef` is null or `followEnabledLists` is undefined.
* **Inconsistent Naming**: There are some inconsistencies in naming conventions, such as using both `usersJSON` and `followsJSON` to store the same type of data. Standardizing these names would improve code clarity.
* **Potential Performance Bottleneck**: The `findListNameByUri` function in `DataDisplay.vue` iterates through `state.lists.allLists` and `listsData.data` on every call. This could become a performance bottleneck if the lists are very large. Consider using a map for faster lookups.

### Merge Readiness
The pull request introduces significant improvements to the follows view. However, there are a few issues that should be addressed before merging, particularly the potential for runtime errors in `applySuggestionsToFollows` and the performance concerns with `findListNameByUri`. Addressing these issues will improve the stability and performance of the code. I am unable to directly approve this pull request, and recommend that others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-24T20:54:20Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request enhances the follows view by integrating list suggestions, improving the user experience. The changes include removing the separate "Suggestions" view, adding a "Suggest Lists" button, implementing a loading state, and allowing users to easily add follows to suggested lists via chips. Pagination and refreshing of follows data are also updated. Overall, the changes seem well-structured and address the intended improvements.

### Summary of Findings
* **Missing Error Handling**: The `applySuggestionsToFollows` function in `src/components/DataDisplay.vue` does not handle the case where `cardRef.followEnabledLists` is undefined, which could lead to unexpected behavior or errors. Adding a check for this condition would improve the robustness of the code.
* **Potential Performance Issue**: The `applySuggestionsToFollows` function in `src/components/DataDisplay.vue` iterates through all `dataCardRefs` and then searches for a `matchingSuggestion` for each follow item. This could be inefficient for large datasets. Consider optimizing this by creating a map of suggestions for faster lookup.
* **Inconsistent Naming**: In `src/components/DataDisplay.vue`, the `handleToggleFollowsLists` function name suggests it only toggles lists for follows, but it iterates through all `dataCardRefs`. A more generic name might be appropriate if it's intended to work with other data types in the future.

### Merge Readiness
The pull request introduces significant improvements to the follows view. However, there are a few issues that should be addressed before merging, particularly the missing error handling in `applySuggestionsToFollows` and the potential performance issue in the same function. Addressing these issues will improve the robustness and efficiency of the code. I am unable to approve this pull request, and recommend that others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-24T21:03:46Z

Review comments:
diff_hunk: @@ -455,4 +439,129 @@ watch(
     }
   }
 );
+
+const handleSuggestions = async () => {
+  if (!dataObject.value || dataObject.value.type !== 'follows') return;
+
+  // Show loading state
+  state.isProcessingSuggestions = true;
+
+  try {
+    // First, get suggestions from the API for the current follows
+    const { suggestionsJSON } = await curateUserLists();
+
+    if (!suggestionsJSON) {
+      throw new Error('Failed to get suggestions');
+    }
+
+    // Parse suggestions
+    const suggestionsData = JSON.parse(suggestionsJSON);
+
+    // Apply suggestions to the current follows view
+    applySuggestionsToFollows(suggestionsData);
+
+    // Show success message
+    acceptAllResult.value =
+      'Successfully applied list suggestions to current follows';
+    acceptAllError.value = false;
+  } catch (error) {
+    console.error('Error getting suggestions:', error);
+    acceptAllResult.value = `Error getting suggestions: ${
+      (error as Error).message
+    }`;
+    acceptAllError.value = true;
+  } finally {
+    // Restore button text
+    state.isProcessingSuggestions = false;
+  }
+};
+
+/**
+ * Apply suggestions to the follows view by toggling the relevant list chips
+ * @param suggestionsData The suggestions data containing recommended lists for profiles
+ */
+const applySuggestionsToFollows = (suggestionsData: {
+  data: SuggestionItem[];
+}) => {
+  if (!dataCardRefs.value.length || !dataObject.value) return;
+
+  // Process each follow card
+  for (const cardRef of dataCardRefs.value) {
+    if (!cardRef) continue;
path: src/components/DataDisplay.vue
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

It's possible that `cardRef` is null or undefined, which would cause an error when trying to access its properties. Consider adding a check to ensure `cardRef` is valid before proceeding.

```
   if (!cardRef) continue;
+    if (!cardRef.followEnabledLists) continue;
```
created_at: 2025-04-24T20:54:19Z
author_association: NONE
diff_hunk: @@ -1,11 +1,17 @@
 <template>
-  <button class="action-button" @click="onClick">
+  <button
+    class="action-button"
+    :class="state.isProcessingSuggestions ? 'action-button--disabled' : ''"
path: src/components/ActionButton.vue
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider using a more descriptive class name, such as `action-button--processing`, to clearly indicate the button's state.

```
:class="state.isProcessingSuggestions ? 'action-button--processing' : ''"
```
created_at: 2025-04-24T20:54:20Z
author_association: NONE
diff_hunk: @@ -455,4 +439,129 @@ watch(
     }
   }
 );
+
+const handleSuggestions = async () => {
+  if (!dataObject.value || dataObject.value.type !== 'follows') return;
+
+  // Show loading state
+  state.isProcessingSuggestions = true;
+
+  try {
+    // First, get suggestions from the API for the current follows
+    const { suggestionsJSON } = await curateUserLists();
+
+    if (!suggestionsJSON) {
+      throw new Error('Failed to get suggestions');
+    }
+
+    // Parse suggestions
+    const suggestionsData = JSON.parse(suggestionsJSON);
+
+    // Apply suggestions to the current follows view
+    applySuggestionsToFollows(suggestionsData);
+
+    // Show success message
+    acceptAllResult.value =
+      'Successfully applied list suggestions to current follows';
+    acceptAllError.value = false;
+  } catch (error) {
+    console.error('Error getting suggestions:', error);
+    acceptAllResult.value = `Error getting suggestions: ${
+      (error as Error).message
+    }`;
+    acceptAllError.value = true;
+  } finally {
+    // Restore button text
+    state.isProcessingSuggestions = false;
+  }
+};
+
+/**
+ * Apply suggestions to the follows view by toggling the relevant list chips
+ * @param suggestionsData The suggestions data containing recommended lists for profiles
+ */
+const applySuggestionsToFollows = (suggestionsData: {
+  data: SuggestionItem[];
+}) => {
+  if (!dataCardRefs.value.length || !dataObject.value) return;
+
+  // Process each follow card
+  for (const cardRef of dataCardRefs.value) {
+    if (!cardRef) continue;
+
+    const followItem = dataObject.value.data[
+      cardRef.$props.index
+    ] as FollowItem;
+    if (!followItem) continue;
+
+    // Find matching suggestion for this follow
+    const matchingSuggestion = suggestionsData.data.find(
+      (item: SuggestionItem) => item.did === followItem.did
+    );
+
+    if (
+      matchingSuggestion &&
+      matchingSuggestion.suggestedLists &&
+      matchingSuggestion.suggestedLists.length > 0
+    ) {
+      // We have suggestions for this follow, toggle the corresponding list chips
+      for (const list of matchingSuggestion.suggestedLists) {
+        const key = `${followItem.did}-${list.uri}`;
+
+        // If the follow has this list enabled option, set it to enabled
+        if (typeof cardRef.followEnabledLists[key] !== 'undefined') {
+          // Set this chip to enabled
+          cardRef.followEnabledLists[key] = true;
+        }
+      }
+    }
+  }
+};
+
+/**
+ * Helper function to find a list name based on URI
+ * Uses multiple sources to ensure we find the correct name
+ */
+const findListNameByUri = (listUri: string): string => {
+  // First try to find the list in state.lists.allLists
+  if (state.lists.allLists && state.lists.allLists.length > 0) {
+    const foundList = state.lists.allLists.find(
+      (list: ListItem) => list.uri === listUri
+    );
+    if (foundList) {
+      return foundList.name;
+    }
+  }
+
+  // Next try to parse from state.listsJSON
+  if (state.listsJSON) {
+    try {
+      const listsData = JSON.parse(state.listsJSON);
+      if (listsData.data && Array.isArray(listsData.data)) {
+        const foundList = listsData.data.find(
+          (list: ListItem) => list.uri === listUri
+        );
+        if (foundList) {
+          return foundList.name;
+        }
+      }
+    } catch (error) {
+      console.error('Error parsing lists JSON:', error);
+    }
+  }
+
+  // Extract list name from URI as a last resort
+  // URIs typically end with something like "app.bsky.graph.list/3jui4lcvpba2z"
+  try {
+    const uriParts = listUri.split('/');
+    if (uriParts.length > 0) {
+      // Try to get the list ID at the end of the URI
+      return `List ${uriParts[uriParts.length - 1]}`;
+    }
+  } catch (error) {
+    console.error('Error extracting list name from URI:', error);
+  }
+
+  return 'Unknown List';
+};
path: src/components/DataDisplay.vue
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

This function iterates through `state.lists.allLists` and `listsData.data` on every call. This could become a performance bottleneck if the lists are very large. Consider using a map for faster lookups.
created_at: 2025-04-24T20:54:20Z
author_association: NONE
diff_hunk: @@ -1,11 +1,17 @@
 <template>
-  <button class="action-button" @click="onClick">
+  <button
+    class="action-button"
+    :class="state.isProcessingSuggestions ? 'action-button--processing' : ''"
+    :disabled="state.isProcessingSuggestions"
path: src/components/ActionButton.vue
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a more descriptive class name instead of `action-button--processing`. This would make it easier to understand what the button is doing and allow for more specific styling. Also, consider using a boolean attribute for `disabled` instead of relying on CSS classes to disable the button.

```
    class="action-button"
    :class="{ 'action-button--is-loading': state.isProcessingSuggestions }"
    :disabled="state.isProcessingSuggestions"
```
created_at: 2025-04-24T21:03:46Z
author_association: NONE
diff_hunk: @@ -455,4 +436,118 @@ watch(
     }
   }
 );
+
+const handleSuggestions = async () => {
+  if (!dataObject.value || dataObject.value.type !== 'follows') return;
+
+  state.isProcessingSuggestions = true;
+
+  try {
+    const { suggestionsJSON } = await curateUserLists();
+
+    if (!suggestionsJSON) {
+      throw new Error('Failed to get suggestions');
+    }
+
+    const suggestionsData = JSON.parse(suggestionsJSON);
+
+    applySuggestionsToFollows(suggestionsData);
+
+    acceptAllResult.value =
+      'Successfully applied list suggestions to current follows';
+    acceptAllError.value = false;
+  } catch (error) {
+    console.error('Error getting suggestions:', error);
+    acceptAllResult.value = `Error getting suggestions: ${
+      (error as Error).message
+    }`;
+    acceptAllError.value = true;
+  } finally {
+    state.isProcessingSuggestions = false;
+  }
+};
+
+/**
+ * Apply suggestions to the follows view by toggling the relevant list chips
+ * @param suggestionsData The suggestions data containing recommended lists for profiles
+ */
+const applySuggestionsToFollows = (suggestionsData: {
+  data: SuggestionItem[];
+}) => {
+  if (!dataCardRefs.value.length || !dataObject.value) return;
+
+  for (const cardRef of dataCardRefs.value) {
+    if (!cardRef) continue;
+
+    const followItem = dataObject.value.data[
+      cardRef.$props.index
+    ] as FollowItem;
+    if (!followItem) continue;
+
+    const matchingSuggestion = suggestionsData.data.find(
+      (item: SuggestionItem) => item.did === followItem.did
+    );
path: src/components/DataDisplay.vue
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The `applySuggestionsToFollows` function iterates through all `dataCardRefs` and then searches for a `matchingSuggestion` for each follow item. This could be inefficient for large datasets. Consider optimizing this by creating a map of suggestions for faster lookup. For example, create a map `const suggestionsMap = suggestionsData.data.reduce((map, item) => { map[item.did] = item; return map; }, {});` and then use `suggestionsMap[followItem.did]` to get the matching suggestion.
created_at: 2025-04-24T21:03:46Z
author_association: NONE

PR comments:
user: login: samuellealb
created_at: 2025-04-24T20:54:06Z
body: /gemini review
author_association: OWNER
user: login: samuellealb
created_at: 2025-04-24T21:03:32Z
body: /gemini review
author_association: OWNER