Compare commits

..

4 commits

Author SHA1 Message Date
nquidox
c74032d1d0 labels added
All checks were successful
/ Make image (push) Successful in 47s
2025-10-29 20:59:03 +03:00
nquidox
53558c4b46 fix 2025-10-29 20:58:16 +03:00
nquidox
30c4232406 post requests fixed 2025-10-29 20:58:02 +03:00
nquidox
13328aeec3 styles added 2025-10-29 20:57:34 +03:00
19 changed files with 763 additions and 69 deletions

View file

@ -0,0 +1,27 @@
<script setup>
const props = defineProps({
color: {
type: String,
default: '#EF2D56FF'
},
bg_color: {
type: String,
default: '#FFFFFF'
}
})
</script>
<template>
<span class="color-dot" :style="{ borderColor: color, backgroundColor: bg_color }"></span>
</template>
<style scoped>
.color-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
border: 1px solid; /* чтобы border-color работал */
vertical-align: middle;
}
</style>

View file

@ -0,0 +1,45 @@
<script setup>
const props = defineProps({
text: {
type: String,
required: true,
default: 'label'
},
color: {
type: String,
default: '#EF2D56FF'
},
bg_color: {
type: String,
default: '#FFFFFF'
}
})
</script>
<template>
<label class="custom_label"
:style="{color: color, backgroundColor: bg_color, borderColor: color}"
>
{{ text }}
</label>
</template>
<style scoped>
.custom_label {
font-family: 'Heebo', sans-serif;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.025rem;
font-style: normal;
text-transform: uppercase;
border-radius: 1.25rem;
-webkit-border-radius: 1.25rem;
-moz-border-radius: 1.25rem;
padding: 0.35rem 0.75rem;
border-style: solid;
border-width: 0.125rem;
-webkit-box-shadow: none;
-moz-box-shadow: none;
-box-shadow: none;
}
</style>

View file

@ -0,0 +1,7 @@
<template>
<n-button type="primary">
<router-link :to="{ name: 'labels' }" class="router-link-button">
Manage labels
</router-link>
</n-button>
</template>

View file

@ -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;
}

View file

@ -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,
},
],
})

View file

@ -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, {

View file

@ -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)

107
src/stores/labelsStore.js Normal file
View file

@ -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,
}
})

View file

@ -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;
}

View file

@ -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
})
</script>
<template>
<div class="sticky-search-container">
<CollectionToolbar v-model="searchQuery" />
<CollectionToolbar v-model="searchQuery" v-model:labelUuids="selectedLabelUuids" />
</div>
<n-grid responsive="screen" cols="2 s:3 m:4 l:5 xl:6 2xl:7" class="grid-main">
<n-gi class="grid-item" v-for="item in filteredMerch" :key="item.merch_uuid">
<router-link :to="`/details/${item.merch_uuid}`" class="card-link">
<CollectionMerchCard :merch="item" />
<CollectionMerchCard :merch="item" :labels="item.labels" />
</router-link>
</n-gi>
</n-grid>

View file

