12245759ac
CI / test (push) Has been cancelled
Web app: new entities (Image, RenderedAsset, SharedImage, Token, DeviceImageHistory), enums, repositories, controllers, message handlers, migrations, tests, frontend upload/library/sticker UI, Vue components. Firmware: EPD background screen binaries + gen scripts, setup_bg header. Infra: ddev config, test bundle, gitignore coverage dir. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
3.7 KiB
Vue
133 lines
3.7 KiB
Vue
<template>
|
|
<div class="approve-card">
|
|
<img :src="item.thumbnailUrl" :alt="`Photo from ${item.sharedBy}`" class="approve-card__thumb" loading="lazy" />
|
|
|
|
<div class="approve-card__body">
|
|
<p class="approve-card__from">From <strong>{{ item.sharedBy }}</strong></p>
|
|
<p class="approve-card__date">{{ formattedDate }}</p>
|
|
|
|
<div class="approve-card__status" v-if="item.status !== 'pending'">
|
|
<span :class="['approve-card__badge', `approve-card__badge--${item.status}`]">
|
|
{{ item.status }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="approve-card__actions">
|
|
<template v-if="item.status === 'pending' || item.status === 'declined'">
|
|
<BaseButton variant="primary" size="sm" :disabled="busy" @click="showPicker = true">
|
|
{{ item.status === 'declined' ? 'Add anyway' : 'Add to frame' }}
|
|
</BaseButton>
|
|
</template>
|
|
<template v-if="item.status === 'pending' || item.status === 'approved'">
|
|
<BaseButton variant="ghost" size="sm" :disabled="busy" @click="decline">
|
|
{{ item.status === 'approved' ? 'Remove' : 'Decline' }}
|
|
</BaseButton>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DevicePicker
|
|
v-model="showPicker"
|
|
:devices="devicesStore.devices"
|
|
:selected="selectedDeviceIds"
|
|
:uploading="busy"
|
|
confirm-label="Add to frames"
|
|
@update:selected="selectedDeviceIds = $event"
|
|
@confirm="approve"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import type { SharedImage } from '@/types'
|
|
import BaseButton from '@/components/BaseButton.vue'
|
|
import DevicePicker from '@/components/DevicePicker.vue'
|
|
import { useImagesStore } from '@/stores/images'
|
|
import { useDevicesStore } from '@/stores/devices'
|
|
|
|
const props = defineProps<{ item: SharedImage }>()
|
|
const emit = defineEmits<{ (e: 'updated', v: SharedImage): void }>()
|
|
|
|
const imagesStore = useImagesStore()
|
|
const devicesStore = useDevicesStore()
|
|
const showPicker = ref(false)
|
|
const busy = ref(false)
|
|
const selectedDeviceIds = ref<number[]>([])
|
|
|
|
const formattedDate = computed(() =>
|
|
new Date(props.item.sharedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
)
|
|
|
|
async function approve() {
|
|
showPicker.value = false
|
|
busy.value = true
|
|
try {
|
|
const updated = await imagesStore.approveShared(props.item.id, selectedDeviceIds.value)
|
|
emit('updated', updated)
|
|
} finally {
|
|
busy.value = false
|
|
selectedDeviceIds.value = []
|
|
}
|
|
}
|
|
|
|
async function decline() {
|
|
busy.value = true
|
|
try {
|
|
const updated = await imagesStore.declineShared(props.item.id)
|
|
emit('updated', updated)
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.approve-card {
|
|
display: flex;
|
|
gap: var(--space-3);
|
|
padding: var(--space-3);
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
|
|
&__thumb {
|
|
width: 80px;
|
|
height: 80px;
|
|
object-fit: cover;
|
|
border-radius: var(--radius-sm);
|
|
flex-shrink: 0;
|
|
background: var(--color-border);
|
|
}
|
|
|
|
&__body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-1);
|
|
}
|
|
|
|
&__from { font-size: var(--text-sm); }
|
|
&__date { font-size: var(--text-xs); color: var(--color-text-muted); }
|
|
|
|
&__badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
text-transform: capitalize;
|
|
|
|
&--approved { background: #d4edda; color: #1a7f4b; }
|
|
&--declined { background: #fde8e8; color: #d93025; }
|
|
}
|
|
|
|
&__actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
margin-top: auto;
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
</style>
|