342 lines
9.0 KiB
TypeScript
342 lines
9.0 KiB
TypeScript
import {
|
|
DeesElement,
|
|
customElement,
|
|
html,
|
|
property,
|
|
css,
|
|
cssManager,
|
|
state,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
import '../dees-icon.js';
|
|
import '../dees-button.js';
|
|
import '../dees-windowlayer.js';
|
|
import { ImageCropper } from './profilepicture.cropper.js';
|
|
import { zIndexRegistry } from '../00zindex.js';
|
|
import type { ProfileShape } from './dees-input-profilepicture.js';
|
|
|
|
@customElement('dees-profilepicture-modal')
|
|
export class ProfilePictureModal extends DeesElement {
|
|
@property({ type: String })
|
|
public initialImage: string = '';
|
|
|
|
@property({ type: String })
|
|
public shape: ProfileShape = 'round';
|
|
|
|
@state()
|
|
private currentStep: 'crop' | 'preview' = 'crop';
|
|
|
|
@state()
|
|
private croppedImage: string = '';
|
|
|
|
@state()
|
|
private isProcessing: boolean = false;
|
|
|
|
private cropper: ImageCropper | null = null;
|
|
private windowLayer: any;
|
|
private zIndex: number = 0;
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: var(--z-index);
|
|
}
|
|
|
|
.modal-container {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
|
border-radius: 16px;
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
width: 90%;
|
|
max-width: 600px;
|
|
max-height: 90vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
animation: modalSlideIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes modalSlideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 24px;
|
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
}
|
|
|
|
.close-button {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.close-button:hover {
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
}
|
|
|
|
.modal-body {
|
|
flex: 1;
|
|
padding: 24px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 24px;
|
|
}
|
|
|
|
.cropper-container {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
aspect-ratio: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.preview-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 24px;
|
|
}
|
|
|
|
.preview-image {
|
|
width: 200px;
|
|
height: 200px;
|
|
object-fit: cover;
|
|
border: 3px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
}
|
|
|
|
.preview-image.round {
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.preview-image.square {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.success-message {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 24px;
|
|
background: ${cssManager.bdTheme('hsl(142 69% 45% / 0.1)', 'hsl(142 69% 55% / 0.1)')};
|
|
color: ${cssManager.bdTheme('hsl(142 69% 45%)', 'hsl(142 69% 55%)')};
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 24px;
|
|
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
display: flex;
|
|
gap: 12px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.instructions {
|
|
text-align: center;
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
font-size: 14px;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 3px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
border-top-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
`,
|
|
];
|
|
|
|
async connectedCallback() {
|
|
super.connectedCallback();
|
|
|
|
// Get z-index from registry
|
|
this.zIndex = zIndexRegistry.getNextZIndex();
|
|
this.style.setProperty('--z-index', this.zIndex.toString());
|
|
|
|
// Add window layer
|
|
this.windowLayer = document.createElement('dees-windowlayer');
|
|
this.windowLayer.addEventListener('click', () => this.close());
|
|
document.body.appendChild(this.windowLayer);
|
|
|
|
// Register with z-index registry
|
|
zIndexRegistry.register(this, this.zIndex);
|
|
}
|
|
|
|
async disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
|
|
// Cleanup
|
|
if (this.cropper) {
|
|
this.cropper.destroy();
|
|
}
|
|
|
|
if (this.windowLayer) {
|
|
this.windowLayer.remove();
|
|
}
|
|
|
|
// Unregister from z-index registry
|
|
zIndexRegistry.unregister(this);
|
|
}
|
|
|
|
render(): TemplateResult {
|
|
return html`
|
|
<div class="modal-container" @click=${(e: Event) => e.stopPropagation()}>
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">
|
|
${this.currentStep === 'crop' ? 'Crop Profile Picture' : 'Preview'}
|
|
</h3>
|
|
<button class="close-button" @click=${this.close}>
|
|
<dees-icon icon="lucide:x" iconSize="20"></dees-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
${this.currentStep === 'crop' ? html`
|
|
<div class="instructions">
|
|
Drag and resize the selection area to crop your profile picture
|
|
</div>
|
|
<div class="cropper-container" id="cropperContainer"></div>
|
|
` : html`
|
|
<div class="preview-container">
|
|
${this.isProcessing ? html`
|
|
<div class="loading-spinner"></div>
|
|
<div class="instructions">Processing image...</div>
|
|
` : html`
|
|
<img
|
|
class="preview-image ${this.shape}"
|
|
src="${this.croppedImage}"
|
|
alt="Cropped preview"
|
|
/>
|
|
<div class="success-message">
|
|
<dees-icon icon="lucide:checkCircle" iconSize="20"></dees-icon>
|
|
<span>Profile picture updated successfully!</span>
|
|
</div>
|
|
`}
|
|
</div>
|
|
`}
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
${this.currentStep === 'crop' ? html`
|
|
<dees-button type="secondary" @click=${this.close}>
|
|
Cancel
|
|
</dees-button>
|
|
<dees-button type="primary" @click=${this.handleCrop}>
|
|
Crop & Save
|
|
</dees-button>
|
|
` : html`
|
|
${!this.isProcessing ? html`
|
|
<dees-button type="primary" @click=${this.close}>
|
|
Done
|
|
</dees-button>
|
|
` : ''}
|
|
`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async firstUpdated() {
|
|
if (this.currentStep === 'crop') {
|
|
await this.initializeCropper();
|
|
}
|
|
}
|
|
|
|
private async initializeCropper(): Promise<void> {
|
|
await this.updateComplete;
|
|
|
|
const container = this.shadowRoot!.getElementById('cropperContainer');
|
|
if (!container) return;
|
|
|
|
this.cropper = new ImageCropper({
|
|
container,
|
|
image: this.initialImage,
|
|
shape: this.shape,
|
|
aspectRatio: 1,
|
|
});
|
|
|
|
await this.cropper.initialize();
|
|
}
|
|
|
|
private async handleCrop(): Promise<void> {
|
|
if (!this.cropper) return;
|
|
|
|
try {
|
|
this.isProcessing = true;
|
|
this.currentStep = 'preview';
|
|
await this.updateComplete;
|
|
|
|
// Get cropped image
|
|
const croppedData = await this.cropper.getCroppedImage();
|
|
this.croppedImage = croppedData;
|
|
|
|
// Simulate processing time for better UX
|
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
|
|
this.isProcessing = false;
|
|
|
|
// Emit save event
|
|
this.dispatchEvent(new CustomEvent('save', {
|
|
detail: { croppedImage: this.croppedImage },
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
|
|
// Auto close after showing success
|
|
setTimeout(() => {
|
|
this.close();
|
|
}, 1500);
|
|
|
|
} catch (error) {
|
|
console.error('Error cropping image:', error);
|
|
this.isProcessing = false;
|
|
}
|
|
}
|
|
|
|
private close(): void {
|
|
this.remove();
|
|
}
|
|
} |