Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Browse Problems">
    <ion-grid :fixed="true">
      <ion-row>
        <ion-col>
          <h1>Browse Problems & Tags</h1>
          <p>
            Search and filter problems by name or tags. Click on a problem to view details or select it for a job.
          </p>
        </ion-col>
      </ion-row>

      <!-- Search and Filter Section -->
      <ion-row>
        <ion-col>
          <ion-card>
            <ion-card-header>
              <ion-card-title>Search & Filter</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <ion-grid>
                <ion-row>
                  <ion-col size="12" size-md="6">
                    <ion-searchbar
                      v-model="searchQuery"
                      placeholder="Search problems..."
                      @ionInput="onSearchInput"
                    ></ion-searchbar>
                  </ion-col>
                  <ion-col size="12" size-md="6">
                    <ion-select
                      v-model="selectedTagFilter"
                      placeholder="Filter by tag"
                      @selectionChange="onTagFilterChange"
                    >
                      <ion-select-option value="">All tags</ion-select-option>
                      <ion-select-option v-for="tag in availableTags" :key="tag.id" :value="tag.id">
                        {{ tag.name }}
                      </ion-select-option>
                    </ion-select>
                  </ion-col>
                </ion-row>
              </ion-grid>
            </ion-card-content>
          </ion-card>
        </ion-col>
      </ion-row>

      <!-- Tags Overview -->
      <ion-row>
        <ion-col>
          <ion-card>
            <ion-card-header>
              <ion-card-title>Available Tags</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <div class="tags-container">
                <span
                  v-for="tag in availableTags"
                  :key="tag.id"
                  class="tag-chip"
                  :class="{ active: selectedTagFilter === tag.id }"
                  @click="selectTag(tag.id)"
                >
                  #{{ tag.name }} ({{ getTagUsageCount(tag.id) }})
                </span>
                <span v-if="availableTags.length === 0" class="no-tags">
                  No tags available
                </span>
              </div>
            </ion-card-content>
          </ion-card>
        </ion-col>
      </ion-row>

      <!-- Problems List -->
      <ion-row>
        <ion-col>
          <div class="results-header">
            <h3>Problems ({{ filteredProblems.length }})</h3>
            <ion-button fill="clear" @click="clearFilters" v-if="hasActiveFilters">
              Clear Filters
            </ion-button>
          </div>

          <ion-card v-for="problem in filteredProblems" :key="problem.id">
            <ion-card-header>
              <ion-card-title>{{ problem.name }}</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <ion-grid>
                <ion-row>
                  <ion-col size="12" size-md="8">
                    <p class="problem-id">
                      Problem ID: {{ problem.refId }}
                    </p>
                    <p class="problem-description" v-if="problem.description">
                      {{ problem.description }}
                    </p>
                    <p class="problem-description no-description" v-else>
                      No description available
                    </p>
                    <div class="problem-tags">
                      <span v-if="getTagsForProblem(problem).length > 0">
                        <span
                          v-for="tagName in getTagsForProblem(problem)"
                          :key="tagName"
                          class="tag"
                        >
                          #{{ tagName }}
                        </span>
                      </span>
                      <span v-else class="no-tags">No tags</span>
                    </div>
                  </ion-col>
                  <ion-col size="12" size-md="4">
                    <div class="button-group">
                      <ion-button 
                        :data-id="problem.id" 
                        @click="viewProblem"
                        color="primary"
                        size="small"
                      >
                        View Details
                      </ion-button>
                    </div>
                  </ion-col>
                </ion-row>
              </ion-grid>
            </ion-card-content>
          </ion-card>

          <ion-card v-if="filteredProblems.length === 0">
            <ion-card-content>
              <p class="no-results">
                No problems found matching your search criteria.
                <ion-button fill="clear" @click="clearFilters" v-if="hasActiveFilters">
                  Clear filters
                </ion-button>
              </p>
            </ion-card-content>
          </ion-card>
        </ion-col>
      </ion-row>
    </ion-grid>
  </BaseLayout>
</template>

<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { useRouter } from "vue-router";
import BaseLayout from "@/components/BaseLayout.vue";
import type { ProblemsRecord, ProblemTagsRecord } from "@/pocketbase-types.ts";
import { getDb } from "@/dataAccess/getDb";
import {
  IonButton,
  IonGrid,
  IonRow,
  IonCol,
  IonCard,
  IonCardHeader,
  IonCardTitle,
  IonCardContent,
  IonSearchbar,
  IonSelect,
  IonSelectOption,
} from "@ionic/vue";

