331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
import {
|
|
DeesElement,
|
|
property,
|
|
html,
|
|
customElement,
|
|
type TemplateResult,
|
|
cssManager,
|
|
css,
|
|
unsafeCSS,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
import * as domtools from '@design.estate/dees-domtools';
|
|
|
|
// Import design tokens
|
|
import { colors, bdTheme } from './00colors.js';
|
|
import { spacing, radius, shadows, transitions } from './00tokens.js';
|
|
import { fontFamilies, typography } from './00fonts.js';
|
|
|
|
// Import components
|
|
import { SioConversationSelector, type IConversation } from './sio-conversation-selector.js';
|
|
import { SioConversationView, type IMessage, type IConversationData, type IAttachment } from './sio-conversation-view.js';
|
|
import { SioImageLightbox, type ILightboxImage } from './sio-image-lightbox.js';
|
|
|
|
// Make sure components are loaded
|
|
SioConversationSelector;
|
|
SioConversationView;
|
|
SioImageLightbox;
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'sio-combox': SioCombox;
|
|
}
|
|
}
|
|
|
|
@customElement('sio-combox')
|
|
export class SioCombox extends DeesElement {
|
|
public static demo = () => html` <sio-combox></sio-combox> `;
|
|
|
|
@property({ type: Object })
|
|
public referenceObject: HTMLElement;
|
|
|
|
@state()
|
|
private selectedConversationId: string | null = null;
|
|
|
|
@state()
|
|
private conversations: IConversation[] = [
|
|
{
|
|
id: '1',
|
|
title: 'Technical Support',
|
|
lastMessage: 'Thanks for your help with the login issue!',
|
|
time: '2 min ago',
|
|
unread: true,
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'Billing Question',
|
|
lastMessage: 'I need help understanding my invoice',
|
|
time: '1 hour ago',
|
|
},
|
|
{
|
|
id: '3',
|
|
title: 'Feature Request',
|
|
lastMessage: 'That would be great! Looking forward to it',
|
|
time: 'Yesterday',
|
|
},
|
|
{
|
|
id: '4',
|
|
title: 'General Inquiry',
|
|
lastMessage: 'Thank you for the information',
|
|
time: '2 days ago',
|
|
}
|
|
];
|
|
|
|
@state()
|
|
private messages: { [conversationId: string]: IMessage[] } = {
|
|
'1': [
|
|
{ id: '1', text: 'Hi, I\'m having trouble logging in', sender: 'user', time: '10:00 AM' },
|
|
{ id: '2', text: 'I can help you with that. Can you tell me what error you\'re seeing?', sender: 'support', time: '10:02 AM' },
|
|
{ id: '3', text: 'It says "Invalid credentials" but I\'m sure my password is correct', sender: 'user', time: '10:03 AM' },
|
|
{ id: '4', text: 'Let me check your account. Please try resetting your password using the forgot password link.', sender: 'support', time: '10:05 AM' },
|
|
{
|
|
id: '5',
|
|
text: 'Here\'s a screenshot of the error',
|
|
sender: 'user',
|
|
time: '10:08 AM',
|
|
attachments: [{
|
|
id: 'att1',
|
|
name: 'error-screenshot.png',
|
|
size: 245780,
|
|
type: 'image/png',
|
|
url: 'https://picsum.photos/400/300?random=1'
|
|
}]
|
|
},
|
|
{ id: '6', text: 'Thanks for your help with the login issue!', sender: 'user', time: '10:10 AM' },
|
|
],
|
|
'2': [
|
|
{ id: '1', text: 'I need help understanding my invoice', sender: 'user', time: '9:00 AM' },
|
|
{ id: '2', text: 'I\'d be happy to help explain your invoice. Which part is unclear?', sender: 'support', time: '9:05 AM' },
|
|
],
|
|
'3': [
|
|
{ id: '1', text: 'I\'d love to see dark mode support in your app!', sender: 'user', time: 'Yesterday' },
|
|
{ id: '2', text: 'Thanks for the suggestion! We\'re actually working on dark mode and it should be available next month.', sender: 'support', time: 'Yesterday' },
|
|
{ id: '3', text: 'That would be great! Looking forward to it', sender: 'user', time: 'Yesterday' },
|
|
],
|
|
'4': [
|
|
{ id: '1', text: 'Can you tell me more about your enterprise plans?', sender: 'user', time: '2 days ago' },
|
|
{ id: '2', text: 'Of course! Our enterprise plans include advanced features like SSO, dedicated support, and custom integrations.', sender: 'support', time: '2 days ago' },
|
|
{ id: '3', text: 'Thank you for the information', sender: 'user', time: '2 days ago' },
|
|
]
|
|
};
|
|
|
|
constructor() {
|
|
super();
|
|
domtools.DomTools.setupDomTools();
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
height: 600px;
|
|
width: 800px;
|
|
background: ${bdTheme('background')};
|
|
border-radius: ${unsafeCSS(radius['2xl'])};
|
|
border: 1px solid ${bdTheme('border')};
|
|
box-shadow: ${unsafeCSS(shadows.xl)};
|
|
overflow: hidden;
|
|
font-family: ${unsafeCSS(fontFamilies.sans)};
|
|
position: relative;
|
|
animation: scaleIn 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
transform-origin: bottom right;
|
|
}
|
|
|
|
@keyframes scaleIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.9) translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
|
|
:host::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: ${unsafeCSS(radius['2xl'])};
|
|
padding: 1px;
|
|
background: linear-gradient(145deg, ${bdTheme('border')}, transparent 50%);
|
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
-webkit-mask-composite: exclude;
|
|
mask-composite: exclude;
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.container {
|
|
display: flex;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Responsive layout */
|
|
@media (max-width: 600px) {
|
|
:host {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.container {
|
|
position: relative;
|
|
}
|
|
|
|
sio-conversation-selector {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
transition: left 300ms ease, opacity 200ms ease;
|
|
}
|
|
|
|
sio-conversation-view {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
transition: left 300ms ease, opacity 200ms ease;
|
|
}
|
|
|
|
/* Mobile navigation states */
|
|
.container.show-list sio-conversation-selector {
|
|
left: 0;
|
|
opacity: 1;
|
|
}
|
|
|
|
.container.show-list sio-conversation-view {
|
|
left: 100%;
|
|
opacity: 0;
|
|
}
|
|
|
|
.container.show-conversation sio-conversation-selector {
|
|
left: -100%;
|
|
opacity: 0;
|
|
}
|
|
|
|
.container.show-conversation sio-conversation-view {
|
|
left: 0;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 601px) {
|
|
sio-conversation-selector {
|
|
width: 320px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
sio-conversation-view {
|
|
flex: 1;
|
|
}
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
const selectedConversation = this.selectedConversationId
|
|
? this.conversations.find(c => c.id === this.selectedConversationId)
|
|
: null;
|
|
|
|
const conversationData: IConversationData | null = selectedConversation
|
|
? {
|
|
id: selectedConversation.id,
|
|
title: selectedConversation.title,
|
|
messages: this.messages[selectedConversation.id] || []
|
|
}
|
|
: null;
|
|
|
|
const containerClass = this.selectedConversationId ? 'show-conversation' : 'show-list';
|
|
|
|
return html`
|
|
<div class="container ${containerClass}">
|
|
<sio-conversation-selector
|
|
.conversations=${this.conversations}
|
|
.selectedConversationId=${this.selectedConversationId}
|
|
@conversation-selected=${this.handleConversationSelected}
|
|
></sio-conversation-selector>
|
|
|
|
<sio-conversation-view
|
|
.conversation=${conversationData}
|
|
@back=${this.handleBack}
|
|
@send-message=${this.handleSendMessage}
|
|
@open-image=${this.handleOpenImage}
|
|
></sio-conversation-view>
|
|
</div>
|
|
|
|
<sio-image-lightbox></sio-image-lightbox>
|
|
`;
|
|
}
|
|
|
|
private handleConversationSelected(event: CustomEvent) {
|
|
const conversation = event.detail.conversation as IConversation;
|
|
this.selectedConversationId = conversation.id;
|
|
|
|
// Mark conversation as read
|
|
const convIndex = this.conversations.findIndex(c => c.id === conversation.id);
|
|
if (convIndex !== -1) {
|
|
this.conversations[convIndex] = { ...this.conversations[convIndex], unread: false };
|
|
this.conversations = [...this.conversations];
|
|
}
|
|
}
|
|
|
|
private handleBack() {
|
|
// For mobile view, go back to conversation list
|
|
this.selectedConversationId = null;
|
|
}
|
|
|
|
private handleSendMessage(event: CustomEvent) {
|
|
const message = event.detail.message as IMessage;
|
|
const conversationId = this.selectedConversationId;
|
|
|
|
if (conversationId) {
|
|
// Add message to the conversation
|
|
if (!this.messages[conversationId]) {
|
|
this.messages[conversationId] = [];
|
|
}
|
|
this.messages[conversationId] = [...this.messages[conversationId], message];
|
|
this.messages = { ...this.messages };
|
|
|
|
// Update conversation's last message
|
|
const convIndex = this.conversations.findIndex(c => c.id === conversationId);
|
|
if (convIndex !== -1) {
|
|
this.conversations[convIndex] = {
|
|
...this.conversations[convIndex],
|
|
lastMessage: message.text,
|
|
time: 'Just now'
|
|
};
|
|
// Move conversation to top
|
|
const [conv] = this.conversations.splice(convIndex, 1);
|
|
this.conversations = [conv, ...this.conversations];
|
|
}
|
|
|
|
// Simulate a response after a delay (remove in production)
|
|
setTimeout(() => {
|
|
const responseMessage: IMessage = {
|
|
id: Date.now().toString(),
|
|
text: 'Thanks for your message! We\'ll get back to you shortly.',
|
|
sender: 'support',
|
|
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
};
|
|
|
|
this.messages[conversationId] = [...this.messages[conversationId], responseMessage];
|
|
this.messages = { ...this.messages };
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
private handleOpenImage(event: CustomEvent) {
|
|
const attachment = event.detail.attachment as IAttachment;
|
|
const lightbox = this.shadowRoot?.querySelector('sio-image-lightbox') as SioImageLightbox;
|
|
|
|
if (lightbox && attachment) {
|
|
const lightboxImage: ILightboxImage = {
|
|
url: attachment.url,
|
|
name: attachment.name,
|
|
size: attachment.size
|
|
};
|
|
lightbox.open(lightboxImage);
|
|
}
|
|
}
|
|
} |