/**
* Modal for creating and editing profile records.
* Supports display name, pronouns, bio, avatar, and banner images with blob upload.
*/
import { putRecord, uploadBlob } from '../oauth';
import { addSection, updateSection, getSiteOwnerDid } from '../config';
import { getRecord } from '../at-client';
import { getCollection } from '../config/nsid';
// Type for blob reference as stored in AT Proto records
interface BlobRef {
$type: 'blob';
ref: {
$link: string;
};
mimeType: string;
size: number;
}
class CreateProfile extends HTMLElement {
private onClose: (() => void) | null = null;
private displayName: string = '';
private pronouns: string = '';
private description: string = '';
private avatarFile: File | null = null;
private bannerFile: File | null = null;
private existingAvatar: BlobRef | null = null;
private existingBanner: BlobRef | null = null;
private editMode: boolean = false;
private editRkey: string | null = null;
private editSectionId: string | null = null;
connectedCallback() {
this.render();
}
setOnClose(callback: () => void) {
this.onClose = callback;
}
show() {
this.style.display = 'flex';
this.render();
}
editProfile(profileData: {
rkey?: string;
sectionId?: string;
displayName?: string;
pronouns?: string;
description?: string;
avatar?: BlobRef;
banner?: BlobRef;
}) {
this.editMode = true;
this.editRkey = profileData.rkey || null;
this.editSectionId = profileData.sectionId || null;
this.displayName = profileData.displayName || '';
this.pronouns = profileData.pronouns || '';
this.description = profileData.description || '';
this.existingAvatar = profileData.avatar || null;
this.existingBanner = profileData.banner || null;
this.avatarFile = null;
this.bannerFile = null;
this.show();
}
hide() {
this.style.display = 'none';
}
private render() {
this.className = 'modal';
this.style.display = 'flex';
const avatarPreview = this.avatarFile
? `
`;
// Add styles for drop zone
const style = document.createElement('style');
style.textContent = `
.create-profile-modal .drop-zone {
border: 2px dashed var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-lg);
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
background: var(--bg-color-alt);
}
.create-profile-modal .drop-zone:hover,
.create-profile-modal .drop-zone.drag-over {
border-color: var(--primary-color);
background: var(--bg-color);
}
.create-profile-modal .drop-zone-content {
pointer-events: none;
}
.create-profile-modal .drop-zone .icon {
font-size: 32px;
display: block;
margin-bottom: var(--spacing-sm);
}
.create-profile-modal .drop-zone .sub-text {
font-size: 0.85em;
opacity: 0.7;
margin-top: var(--spacing-xs);
}
.create-profile-modal .selected-file {
margin-top: var(--spacing-sm);
font-weight: bold;
color: var(--primary-color);
font-size: 0.9em;
}
.create-profile-modal .selected-file.existing {
color: var(--text-color-muted);
}
.create-profile-modal .button-small {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.85em;
}
`;
this.appendChild(style);
this.attachEventListeners();
}
private attachEventListeners() {
const displayNameInput = this.querySelector('#profile-display-name') as HTMLInputElement;
const pronounsInput = this.querySelector('#profile-pronouns') as HTMLInputElement;
const descriptionTextarea = this.querySelector('#profile-description') as HTMLTextAreaElement;
const avatarDropZone = this.querySelector('#avatar-drop-zone') as HTMLDivElement;
const avatarInput = this.querySelector('#avatar-input') as HTMLInputElement;
const bannerDropZone = this.querySelector('#banner-drop-zone') as HTMLDivElement;
const bannerInput = this.querySelector('#banner-input') as HTMLInputElement;
const createBtn = this.querySelector('#create-profile-btn') as HTMLButtonElement;
const cancelBtn = this.querySelector('.modal-close') as HTMLButtonElement;
const clearAvatarBtn = this.querySelector('.clear-avatar-btn') as HTMLButtonElement;
const clearBannerBtn = this.querySelector('.clear-banner-btn') as HTMLButtonElement;
// Handle display name input
displayNameInput?.addEventListener('input', (e) => {
this.displayName = (e.target as HTMLInputElement).value.trim();
});
// Handle pronouns input
pronounsInput?.addEventListener('input', (e) => {
this.pronouns = (e.target as HTMLInputElement).value.trim();
});
// Handle description input
descriptionTextarea?.addEventListener('input', (e) => {
this.description = (e.target as HTMLTextAreaElement).value;
});
// Avatar drop zone handlers
this.setupDropZone(avatarDropZone, avatarInput, 'avatar', 1000000);
// Banner drop zone handlers
this.setupDropZone(bannerDropZone, bannerInput, 'banner', 2000000);
// Clear buttons
clearAvatarBtn?.addEventListener('click', (e) => {
e.preventDefault();
this.avatarFile = null;
this.existingAvatar = null;
this.render();
});
clearBannerBtn?.addEventListener('click', (e) => {
e.preventDefault();
this.bannerFile = null;
this.existingBanner = null;
this.render();
});
// Handle create/save button
createBtn?.addEventListener('click', async () => {
if (createBtn) {
createBtn.disabled = true;
createBtn.textContent = this.editMode ? 'Saving...' : 'Creating...';
}
try {
if (this.editMode) {
await this.updateProfileRecord();
} else {
await this.createProfileRecord();
}
this.close();
} catch (error) {
console.error(`Failed to ${this.editMode ? 'update' : 'create'} profile:`, error);
alert(`Failed to ${this.editMode ? 'update' : 'create'} profile: ${error instanceof Error ? error.message : 'Unknown error'}`);
if (createBtn) {
createBtn.disabled = false;
createBtn.textContent = this.editMode ? 'Save Changes' : 'Create Profile';
}
}
});
// Handle cancel button
cancelBtn?.addEventListener('click', () => this.close());
// Handle backdrop click
this.addEventListener('click', (e) => {
if (e.target === this) {
this.close();
}
});
}
private setupDropZone(
dropZone: HTMLDivElement | null,
fileInput: HTMLInputElement | null,
type: 'avatar' | 'banner',
maxSize: number
) {
if (!dropZone || !fileInput) return;
dropZone.addEventListener('click', () => {
fileInput.click();
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
this.handleFileSelection(file, type, maxSize);
}
});
fileInput.addEventListener('change', (e) => {
const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) {
this.handleFileSelection(files[0], type, maxSize);
}
});
}
private handleFileSelection(file: File, type: 'avatar' | 'banner', maxSize: number) {
if (!file.type.startsWith('image/')) {
alert('Please select an image file.');
return;
}
const acceptedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
if (!acceptedTypes.includes(file.type)) {
alert('Please select a PNG, JPEG, WebP, or GIF image.');
return;
}
if (file.size > maxSize) {
const maxMB = maxSize / 1000000;
alert(`File is too large. Maximum size is ${maxMB}MB.`);
return;
}
if (type === 'avatar') {
this.avatarFile = file;
this.existingAvatar = null;
} else {
this.bannerFile = file;
this.existingBanner = null;
}
this.render();
}
private async uploadImageBlob(file: File): Promise