"use client"; import { useEffect, useState, useCallback } from "react"; import { api } from "../api"; import { extractApiErrorMessage, extractFieldErrors } from "../utils/error-handling"; import { Permission } from "../auth-context"; import { Header } from "../components/Header"; import { Toast } from "../components/Toast"; import { LoadingState } from "../components/LoadingState"; import { components } from "../generated/api"; import { useRequireAuth } from "../hooks/useRequireAuth"; import { useDebouncedValidation } from "../hooks/useDebouncedValidation"; import { layoutStyles, cardStyles, formStyles, buttonStyles, utilityStyles, } from "../styles/shared"; import { validateProfileFields } from "../utils/validation"; // Use generated type from OpenAPI schema type ProfileData = components["schemas"]["ProfileResponse"]; // UI-specific types (not from API) interface FormData { contact_email: string; telegram: string; signal: string; nostr_npub: string; } function toFormData(data: ProfileData): FormData { return { contact_email: data.contact_email || "", telegram: data.telegram || "", signal: data.signal || "", nostr_npub: data.nostr_npub || "", }; } export default function ProfilePage() { const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.MANAGE_OWN_PROFILE, fallbackRedirect: "/admin/trades", }); const [originalData, setOriginalData] = useState(null); const [formData, setFormData] = useState({ contact_email: "", telegram: "", signal: "", nostr_npub: "", }); const [godfatherEmail, setGodfatherEmail] = useState(null); const [isLoadingProfile, setIsLoadingProfile] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); const { errors, setErrors, validate: validateForm, } = useDebouncedValidation(formData, validateProfileFields, 500); // Check if form has changes const hasChanges = useCallback(() => { if (!originalData) return false; return ( formData.contact_email !== originalData.contact_email || formData.telegram !== originalData.telegram || formData.signal !== originalData.signal || formData.nostr_npub !== originalData.nostr_npub ); }, [formData, originalData]); // Check if form is valid const isValid = useCallback(() => { return Object.keys(errors).length === 0; }, [errors]); const fetchProfile = useCallback(async () => { try { const data = await api.get("/api/profile"); const formValues = toFormData(data); setFormData(formValues); setOriginalData(formValues); setGodfatherEmail(data.godfather_email ?? null); } catch (err) { console.error("Profile load error:", err); setToast({ message: "Failed to load profile", type: "error" }); } finally { setIsLoadingProfile(false); } }, []); useEffect(() => { if (user && isAuthorized) { fetchProfile(); } }, [user, isAuthorized, fetchProfile]); const handleInputChange = (field: keyof FormData) => (e: React.ChangeEvent) => { let value = e.target.value; // For telegram: auto-prepend @ if user starts with a valid letter if (field === "telegram" && value && !value.startsWith("@")) { // Check if first char is a valid telegram handle start (letter) if (/^[a-zA-Z]/.test(value)) { value = "@" + value; } } const newFormData = { ...formData, [field]: value }; setFormData(newFormData); // Trigger debounced validation with the new data validateForm(newFormData); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Validate all fields const validationErrors = validateProfileFields(formData); setErrors(validationErrors); if (Object.keys(validationErrors).length > 0) { return; } setIsSubmitting(true); try { const data = await api.put("/api/profile", { contact_email: formData.contact_email || null, telegram: formData.telegram || null, signal: formData.signal || null, nostr_npub: formData.nostr_npub || null, }); const formValues = toFormData(data); setFormData(formValues); setOriginalData(formValues); setToast({ message: "Profile saved successfully!", type: "success" }); } catch (err) { console.error("Profile save error:", err); const fieldErrors = extractFieldErrors(err); if (fieldErrors?.detail?.field_errors) { setErrors(fieldErrors.detail.field_errors); setToast({ message: "Please fix the errors below", type: "error" }); } else { setToast({ message: extractApiErrorMessage(err, "Network error. Please try again."), type: "error", }); } } finally { setIsSubmitting(false); } }; if (isLoading || isLoadingProfile) { return ; } if (!user || !isAuthorized) { return null; } const canSubmit = hasChanges() && isValid() && !isSubmitting; return (
{/* Toast notification */} {toast && ( setToast(null)} /> )}

My Profile

Manage your contact information

{/* Login email - read only */}
This is your login email and cannot be changed here.
{/* Godfather - shown if user was invited */} {godfatherEmail && (
{godfatherEmail}
The user who invited you to join.
)}

Contact Details

These are for communication purposes only — they won't affect your login.

{/* Contact email */}
{errors.contact_email && ( {errors.contact_email} )}
{/* Telegram */}
{errors.telegram && {errors.telegram}}
{/* Signal */}
{errors.signal && {errors.signal}}
{/* Nostr npub */}
{errors.nostr_npub && {errors.nostr_npub}}
); } // Page-specific styles const styles: Record = { profileCard: { ...cardStyles.card, width: "100%", maxWidth: "480px", }, labelWithBadge: { fontFamily: "'DM Sans', system-ui, sans-serif", color: "rgba(255, 255, 255, 0.7)", fontSize: "0.875rem", fontWeight: 500, display: "flex", alignItems: "center", gap: "0.5rem", }, godfatherBox: { padding: "0.875rem 1rem", background: "rgba(99, 102, 241, 0.08)", border: "1px solid rgba(99, 102, 241, 0.2)", borderRadius: "12px", }, godfatherEmail: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "1rem", color: "rgba(129, 140, 248, 0.9)", }, sectionLabel: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.875rem", fontWeight: 600, color: "rgba(255, 255, 255, 0.8)", margin: 0, textTransform: "uppercase", letterSpacing: "0.05em", }, sectionHint: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.8rem", color: "rgba(255, 255, 255, 0.4)", margin: 0, marginBottom: "0.5rem", }, };