/** * @file Edit profile modal component for updating player profile settings. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Complex form with many fields */ /* eslint-disable complexity -- Many conditional render paths for toggles */ /* eslint-disable max-lines -- Large modal with profile and settings forms */ /* eslint-disable max-statements -- Many state initialisations and handlers */ import { DEFAULT_PROFILE_SETTINGS, type NumberFormat, type ProfileSettings, } from "@elysium/types"; import { type ChangeEvent, type JSX, useEffect, useState } from "react"; import { updateProfile } from "../../api/client.js"; import { useGame } from "../../context/gameContext.js"; import { requestNotificationPermission, } from "../../utils/notification.js"; interface EditProfileModalProperties { readonly onClose: ()=> void; } interface StatToggle { key: keyof ProfileSettings; label: string; icon: string; } const currentRunToggles: Array = [ { icon: "πŸͺ™", key: "showCurrentGold", label: "Gold Earned This Run" }, { icon: "πŸ‘†", key: "showCurrentClicks", label: "Clicks This Run" }, { icon: "✨", key: "showApotheosis", label: "Apotheosis Badge" }, { icon: "🌌", key: "showTranscendence", label: "Transcendence Badge" }, { icon: "⭐", key: "showPrestige", label: "Prestige Level" }, { icon: "πŸ’€", key: "showBossesDefeated", label: "Bosses Defeated" }, { icon: "πŸ“œ", key: "showQuestsCompleted", label: "Quests Completed" }, { icon: "βš”οΈ", key: "showAdventurersRecruited", label: "Adventurers Recruited", }, { icon: "πŸ†", key: "showAchievementsUnlocked", label: "Achievements Unlocked", }, ]; const allTimeToggles: Array = [ { icon: "πŸͺ™", key: "showTotalGold", label: "Total Gold Earned" }, { icon: "πŸ‘†", key: "showTotalClicks", label: "Total Clicks" }, { icon: "πŸ’€", key: "showLifetimeBossesDefeated", label: "Bosses Defeated", }, { icon: "πŸ“œ", key: "showLifetimeQuestsCompleted", label: "Quests Completed", }, { icon: "βš”οΈ", key: "showLifetimeAdventurersRecruited", label: "Adventurers Recruited", }, { icon: "πŸ†", key: "showLifetimeAchievementsUnlocked", label: "Achievements Unlocked", }, { icon: "πŸ“…", key: "showGuildFounded", label: "Guild Founded Date" }, ]; const numberFormatOptions: Array<{ value: NumberFormat; label: string; example: string; }> = [ { example: "1.23Qa", label: "Suffix", value: "suffix" }, { example: "1.23e15", label: "Scientific", value: "scientific" }, { example: "1.23E15", label: "Engineering", value: "engineering" }, ]; /** * Renders the edit profile modal for updating player display settings. * @param props - The modal properties. * @param props.onClose - Callback to close the modal. * @returns The JSX element. */ const EditProfileModal = ({ onClose, }: EditProfileModalProperties): JSX.Element => { const { state, numberFormat: currentNumberFormat, setNumberFormat, setEnableSounds, setEnableNotifications, } = useGame(); const player = state?.player; const [ characterName, setCharacterName ] = useState( player?.characterName ?? "", ); const [ bio, setBio ] = useState(""); const [ profileSettings, setProfileSettings ] = useState({ ...DEFAULT_PROFILE_SETTINGS, numberFormat: currentNumberFormat, }); const [ loadingProfile, setLoadingProfile ] = useState(true); const [ saving, setSaving ] = useState(false); const [ error, setError ] = useState(null); const [ saved, setSaved ] = useState(false); useEffect(() => { if (player?.discordId === undefined || player.discordId === "") { return; } fetch(`/api/profile/${player.discordId}`). then(async(response) => { if (!response.ok) { return; } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- API response cast const data = (await response.json()) as { bio: string; profileSettings: ProfileSettings; characterName: string; }; setBio(data.bio); setProfileSettings({ ...DEFAULT_PROFILE_SETTINGS, ...data.profileSettings, }); setCharacterName( data.characterName === "" ? player.characterName : data.characterName, ); }). catch(() => { /* Fall back to local state if fetch fails β€” not a blocking error */ }). finally(() => { setLoadingProfile(false); }); }, [ player?.discordId, player?.characterName ]); async function handleSave(): Promise { setSaving(true); setError(null); try { await updateProfile({ bio, characterName, profileSettings, }); setNumberFormat(profileSettings.numberFormat); setEnableSounds(profileSettings.enableSounds); setEnableNotifications(profileSettings.enableNotifications); setSaved(true); setTimeout(onClose, 900); } catch (error_: unknown) { setError(error_ instanceof Error ? error_.message : "Failed to save"); } finally { setSaving(false); } } function handleSaveClick(): void { void handleSave(); } function toggleSetting(key: keyof ProfileSettings): void { setProfileSettings((previous) => { const current = previous[key]; const toggled = typeof current === "boolean" ? !current : current; return { ...previous, [key]: toggled }; }); } function handleLeaderboardToggle(): void { toggleSetting("showOnLeaderboards"); } function handleNameChange(event: ChangeEvent): void { setCharacterName(event.target.value); } function handleBioChange(event: ChangeEvent): void { setBio(event.target.value); } function handleSoundsToggle(): void { toggleSetting("enableSounds"); } async function handleNotificationsEnable(): Promise { if (profileSettings.enableNotifications) { toggleSetting("enableNotifications"); return; } const granted = await requestNotificationPermission(); if (granted) { toggleSetting("enableNotifications"); } else { setError( "Browser notification permission was denied." + " Please enable it in your browser settings.", ); } } function handleNotificationsToggle(): void { void handleNotificationsEnable(); } function handlePrestigeAnnouncementsToggle(): void { toggleSetting("enablePrestigeAnnouncements"); } const isSaveDisabled = saving || characterName.trim() === ""; let saveLabel = "Save Profile"; if (saving) { saveLabel = "Saving…"; } if (saved) { saveLabel = "βœ“ Saved!"; } return (

{"Edit Profile"}

{loadingProfile ?

{"Loading your profile…"}

:
{characterName.length} {" / 32"}