From 13328aeec38cecfdc24bf859978c41c67bf0b303 Mon Sep 17 00:00:00 2001 From: nquidox Date: Wed, 29 Oct 2025 20:57:34 +0300 Subject: [PATCH 01/12] styles added --- src/styles/styles.css | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/styles/styles.css b/src/styles/styles.css index 8219206..67d9708 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -2,6 +2,10 @@ margin-top: 10px; } +.mt-20 { + margin-top: 20px; +} + .mb-10 { margin-bottom: 10px; } @@ -166,3 +170,30 @@ body, max-height: 70vh; object-fit: contain; } + +.router-link-button{ + color: inherit; + text-decoration: none; +} + +.label-container { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.label-column { + display: flex; + flex-direction: column; +} + +.label-row { + display: flex; + align-items: center; + gap: 10px; +} + +.label-preview { + justify-content: center; + margin-bottom: 20px; +} From 30c42324068a7d586c35f8920abe6e4ce34797db Mon Sep 17 00:00:00 2001 From: nquidox Date: Wed, 29 Oct 2025 20:58:02 +0300 Subject: [PATCH 02/12] post requests fixed --- src/services/apiClient.js | 52 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/services/apiClient.js b/src/services/apiClient.js index 60d5d46..7bc1f10 100644 --- a/src/services/apiClient.js +++ b/src/services/apiClient.js @@ -6,23 +6,23 @@ let isRefreshing = false let refreshPromise = null function createConfig(options = {}) { - const authStore = useAuthStore() + const token = localStorage.getItem('accessToken'); const headers = { 'Content-Type': 'application/json', ...options.headers, - } + }; - if (authStore.accessToken) { - headers['Authorization'] = `Bearer ${authStore.accessToken}` + if (token) { + headers['Authorization'] = `Bearer ${token}`; } return { headers, - credentials: 'include', ...options, - } + }; } + async function refreshAccessToken() { const authStore = useAuthStore() @@ -53,7 +53,8 @@ async function refreshAccessToken() { }) .catch((error) => { - throw error + console.error('Refresh error:', error) + throw new Error('REFRESH_FAILED') }) .finally(() => { @@ -66,29 +67,32 @@ async function refreshAccessToken() { async function request(url, options = {}, isRetry = false) { const config = createConfig(options) - const response = await fetch(`${BASE_URL}${url}`, config) if (response.status === 401 && !isRetry) { try { const data = await refreshAccessToken() - const token = data.access_token - if (!token) { - throw new Error('Refresh response did not contain access_token') - } + if (!token) throw new Error('Refresh response did not contain access_token') + const newOptions = { ...options, headers: { ...options.headers, - 'Authorization': `Bearer ${token}`, + Authorization: `Bearer ${token}`, }, } + return await request(url, newOptions, true) } catch (e) { - const authStore = useAuthStore() - authStore.forceLogout() + if (e.message === 'REFRESH_FAILED') { + const authStore = useAuthStore() + authStore.forceLogout() + console.warn('Force logout (refresh failed)', url) + } else { + console.error('Unexpected error during refresh', e) + } throw e } } @@ -100,9 +104,15 @@ async function request(url, options = {}, isRetry = false) { } catch { errorData = {} } - throw new Error(errorData.message || `HTTP Error: ${response.status}`) + + return { + status: response.status, + ok: false, + error: errorData.message || `HTTP Error: ${response.status}`, + } } + let data = null const contentType = response.headers.get('content-type') if (contentType?.includes('application/json')) { @@ -113,11 +123,7 @@ async function request(url, options = {}, isRetry = false) { } } - return { - status: response.status, - ok: response.ok, - data, - } + return { status: response.status, ok: response.ok, data } } export const apiClient = { @@ -132,9 +138,7 @@ export const apiClient = { return request(url, { method: 'POST', body: isFormData ? data : JSON.stringify(data), - headers: isFormData - ? {} - : { 'Content-Type': 'application/json' } + // headers: isFormData ? {} : { 'Content-Type': 'application/json' } }) }, put: (url, data) => request(url, { From 53558c4b46dd76e59ddf67eaa00d00ce06b366bb Mon Sep 17 00:00:00 2001 From: nquidox Date: Wed, 29 Oct 2025 20:58:16 +0300 Subject: [PATCH 03/12] fix --- src/stores/authStore.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/authStore.js b/src/stores/authStore.js index 7cdfc82..cede1f1 100644 --- a/src/stores/authStore.js +++ b/src/stores/authStore.js @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { computed, ref } from 'vue' +import { computed, nextTick, ref } from 'vue' import { apiClient } from '@/services/apiClient' import router from '@/router/index.js' @@ -54,6 +54,7 @@ export const useAuthStore = defineStore('auth', () => { try { const response = await apiClient.post('/user/auth/login', { email, password }) setToken(response.data.access_token) + await nextTick() router.push({ name: 'collection'}) } catch (error) { console.error('Login error:', error) From c74032d1d047fc862f197a62757c795af1e96a2b Mon Sep 17 00:00:00 2001 From: nquidox Date: Wed, 29 Oct 2025 20:58:47 +0300 Subject: [PATCH 04/12] labels added --- src/components/LabelDotTemplate.vue | 27 ++++ src/components/LabelTemplate.vue | 45 ++++++ src/components/ManageLabels.vue | 7 + src/components/Navbar/NavBar.vue | 3 +- src/router/index.js | 6 + src/stores/labelsStore.js | 107 +++++++++++++ src/views/CollectionView.vue | 26 +++- .../CollectionView/CollectionMerchCard.vue | 36 ++++- .../CollectionView/CollectionToolbar.vue | 67 ++++---- src/views/DetailsView.vue | 2 + src/views/DetailsView/AttachLabel.vue | 147 ++++++++++++++++++ src/views/LabelsView.vue | 78 ++++++++++ src/views/LabelsView/LabelCard.vue | 143 +++++++++++++++++ src/views/LabelsView/LabelForm.vue | 36 +++++ src/views/PersonalView/PersonalMainBlock.vue | 9 +- .../PersonalView/PersonalSessionBlock.vue | 7 +- 16 files changed, 702 insertions(+), 44 deletions(-) create mode 100644 src/components/LabelDotTemplate.vue create mode 100644 src/components/LabelTemplate.vue create mode 100644 src/components/ManageLabels.vue create mode 100644 src/stores/labelsStore.js create mode 100644 src/views/DetailsView/AttachLabel.vue create mode 100644 src/views/LabelsView.vue create mode 100644 src/views/LabelsView/LabelCard.vue create mode 100644 src/views/LabelsView/LabelForm.vue 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 })