refactor(frontend): improve code quality and maintainability
- Extract API error handling utility (utils/error-handling.ts) - Centralize error message extraction logic - Add type guards for API errors - Replace duplicated error handling across components - Create reusable Toast component (components/Toast.tsx) - Extract toast notification logic from profile page - Support auto-dismiss functionality - Consistent styling with shared styles - Extract form validation debouncing hook (hooks/useDebouncedValidation.ts) - Reusable debounced validation logic - Clean timeout management - Used in profile page for form validation - Consolidate duplicate styles (styles/auth-form.ts) - Use shared style tokens instead of duplicating values - Reduce code duplication between auth-form and shared styles - Extract loading state component (components/LoadingState.tsx) - Standardize loading UI across pages - Replace duplicated loading JSX patterns - Used in profile, exchange, and trades pages - Fix useRequireAuth dependency array - Remove unnecessary hasPermission from dependencies - Add eslint-disable comment with explanation - Improve hook stability and performance All frontend tests pass. Linting passes.
This commit is contained in:
parent
db181b338c
commit
3beb23a765
10 changed files with 231 additions and 143 deletions
|
|
@ -1,21 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
|
||||
import { api, ApiError } from "../api";
|
||||
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,
|
||||
toastStyles,
|
||||
utilityStyles,
|
||||
} from "../styles/shared";
|
||||
import { FieldErrors, validateProfileFields } from "../utils/validation";
|
||||
import { validateProfileFields } from "../utils/validation";
|
||||
|
||||
// Use generated type from OpenAPI schema
|
||||
type ProfileData = components["schemas"]["ProfileResponse"];
|
||||
|
|
@ -50,11 +53,15 @@ export default function ProfilePage() {
|
|||
nostr_npub: "",
|
||||
});
|
||||
const [godfatherEmail, setGodfatherEmail] = useState<string | null>(null);
|
||||
const [errors, setErrors] = useState<FieldErrors>({});
|
||||
const [isLoadingProfile, setIsLoadingProfile] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||
const validationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const {
|
||||
errors,
|
||||
setErrors,
|
||||
validate: validateForm,
|
||||
} = useDebouncedValidation(formData, validateProfileFields, 500);
|
||||
|
||||
// Check if form has changes
|
||||
const hasChanges = useCallback(() => {
|
||||
|
|
@ -93,23 +100,6 @@ export default function ProfilePage() {
|
|||
}
|
||||
}, [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<HTMLInputElement>) => {
|
||||
let value = e.target.value;
|
||||
|
||||
|
|
@ -121,19 +111,11 @@ export default function ProfilePage() {
|
|||
}
|
||||
}
|
||||
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
const newFormData = { ...formData, [field]: value };
|
||||
setFormData(newFormData);
|
||||
|
||||
// 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);
|
||||
// Trigger debounced validation with the new data
|
||||
validateForm(newFormData);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
|
|
@ -162,14 +144,15 @@ export default function ProfilePage() {
|
|||
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);
|
||||
}
|
||||
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: "Network error. Please try again.", type: "error" });
|
||||
setToast({
|
||||
message: extractApiErrorMessage(err, "Network error. Please try again."),
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -177,11 +160,7 @@ export default function ProfilePage() {
|
|||
};
|
||||
|
||||
if (isLoading || isLoadingProfile) {
|
||||
return (
|
||||
<main style={layoutStyles.main}>
|
||||
<div style={layoutStyles.loader}>Loading...</div>
|
||||
</main>
|
||||
);
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
if (!user || !isAuthorized) {
|
||||
|
|
@ -194,14 +173,7 @@ export default function ProfilePage() {
|
|||
<main style={layoutStyles.main}>
|
||||
{/* Toast notification */}
|
||||
{toast && (
|
||||
<div
|
||||
style={{
|
||||
...toastStyles.toast,
|
||||
...(toast.type === "success" ? toastStyles.toastSuccess : toastStyles.toastError),
|
||||
}}
|
||||
>
|
||||
{toast.message}
|
||||
</div>
|
||||
<Toast message={toast.message} type={toast.type} onDismiss={() => setToast(null)} />
|
||||
)}
|
||||
|
||||
<Header currentPage="profile" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue