- Fixed modal background opacity from 0.5 to 0.75 for better visibility - Fixed X button close functionality in TicketTypeModal - Resolved CORS issues by removing credentials: 'include' from Supabase client - Fixed onSave callback signature mismatch in TicketsTab component - Removed old initEmbedModal function references causing JavaScript errors - Added comprehensive Playwright tests for ticket button functionality - Verified modal works correctly in both light and dark modes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
504 lines
21 KiB
Plaintext
504 lines
21 KiB
Plaintext
---
|
|
import SimpleEmbedTest from './SimpleEmbedTest.tsx';
|
|
|
|
interface Props {
|
|
eventId: string;
|
|
}
|
|
|
|
const { eventId } = Astro.props;
|
|
---
|
|
|
|
<div class="backdrop-blur-xl rounded-3xl shadow-2xl mb-8 overflow-hidden ring-1 transition-all duration-200 hover:shadow-3xl"
|
|
style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border-subtle); ring-color: var(--glass-ring-dark);"
|
|
data-theme-card="true">
|
|
<div class="px-8 py-12" style="color: var(--glass-text-primary);">
|
|
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center">
|
|
<div class="flex-1 mb-6 lg:mb-0">
|
|
<h1 id="event-title" class="text-3xl font-light mb-2 tracking-wide" style="color: var(--glass-text-primary);">Loading...</h1>
|
|
<div class="flex flex-wrap items-center gap-4 text-sm mb-3" style="color: var(--glass-text-secondary);">
|
|
<div class="flex items-center gap-1">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
</svg>
|
|
<span id="event-venue">--</span>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
</svg>
|
|
<span id="event-date">--</span>
|
|
</div>
|
|
</div>
|
|
<div class="max-w-2xl">
|
|
<p id="event-description" class="text-sm leading-relaxed transition-all duration-300 overflow-hidden body-text" style="color: var(--glass-text-tertiary);">Loading event details...</p>
|
|
<button id="description-toggle" class="text-xs mt-1 hidden transition-colors hover:opacity-80" style="color: var(--glass-text-accent);">Show more</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col items-end space-y-4">
|
|
<!-- Revenue Display -->
|
|
<div class="text-right">
|
|
<div class="text-2xl font-semibold" id="total-revenue" style="color: var(--glass-text-primary);">$0</div>
|
|
<div class="text-xs" style="color: var(--glass-text-secondary);">Total Revenue</div>
|
|
</div>
|
|
|
|
<!-- Compact Button Grid -->
|
|
<div class="grid grid-cols-3 gap-2 lg:gap-3">
|
|
<!-- Top Row: Quick Actions -->
|
|
<a
|
|
id="preview-link"
|
|
href="#"
|
|
target="_blank"
|
|
class="px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out backdrop-blur-sm flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg ring-1 hover:ring-2"
|
|
style="background: var(--glass-bg-button); color: var(--glass-text-primary); border: 1px solid var(--glass-border); ring-color: var(--glass-ring-dark);"
|
|
title="Preview Page"
|
|
>
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
</svg>
|
|
Preview
|
|
</a>
|
|
<button
|
|
id="embed-code-btn"
|
|
class="px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out backdrop-blur-sm flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg ring-1 hover:ring-2"
|
|
style="background: var(--glass-bg-button); color: var(--glass-text-primary); border: 1px solid var(--glass-border); ring-color: var(--glass-ring-dark);"
|
|
title="Get Embed Code"
|
|
>
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
|
</svg>
|
|
Embed
|
|
</button>
|
|
<button
|
|
id="edit-event-btn"
|
|
class="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg"
|
|
title="Edit Event"
|
|
>
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
</svg>
|
|
Edit
|
|
</button>
|
|
|
|
<!-- Bottom Row: Tools -->
|
|
<a
|
|
href="/scan"
|
|
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
|
|
title="Scanner"
|
|
>
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h4m-6 0h-2v4m0-11v3m0 0h.01M12 12h.01M16 8h4m-6 0h-2v4m0-11v3m0 0h.01M12 12h.01"></path>
|
|
</svg>
|
|
Scanner
|
|
</a>
|
|
<a
|
|
id="kiosk-link"
|
|
href="#"
|
|
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
|
|
title="Sales Kiosk"
|
|
>
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
|
</svg>
|
|
Kiosk
|
|
</a>
|
|
<button
|
|
id="generate-kiosk-pin-btn"
|
|
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
|
|
title="Generate Kiosk PIN"
|
|
>
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
PIN
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Simple Embed Test -->
|
|
<SimpleEmbedTest
|
|
client:load
|
|
eventId={eventId}
|
|
/>
|
|
|
|
<script define:vars={{ eventId }}>
|
|
// Initialize event header when page loads
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await loadEventHeader();
|
|
});
|
|
|
|
// Format currency helper function
|
|
function formatCurrency(amountInCents) {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(amountInCents / 100);
|
|
}
|
|
|
|
// Format date helper function
|
|
function formatDate(dateString) {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
async function loadEventHeader() {
|
|
try {
|
|
// Load event details using server-side API
|
|
const eventResponse = await fetch(`/api/events/${eventId}`);
|
|
if (!eventResponse.ok) {
|
|
throw new Error('Failed to load event');
|
|
}
|
|
|
|
const event = await eventResponse.json();
|
|
|
|
// Load event stats using server-side API
|
|
const statsResponse = await fetch(`/api/events/${eventId}/stats`);
|
|
let stats = null;
|
|
if (statsResponse.ok) {
|
|
stats = await statsResponse.json();
|
|
}
|
|
|
|
// Update event details
|
|
document.getElementById('event-title').textContent = event.title;
|
|
document.getElementById('event-venue').textContent = event.venue;
|
|
|
|
// Use start_time from database
|
|
document.getElementById('event-date').textContent = formatDate(event.start_time);
|
|
|
|
// Handle description truncation
|
|
const descriptionEl = document.getElementById('event-description');
|
|
const toggleBtn = document.getElementById('description-toggle');
|
|
const fullDescription = event.description;
|
|
const maxLength = 120; // Show about one line
|
|
|
|
if (fullDescription && fullDescription.length > maxLength) {
|
|
// Set initial truncated text
|
|
descriptionEl.textContent = fullDescription.substring(0, maxLength) + '...';
|
|
descriptionEl.dataset.fullText = fullDescription;
|
|
descriptionEl.dataset.truncatedText = fullDescription.substring(0, maxLength) + '...';
|
|
descriptionEl.dataset.expanded = 'false';
|
|
|
|
// Show toggle button
|
|
toggleBtn.classList.remove('hidden');
|
|
|
|
// Add click handler
|
|
toggleBtn.addEventListener('click', () => {
|
|
const isExpanded = descriptionEl.dataset.expanded === 'true';
|
|
|
|
if (isExpanded) {
|
|
descriptionEl.textContent = descriptionEl.dataset.truncatedText;
|
|
toggleBtn.textContent = 'Show more';
|
|
descriptionEl.dataset.expanded = 'false';
|
|
} else {
|
|
descriptionEl.textContent = descriptionEl.dataset.fullText;
|
|
toggleBtn.textContent = 'Show less';
|
|
descriptionEl.dataset.expanded = 'true';
|
|
}
|
|
});
|
|
} else {
|
|
// Description is short enough, show full text
|
|
descriptionEl.textContent = fullDescription;
|
|
}
|
|
|
|
document.getElementById('preview-link').href = `/e/${event.slug}`;
|
|
document.getElementById('kiosk-link').href = `/kiosk/${event.slug}`;
|
|
|
|
// Update revenue from stats
|
|
if (stats) {
|
|
document.getElementById('total-revenue').textContent = formatCurrency(stats.totalRevenue);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading event header:', error);
|
|
}
|
|
}
|
|
|
|
// Event handlers will be added after functions are defined
|
|
|
|
|
|
|
|
// Function to show edit event modal
|
|
function showEditEventModal(event) {
|
|
// Create modal backdrop
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center p-4';
|
|
backdrop.style.display = 'flex';
|
|
|
|
// Create modal content
|
|
const modal = document.createElement('div');
|
|
modal.className = 'bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto';
|
|
|
|
// Format date for input (YYYY-MM-DD)
|
|
const eventDate = new Date(event.start_time);
|
|
const formattedDate = eventDate.toISOString().split('T')[0];
|
|
|
|
// Format time for input (HH:MM)
|
|
const formattedTime = eventDate.toTimeString().slice(0, 5);
|
|
|
|
modal.innerHTML = `
|
|
<div class="p-6">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900">Edit Event</h2>
|
|
<button id="close-edit-modal" class="text-gray-400 hover:text-gray-600">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="edit-event-form" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Event Title</label>
|
|
<input type="text" id="edit-title" value="${event.title || ''}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Venue</label>
|
|
<input type="text" id="edit-venue" value="${event.venue || ''}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Date</label>
|
|
<input type="date" id="edit-date" value="${formattedDate}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Time</label>
|
|
<input type="time" id="edit-time" value="${formattedTime}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
|
<textarea id="edit-description" rows="4"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent">${event.description || ''}</textarea>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 pt-4">
|
|
<button type="button" id="cancel-edit"
|
|
class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" id="save-edit"
|
|
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500">
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
backdrop.appendChild(modal);
|
|
document.body.appendChild(backdrop);
|
|
|
|
// Add event listeners
|
|
document.getElementById('close-edit-modal').addEventListener('click', () => {
|
|
document.body.removeChild(backdrop);
|
|
});
|
|
|
|
document.getElementById('cancel-edit').addEventListener('click', () => {
|
|
document.body.removeChild(backdrop);
|
|
});
|
|
|
|
backdrop.addEventListener('click', (e) => {
|
|
if (e.target === backdrop) {
|
|
document.body.removeChild(backdrop);
|
|
}
|
|
});
|
|
|
|
// Handle form submission
|
|
document.getElementById('edit-event-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const submitBtn = document.getElementById('save-edit');
|
|
const originalText = submitBtn.textContent;
|
|
|
|
try {
|
|
// Show loading state
|
|
submitBtn.textContent = 'Saving...';
|
|
submitBtn.disabled = true;
|
|
|
|
// Prepare update data
|
|
const title = document.getElementById('edit-title').value.trim();
|
|
const venue = document.getElementById('edit-venue').value.trim();
|
|
const date = document.getElementById('edit-date').value;
|
|
const time = document.getElementById('edit-time').value;
|
|
const description = document.getElementById('edit-description').value.trim();
|
|
|
|
if (!title || !venue || !date || !time) {
|
|
throw new Error('Please fill in all required fields');
|
|
}
|
|
|
|
// Combine date and time
|
|
const startTime = new Date(date + 'T' + time).toISOString();
|
|
|
|
const updateData = {
|
|
title,
|
|
venue,
|
|
start_time: startTime,
|
|
description
|
|
};
|
|
|
|
// Send update request
|
|
const response = await fetch(`/api/events/${eventId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(updateData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to update event');
|
|
}
|
|
|
|
// Close modal
|
|
document.body.removeChild(backdrop);
|
|
|
|
// Reload the page to show updated information
|
|
window.location.reload();
|
|
|
|
} catch (error) {
|
|
alert('Failed to update event: ' + error.message);
|
|
submitBtn.textContent = originalText;
|
|
submitBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Copy to clipboard function
|
|
window.copyToClipboard = function(text) {
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
// Show temporary success message
|
|
const btn = document.activeElement;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Copied!';
|
|
btn.className = btn.className.replace('bg-blue-600 hover:bg-blue-700', 'bg-green-600');
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.className = btn.className.replace('bg-green-600', 'bg-blue-600 hover:bg-blue-700');
|
|
}, 2000);
|
|
});
|
|
} else {
|
|
// Fallback for older browsers
|
|
try {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.top = '0';
|
|
textArea.style.left = '0';
|
|
textArea.style.opacity = '0';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
const successful = document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
if (successful) {
|
|
alert('Copied to clipboard!');
|
|
} else {
|
|
alert('Could not copy to clipboard');
|
|
}
|
|
} catch (err) {
|
|
alert('Copy not supported in this browser');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Generate Kiosk PIN functionality - temporarily disabled due to import issues
|
|
document.getElementById('generate-kiosk-pin-btn').addEventListener('click', async () => {
|
|
alert('Kiosk PIN generation is temporarily disabled. Please use the admin panel for PIN management.');
|
|
});
|
|
|
|
// Add event listeners after DOM is ready and functions are defined
|
|
function addEventListeners() {
|
|
console.log('[DEBUG] Adding event listeners...');
|
|
|
|
// Edit Event Button functionality
|
|
const editBtn = document.getElementById('edit-event-btn');
|
|
console.log('[DEBUG] Edit button found:', !!editBtn);
|
|
|
|
if (editBtn) {
|
|
console.log('[DEBUG] Adding click listener to edit button');
|
|
editBtn.addEventListener('click', async () => {
|
|
console.log('[DEBUG] Edit button clicked!');
|
|
alert('Edit button clicked - this proves the event listener is working!');
|
|
|
|
// Load current event data and show edit modal
|
|
try {
|
|
console.log('[DEBUG] Fetching event data...');
|
|
|
|
// Get eventId from the URL instead of the variable
|
|
const urlParts = window.location.pathname.split('/');
|
|
const currentEventId = urlParts[urlParts.indexOf('events') + 1];
|
|
console.log('[DEBUG] Event ID from URL:', currentEventId);
|
|
|
|
const eventResponse = await fetch(`/api/events/${currentEventId}`);
|
|
if (!eventResponse.ok) {
|
|
throw new Error('Failed to load event details');
|
|
}
|
|
|
|
const event = await eventResponse.json();
|
|
console.log('[DEBUG] Event data loaded:', event.title);
|
|
showEditEventModal(event);
|
|
} catch (error) {
|
|
console.error('[DEBUG] Error:', error);
|
|
alert('Failed to load event details: ' + error.message);
|
|
}
|
|
});
|
|
} else {
|
|
console.error('[DEBUG] Edit button not found!');
|
|
}
|
|
|
|
// Embed Code Button functionality
|
|
const embedBtn = document.getElementById('embed-code-btn');
|
|
if (embedBtn) {
|
|
embedBtn.addEventListener('click', () => {
|
|
// Get event details for the embed modal
|
|
const previewLink = document.getElementById('preview-link').href;
|
|
const eventSlug = previewLink ? previewLink.split('/e/')[1] : 'loading';
|
|
|
|
// Get eventId from the URL
|
|
const urlParts = window.location.pathname.split('/');
|
|
const currentEventId = urlParts[urlParts.indexOf('events') + 1];
|
|
|
|
// Show embed modal using React component
|
|
if (window.openEmbedModal) {
|
|
window.openEmbedModal(currentEventId, eventSlug);
|
|
} else {
|
|
// Fallback: show simple alert for debugging
|
|
alert(`Embed Modal Debug:\nEvent ID: ${currentEventId}\nEvent Slug: ${eventSlug}\nwindow.openEmbedModal: ${typeof window.openEmbedModal}`);
|
|
console.log('Embed button clicked but window.openEmbedModal not available');
|
|
console.log('Available window properties:', Object.keys(window).filter(k => k.includes('embed')));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add event listeners after DOM is loaded
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
addEventListeners();
|
|
});
|
|
} else {
|
|
addEventListeners();
|
|
}
|
|
</script> |