const router = useRouter();
const problems = ref<ProblemsRecord[]>([]);
const problemTags = ref<ProblemTagsRecord[]>([]);
const searchQuery = ref("");
const selectedTagFilter = ref("");

// Computed properties
const availableTags = computed(() => 
  problemTags.value.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
);

const hasActiveFilters = computed(() => {
  return searchQuery.value.trim() !== "" || selectedTagFilter.value !== "";
});

const filteredProblems = computed(() => {
  let filtered = [...problems.value];

  // Filter by search query
  if (searchQuery.value.trim() !== "") {
    const query = searchQuery.value.toLowerCase();
    filtered = filtered.filter(problem => 
      problem.name?.toLowerCase().includes(query) ||
      problem.refId?.toLowerCase().includes(query) ||
      getTagsForProblem(problem).some(tagName => 
        tagName.toLowerCase().includes(query)
      )
    );
  }

  // Filter by selected tag
  if (selectedTagFilter.value !== "") {
    filtered = filtered.filter(problem => {
      const problemTagIds = getProblemTagIds(problem);
      return problemTagIds.includes(selectedTagFilter.value);
    });
  }

  return filtered;
});

// Helper functions
const getProblemTagIds = (problem: ProblemsRecord): string[] => {
  if (!problem.problemTags) return [];
  
  if (typeof problem.problemTags === 'string') {
    try {
      return JSON.parse(problem.problemTags);
    } catch (e) {
      return [];
    }
  } else if (Array.isArray(problem.problemTags)) {
    return problem.problemTags;
  }
  
  return [];
};

const getTagsForProblem = (problem: ProblemsRecord): string[] => {
  const tagIds = getProblemTagIds(problem);
  
  return tagIds
    .map((tagId) => problemTags.value.find((tag) => tag.id === tagId))
    .filter((tag) => tag !== undefined)
    .map((tag) => tag!.name!)
    .filter((name) => name !== undefined);
};

const getTagUsageCount = (tagId: string): number => {
  return problems.value.filter(problem => 
    getProblemTagIds(problem).includes(tagId)
  ).length;
};

// Event handlers
const onSearchInput = (event: CustomEvent) => {
  searchQuery.value = event.detail.value;
};

const onTagFilterChange = (event: CustomEvent) => {
  selectedTagFilter.value = event.detail.value;
};

const selectTag = (tagId: string) => {
  selectedTagFilter.value = selectedTagFilter.value === tagId ? "" : tagId;
};

const clearFilters = () => {
  searchQuery.value = "";
  selectedTagFilter.value = "";
};

const viewProblem = (e: MouseEvent) => {
  const target = e?.target as HTMLElement;
  if (target) {
    router.push(`/browse/problem/${target.dataset.id}`);
  }
};

// Lifecycle
onMounted(async () => {
  const db = await getDb();
  if (db) {
    problems.value = (await db.selectAll("problems")) || [];
    problemTags.value = (await db.selectAll("problemTags")) || [];
  }
});
</script>

<style scoped>
.tags-container {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.tag-chip {
  display: inline-block;
  background-color: var(--ion-color-light);
  color: var(--ion-color-dark);
  padding: 6px 12px;
  border-radius: 16px;
  font-size: 0.85em;
  cursor: pointer;
  transition: all 0.2s ease;
  border: 2px solid transparent;
}

.tag-chip:hover {
  background-color: var(--ion-color-primary-tint);
  color: var(--ion-color-primary-contrast);
}

.tag-chip.active {
  background-color: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
  border-color: var(--ion-color-primary-shade);
}

.tag {
  display: inline-block;
  background-color: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
  padding: 2px 8px;
  margin: 2px 4px 2px 0;
  border-radius: 12px;
  font-size: 0.8em;
}

.problem-tags {
  margin-top: 8px;
}

.problem-id {
  color: var(--ion-color-medium);
  font-size: 0.85em;
  margin-bottom: 4px;
  font-weight: 500;
}

.problem-description {
  color: var(--ion-color-dark);
  font-size: 0.9em;
  margin-bottom: 8px;
  line-height: 1.4;
}

.no-description {
  color: var(--ion-color-medium);
  font-style: italic;
}

.no-tags {
  color: var(--ion-color-medium);
  font-style: italic;
}

.button-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
}

.results-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.results-header h3 {
  margin: 0;
}

.no-results {
  text-align: center;
  color: var(--ion-color-medium);
  margin: 20px 0;
}

@media (min-width: 768px) {
  .button-group {
    flex-direction: row;
    gap: 8px;
    justify-content: flex-end;
  }
}
</style>