1 Commits

Author SHA1 Message Date
8334c848f1 Merge pull request 'try to do better' (#17) from refactor/frontend-code-quality into master
Some checks failed
Build and Publish Images / build-and-push (push) Failing after 13s
Reviewed-on: http://192.168.31.100:3847/Arrelin/family_budget/pulls/17
2026-01-29 12:18:22 +00:00
34 changed files with 61 additions and 43 deletions

View File

@@ -1,5 +1,4 @@
import React, { Component } from 'react';
import type { ReactNode } from 'react';
import React, { Component, ReactNode } from 'react';
import { AlertTriangle } from 'lucide-react';
import { Button } from './ui';
@@ -23,7 +22,7 @@ export class ErrorBoundary extends Component<Props, State> {
};
}
static getDerivedStateFromError(_error: Error): Partial<State> {
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true };
}
@@ -58,7 +57,7 @@ export class ErrorBoundary extends Component<Props, State> {
<p className="text-gray-600 dark:text-gray-400 mb-6">
We're sorry, but something unexpected happened. Please try refreshing the page or going back to the home page.
</p>
{import.meta.env.DEV && this.state.error && (
{process.env.NODE_ENV === 'development' && this.state.error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/10 rounded-lg text-left">
<p className="text-sm font-mono text-red-800 dark:text-red-300 break-all">
{this.state.error.toString()}

View File

@@ -1,3 +1,4 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ShoppingCart, CheckCheck, Trash2 } from 'lucide-react';
import { useShoppingList, useConfirm } from '../hooks';
@@ -15,6 +16,7 @@ export default function ShoppingListModal({ familyId, onClose }: ShoppingListMod
const { t } = useTranslation();
const { items, loading, createItem, deleteItem, togglePurchased, markAllAsPurchased, clearAll } = useShoppingList(familyId);
const { confirmState, confirm, cancel } = useConfirm();
const [pendingAction, setPendingAction] = useState<{ type: 'delete' | 'markAll' | 'clearAll'; itemId?: number } | null>(null);
const stats = shoppingService.getStats(items);
@@ -23,18 +25,24 @@ export default function ShoppingListModal({ familyId, onClose }: ShoppingListMod
};
const handleDelete = async (itemId: number) => {
setPendingAction({ type: 'delete', itemId });
await confirm(t('shopping.deleteConfirm'), t('shopping.deleteMessage'));
await deleteItem(itemId);
setPendingAction(null);
};
const handleMarkAll = async () => {
setPendingAction({ type: 'markAll' });
await confirm(t('shopping.markAllConfirm'), t('shopping.markAllMessage'));
await markAllAsPurchased();
setPendingAction(null);
};
const handleClearAll = async () => {
setPendingAction({ type: 'clearAll' });
await confirm(t('shopping.clearAllConfirm'), t('shopping.clearAllMessage'));
await clearAll();
setPendingAction(null);
};
const handleUpdate = async (itemId: number, name: string) => {

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, X } from 'lucide-react';
import { Button, Input } from '../ui';
import type { CreateCategoryRequest } from '../../types';
import { CreateCategoryRequest } from '../../types';
interface AddCategorySectionProps {
showForm: boolean;

View File

@@ -1,11 +1,11 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Tag, TrendingDown, Plus, Trash2, RotateCcw, History, X, DollarSign, MessageSquare, Calendar } from 'lucide-react';
import type { CategoryWithRemaining } from '../../services';
import { CategoryWithRemaining } from '../../services';
import { categoryService, expenseService } from '../../services';
import { useExpenses } from '../../hooks';
import { format } from '../../utils/format';
import { Button, Input } from '../ui';
import { Button, Input, Badge } from '../ui';
interface CategoryCardProps {
category: CategoryWithRemaining;

View File

@@ -1,4 +1,4 @@
import type { CategoryWithRemaining } from '../../services';
import { CategoryWithRemaining } from '../../services';
import { CategoryCard } from './CategoryCard';
interface CategoryListProps {

View File

@@ -1,6 +1,6 @@
import { Wallet, ShoppingCart } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { CategoryWithRemaining } from '../../services';
import { CategoryWithRemaining } from '../../services';
import { format } from '../../utils/format';
interface FamilySummaryProps {

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Copy, Check, Loader2 } from 'lucide-react';
import { Modal, Button } from '../ui';
import { useInviteLink } from '../../hooks';
import type { InviteLinkResponse } from '../../types';
import { InviteLinkResponse } from '../../types';
interface InviteModalProps {
onClose: () => void;

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Users, Edit3, Save, X, AlertTriangle, Loader2 } from 'lucide-react';
import type { Family } from '../../types';
import { Family } from '../../types';
import { Card, Button, Input } from '../ui';
import { familyService } from '../../services';
import { showToast } from '../../utils/toast';

View File

@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next';
import type { FamilyMember, User } from '../../types';
import { Loader2 } from 'lucide-react';
import { FamilyMember, User } from '../../types';
import { Badge, LoadingSpinner } from '../ui';
interface MembersSectionProps {

View File

@@ -1,6 +1,6 @@
import { Settings, Palette, Languages } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { Theme } from '../../types';
import { Theme } from '../../types';
import { Card } from '../ui';
import { ThemeSelector } from './ThemeSelector';
import { LanguageSelector } from './LanguageSelector';

View File

@@ -1,6 +1,6 @@
import { Check } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { Theme } from '../../types';
import { Theme } from '../../types';
import { THEMES } from '../../constants';
interface ThemeSelectorProps {

View File

@@ -1,6 +1,6 @@
import { User as UserIcon, LogOut } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { User } from '../../types';
import { User } from '../../types';
import { Button, Card } from '../ui';
interface UserInfoProps {

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Trash2, Check, Pencil, X } from 'lucide-react';
import type { ShoppingItem } from '../../types';
import { ShoppingItem } from '../../types';
import { Button, Input } from '../ui';
interface ShoppingItemCardProps {
@@ -11,6 +12,7 @@ interface ShoppingItemCardProps {
}
export function ShoppingItemCard({ item, onToggle, onDelete, onUpdate }: ShoppingItemCardProps) {
const { t } = useTranslation();
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(item.name);

View File

@@ -1,5 +1,4 @@
import { useState } from 'react';
import type { KeyboardEvent } from 'react';
import { useState, KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus } from 'lucide-react';
import { Button, Input } from '../ui';

View File

@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next';
import type { ShoppingItem } from '../../types';
import { ShoppingItem } from '../../types';
import { ShoppingItemCard } from './ShoppingItemCard';
import { shoppingService } from '../../services';

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import { ReactNode } from 'react';
interface BadgeProps {
children: ReactNode;

View File

@@ -1,4 +1,4 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { ButtonHTMLAttributes, ReactNode } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'success' | 'danger' | 'secondary' | 'ghost';

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import { ReactNode } from 'react';
interface CardProps {
children: ReactNode;

View File

@@ -1,5 +1,4 @@
import { forwardRef } from 'react';
import type { InputHTMLAttributes } from 'react';
import { InputHTMLAttributes, forwardRef } from 'react';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;

View File

@@ -1,5 +1,4 @@
import { useEffect } from 'react';
import type { ReactNode } from 'react';
import { ReactNode, useEffect } from 'react';
interface ModalProps {
isOpen: boolean;

View File

@@ -1,4 +1,4 @@
import type { Theme } from '../types';
import { Theme } from '../types';
export const THEMES: { id: Theme; gradient: string; name: string }[] = [
{ id: 'light', gradient: 'bg-gradient-to-r from-gray-100 to-gray-200', name: 'Light' },

View File

@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { categoryService } from '../services';
import type { CategoryWithRemaining } from '../services';
import type { CreateCategoryRequest } from '../types';
import { categoryService, CategoryWithRemaining } from '../services';
import { CreateCategoryRequest } from '../types';
import { showToast } from '../utils/toast';
import { showErrorToast } from '../utils/errorHandler';

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { expenseService } from '../services';
import type { Expense, CreateExpenseRequest } from '../types';
import { Expense, CreateExpenseRequest } from '../types';
import { showToast } from '../utils/toast';
import { showErrorToast } from '../utils/errorHandler';

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { familyService } from '../services';
import type { FamilyMember } from '../types';
import { FamilyMember } from '../types';
import { showErrorToast } from '../utils/errorHandler';
export function useFamilyMembers(familyId: number | null) {

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { inviteService } from '../services';
import type { InviteLinkResponse, CreateInviteLinkRequest } from '../types';
import { InviteLinkResponse, CreateInviteLinkRequest } from '../types';
import { showToast } from '../utils/toast';
import { showErrorToast } from '../utils/errorHandler';

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { shoppingService } from '../services';
import type { ShoppingItem, CreateShoppingItemRequest } from '../types';
import { ShoppingItem, CreateShoppingItemRequest } from '../types';
import { showToast } from '../utils/toast';
import { showErrorToast } from '../utils/errorHandler';

View File

@@ -5,8 +5,9 @@ import { User as UserIcon } from 'lucide-react';
import { familyApi, authApi } from '../api/client';
import { useStore } from '../store/useStore';
import { useFamilyMembers, useConfirm } from '../hooks';
import type { Theme } from '../types';
import { Theme } from '../types';
import { ProfileHeader } from '../components/profile/ProfileHeader';
import { UserInfo } from '../components/profile/UserInfo';
import { FamilySection } from '../components/profile/FamilySection';
import { MembersSection } from '../components/profile/MembersSection';
import { SettingsSection } from '../components/profile/SettingsSection';
@@ -18,7 +19,7 @@ export default function Profile() {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { user, selectedFamily, setSelectedFamily, setUser, preferences, setPreferences } = useStore();
const { members, loading: membersLoading } = useFamilyMembers(user?.family_id || null);
const { members, loading: membersLoading, loadMembers } = useFamilyMembers(user?.family_id || null);
const { confirmState, confirm, cancel } = useConfirm();
const [leavingFamily, setLeavingFamily] = useState(false);
@@ -60,6 +61,17 @@ export default function Profile() {
}
};
const handleLogout = async () => {
try {
await authApi.logout();
setUser(null);
setSelectedFamily(null);
navigate('/login');
} catch (error) {
showErrorToast(error);
}
};
const handleThemeChange = (theme: Theme) => {
setPreferences({ ...preferences, theme });
showToast.success(t('profile.themeChanged'));

View File

@@ -1,5 +1,5 @@
import { categoryApi, expenseApi } from '../api/client';
import type { Category, CreateCategoryRequest } from '../types';
import { Category, CreateCategoryRequest, RemainingLimit } from '../types';
import { handleApiError } from '../utils/errorHandler';
export interface CategoryWithRemaining extends Category {

View File

@@ -1,5 +1,5 @@
import { expenseApi } from '../api/client';
import type { Expense, CreateExpenseRequest } from '../types';
import { Expense, CreateExpenseRequest } from '../types';
import { handleApiError } from '../utils/errorHandler';
export const expenseService = {

View File

@@ -1,5 +1,5 @@
import { familyApi } from '../api/client';
import type { Family, CreateFamilyRequest, CreateMyFamilyRequest, CreateMyFamilyResponse, VerifyFamilyPasswordRequest, FamilyMember } from '../types';
import { Family, CreateFamilyRequest, CreateMyFamilyRequest, CreateMyFamilyResponse, VerifyFamilyPasswordRequest, FamilyMember } from '../types';
import { handleApiError } from '../utils/errorHandler';
export const familyService = {

View File

@@ -1,5 +1,5 @@
import { inviteLinkApi } from '../api/client';
import type { CreateInviteLinkRequest, InviteLinkResponse, ValidateInviteResponse, JoinFamilyResponse } from '../types';
import { CreateInviteLinkRequest, InviteLinkResponse, ValidateInviteResponse, JoinFamilyResponse } from '../types';
import { handleApiError } from '../utils/errorHandler';
export const inviteService = {

View File

@@ -1,5 +1,5 @@
import { shoppingItemApi } from '../api/client';
import type { ShoppingItem, CreateShoppingItemRequest, UpdateShoppingItemRequest, MarkAsPurchasedRequest } from '../types';
import { ShoppingItem, CreateShoppingItemRequest, UpdateShoppingItemRequest, MarkAsPurchasedRequest } from '../types';
import { handleApiError } from '../utils/errorHandler';
export const shoppingService = {

View File

@@ -124,7 +124,7 @@ export const useStore = create<AppState>((set, get) => ({
},
clearCache: () => {
set(() => ({
set((state) => ({
cache: {
categories: new Map(),
members: new Map(),

View File

@@ -1,5 +1,5 @@
import { AxiosError } from 'axios';
import { AppError } from '../types/errors';
import { ApiError, AppError } from '../types/errors';
import { showToast } from './toast';
export function handleApiError(error: unknown): never {