@ -1,14 +1,33 @@
<script setup>
import BoxIcon from '@/components/icons/BoxIcon.vue'
import { useMerchImagesApi } from '@/api/merchImages.js'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import LabelDotTemplate from '@/components/LabelDotTemplate.vue'
import { useLabelsStore } from '@/stores/labelsStore.js'
const { getImageUrl } = useMerchImagesApi()
const props = defineProps({
merch: {
type: Object,
required: true,
},
labels: {
type: Array,
default: () => [],
},
})
const store = useLabelsStore()
const resolvedLabelDots = computed(() => {
const labelMap = new Map()
for (const label of store.labels) {
labelMap.set(label.label_uuid, label)
}
return props.labels
.map(uuid => labelMap.get(uuid))
.filter(Boolean)
})
const fileList = ref([])
@ -44,6 +63,14 @@ onMounted(async () => {
<n-card class="responsive-card">
<template #header>
<h3 class="card-title">{{ merch.name }}</h3>
<div v-if="resolvedLabelDots.length > 0" class="label-dots">
<LabelDotTemplate
v-for="label in resolvedLabelDots"
:key="label.label_uuid"
:color="label.color"
:bg_color="label.bg_color"
/>
</div>
</template>
<template #cover>
<div class="cover-wrapper">
@ -101,4 +128,11 @@ onMounted(async () => {
height: auto;
max-width: 80%;
}
.label-dots {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: right;
}
</style>

View file

@ -1,47 +1,59 @@
<script setup>
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import router from '@/router/index.js'
import { useLabelsStore } from '@/stores/labelsStore.js'
const value = ref(null)
const options = [
{ label: 'Placeholder 1', value: 'Placeholder 1' },
{ label: 'Placeholder 2', value: 'Placeholder 2' },
]
const addMerch = () => {
router.push({ name: 'addMerch' })
}
const store = useLabelsStore()
const props = defineProps({
modelValue: {
type: String,
default: '',
},
labelUuids: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['update:modelValue', 'update:labelUuids'])
const addMerch = () => {
router.push({ name: 'addMerch' })
}
const localSearch = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const emit = defineEmits(['update:modelValue'])
const localValue = ref(props.modelValue)
watch(
() => props.modelValue,
(newVal) => {
localValue.value = newVal
const selectedLabelUuids = computed({
get() {
return props.labelUuids
},
set(value) {
emit('update:labelUuids', value)
}
)
})
watch(localValue, (newVal) => {
emit('update:modelValue', newVal)
const labelOptions = computed(() => {
return store.labels.map((label) => ({
label: label.name,
value: label.label_uuid,
}))
})
</script>
<template>
<div class="toolbar">
<n-button type="primary" class="toolbar-item" @click="addMerch"> Add merch </n-button>
<n-button type="primary" class="toolbar-item" @click="addMerch"> Add merch</n-button>
<div class="search-wrapper toolbar-item">
<n-input v-model:value="localValue" placeholder="Search..." clearable />
<n-input v-model:value="localSearch" placeholder="Search..." clearable />
</div>
<div class="selector toolbar-item">
@ -49,9 +61,10 @@ watch(localValue, (newVal) => {
placeholder="Select label"
clearable
multiple
v-model:value="value"
:options="options"
class="mobile-full-width" />
v-model:value="selectedLabelUuids"
:options="labelOptions"
class="mobile-full-width"
/>
</div>
</div>
</template>

View file

@ -8,6 +8,7 @@ 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()
@ -107,6 +108,7 @@ onMounted(() => {
<div>
<p><strong>Uuid:</strong> {{ merchDetails.merch_uuid }}</p>
<p><strong>Name:</strong> {{ merchDetails.name }}</p>
<AttachLabel :merch-uuid="props.merch_uuid" />
</div>
<DetailsViewImages :merch-uuid="merchDetails.merch_uuid"/>
</div>

View file

@ -0,0 +1,147 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useLabelsStore } from '@/stores/labelsStore.js'
import { useMessage } from 'naive-ui'
import LabelTemplate from '@/components/LabelTemplate.vue'
const store = useLabelsStore()
const messages = useMessage()
const props = defineProps({
merchUuid: {
type: String,
default: '',
},
})
const attachedLabels = ref([])
const selectedLabelUuids = ref([])
const loading = ref(false)
const labelOptions = computed(() => {
const attachedUuids = new Set(attachedLabels.value.map(l => l.label_uuid))
return store.labels
.filter(label => !attachedUuids.has(label.label_uuid))
.map(label => ({
label: label.name,
value: label.label_uuid,
}))
})
const fetchAttachedLabels = async () => {
if (!props.merchUuid) return
loading.value = true
try {
const response = await store.getMerchLabels(props.merchUuid)
const uuids = response.data || []
if (!Array.isArray(uuids)) {
attachedLabels.value = []
return
}
const labelMap = new Map()
for (const label of store.labels) {
labelMap.set(label.label_uuid, label)
}
attachedLabels.value = uuids
.map(uuid => labelMap.get(uuid))
.filter(Boolean)
} catch (error) {
messages.error('Failed to load attached labels: ' + error)
attachedLabels.value = []
} finally {
loading.value = false
}
}
const handleAttach = async () => {
if (!props.merchUuid) {
messages.error('Merch UUID is missing')
return
}
if (selectedLabelUuids.value.length === 0) {
messages.warning('No labels selected')
return
}
try {
await Promise.all(
selectedLabelUuids.value.map(labelUuid =>
store.attachLabel(props.merchUuid, labelUuid)
)
)
messages.success('Label(s) attached successfully')
selectedLabelUuids.value = []
await fetchAttachedLabels()
} catch (error) {
messages.error('Failed to attach label(s): ' + error)
}
}
const handleDetach = async (labelUuid) => {
if (!props.merchUuid) {
messages.error('Merch UUID is missing')
return
}
try {
await store.detachLabel(props.merchUuid, labelUuid)
messages.success('Label detached successfully')
await fetchAttachedLabels()
} catch (error) {
messages.error('Failed to detach label: ' + error)
}
}
onMounted(() => {
fetchAttachedLabels()
})
</script>
<template>
<p><strong>Select labels to attach:</strong></p>
<div class="label-row">
<n-select
v-model:value="selectedLabelUuids"
multiple
:options="labelOptions"
:loading="loading"
/>
<n-button
type="primary"
@click="handleAttach"
:disabled="selectedLabelUuids.length === 0 || loading"
style="width: 100px; margin-left: 12px"
>
Attach
</n-button>
</div>
<p><strong>Attached labels. Click label to detach it.</strong></p>
<div v-if="attachedLabels.length > 0" class="mt-10 label-row">
<LabelTemplate
v-for="label in attachedLabels"
:key="label.label_uuid"
:text="label.name"
:color="label.color"
:bg_color="label.bg_color"
@click="handleDetach(label.label_uuid)"
style="cursor: pointer"
/>
</div>
<div class="mt-20">
<router-link :to="{ name: 'labels' }">
<n-button type="primary">
Manage labels
</n-button>
</router-link>
</div>
</template>
<style scoped>
</style>

78
src/views/LabelsView.vue Normal file
View file

@ -0,0 +1,78 @@
<script setup>
import { useLabelsStore } from '@/stores/labelsStore.js'
import LabelCard from '@/views/LabelsView/LabelCard.vue'
import { NModal, useMessage } from 'naive-ui'
import LabelForm from '@/views/LabelsView/LabelForm.vue'
import { ref } from 'vue'
import ScrollToTopButton from '@/components/ScrollToTopButton.vue'
const store = useLabelsStore()
const messages = useMessage()
const editForm = ref({
name: '',
color: '#EF2D56FF',
bg_color: '#FFFFFF',
})
const handleCreate = async () => {
try {
const name = editForm.value.name?.trim() || 'preview'
await store.createLabel({
name,
color: editForm.value.color,
bg_color: editForm.value.bg_color,
})
editForm.value = {
name: '',
color: '#EF2D56FF',
bg_color: '#FFFFFF',
}
showCreateModal.value = false
messages.success('Label created.')
} catch (error) {
messages.error(error)
console.error('Failed to create label in component:', error)
}
}
const showCreateModal = ref(false)
const cancelCreate = () => {
showCreateModal.value = false
}
</script>
<template>
<h3>Manage labels</h3>
<n-divider title-placement="left">Create label</n-divider>
<n-modal v-model:show="showCreateModal" preset="dialog" title="Create new label">
<template #default>
<p>Enter new values to create label.</p>
<LabelForm :edit-form="editForm" />
</template>
<template #action>
<n-button @click="handleCreate" type="primary">Create</n-button>
<n-button @click="cancelCreate">Cancel</n-button>
</template>
</n-modal>
<div class="mt-10 c-center">
<n-button type="primary" @click="showCreateModal=true" :loading="false" class="w360">
Create Label
</n-button>
</div>
<n-divider title-placement="left">Current labels</n-divider>
<h4 class="text-center">Tip: click on a record to edit / delete label.</h4>
<div v-for="label in store.labels" :key="label.label_uuid">
<LabelCard :label-uuid="label.label_uuid" />
</div>
<ScrollToTopButton />
</template>
<style scoped></style>

View file

@ -0,0 +1,143 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { NModal, useMessage } from 'naive-ui'
import { useLabelsStore } from '@/stores/labelsStore.js'
import LabelTemplate from '@/components/LabelTemplate.vue'
import LabelDotTemplate from '@/components/LabelDotTemplate.vue'
import LabelForm from '@/views/LabelsView/LabelForm.vue'
const store = useLabelsStore()
const message = useMessage()
const props = defineProps({
labelUuid: {
type: String,
required: true,
},
})
const labelData = computed(() => {
return store.labels.find((label) => label.label_uuid === props.labelUuid)
})
const editForm = ref({})
const showEditModal = ref(false)
const editLabelRecordHandler = () => {
if (!labelData.value) return
editForm.value = { ...labelData.value }
showEditModal.value = true
}
const cancelEdit = () => {
showEditModal.value = false
}
const handleUpdate = async () => {
try {
await store.updateLabel(editForm.value.label_uuid, {
name: editForm.value.name,
color: editForm.value.color,
bg_color: editForm.value.bg_color,
})
showEditModal.value = false
message.success('Label updated successfully.')
} catch (error) {
message.error(error.message)
}
}
const confirmDelete = ref(false)
const handleDelete = async () => {
if (!confirmDelete.value) return
try {
await store.deleteLabel(props.labelUuid)
showEditModal.value = false
message.success('Label deleted successfully.')
} catch (error) {
message.error(error.message)
}
}
//на случай изменений извне, хотя таких быть не должно
watch(
() => props.labelData,
(newVal) => {
if (showEditModal.value) return
Object.assign(editForm.value, newVal)
},
{ deep: true },
)
</script>
<template>
<n-list hoverable clickable class="bottom-border">
<n-list-item @click="editLabelRecordHandler">
<div class="label-container">
<div>
<n-thing>Name: {{ labelData.name }}</n-thing>
<n-thing>UUID: {{ labelData.label_uuid }}</n-thing>
</div>
<div class="label-column">
<n-thing>
<div class="label-row">
<LabelDotTemplate :color="labelData.color" :bg_color="labelData.bg_color" />
<LabelTemplate
:text="labelData.name"
:color="labelData.color"
:bg_color="labelData.bg_color"
/>
</div>
</n-thing>
</div>
</div>
</n-list-item>
</n-list>
<n-modal v-model:show="showEditModal" preset="dialog" title="Manage label record">
<template #default>
<p>Enter new values to update. Or hit delete button to delete record.</p>
<LabelForm :edit-form="editForm" />
</template>
<template #action>
<div class="modal-actions">
<div class="checkbox-row">
<n-checkbox v-model:checked="confirmDelete"> Check to confirm delete </n-checkbox>
</div>
<div class="buttons-row">
<n-button @click="handleUpdate" type="warning">Update</n-button>
<n-button @click="handleDelete" type="error" :disabled="!confirmDelete">Delete</n-button>
<n-button @click="cancelEdit">Cancel</n-button>
</div>
</div>
</template>
</n-modal>
</template>
<style scoped>
.bottom-border {
border-bottom: rgba(24, 160, 88, 0.25) solid 1px;
}
.modal-actions {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.checkbox-row,
.buttons-row {
display: flex;
align-items: center;
}
.buttons-row {
justify-content: flex-end;
gap: 8px;
}
</style>

View file

@ -0,0 +1,36 @@
<!-- LabelForm.vue -->
<script setup>
import LabelDotTemplate from '@/components/LabelDotTemplate.vue'
import LabelTemplate from '@/components/LabelTemplate.vue'
defineProps({
editForm: {
type: Object,
required: true
}
})
</script>
<template>
<div class="label-row label-preview">
<span>Preview: </span>
<LabelDotTemplate :color="editForm.color" :bg_color="editForm.bg_color" />
<LabelTemplate
:text="editForm.name || 'preview'"
:color="editForm.color"
:bg_color="editForm.bg_color"
/>
</div>
<n-form :model="editForm" label-placement="left" label-width="100">
<n-form-item label="Name" path="name">
<n-input v-model:value="editForm.name" />
</n-form-item>
<n-form-item label="Border and text color" path="color">
<n-color-picker v-model:value="editForm.color" />
</n-form-item>
<n-form-item label="Background color" path="bg_color">
<n-color-picker v-model:value="editForm.bg_color" />
</n-form-item>
</n-form>
</template>

View file

@ -14,27 +14,26 @@ onMounted(() => {
</script>
<template>
<n-card :bordered="false" title="Main">
<n-divider title-placement="left">Main</n-divider>
<n-list hoverable clickable>
<n-list-item>
<n-thing title="Email" content-style="margin-top: 10px;">
<n-thing title="Email" class="mt-10">
{{ userData?.email || '---' }}
</n-thing>
</n-list-item>
<n-list-item>
<n-thing title="Username" content-style="margin-top: 10px;">
<n-thing title="Username" class="mt-10">
{{ userData?.username || '---' }}
</n-thing>
</n-list-item>
<n-list-item>
<n-thing title="Created At" content-style="margin-top: 10px;">
<n-thing title="Created At" class="mt-10">
{{ userData?.created_at || '---' }}
</n-thing>
</n-list-item>
</n-list>
</n-card>
</template>
<style scoped></style>

View file

@ -35,16 +35,16 @@ const onLogout = () => {
</script>
<template>
<n-card :bordered="false" title="Session">
<n-divider title-placement="left">Session</n-divider>
<n-list hoverable clickable>
<n-list-item>
<n-thing title="Session id" content-style="margin-top: 10px;">
<n-thing title="Session id" class="mt-10">
{{ currentSession?.uuid || '---' }}
</n-thing>
</n-list-item>
<n-list-item>
<n-thing title="Expires at" content-style="margin-top: 10px;">
<n-thing title="Expires at" class="mt-10">
{{ formattedDate }}
</n-thing>
</n-list-item>
@ -52,7 +52,6 @@ const onLogout = () => {
<n-button type="info" class="center-button" @click="onLogout">Log out</n-button>
</div>
</n-list>
</n-card>
</template>
<style scoped>