mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-06-04 09:41:32 +02:00
Feature: Updated Profile Settings Page (#82)
* refactor: enhance ProfileSettingsPage and UpdateProfileForm with improved layout and state management * refactor: remove unused icons from UpdateProfileForm component * refactor: improve loading state handling in UpdateProfileForm with skeleton UI * refactor: remove redundant displayName property from profile update payload * refactor: adjust container class for improved layout in ProfileSettingsPage --------- Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
@@ -10,7 +10,6 @@ export default function ProfileSettingsPage() {
|
||||
document.title = `Settings | LUMINA`;
|
||||
}, []);
|
||||
|
||||
|
||||
let pubkey = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
pubkey = window.localStorage.getItem('pubkey');
|
||||
@@ -23,10 +22,20 @@ export default function ProfileSettingsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center py-6 px-6">
|
||||
<UpdateProfileForm />
|
||||
<div className="container mx-auto py-8 px-4 sm:px-6">
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Profile Settings</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Update your profile information that will be visible to others on the Nostr network
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card shadow-sm">
|
||||
<div className="p-6">
|
||||
<UpdateProfileForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { nip19 } from "nostr-tools"
|
||||
@@ -10,10 +10,16 @@ import { verifyEvent } from 'nostr-tools/pure'
|
||||
import { hexToBytes } from '@noble/hashes/utils'
|
||||
import { useNostr, useProfile } from 'nostr-react';
|
||||
import { signEvent } from '@/utils/utils';
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Loader2, Globe, Image, ImageIcon, BadgeCheck, Zap } from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function UpdateProfileForm() {
|
||||
|
||||
const { publish } = useNostr();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const [isDataLoaded, setIsDataLoaded] = useState(false);
|
||||
|
||||
let npub = '';
|
||||
let pubkey = '';
|
||||
@@ -34,77 +40,326 @@ export function UpdateProfileForm() {
|
||||
}
|
||||
}
|
||||
|
||||
let { data: userData } = useProfile({
|
||||
const { data: userData, isLoading: isUserDataLoading } = useProfile({
|
||||
pubkey,
|
||||
});
|
||||
|
||||
const [username, setUsername] = useState(userData?.name);
|
||||
const [displayName, setDisplayName] = useState(userData?.display_name);
|
||||
const [bio, setBio] = useState(userData?.about);
|
||||
const [username, setUsername] = useState<string | undefined>('');
|
||||
const [displayName, setDisplayName] = useState<string | undefined>('');
|
||||
const [bio, setBio] = useState<string | undefined>('');
|
||||
const [picture, setPicture] = useState<string | undefined>('');
|
||||
const [banner, setBanner] = useState<string | undefined>('');
|
||||
const [nip05, setNip05] = useState<string | undefined>('');
|
||||
const [lud16, setLud16] = useState<string | undefined>('');
|
||||
const [website, setWebsite] = useState<string | undefined>('');
|
||||
|
||||
// Update form data when userData changes
|
||||
useEffect(() => {
|
||||
if (userData && !isDataLoaded) {
|
||||
setUsername(userData.name);
|
||||
setDisplayName(userData.display_name);
|
||||
setBio(userData.about);
|
||||
setPicture(userData.picture);
|
||||
setBanner(userData.banner);
|
||||
setNip05(userData.nip05);
|
||||
setLud16(userData.lud16);
|
||||
setWebsite(userData.website);
|
||||
setIsDataLoaded(true);
|
||||
}
|
||||
}, [userData, isDataLoaded]);
|
||||
|
||||
// Field change handlers
|
||||
const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUsername(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
const handleDisplayNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDisplayName(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
const handleBioChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setBio(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
const handlePictureChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPicture(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
const handleBannerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setBanner(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
const handleNip05Change = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNip05(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
const handleLud16Change = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setLud16(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
const handleWebsiteChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setWebsite(event.target.value);
|
||||
setIsSaved(false);
|
||||
};
|
||||
|
||||
async function handleProfileUpdate() {
|
||||
const username = (document.getElementById('username') as HTMLInputElement).value;
|
||||
const bio = (document.getElementById('bio') as HTMLInputElement).value;
|
||||
const displayname = (document.getElementById('displayname') as HTMLInputElement).value;
|
||||
setIsSubmitting(true);
|
||||
setIsSaved(false);
|
||||
|
||||
if (loginType) {
|
||||
let event = {
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: `{"name": "${username}", "about": "${bio}"}`,
|
||||
pubkey: pubkey,
|
||||
id: "",
|
||||
sig: "",
|
||||
};
|
||||
try {
|
||||
let event = {
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: JSON.stringify({
|
||||
name: username,
|
||||
display_name: displayName,
|
||||
about: bio,
|
||||
picture: picture,
|
||||
banner: banner,
|
||||
nip05: nip05,
|
||||
lud16: lud16,
|
||||
website: website,
|
||||
}),
|
||||
pubkey: pubkey,
|
||||
id: "",
|
||||
sig: "",
|
||||
};
|
||||
|
||||
let signedEvent = await signEvent(loginType, event);
|
||||
let signedEvent = await signEvent(loginType, event);
|
||||
|
||||
if (signedEvent === null) {
|
||||
alert('Failed to sign the event. Please check your connection and try again.');
|
||||
return;
|
||||
}
|
||||
if (signedEvent === null) {
|
||||
throw new Error('Failed to sign the event');
|
||||
}
|
||||
|
||||
let isGood = verifyEvent(signedEvent);
|
||||
let isGood = verifyEvent(signedEvent);
|
||||
|
||||
if (isGood) {
|
||||
publish(signedEvent);
|
||||
window.location.href = `/profile/${npub}`;
|
||||
if (isGood) {
|
||||
publish(signedEvent);
|
||||
setIsSaved(true);
|
||||
setTimeout(() => {
|
||||
window.location.href = `/profile/${npub}`;
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating profile:", error);
|
||||
alert('Failed to update profile. Please check your connection and try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isUserDataLoading && !isDataLoaded) {
|
||||
return (
|
||||
<div className="w-full space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Skeleton className="h-20 w-20 rounded-full" />
|
||||
<div>
|
||||
<Skeleton className="h-8 w-48 mb-2" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
</div>
|
||||
<Card className="border rounded-lg">
|
||||
<CardContent className="p-6 space-y-5">
|
||||
<Skeleton className="h-10 w-full mb-4" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<Skeleton className="h-28 w-full" />
|
||||
<Skeleton className="h-28 w-full" />
|
||||
</div>
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<Skeleton className="h-28 w-full" />
|
||||
<Skeleton className="h-28 w-full" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-full">
|
||||
<div className="py-4">
|
||||
<div className="w-full space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="h-20 w-20 border">
|
||||
<AvatarImage src={picture} alt={username || "Profile"} />
|
||||
<AvatarFallback className="text-lg">
|
||||
{username?.charAt(0) || "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<Label>Your npub (Public Key):</Label>
|
||||
<Input type="text" placeholder="npub1.." value={npub} readOnly />
|
||||
<h2 className="text-2xl font-semibold">{displayName || username || "Your Profile"}</h2>
|
||||
<p className="text-sm text-muted-foreground break-all">{nip05 || npub}</p>
|
||||
</div>
|
||||
<div className='py-4'>
|
||||
<Label>Your Username</Label>
|
||||
<Input type="text" id="username" placeholder="Satoshi" value={username} onChange={handleUsernameChange} />
|
||||
</div>
|
||||
<div className='py-4'>
|
||||
<Label>Your Displayed Name</Label>
|
||||
<Input type="text" id="displayname" placeholder="Satoshi" value={displayName} onChange={handleDisplayNameChange} />
|
||||
</div>
|
||||
<div className='py-4'>
|
||||
<Label>Your Bio</Label>
|
||||
{/* <Input type="text" id="bio" placeholder="Type something about you.." /> */}
|
||||
<Textarea id="bio" placeholder="Type something about you.." rows={10} value={bio} onChange={handleBioChange} />
|
||||
</div>
|
||||
<Button variant={'default'} type="submit" className='w-full' onClick={handleProfileUpdate}>Submit</Button>
|
||||
</div>
|
||||
|
||||
<Card className="border rounded-lg">
|
||||
<CardContent className="p-6 space-y-5">
|
||||
<div>
|
||||
<Label htmlFor="npub" className="text-sm font-medium">Your npub (Public Key)</Label>
|
||||
<Input
|
||||
id="npub"
|
||||
type="text"
|
||||
value={npub}
|
||||
readOnly
|
||||
className="font-mono text-sm mt-1 bg-muted/50"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Your public identity on the Nostr network</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<Label htmlFor="username" className="text-sm font-medium">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="e.g., satoshi"
|
||||
value={username || ""}
|
||||
onChange={handleUsernameChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Your unique username on the network</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="displayname" className="text-sm font-medium">Display Name</Label>
|
||||
<Input
|
||||
id="displayname"
|
||||
type="text"
|
||||
placeholder="e.g., Satoshi Nakamoto"
|
||||
value={displayName || ""}
|
||||
onChange={handleDisplayNameChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">How your name appears to others</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="bio" className="text-sm font-medium">Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
placeholder="Tell the world about yourself..."
|
||||
rows={5}
|
||||
value={bio || ""}
|
||||
onChange={handleBioChange}
|
||||
className="mt-1 resize-none"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">A short description about yourself</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<Label htmlFor="picture" className="text-sm font-medium flex items-center gap-1">
|
||||
<ImageIcon className="h-4 w-4" /> Profile Picture URL
|
||||
</Label>
|
||||
<Input
|
||||
id="picture"
|
||||
type="text"
|
||||
placeholder="https://example.com/your-picture.jpg"
|
||||
value={picture || ""}
|
||||
onChange={handlePictureChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">URL to your profile image</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="banner" className="text-sm font-medium flex items-center gap-1">
|
||||
<Image className="h-4 w-4" /> Banner Image URL
|
||||
</Label>
|
||||
<Input
|
||||
id="banner"
|
||||
type="text"
|
||||
placeholder="https://example.com/your-banner.jpg"
|
||||
value={banner || ""}
|
||||
onChange={handleBannerChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">URL to your profile banner image</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<Label htmlFor="nip05" className="text-sm font-medium flex items-center gap-1">
|
||||
<BadgeCheck className="h-4 w-4" /> NIP-05 Identifier
|
||||
</Label>
|
||||
<Input
|
||||
id="nip05"
|
||||
type="text"
|
||||
placeholder="_@example.com"
|
||||
value={nip05 || ""}
|
||||
onChange={handleNip05Change}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Your verified Nostr identifier</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="lud16" className="text-sm font-medium flex items-center gap-1">
|
||||
<Zap className="h-4 w-4" /> Lightning Address
|
||||
</Label>
|
||||
<Input
|
||||
id="lud16"
|
||||
type="text"
|
||||
placeholder="you@wallet.com"
|
||||
value={lud16 || ""}
|
||||
onChange={handleLud16Change}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Your Lightning address for receiving payments</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="website" className="text-sm font-medium flex items-center gap-1">
|
||||
<Globe className="h-4 w-4" /> Website
|
||||
</Label>
|
||||
<Input
|
||||
id="website"
|
||||
type="text"
|
||||
placeholder="https://example.com"
|
||||
value={website || ""}
|
||||
onChange={handleWebsiteChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Your personal website or social media link</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = `/profile/${npub}`}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleProfileUpdate}
|
||||
disabled={isSubmitting}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : isSaved ? (
|
||||
"Saved!"
|
||||
) : (
|
||||
"Save Changes"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user