357 lines
9.3 KiB
JavaScript
357 lines
9.3 KiB
JavaScript
import { t, i18n } from '../i18n.js'
|
|
import { verificationService } from '../services/verification.js'
|
|
|
|
class VerificationWidget extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['listing-id', 'verified', 'verified-date']
|
|
}
|
|
|
|
constructor() {
|
|
super()
|
|
this.state = 'idle'
|
|
this.currentCode = null
|
|
this.timerInterval = null
|
|
this.i18nUnsubscribe = null
|
|
}
|
|
|
|
connectedCallback() {
|
|
const verified = this.getAttribute('verified')
|
|
if (verified === 'true') {
|
|
this.state = 'verified'
|
|
}
|
|
this.render()
|
|
this.i18nUnsubscribe = i18n.subscribe(() => this.render())
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.clearTimer()
|
|
if (this.i18nUnsubscribe) this.i18nUnsubscribe()
|
|
}
|
|
|
|
attributeChangedCallback() {
|
|
if (this.isConnected) {
|
|
const verified = this.getAttribute('verified')
|
|
if (verified === 'true') {
|
|
this.state = 'verified'
|
|
}
|
|
this.render()
|
|
}
|
|
}
|
|
|
|
clearTimer() {
|
|
if (this.timerInterval) {
|
|
clearInterval(this.timerInterval)
|
|
this.timerInterval = null
|
|
}
|
|
}
|
|
|
|
startVerification() {
|
|
this.currentCode = verificationService.generateCode()
|
|
this.state = 'active'
|
|
this.render()
|
|
this.startCountdown()
|
|
}
|
|
|
|
startCountdown() {
|
|
this.clearTimer()
|
|
this.timerInterval = setInterval(() => {
|
|
if (!this.currentCode) {
|
|
this.clearTimer()
|
|
return
|
|
}
|
|
const remaining = this.getRemainingTime()
|
|
if (remaining <= 0) {
|
|
this.clearTimer()
|
|
this.state = 'expired'
|
|
this.render()
|
|
this.setupEventListeners()
|
|
return
|
|
}
|
|
const timerEl = this.querySelector('.verification-timer')
|
|
if (timerEl) {
|
|
timerEl.textContent = this.formatTime(remaining)
|
|
if (remaining <= 60000) {
|
|
timerEl.classList.add('verification-timer--warning')
|
|
}
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
getRemainingTime() {
|
|
if (!this.currentCode) return 0
|
|
const expiresAt = new Date(this.currentCode.expiresAt).getTime()
|
|
return Math.max(0, expiresAt - Date.now())
|
|
}
|
|
|
|
formatTime(ms) {
|
|
const totalSeconds = Math.floor(ms / 1000)
|
|
const minutes = Math.floor(totalSeconds / 60)
|
|
const seconds = totalSeconds % 60
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
async handleUpload(e) {
|
|
const file = e.target.files?.[0]
|
|
if (!file) return
|
|
|
|
const listingId = this.getAttribute('listing-id')
|
|
if (!listingId || !this.currentCode) return
|
|
|
|
this.state = 'uploading'
|
|
this.render()
|
|
|
|
try {
|
|
const success = await verificationService.verify(listingId, this.currentCode.code, file)
|
|
if (success) {
|
|
this.clearTimer()
|
|
this.state = 'verified'
|
|
this.render()
|
|
this.dispatchEvent(new CustomEvent('verification-complete', {
|
|
bubbles: true,
|
|
detail: { listingId }
|
|
}))
|
|
} else {
|
|
this.state = 'active'
|
|
this.render()
|
|
}
|
|
} catch (err) {
|
|
console.error('Verification failed:', err)
|
|
this.state = 'active'
|
|
this.render()
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (this.state === 'verified') {
|
|
this.innerHTML = this.renderVerified()
|
|
} else if (this.state === 'active' || this.state === 'uploading') {
|
|
this.innerHTML = this.renderActive()
|
|
} else if (this.state === 'expired') {
|
|
this.innerHTML = this.renderExpired()
|
|
} else {
|
|
this.innerHTML = this.renderIdle()
|
|
}
|
|
this.setupEventListeners()
|
|
}
|
|
|
|
renderIdle() {
|
|
return `
|
|
<div class="verification-widget">
|
|
<button class="verification-toggle" type="button">
|
|
<span class="verification-toggle-label">✓ ${t('verification.verify')}</span>
|
|
<span class="verification-toggle-subtitle">${t('verification.optional')}</span>
|
|
</button>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
renderActive() {
|
|
const remaining = this.getRemainingTime()
|
|
const isUploading = this.state === 'uploading'
|
|
return `
|
|
<div class="verification-widget verification-widget--active">
|
|
<div class="verification-code">${this.currentCode.code}</div>
|
|
<div class="verification-timer${remaining <= 60000 ? ' verification-timer--warning' : ''}">${this.formatTime(remaining)}</div>
|
|
<p class="verification-instructions">${t('verification.instructions')}</p>
|
|
<label class="btn btn-outline verification-upload${isUploading ? ' verification-upload--loading' : ''}">
|
|
${isUploading
|
|
? `<span class="verification-spinner"></span>`
|
|
: t('verification.upload')}
|
|
<input type="file" accept="image/*" capture="environment" hidden${isUploading ? ' disabled' : ''}>
|
|
</label>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
renderExpired() {
|
|
return `
|
|
<div class="verification-widget verification-widget--expired">
|
|
<p class="verification-expired-text">${t('verification.expired')}</p>
|
|
<button class="btn btn-outline verification-regenerate" type="button">✓ ${t('verification.verify')}</button>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
renderVerified() {
|
|
const verifiedDate = this.getAttribute('verified-date')
|
|
const dateStr = verifiedDate
|
|
? t('verification.verifiedDate', { date: new Date(verifiedDate).toLocaleDateString() })
|
|
: ''
|
|
return `
|
|
<div class="verification-widget verification-widget--verified">
|
|
<div class="verification-success">
|
|
<svg class="verification-success-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
<polyline points="20 6 9 17 4 12"></polyline>
|
|
</svg>
|
|
<span>${t('verification.verified')}</span>
|
|
</div>
|
|
${dateStr ? `<span class="verification-date">${dateStr}</span>` : ''}
|
|
</div>
|
|
`
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const toggleBtn = this.querySelector('.verification-toggle')
|
|
if (toggleBtn) {
|
|
toggleBtn.addEventListener('click', () => this.startVerification())
|
|
}
|
|
|
|
const regenerateBtn = this.querySelector('.verification-regenerate')
|
|
if (regenerateBtn) {
|
|
regenerateBtn.addEventListener('click', () => this.startVerification())
|
|
}
|
|
|
|
const fileInput = this.querySelector('input[type="file"]')
|
|
if (fileInput) {
|
|
fileInput.addEventListener('change', (e) => this.handleUpload(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('verification-widget', VerificationWidget)
|
|
|
|
const style = document.createElement('style')
|
|
style.textContent = `
|
|
.verification-widget {
|
|
padding: var(--space-md);
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.verification-toggle {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
width: 100%;
|
|
padding: var(--space-md) var(--space-lg);
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
cursor: pointer;
|
|
transition: var(--transition-fast);
|
|
}
|
|
|
|
.verification-toggle:hover {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.verification-toggle-label {
|
|
font-size: var(--font-size-base);
|
|
font-weight: 500;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.verification-toggle-subtitle {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.verification-widget--active {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--space-md);
|
|
border: 1px solid var(--color-border);
|
|
background: var(--color-bg-secondary);
|
|
}
|
|
|
|
.verification-code {
|
|
font-family: monospace;
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
letter-spacing: 0.5em;
|
|
color: var(--color-text);
|
|
text-align: center;
|
|
padding: var(--space-md) var(--space-lg);
|
|
background: var(--color-bg);
|
|
border-radius: var(--radius-md);
|
|
user-select: all;
|
|
}
|
|
|
|
.verification-timer {
|
|
font-size: var(--font-size-lg);
|
|
color: var(--color-text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.verification-timer--warning {
|
|
color: var(--color-warning);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.verification-instructions {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-muted);
|
|
text-align: center;
|
|
margin: 0;
|
|
}
|
|
|
|
.verification-upload {
|
|
position: relative;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.verification-upload--loading {
|
|
pointer-events: none;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.verification-spinner {
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid var(--color-border);
|
|
border-top-color: var(--color-primary);
|
|
border-radius: var(--radius-full);
|
|
animation: verification-spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes verification-spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.verification-widget--expired {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--space-md);
|
|
border: 1px solid var(--color-border);
|
|
background: var(--color-bg-secondary);
|
|
}
|
|
|
|
.verification-expired-text {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-error);
|
|
margin: 0;
|
|
}
|
|
|
|
.verification-widget--verified {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
}
|
|
|
|
.verification-success {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
color: var(--color-success);
|
|
font-weight: 600;
|
|
font-size: var(--font-size-base);
|
|
}
|
|
|
|
.verification-success-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.verification-date {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--color-text-muted);
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|
|
|
|
export { VerificationWidget }
|