DONE: digital card with profile display: WIP: membership condition on letter, is_first_time column in users table
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||
import { downloadMemberDigitalCard } from '../services/member-digital-card.service'
|
||||
import { toProxiedStorageUrl } from '../utils/member-digital-card.utils'
|
||||
import MemberDigitalCardFlip from './MemberDigitalCardFlip.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
memberNumber?: string | number | null
|
||||
memberName?: string | null
|
||||
memberType?: string | null
|
||||
companyName?: string | null
|
||||
profileUrl?: string | null
|
||||
imageUrl?: string | null
|
||||
large?: boolean
|
||||
}>()
|
||||
|
||||
const previewMaxWidthClass = computed(() =>
|
||||
props.large ? 'max-w-sm sm:max-w-md lg:max-w-lg' : 'max-w-68 sm:max-w-xs',
|
||||
)
|
||||
|
||||
const resolvedImageUrl = computed(() => toProxiedStorageUrl(props.imageUrl))
|
||||
|
||||
const isFlipped = ref(false)
|
||||
const expandedOpen = ref(false)
|
||||
const isPortraitPhone = ref(false)
|
||||
const downloading = ref(false)
|
||||
|
||||
let portraitQuery: MediaQueryList | null = null
|
||||
|
||||
function updatePortraitPhone() {
|
||||
isPortraitPhone.value = portraitQuery?.matches ?? false
|
||||
}
|
||||
|
||||
function openExpanded() {
|
||||
expandedOpen.value = true
|
||||
}
|
||||
|
||||
function closeExpanded() {
|
||||
expandedOpen.value = false
|
||||
}
|
||||
|
||||
function toggleFlip() {
|
||||
isFlipped.value = !isFlipped.value
|
||||
}
|
||||
|
||||
async function downloadCard() {
|
||||
if (downloading.value) return
|
||||
|
||||
downloading.value = true
|
||||
const side = isFlipped.value ? 'belakang' : 'depan'
|
||||
|
||||
try {
|
||||
await downloadMemberDigitalCard(props.memberNumber, side)
|
||||
|
||||
await Swal.fire({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: 'success',
|
||||
title: `Kad ${side} berjaya disimpan.`,
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
})
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Ralat',
|
||||
text: getApiErrorMessage(error, 'Gagal menyimpan kad.'),
|
||||
})
|
||||
} finally {
|
||||
downloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(expandedOpen, (open) => {
|
||||
document.body.style.overflow = open ? 'hidden' : ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
portraitQuery = window.matchMedia('(max-width: 767px) and (orientation: portrait)')
|
||||
updatePortraitPhone()
|
||||
portraitQuery.addEventListener('change', updatePortraitPhone)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.style.overflow = ''
|
||||
portraitQuery?.removeEventListener('change', updatePortraitPhone)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full flex-col items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="relative w-full cursor-pointer border-0 bg-transparent p-0 transition-transform active:scale-[0.98]"
|
||||
:class="previewMaxWidthClass"
|
||||
aria-label="Buka kad digital penuh"
|
||||
@click="openExpanded">
|
||||
<MemberDigitalCardFlip :member-number="memberNumber" :member-name="memberName"
|
||||
:member-type="memberType" :company-name="companyName" :profile-url="profileUrl"
|
||||
:image-url="resolvedImageUrl" :is-flipped="isFlipped" />
|
||||
</button>
|
||||
|
||||
<p class="text-center text-[11px] text-slate-500">
|
||||
Klik kad untuk paparan penuh
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-2">
|
||||
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none text-xs"
|
||||
:aria-pressed="isFlipped" :disabled="downloading" @click="toggleFlip">
|
||||
<Lucide class="mr-2 size-4" icon="RotateCw" />
|
||||
{{ isFlipped ? 'Papar depan kad' : 'Imbas kod QR' }}
|
||||
</Button>
|
||||
|
||||
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none text-xs"
|
||||
:disabled="downloading" @click="downloadCard">
|
||||
<Lucide class="mr-2 size-4" :icon="downloading ? 'LoaderCircle' : 'Download'"
|
||||
:class="{ 'animate-spin': downloading }" />
|
||||
{{ downloading ? 'Menyimpan...' : 'Simpan kad' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="expandedOpen"
|
||||
class="fixed inset-0 z-70 flex flex-col items-center justify-center gap-4 bg-black/90 p-5"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Kad digital anggota"
|
||||
@click.self="closeExpanded">
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-4 top-4 flex size-10 items-center justify-center rounded-full border border-white/20 bg-white/10 text-white"
|
||||
aria-label="Tutup"
|
||||
@click="closeExpanded">
|
||||
<Lucide class="size-5" icon="X" />
|
||||
</button>
|
||||
|
||||
<div v-if="isPortraitPhone" class="flex items-center justify-center" @click.stop>
|
||||
<div class="w-[min(90vh,34rem)] rotate-90">
|
||||
<MemberDigitalCardFlip :member-number="memberNumber" :member-name="memberName"
|
||||
:member-type="memberType" :company-name="companyName" :profile-url="profileUrl"
|
||||
:image-url="resolvedImageUrl" :is-flipped="isFlipped" expanded />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-[min(100vw-2rem,32rem)] lg:w-[min(100vw-2rem,40rem)]" @click.stop>
|
||||
<MemberDigitalCardFlip :member-number="memberNumber" :member-name="memberName"
|
||||
:member-type="memberType" :company-name="companyName" :profile-url="profileUrl"
|
||||
:image-url="resolvedImageUrl" :is-flipped="isFlipped" expanded />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="border border-white/20 bg-white/10 text-xs text-white shadow-none hover:bg-white/15"
|
||||
:aria-pressed="isFlipped"
|
||||
:disabled="downloading"
|
||||
@click.stop="toggleFlip">
|
||||
<Lucide class="mr-2 size-4" icon="RotateCw" />
|
||||
{{ isFlipped ? 'Papar depan kad' : 'Imbas kod QR' }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="border border-white/20 bg-white/10 text-xs text-white shadow-none hover:bg-white/15"
|
||||
:disabled="downloading"
|
||||
@click.stop="downloadCard">
|
||||
<Lucide class="mr-2 size-4" :icon="downloading ? 'LoaderCircle' : 'Download'"
|
||||
:class="{ 'animate-spin': downloading }" />
|
||||
{{ downloading ? 'Menyimpan...' : 'Simpan kad' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-white/60">
|
||||
Klik di luar kad untuk tutup
|
||||
</p>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user