"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { api, ApiError } from "../api"; import { Permission } from "../auth-context"; import { Header } from "../components/Header"; import { components } from "../generated/api"; import { useRequireAuth } from "../hooks/useRequireAuth"; import { layoutStyles, cardStyles, formStyles, buttonStyles, toastStyles, utilityStyles, } from "../styles/shared"; import { FieldErrors, 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 [errors, setErrors] = useState({}); const [isLoadingProfile, setIsLoadingProfile] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); const validationTimeoutRef = useRef(null); // 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]); // Auto-dismiss toast after 3 seconds useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(null), 3000); return () => clearTimeout(timer); } }, [toast]); // Cleanup validation timeout on unmount useEffect(() => { return () => { if (validationTimeoutRef.current) { clearTimeout(validationTimeoutRef.current); } }; }, []); 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; } } setFormData((prev) => ({ ...prev, [field]: value })); // Clear any pending validation timeout if (validationTimeoutRef.current) { clearTimeout(validationTimeoutRef.current); } // Debounce validation - wait 500ms after user stops typing validationTimeoutRef.current = setTimeout(() => { const newFormData = { ...formData, [field]: value }; const newErrors = validateProfileFields(newFormData); setErrors(newErrors); }, 500); }; 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); if (err instanceof ApiError && err.status === 422) { const errorData = err.data as { detail?: { field_errors?: FieldErrors } }; if (errorData?.detail?.field_errors) { setErrors(errorData.detail.field_errors); } setToast({ message: "Please fix the errors below", type: "error" }); } else { setToast({ message: "Network error. Please try again.", type: "error" }); } } finally { setIsSubmitting(false); } }; if (isLoading || isLoadingProfile) { return (
Loading...
); } if (!user || !isAuthorized) { return null; } const canSubmit = hasChanges() && isValid() && !isSubmitting; return (
{/* Toast notification */} {toast && (
{toast.message}
)}

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", }, };