diff --git a/Dockerfile b/Dockerfile index 14344dc..7737ca2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,6 @@ FROM node:24.10-alpine3.22 AS builder WORKDIR /app -RUN apk add --no-cache python3 make g++ libc6-compat - COPY package*.json ./ RUN npm ci --prefer-offline --no-audit diff --git a/package.json b/package.json index 88ed54a..3c12e7c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "prettier": "3.5.3", "vfonts": "^0.0.3", "vite": "^6.2.1", - "vite-plugin-vue-devtools": "^8.0.3" + "vite-plugin-vue-devtools": "^7.7.2" } } diff --git a/src/api/zeroPrices.js b/src/api/zeroPrices.js deleted file mode 100644 index a7db41a..0000000 --- a/src/api/zeroPrices.js +++ /dev/null @@ -1,40 +0,0 @@ -import { apiClient } from '@/services/apiClient.js' - -export const useZeroPrices = () => { - const getZeroPrices = async () => { - try { - return await apiClient.get('/merch/zeroprices') - } catch (error) { - console.log('Get zero prices error: ', error) - throw error - } - } - - const deleteZeroPrices = async (list) => { - try { - await apiClient.delete('/merch/zeroprices', list) - } - catch (error) { - console.log('Delete target zero prices error: ', error) - throw error - } - } - - const deleteZeroPricesPeriod = async (start, end) => { - const params = new URLSearchParams({ start, end }).toString() - const url = `/merch/zeroprices/period${params ? `?${params}` : ''}` - const response = await apiClient.delete(url) - - if (response.status === 200) { - return response - } else { - console.log('Delete period select zero prices error: ', response) - } - } - - return { - getZeroPrices, - deleteZeroPrices, - deleteZeroPricesPeriod, - } -} diff --git a/src/components/ChartBlock.vue b/src/components/ChartBlock.vue index 11bd874..96a5243 100644 --- a/src/components/ChartBlock.vue +++ b/src/components/ChartBlock.vue @@ -13,7 +13,6 @@ import { } from 'chart.js' import { Line } from 'vue-chartjs' import 'chartjs-adapter-date-fns' -import { originColors } from '@/services/colors.js' ChartJS.register( Title, @@ -34,6 +33,11 @@ const props = defineProps({ }, }) +const originColors = { + surugaya: '#2d3081', + mandarake: '#924646', +} + const chartData = ref({ datasets: [], }) diff --git a/src/components/LabelDotTemplate.vue b/src/components/LabelDotTemplate.vue deleted file mode 100644 index c37db3f..0000000 --- a/src/components/LabelDotTemplate.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/src/components/LabelTemplate.vue b/src/components/LabelTemplate.vue deleted file mode 100644 index f26f669..0000000 --- a/src/components/LabelTemplate.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - - - {{ text }} - - - - diff --git a/src/components/ManageLabels.vue b/src/components/ManageLabels.vue deleted file mode 100644 index 6d57f82..0000000 --- a/src/components/ManageLabels.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - - Manage labels - - - diff --git a/src/components/Navbar/NavBar.vue b/src/components/Navbar/NavBar.vue index a05c67c..d8774fb 100644 --- a/src/components/Navbar/NavBar.vue +++ b/src/components/Navbar/NavBar.vue @@ -17,7 +17,6 @@ const mainMenu = computed(() => { { label: 'Collection', key: 'collection' }, { label: 'Charts', key: 'charts' }, { label: 'Parsers', key: 'parsers' }, - { label: 'Zero prices', key: 'zeroprices' }, ] }) @@ -29,7 +28,6 @@ const authMenu = computed(() => { } return [ - { label: 'Labels', key: 'labels' }, { label: 'Personal', key: 'personal' }, ] }) @@ -139,7 +137,7 @@ const renderLabel = (option) => { display: flex; align-items: center; margin-left: auto; - min-width: 250px; + min-width: 150px; padding-right: 16px; box-sizing: border-box; } diff --git a/src/router/index.js b/src/router/index.js index 0a0ff85..53e4751 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -7,8 +7,6 @@ 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' -import ZeroPricesView from '@/views/ZeroPricesView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -54,16 +52,6 @@ const router = createRouter({ component: DetailsView, props: true, }, - { - path: '/labels', - name: 'labels', - component: LabelsView, - }, - { - path: '/zeroprices', - name: 'zeroprices', - component: ZeroPricesView, - }, ], }) diff --git a/src/services/apiClient.js b/src/services/apiClient.js index 639fb84..60d5d46 100644 --- a/src/services/apiClient.js +++ b/src/services/apiClient.js @@ -6,25 +6,23 @@ let isRefreshing = false let refreshPromise = null function createConfig(options = {}) { - const token = localStorage.getItem('accessToken'); - const isFormData = options.body instanceof FormData; - + const authStore = useAuthStore() const headers = { - ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + 'Content-Type': 'application/json', ...options.headers, - }; + } - if (token) { - headers['Authorization'] = `Bearer ${token}`; + if (authStore.accessToken) { + headers['Authorization'] = `Bearer ${authStore.accessToken}` } return { headers, + credentials: 'include', ...options, - }; + } } - async function refreshAccessToken() { const authStore = useAuthStore() @@ -55,8 +53,7 @@ async function refreshAccessToken() { }) .catch((error) => { - console.error('Refresh error:', error) - throw new Error('REFRESH_FAILED') + throw error }) .finally(() => { @@ -69,32 +66,29 @@ 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') + const token = data.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) { - 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) - } + const authStore = useAuthStore() + authStore.forceLogout() throw e } } @@ -106,15 +100,9 @@ async function request(url, options = {}, isRetry = false) { } catch { errorData = {} } - - return { - status: response.status, - ok: false, - error: errorData.message || `HTTP Error: ${response.status}`, - } + throw new Error(errorData.message || `HTTP Error: ${response.status}`) } - let data = null const contentType = response.headers.get('content-type') if (contentType?.includes('application/json')) { @@ -125,7 +113,11 @@ 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 = { @@ -140,14 +132,14 @@ export const apiClient = { return request(url, { method: 'POST', body: isFormData ? data : JSON.stringify(data), + headers: isFormData + ? {} + : { 'Content-Type': 'application/json' } }) }, put: (url, data) => request(url, { method: 'PUT', body: JSON.stringify(data), }), - delete: (url, data) => request(url, { - method: 'DELETE', - body: data ? JSON.stringify(data) : undefined, - }), + delete: (url) => request(url, { method: 'DELETE' }), } diff --git a/src/services/colors.js b/src/services/colors.js deleted file mode 100644 index c337f2a..0000000 --- a/src/services/colors.js +++ /dev/null @@ -1,4 +0,0 @@ -export const originColors = { - surugaya: '#2d3081', - mandarake: '#924646', -}; diff --git a/src/stores/authStore.js b/src/stores/authStore.js index 977cd25..7cdfc82 100644 --- a/src/stores/authStore.js +++ b/src/stores/authStore.js @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { computed, nextTick, ref } from 'vue' +import { computed, ref } from 'vue' import { apiClient } from '@/services/apiClient' import router from '@/router/index.js' @@ -54,11 +54,9 @@ 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) - throw error } } diff --git a/src/stores/labelsStore.js b/src/stores/labelsStore.js deleted file mode 100644 index ab8ef28..0000000 --- a/src/stores/labelsStore.js +++ /dev/null @@ -1,107 +0,0 @@ -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?_=${Date.now()}`) - 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}?_=${Date.now()}`, 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/styles/styles.css b/src/styles/styles.css index 815f71a..8219206 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -2,10 +2,6 @@ margin-top: 10px; } -.mt-20 { - margin-top: 20px; -} - .mb-10 { margin-bottom: 10px; } @@ -170,35 +166,3 @@ 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; -} - -.padding-lr-30 { - padding-left: 30px; - padding-right: 30px; -} diff --git a/src/views/CollectionView.vue b/src/views/CollectionView.vue index 52818c0..d3fc1a5 100644 --- a/src/views/CollectionView.vue +++ b/src/views/CollectionView.vue @@ -4,14 +4,12 @@ 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 { @@ -27,39 +25,27 @@ const fetchMerch = async () => { onMounted(() => { fetchMerch() - getLabels() }) const searchQuery = ref('') -const selectedLabelUuids = ref([]) const filteredMerch = computed(() => { - let result = merchList.value || [] - - if (searchQuery.value.trim()) { - const q = searchQuery.value.toLowerCase() - result = result.filter((item) => item.name.toLowerCase().includes(q)) + if (!searchQuery.value.trim()) { + return merchList.value } - - 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 + const q = searchQuery.value.toLowerCase() + return merchList.value.filter((item) => item.name.toLowerCase().includes(q)) }) - + - + diff --git a/src/views/CollectionView/CollectionMerchCard.vue b/src/views/CollectionView/CollectionMerchCard.vue index c3ca223..bb90992 100644 --- a/src/views/CollectionView/CollectionMerchCard.vue +++ b/src/views/CollectionView/CollectionMerchCard.vue @@ -1,33 +1,14 @@ - Add merch + Add merch - + @@ -60,12 +49,13 @@ const labelOptions = computed(() => { placeholder="Select label" clearable multiple - v-model:value="selectedLabelUuids" - :options="labelOptions" - class="mobile-full-width" - /> + v-model:value="value" + :options="options" + class="mobile-full-width" /> - + diff --git a/src/views/DetailsView.vue b/src/views/DetailsView.vue index 7a2e53d..95b27c0 100644 --- a/src/views/DetailsView.vue +++ b/src/views/DetailsView.vue @@ -8,7 +8,6 @@ import { useChartsApi } from '@/api/charts.js' import EditLink from '@/views/DetailsView/EditLink.vue' import CopyToClipboard from '@/components/CopyToClipboard.vue' import DetailsViewImages from '@/views/DetailsView/DetailsViewImages.vue' -import AttachLabel from '@/views/DetailsView/AttachLabel.vue' const { getMerchDetails, deleteMerch } = useMerchApi() const { getDistinctPrices } = useChartsApi() @@ -108,7 +107,6 @@ onMounted(() => { Uuid: {{ merchDetails.merch_uuid }} Name: {{ merchDetails.name }} - diff --git a/src/views/DetailsView/AttachLabel.vue b/src/views/DetailsView/AttachLabel.vue deleted file mode 100644 index 18ca912..0000000 --- a/src/views/DetailsView/AttachLabel.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - Select labels to attach: - - - - Attach - - - - Attached labels. Click label to detach it. - - - - - - - - Manage labels - - - - - - diff --git a/src/views/DetailsView/DetailsViewImages.vue b/src/views/DetailsView/DetailsViewImages.vue index dc7a258..7284760 100644 --- a/src/views/DetailsView/DetailsViewImages.vue +++ b/src/views/DetailsView/DetailsViewImages.vue @@ -54,21 +54,37 @@ function onFileInputChange(event) { event.target.value = '' } -async function fetchImage(bustCache = false) { +async function handleUpload({ fileList: newFileList }) { + const file = newFileList[newFileList.length - 1] try { - let imgUrl = getImageUrl(props.merchUuid, 'full') + await uploadImage(props.merchUuid, file.file) - if (bustCache) { - const separator = imgUrl.includes('?') ? '&' : '?' - imgUrl += `${separator}_t=${Date.now()}` - } + const { imgUrl } = await getImageUrl(props.merchUuid, 'full') + + message.success('Image uploaded successfully.') + + fileList.value = [ + { + name: file.name, + url: imgUrl, + status: 'finished', + }, + ] + } catch (error) { + message.error('Upload error: ' + (error.message || 'Unknown error.')) + } +} + +onMounted(async () => { + try { + const imgUrl = getImageUrl(props.merchUuid, 'full'); await new Promise((resolve, reject) => { - const img = new Image() - img.src = imgUrl - img.onload = () => resolve(imgUrl) - img.onerror = () => reject(new Error('Image not found')) - }) + const img = new Image(); + img.src = imgUrl; + img.onload = () => resolve(imgUrl); + img.onerror = () => reject(new Error('Image not found')); + }); fileList.value = [ { @@ -76,28 +92,13 @@ async function fetchImage(bustCache = false) { url: imgUrl, status: 'finished', }, - ] + ]; } catch (error) { - fileList.value = [] + fileList.value = []; if (!error.message.includes('404')) { - console.error('Error getting image:', error) + console.error('Error getting image: ', error); } } -} - -async function handleUpload({ fileList: newFileList }) { - const file = newFileList[newFileList.length - 1] - try { - await uploadImage(props.merchUuid, file.file) - message.success('Image uploaded successfully.') - await fetchImage(true) - } catch (error) { - message.error('Upload error: ' + (error.message || 'Unknown error.')) - } -} - -onMounted(async () => { - await fetchImage(false) }); const showConfirmDelete = ref(false) diff --git a/src/views/LabelsView.vue b/src/views/LabelsView.vue deleted file mode 100644 index 00637d7..0000000 --- a/src/views/LabelsView.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - - Manage labels - Create label - - - Enter new values to create label. - - - - Create - Cancel - - - - - Create Label - - - - Current labels - Tip: click on a record to edit / delete label. - - - - - - - - - diff --git a/src/views/LabelsView/LabelCard.vue b/src/views/LabelsView/LabelCard.vue deleted file mode 100644 index 0b9d4e7..0000000 --- a/src/views/LabelsView/LabelCard.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - Name: {{ labelData.name }} - UUID: {{ labelData.label_uuid }} - - - - - - - - - - - - - - - - - - Enter new values to update. Or hit delete button to delete record. - - - - - - Check to confirm delete - - - Update - Delete - Cancel - - - - - - - diff --git a/src/views/LabelsView/LabelForm.vue b/src/views/LabelsView/LabelForm.vue deleted file mode 100644 index c10e70c..0000000 --- a/src/views/LabelsView/LabelForm.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Preview: - - - - - - - - - - - - - - - - diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 756bb8d..0a2015e 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -2,23 +2,16 @@ import { reactive, ref } from 'vue' import { useAuthStore } from '@/stores/authStore.js' import { storeToRefs } from 'pinia' -import { useMessage } from 'naive-ui' const store = useAuthStore() -const messages = useMessage() const { activeTab } = storeToRefs(store) const signInEmail = ref('') const signInPassword = ref('') -const onSignIn = async () => { - try{ - await store.login(signInEmail.value, signInPassword.value) - messages.success('Login success') - } catch (error) { - messages.error("Login error") - } +const onSignIn = () => { + store.login(signInEmail.value, signInPassword.value) } const signUp = reactive({ @@ -27,35 +20,8 @@ const signUp = reactive({ reenterPassword: '', }) -const onSignUp = async () => { - if (!signUp.email.trim()) { - messages.error('Email is required') - return - } - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(signUp.email)) { - messages.error('Please enter a valid email address') - return - } - - if (!signUp.password.trim()) { - messages.error('Password is required') - return - } - - if (signUp.password !== signUp.reenterPassword) { - messages.error('Passwords do not match') - return - } - - try{ - await store.register(signUp.email, signUp.password) - messages.success('Register success') - activeTab.value = 'signin' - } catch (error) { - messages.error("Register error") - } +const onSignUp = () => { + store.register(signUp.email, signUp.password) } @@ -77,7 +43,7 @@ const onSignUp = async () => { - Sign In + Sign In @@ -92,7 +58,7 @@ const onSignUp = async () => { - Sign up + Sign up diff --git a/src/views/PersonalView/PersonalMainBlock.vue b/src/views/PersonalView/PersonalMainBlock.vue index cc45fca..0e89baf 100644 --- a/src/views/PersonalView/PersonalMainBlock.vue +++ b/src/views/PersonalView/PersonalMainBlock.vue @@ -14,26 +14,27 @@ onMounted(() => { - Main + - + {{ userData?.email || '---' }} - + {{ userData?.username || '---' }} - + {{ userData?.created_at || '---' }} + diff --git a/src/views/PersonalView/PersonalSessionBlock.vue b/src/views/PersonalView/PersonalSessionBlock.vue index a67743a..f8192d4 100644 --- a/src/views/PersonalView/PersonalSessionBlock.vue +++ b/src/views/PersonalView/PersonalSessionBlock.vue @@ -35,16 +35,16 @@ const onLogout = () => { - Session + - + {{ currentSession?.uuid || '---' }} - + {{ formattedDate }} @@ -52,6 +52,7 @@ const onLogout = () => { Log out + diff --git a/src/views/ZeroPricesView/PeriodSelectTab.vue b/src/views/ZeroPricesView/PeriodSelectTab.vue deleted file mode 100644 index 4239f15..0000000 --- a/src/views/ZeroPricesView/PeriodSelectTab.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - Delete - - - - diff --git a/src/views/ZeroPricesView/TargetZeroesTab.vue b/src/views/ZeroPricesView/TargetZeroesTab.vue deleted file mode 100644 index ba71ba5..0000000 --- a/src/views/ZeroPricesView/TargetZeroesTab.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - Zero prices - No data - - - - - - - - - - - - diff --git a/src/views/ZeroPricesView/ZeroPriceCard.vue b/src/views/ZeroPricesView/ZeroPriceCard.vue deleted file mode 100644 index 32bd31f..0000000 --- a/src/views/ZeroPricesView/ZeroPriceCard.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - Delete - - - Name: {{ props.zeroPrice.name }} - Created: {{ props.zeroPrice.created_at }} - Origin: - - {{ props.zeroPrice.origin }} - - - - - - - diff --git a/src/views/ZeroPricesView/ZeroPricesToolbar.vue b/src/views/ZeroPricesView/ZeroPricesToolbar.vue deleted file mode 100644 index abe01cb..0000000 --- a/src/views/ZeroPricesView/ZeroPricesToolbar.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - Select records to delete - - - {{ props.selected.length }} items selected - - - Click here to select all - - - - - Delete Selected - - - - - -
Uuid: {{ merchDetails.merch_uuid }}
Name: {{ merchDetails.name }}
Select labels to attach:
Attached labels. Click label to detach it.
Enter new values to create label.
Enter new values to update. Or hit delete button to delete record.