Hello from MCP server
<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>