diff --git a/src/components/LabelDotTemplate.vue b/src/components/LabelDotTemplate.vue new file mode 100644 index 0000000..c37db3f --- /dev/null +++ b/src/components/LabelDotTemplate.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/components/LabelTemplate.vue b/src/components/LabelTemplate.vue new file mode 100644 index 0000000..f26f669 --- /dev/null +++ b/src/components/LabelTemplate.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/components/ManageLabels.vue b/src/components/ManageLabels.vue new file mode 100644 index 0000000..6d57f82 --- /dev/null +++ b/src/components/ManageLabels.vue @@ -0,0 +1,7 @@ + diff --git a/src/components/Navbar/NavBar.vue b/src/components/Navbar/NavBar.vue index d8774fb..c2fcb65 100644 --- a/src/components/Navbar/NavBar.vue +++ b/src/components/Navbar/NavBar.vue @@ -28,6 +28,7 @@ const authMenu = computed(() => { } return [ + { label: 'Labels', key: 'labels' }, { label: 'Personal', key: 'personal' }, ] }) @@ -137,7 +138,7 @@ const renderLabel = (option) => { display: flex; align-items: center; margin-left: auto; - min-width: 150px; + min-width: 250px; padding-right: 16px; box-sizing: border-box; } diff --git a/src/router/index.js b/src/router/index.js index 53e4751..1a83844 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -7,6 +7,7 @@ import ParsersView from '@/views/ParsersView.vue' import PersonalView from '@/views/PersonalView.vue' import AddMerchView from '@/views/AddMerchView.vue' import DetailsView from '@/views/DetailsView.vue' +import LabelsView from '@/views/LabelsView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -52,6 +53,11 @@ const router = createRouter({ component: DetailsView, props: true, }, + { + path: '/labels', + name: 'labels', + component: LabelsView, + }, ], }) diff --git a/src/stores/labelsStore.js b/src/stores/labelsStore.js new file mode 100644 index 0000000..968d3ff --- /dev/null +++ b/src/stores/labelsStore.js @@ -0,0 +1,107 @@ +import { defineStore } from 'pinia' +import { apiClient } from '@/services/apiClient.js' +import { ref } from 'vue' + +function safeParseJSON(str, fallback = []) { + if (str === null || str === undefined || str === 'null' || str === '') { + return fallback + } + try { + const parsed = JSON.parse(str) + return Array.isArray(parsed) ? parsed : fallback + } catch (e) { + console.warn('Failed to parse localStorage item "labels", using fallback:', e) + return fallback + } +} + +export const useLabelsStore = defineStore('labels', () => { + //state + const labels = ref(safeParseJSON(localStorage.getItem('labels'), [])) + + //getters + + //action + const createLabel = async (newLabel) => { + try { + await apiClient.post('/merch/labels', newLabel) + await getLabels() + } catch (error) { + console.error('Failed to create label:', error) + throw error + } + } + + const getLabels = async () => { + try { + const response = await apiClient.get('/merch/labels') + const labelList = Array.isArray(response.data) ? response.data : [] + labels.value = labelList + localStorage.setItem('labels', JSON.stringify(labelList)) + } catch (error) { + console.error('Failed to fetch labels:', error) + labels.value = [] + } + } + + const updateLabel = async (uuid, updatedData) => { + try { + await apiClient.put(`/merch/labels/${uuid}`, updatedData) + await getLabels() + } catch (error) { + console.error('Failed to update label:', error) + throw error + } + } + + const deleteLabel = async (uuid) => { + try { + await apiClient.delete(`/merch/labels/${uuid}`) + await getLabels() + } catch (error) { + console.error('Failed to delete label:', error) + throw error + } + } + + const attachLabel = async (merchUuid, labelUuid) => { + try { + const payload = { merch_uuid: merchUuid, label_uuid: labelUuid } + await apiClient.post('/merch/labels/attach', payload) + } catch (error) { + console.error('Failed to attach label:', error) + throw error + } + } + + const detachLabel = async (merchUuid, labelUuid) => { + try { + const payload = { merch_uuid: merchUuid, label_uuid: labelUuid } + await apiClient.post('/merch/labels/detach', payload) + } catch (error) { + console.error('Failed to detach label:', error) + throw error + } + } + + const getMerchLabels = async (merchUuid) => { + try { + const response = await apiClient.get(`/merch/labels/${merchUuid}`) + return response + } catch (error) { + console.error('Failed to get merch labels:', error) + throw error + } + } + + return { + labels, + createLabel, + getLabels, + updateLabel, + deleteLabel, + attachLabel, + detachLabel, + getMerchLabels, + } +}) diff --git a/src/views/CollectionView.vue b/src/views/CollectionView.vue index d3fc1a5..52818c0 100644 --- a/src/views/CollectionView.vue +++ b/src/views/CollectionView.vue @@ -4,12 +4,14 @@ import CollectionMerchCard from '@/views/CollectionView/CollectionMerchCard.vue' import { computed, onMounted, ref } from 'vue' import { useMerchApi } from '@/api/merch.js' import ScrollToTopButton from '@/components/ScrollToTopButton.vue' +import { useLabelsStore } from '@/stores/labelsStore' const merchList = ref(null) const loading = ref(true) const error = ref(null) const { getMerchList } = useMerchApi() +const { getLabels } = useLabelsStore() const fetchMerch = async () => { try { @@ -25,27 +27,39 @@ const fetchMerch = async () => { onMounted(() => { fetchMerch() + getLabels() }) const searchQuery = ref('') +const selectedLabelUuids = ref([]) const filteredMerch = computed(() => { - if (!searchQuery.value.trim()) { - return merchList.value + let result = merchList.value || [] + + if (searchQuery.value.trim()) { + const q = searchQuery.value.toLowerCase() + result = result.filter((item) => item.name.toLowerCase().includes(q)) } - const q = searchQuery.value.toLowerCase() - return merchList.value.filter((item) => item.name.toLowerCase().includes(q)) + + if (selectedLabelUuids.value.length > 0) { + const selectedSet = new Set(selectedLabelUuids.value) + result = result.filter((item) => { + return item.labels && item.labels.some((labelUuid) => selectedSet.has(labelUuid)) + }) + } + + return result })