mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #1336 from PR0M3TH3AN/edit-profile-modal-17464840854466078070
Add Edit Profile feature to Profile Modal
This commit is contained in:
3 files changed
+438
-7
No files matched your search
@@ -230,13 +230,22 @@
|
||||
<span id="profileModalName" class="truncate text-sm font-semibold text-text">Loading...</span>
|
||||
<span id="profileModalNpub" class="break-all font-mono text-xs text-muted">npub1...</span>
|
||||
</div>
|
||||
<a
|
||||
id="profileChannelLink"
|
||||
href="#"
|
||||
class="btn-ghost focus-ring profile-modal__channel-link hidden text-xs"
|
||||
>
|
||||
View channel
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center gap-2 profile-modal__actions">
|
||||
<a
|
||||
id="profileChannelLink"
|
||||
href="#"
|
||||
class="btn-ghost focus-ring hidden text-xs"
|
||||
>
|
||||
View channel
|
||||
</a>
|
||||
<button
|
||||
id="profileEditBtn"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring text-xs"
|
||||
>
|
||||
Edit profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -436,6 +445,166 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="profilePaneEdit"
|
||||
class="profile-pane hidden space-y-6"
|
||||
role="tabpanel"
|
||||
aria-label="Edit profile"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<button
|
||||
id="profileEditBackBtn"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring p-1 -ml-1 text-muted hover:text-text"
|
||||
aria-label="Back to account"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<h3 class="text-lg font-bold text-text">Edit Profile</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- Basic Info -->
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="block text-sm text-muted-strong" for="editNameInput">
|
||||
<span class="mb-2 block font-medium">Username</span>
|
||||
<input
|
||||
id="editNameInput"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</label>
|
||||
<label class="block text-sm text-muted-strong" for="editDisplayNameInput">
|
||||
<span class="mb-2 block font-medium">Display Name</span>
|
||||
<input
|
||||
id="editDisplayNameInput"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="Display Name (optional)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="block text-sm text-muted-strong" for="editAboutInput">
|
||||
<span class="mb-2 block font-medium">About</span>
|
||||
<textarea
|
||||
id="editAboutInput"
|
||||
class="input min-h-[80px]"
|
||||
placeholder="Tell us about yourself..."
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm text-muted-strong" for="editWebsiteInput">
|
||||
<span class="mb-2 block font-medium">Website</span>
|
||||
<input
|
||||
id="editWebsiteInput"
|
||||
type="url"
|
||||
class="input"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Verifications -->
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="block text-sm text-muted-strong" for="editNip05Input">
|
||||
<span class="mb-2 block font-medium">NIP-05 Verification</span>
|
||||
<input
|
||||
id="editNip05Input"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="[email protected]"
|
||||
/>
|
||||
</label>
|
||||
<label class="block text-sm text-muted-strong" for="editLud16Input">
|
||||
<span class="mb-2 block font-medium">Lightning Address (LUD16)</span>
|
||||
<input
|
||||
id="editLud16Input"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="[email protected]"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Images -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="block text-sm text-muted-strong" for="editPictureInput">
|
||||
<span class="mb-2 block font-medium">Profile Picture URL</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="editPictureInput"
|
||||
type="url"
|
||||
class="input flex-1"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
<input type="file" id="editPictureFile" hidden accept="image/*" />
|
||||
<button
|
||||
id="editPictureUploadBtn"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring"
|
||||
title="Upload profile picture"
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
<div id="editPictureStorageHint" class="text-xs text-muted hidden">
|
||||
<a href="#" id="editPictureConfigureLink" class="underline hover:text-text">Configure storage</a> to enable uploads.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="block text-sm text-muted-strong" for="editBannerInput">
|
||||
<span class="mb-2 block font-medium">Banner Image URL</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="editBannerInput"
|
||||
type="url"
|
||||
class="input flex-1"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
<input type="file" id="editBannerFile" hidden accept="image/*" />
|
||||
<button
|
||||
id="editBannerUploadBtn"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring"
|
||||
title="Upload banner image"
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
<div id="editBannerStorageHint" class="text-xs text-muted hidden">
|
||||
<a href="#" id="editBannerConfigureLink" class="underline hover:text-text">Configure storage</a> to enable uploads.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-wrap items-center gap-3 pt-4 border-t border-border/60">
|
||||
<button
|
||||
id="editSaveBtn"
|
||||
type="button"
|
||||
class="btn focus-ring"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
<button
|
||||
id="editCancelBtn"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<p id="editStatusText" class="text-sm text-muted" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="profilePaneRelays"
|
||||
class="profile-pane hidden space-y-6"
|
||||
|
||||
@@ -440,6 +440,7 @@ export default class ApplicationBootstrap {
|
||||
nostrClient,
|
||||
nostrService: app.nostrService,
|
||||
storageService,
|
||||
r2Service: app.r2Service,
|
||||
subscriptions,
|
||||
accessControl,
|
||||
moderation: moderationService,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
formatHashtag,
|
||||
} from "../utils/hashtagNormalization.js";
|
||||
import { getActiveSigner } from "../nostr/client.js";
|
||||
import { buildPublicUrl, buildR2Key } from "../r2.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
@@ -189,6 +190,12 @@ const SERVICE_CONTRACT = [
|
||||
? fallbackMessage.trim()
|
||||
: "Failed to login. Please try again.",
|
||||
},
|
||||
{
|
||||
key: "r2Service",
|
||||
type: "object",
|
||||
description: "Service for handling file uploads (R2/S3).",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
key: "nostrService",
|
||||
type: "object",
|
||||
@@ -1297,7 +1304,11 @@ export class ProfileModalController {
|
||||
document.getElementById("profileNavHistory") || null;
|
||||
this.navButtons.admin = document.getElementById("profileNavAdmin") || null;
|
||||
|
||||
this.profileEditBtn = document.getElementById("profileEditBtn") || null;
|
||||
this.profileEditBackBtn = document.getElementById("profileEditBackBtn") || null;
|
||||
|
||||
this.panes.account = document.getElementById("profilePaneAccount") || null;
|
||||
this.panes.edit = document.getElementById("profilePaneEdit") || null;
|
||||
this.panes.relays = document.getElementById("profilePaneRelays") || null;
|
||||
this.panes.wallet = document.getElementById("profilePaneWallet") || null;
|
||||
this.panes.storage = document.getElementById("profilePaneStorage") || null;
|
||||
@@ -1528,6 +1539,29 @@ export class ProfileModalController {
|
||||
this.adminAddBlacklistButton = this.addBlacklistButton;
|
||||
this.adminBlacklistInput = this.blacklistInput;
|
||||
|
||||
this.editNameInput = document.getElementById("editNameInput") || null;
|
||||
this.editDisplayNameInput = document.getElementById("editDisplayNameInput") || null;
|
||||
this.editAboutInput = document.getElementById("editAboutInput") || null;
|
||||
this.editWebsiteInput = document.getElementById("editWebsiteInput") || null;
|
||||
this.editNip05Input = document.getElementById("editNip05Input") || null;
|
||||
this.editLud16Input = document.getElementById("editLud16Input") || null;
|
||||
this.editPictureInput = document.getElementById("editPictureInput") || null;
|
||||
this.editBannerInput = document.getElementById("editBannerInput") || null;
|
||||
|
||||
this.editPictureFile = document.getElementById("editPictureFile") || null;
|
||||
this.editPictureUploadBtn = document.getElementById("editPictureUploadBtn") || null;
|
||||
this.editPictureStorageHint = document.getElementById("editPictureStorageHint") || null;
|
||||
this.editPictureConfigureLink = document.getElementById("editPictureConfigureLink") || null;
|
||||
|
||||
this.editBannerFile = document.getElementById("editBannerFile") || null;
|
||||
this.editBannerUploadBtn = document.getElementById("editBannerUploadBtn") || null;
|
||||
this.editBannerStorageHint = document.getElementById("editBannerStorageHint") || null;
|
||||
this.editBannerConfigureLink = document.getElementById("editBannerConfigureLink") || null;
|
||||
|
||||
this.editSaveBtn = document.getElementById("editSaveBtn") || null;
|
||||
this.editCancelBtn = document.getElementById("editCancelBtn") || null;
|
||||
this.editStatusText = document.getElementById("editStatusText") || null;
|
||||
|
||||
if (this.createWatchHistoryRenderer) {
|
||||
this.ensureProfileHistoryRenderer();
|
||||
}
|
||||
@@ -2806,6 +2840,68 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileEditBtn instanceof HTMLElement) {
|
||||
this.profileEditBtn.addEventListener("click", () => {
|
||||
this.handleEditProfile();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileEditBackBtn instanceof HTMLElement) {
|
||||
this.profileEditBackBtn.addEventListener("click", () => {
|
||||
this.selectPane("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editCancelBtn instanceof HTMLElement) {
|
||||
this.editCancelBtn.addEventListener("click", () => {
|
||||
this.selectPane("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editSaveBtn instanceof HTMLElement) {
|
||||
this.editSaveBtn.addEventListener("click", () => {
|
||||
void this.handleSaveProfile();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureUploadBtn instanceof HTMLElement) {
|
||||
this.editPictureUploadBtn.addEventListener("click", () => {
|
||||
if (this.editPictureFile) this.editPictureFile.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureFile instanceof HTMLElement) {
|
||||
this.editPictureFile.addEventListener("change", () => {
|
||||
void this.handleUpload("picture");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerUploadBtn instanceof HTMLElement) {
|
||||
this.editBannerUploadBtn.addEventListener("click", () => {
|
||||
if (this.editBannerFile) this.editBannerFile.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerFile instanceof HTMLElement) {
|
||||
this.editBannerFile.addEventListener("change", () => {
|
||||
void this.handleUpload("banner");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureConfigureLink instanceof HTMLElement) {
|
||||
this.editPictureConfigureLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.selectPane("storage");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerConfigureLink instanceof HTMLElement) {
|
||||
this.editBannerConfigureLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.selectPane("storage");
|
||||
});
|
||||
}
|
||||
|
||||
this.updateMessagesReloadState();
|
||||
}
|
||||
|
||||
@@ -5943,6 +6039,171 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
handleEditProfile() {
|
||||
this.selectPane("edit");
|
||||
void this.populateEditPane();
|
||||
}
|
||||
|
||||
async populateEditPane() {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheEntry = this.services.getProfileCacheEntry(pubkey);
|
||||
const profile = cacheEntry?.profile || {};
|
||||
|
||||
if (this.editNameInput) this.editNameInput.value = profile.name || "";
|
||||
if (this.editDisplayNameInput)
|
||||
this.editDisplayNameInput.value = profile.display_name || "";
|
||||
if (this.editAboutInput) this.editAboutInput.value = profile.about || "";
|
||||
if (this.editWebsiteInput)
|
||||
this.editWebsiteInput.value = profile.website || "";
|
||||
if (this.editNip05Input) this.editNip05Input.value = profile.nip05 || "";
|
||||
if (this.editLud16Input) this.editLud16Input.value = profile.lud16 || "";
|
||||
if (this.editPictureInput)
|
||||
this.editPictureInput.value = profile.picture || "";
|
||||
if (this.editBannerInput) this.editBannerInput.value = profile.banner || "";
|
||||
|
||||
void this.checkStorageForUploads(pubkey);
|
||||
}
|
||||
|
||||
async checkStorageForUploads(pubkey) {
|
||||
const r2Service = this.services.r2Service;
|
||||
if (!r2Service) return;
|
||||
|
||||
let hasStorage = false;
|
||||
try {
|
||||
const credentials = await r2Service.resolveConnection(pubkey);
|
||||
hasStorage = !!credentials;
|
||||
} catch (e) {
|
||||
hasStorage = false;
|
||||
}
|
||||
|
||||
const updateUI = (uploadBtn, hint, has) => {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = !has;
|
||||
if (!has) uploadBtn.setAttribute("aria-disabled", "true");
|
||||
else uploadBtn.removeAttribute("aria-disabled");
|
||||
}
|
||||
if (hint) {
|
||||
if (has) hint.classList.add("hidden");
|
||||
else hint.classList.remove("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
updateUI(
|
||||
this.editPictureUploadBtn,
|
||||
this.editPictureStorageHint,
|
||||
hasStorage,
|
||||
);
|
||||
updateUI(this.editBannerUploadBtn, this.editBannerStorageHint, hasStorage);
|
||||
}
|
||||
|
||||
async handleUpload(type) {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!pubkey) return;
|
||||
|
||||
const r2Service = this.services.r2Service;
|
||||
if (!r2Service) return;
|
||||
|
||||
const fileInput =
|
||||
type === "picture" ? this.editPictureFile : this.editBannerFile;
|
||||
const urlInput =
|
||||
type === "picture" ? this.editPictureInput : this.editBannerInput;
|
||||
const uploadBtn =
|
||||
type === "picture" ? this.editPictureUploadBtn : this.editBannerUploadBtn;
|
||||
|
||||
if (!fileInput || !fileInput.files.length) return;
|
||||
const file = fileInput.files[0];
|
||||
|
||||
try {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = "Uploading...";
|
||||
}
|
||||
|
||||
const credentials = await r2Service.resolveConnection(pubkey);
|
||||
if (!credentials) {
|
||||
this.showError("Storage configuration missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const key = buildR2Key(pubkey, file);
|
||||
await r2Service.uploadFile({
|
||||
file,
|
||||
...credentials,
|
||||
bucket: credentials.bucket,
|
||||
key,
|
||||
});
|
||||
|
||||
const url = buildPublicUrl(credentials.baseDomain, key);
|
||||
if (urlInput) urlInput.value = url;
|
||||
|
||||
fileInput.value = "";
|
||||
} catch (error) {
|
||||
this.showError("Upload failed: " + (error.message || "Unknown error"));
|
||||
devLogger.error("Upload error:", error);
|
||||
} finally {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = "Upload";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleSaveProfile() {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!pubkey) return;
|
||||
|
||||
const profile = {
|
||||
name: this.editNameInput?.value?.trim() || "",
|
||||
display_name: this.editDisplayNameInput?.value?.trim() || "",
|
||||
about: this.editAboutInput?.value?.trim() || "",
|
||||
website: this.editWebsiteInput?.value?.trim() || "",
|
||||
nip05: this.editNip05Input?.value?.trim() || "",
|
||||
lud16: this.editLud16Input?.value?.trim() || "",
|
||||
picture: this.editPictureInput?.value?.trim() || "",
|
||||
banner: this.editBannerInput?.value?.trim() || "",
|
||||
};
|
||||
|
||||
if (this.editSaveBtn) {
|
||||
this.editSaveBtn.disabled = true;
|
||||
this.editSaveBtn.textContent = "Saving...";
|
||||
}
|
||||
|
||||
try {
|
||||
const content = JSON.stringify(profile);
|
||||
const event = {
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey,
|
||||
tags: [],
|
||||
content,
|
||||
};
|
||||
|
||||
const result =
|
||||
await this.services.nostrClient.signAndPublishEvent(event);
|
||||
|
||||
if (result && result.signedEvent) {
|
||||
if (this.services.nostrClient.handleEvent) {
|
||||
this.services.nostrClient.handleEvent(result.signedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
this.showSuccess("Profile updated!");
|
||||
this.selectPane("account");
|
||||
this.renderSavedProfiles();
|
||||
} catch (error) {
|
||||
this.showError("Failed to save profile: " + error.message);
|
||||
} finally {
|
||||
if (this.editSaveBtn) {
|
||||
this.editSaveBtn.disabled = false;
|
||||
this.editSaveBtn.textContent = "Save Profile";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async populateStoragePane() {
|
||||
const storageService = this.services.storageService;
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
|
||||
Reference in new issue
Block a user