Massive bug fix. Clear button added into fields.

This commit is contained in:
AG
2025-12-05 20:31:02 +02:00
parent 41d1d0f16a
commit 27afacee3f
14 changed files with 155 additions and 120 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { toTitleCase } from '../utils/text';
import { X, Dumbbell, User, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, Percent } from 'lucide-react';
import { ExerciseDef, ExerciseType, Language } from '../types';
import { t } from '../services/i18n';
@@ -82,6 +83,7 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
type="text"
autoFocus
autocapitalize="words"
onBlur={() => setNewName(toTitleCase(newName))}
/>
{error && (
<p className="text-xs text-error mt-2 ml-3">{error}</p>

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { X } from 'lucide-react';
interface FilledInputProps {
label: string;
value: string | number;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClear?: () => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
type?: string;
@@ -13,28 +15,53 @@ interface FilledInputProps {
inputMode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal";
autocapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
autoComplete?: string;
rightElement?: React.ReactNode;
}
const FilledInput: React.FC<FilledInputProps> = ({ label, value, onChange, onFocus, onBlur, type = "number", icon, autoFocus, step, inputMode, autocapitalize, autoComplete }) => (
<div className="relative group bg-surface-container-high rounded-t-lg border-b border-outline-variant hover:bg-white/5 focus-within:border-primary transition-colors">
<label className="absolute top-2 left-4 text-[10px] font-medium text-on-surface-variant flex items-center gap-1">
{icon} {label}
</label>
<input
type={type}
step={step}
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
autoFocus={autoFocus}
className="w-full pt-6 pb-2 px-4 bg-transparent text-2xl text-on-surface focus:outline-none placeholder-transparent"
placeholder="0"
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
autoCapitalize={autocapitalize}
autoComplete={autoComplete}
/>
</div>
);
const FilledInput: React.FC<FilledInputProps> = ({ label, value, onChange, onClear, onFocus, onBlur, type = "number", icon, autoFocus, step, inputMode, autocapitalize, autoComplete, rightElement }) => {
const handleClear = () => {
const syntheticEvent = {
target: { value: '' }
} as React.ChangeEvent<HTMLInputElement>;
onChange(syntheticEvent);
if (onClear) onClear();
};
return (
<div className="relative group bg-surface-container-high rounded-t-lg border-b border-outline-variant hover:bg-white/5 focus-within:border-primary transition-colors">
<label className="absolute top-2 left-4 text-[10px] font-medium text-on-surface-variant flex items-center gap-1">
{icon} {label}
</label>
<input
type={type}
step={step}
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
autoFocus={autoFocus}
className={`w-full pt-6 pb-2 pl-4 bg-transparent text-2xl text-on-surface focus:outline-none placeholder-transparent ${rightElement ? 'pr-20' : 'pr-10'}`}
placeholder="0"
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
autoCapitalize={autocapitalize}
autoComplete={autoComplete}
/>
{value !== '' && value !== 0 && (
<button
onClick={handleClear}
className={`absolute top-1/2 -translate-y-1/2 p-2 text-on-surface-variant hover:text-on-surface rounded-full opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity ${rightElement ? 'right-12' : 'right-2'}`}
tabIndex={-1}
>
<X size={16} />
</button>
)}
{rightElement && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
{rightElement}
</div>
)}
</div>
);
};
export default FilledInput;

View File

@@ -14,6 +14,7 @@ interface HistoryProps {
const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSession, lang }) => {
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
const calculateSessionWork = (session: WorkoutSession) => {
@@ -86,6 +87,18 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
if (deletingId && onDeleteSession) {
onDeleteSession(deletingId);
setDeletingId(null);
} else if (deletingSetInfo && onUpdateSession) {
// Find the session
const session = sessions.find(s => s.id === deletingSetInfo.sessionId);
if (session) {
// Create updated session with the set removed
const updatedSession = {
...session,
sets: session.sets.filter(s => s.id !== deletingSetInfo.setId)
};
onUpdateSession(updatedSession);
}
setDeletingSetInfo(null);
}
}
@@ -238,8 +251,7 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
// Find the session and set up for deletion
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
if (parentSession) {
setEditingSession(JSON.parse(JSON.stringify(parentSession)));
setDeletingId(set.id); // Use set ID for deletion
setDeletingSetInfo({ sessionId: parentSession.id, setId: set.id });
}
}}
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors"
@@ -258,14 +270,21 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
</div>
{/* DELETE CONFIRMATION DIALOG (MD3) */}
{deletingId && (
{(deletingId || deletingSetInfo) && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-surface-container w-full max-w-xs rounded-[28px] p-6 shadow-elevation-3">
<h3 className="text-xl font-normal text-on-surface mb-2">{t('delete_workout', lang)}</h3>
<p className="text-sm text-on-surface-variant mb-8">{t('delete_confirm', lang)}</p>
<h3 className="text-xl font-normal text-on-surface mb-2">
{deletingId ? t('delete_workout', lang) : t('delete_set', lang) || 'Delete Set'}
</h3>
<p className="text-sm text-on-surface-variant mb-8">
{deletingId ? t('delete_confirm', lang) : t('delete_set_confirm', lang) || 'Are you sure you want to delete this set?'}
</p>
<div className="flex justify-end gap-2">
<button
onClick={() => setDeletingId(null)}
onClick={() => {
setDeletingId(null);
setDeletingSetInfo(null);
}}
className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5"
>
{t('cancel', lang)}

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import FilledInput from './FilledInput';
import { login, changePassword } from '../services/auth';
import { User, Language } from '../types';
import { Dumbbell, ArrowRight, Lock, Mail, Globe } from 'lucide-react';
@@ -58,15 +59,12 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
<p className="text-sm text-on-surface-variant mb-6">{t('change_pass_desc', language)}</p>
<div className="space-y-4">
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
<label className="text-[10px] text-on-surface-variant font-medium">{t('change_pass_new', language)}</label>
<input
type="password"
className="w-full bg-transparent text-lg text-on-surface focus:outline-none pt-1"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>
<FilledInput
label={t('change_pass_new', language)}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
type="password"
/>
<button
onClick={handleChangePassword}
className="w-full py-3 bg-primary text-on-primary rounded-full font-medium"
@@ -104,32 +102,22 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
<form onSubmit={handleLogin} className="w-full max-w-sm space-y-6">
<div className="space-y-4">
<div className="group bg-surface-container-high rounded-t-lg border-b border-outline-variant focus-within:border-primary transition-colors">
<div className="flex items-center px-4 pt-4">
<Mail size={16} className="text-on-surface-variant mr-2" />
<label className="text-xs text-on-surface-variant font-medium">{t('login_email', language)}</label>
</div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-transparent px-4 pb-3 pt-1 text-lg text-on-surface focus:outline-none"
placeholder="user@gymflow.ai"
/>
</div>
<FilledInput
label={t('login_email', language)}
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
icon={<Mail size={16} />}
inputMode="email"
/>
<div className="group bg-surface-container-high rounded-t-lg border-b border-outline-variant focus-within:border-primary transition-colors">
<div className="flex items-center px-4 pt-4">
<Lock size={16} className="text-on-surface-variant mr-2" />
<label className="text-xs text-on-surface-variant font-medium">{t('login_password', language)}</label>
</div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-transparent px-4 pb-3 pt-1 text-lg text-on-surface focus:outline-none"
/>
</div>
<FilledInput
label={t('login_password', language)}
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
icon={<Lock size={16} />}
/>
</div>
{error && <div className="text-error text-sm text-center bg-error-container/10 p-2 rounded-lg">{error}</div>}

View File

@@ -6,30 +6,15 @@ import { getPlans, savePlan, deletePlan, getExercises, saveExercise } from '../s
import { t } from '../services/i18n';
import { generateId } from '../utils/uuid';
import FilledInput from './FilledInput';
import { toTitleCase } from '../utils/text';
interface PlansProps {
userId: string;
onStartPlan: (plan: WorkoutPlan) => void;
lang: Language;
}
const FilledInput = ({ label, value, onChange, type = "number", icon, autoFocus, step }: any) => (
<div className="relative group bg-surface-container-high rounded-t-lg border-b border-outline-variant hover:bg-white/5 focus-within:border-primary transition-colors">
<label className="absolute top-2 left-4 text-[10px] font-medium text-on-surface-variant flex items-center gap-1">
{icon} {label}
</label>
<input
type={type}
step={step}
inputMode={type === 'number' ? 'decimal' : 'text'}
autoFocus={autoFocus}
className="w-full pt-6 pb-2 px-4 bg-transparent text-2xl text-on-surface focus:outline-none placeholder-transparent"
placeholder=" "
value={value}
onChange={onChange}
/>
</div>
);
const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
const [isEditing, setIsEditing] = useState(false);
@@ -195,6 +180,8 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
placeholder={t('plan_name_ph', lang)}
value={name}
onChange={(e) => setName(e.target.value)}
autoCapitalize="words"
onBlur={() => setName(toTitleCase(name))}
/>
</div>
@@ -305,6 +292,8 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
onChange={(e: any) => setNewExName(e.target.value)}
type="text"
autoFocus
autocapitalize="words"
onBlur={() => setNewExName(toTitleCase(newExName))}
/>
<div>

View File

@@ -6,6 +6,7 @@ import { getExercises, saveExercise } from '../services/storage';
import { getWeightHistory, logWeight } from '../services/weight';
import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, Plus } from 'lucide-react';
import ExerciseModal from './ExerciseModal';
import FilledInput from './FilledInput';
import { t } from '../services/i18n';
import Snackbar from './Snackbar';
@@ -375,16 +376,14 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
<Plus size={16} /> {t('create_exercise', lang)}
</button>
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
<label className="text-[10px] text-on-surface-variant font-medium">{t('filter_by_name', lang) || 'Filter by name'}</label>
<input
type="text"
value={exerciseNameFilter}
onChange={(e) => setExerciseNameFilter(e.target.value)}
placeholder={t('type_to_filter', lang) || 'Type to filter...'}
className="w-full bg-transparent text-on-surface focus:outline-none"
/>
</div>
<FilledInput
label={t('filter_by_name', lang) || 'Filter by name'}
value={exerciseNameFilter}
onChange={(e: any) => setExerciseNameFilter(e.target.value)}
icon={<i className="hidden" />} // No icon needed or maybe use Search icon? Profile doesn't import Search. I'll omit icon if optional.
type="text"
autoFocus={false}
/>
<div className="flex items-center justify-end gap-2">
<label className="text-xs text-on-surface-variant">{t('show_archived', lang)}</label>
@@ -476,19 +475,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
{/* Create User */}
<div className="space-y-3 mb-6">
<h4 className="text-xs font-medium text-on-surface-variant">{t('create_user', lang)}</h4>
<input
type="email"
placeholder="Email"
<FilledInput
label="Email"
value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)}
className="w-full bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
type="email"
/>
<input
type="text"
placeholder={t('login_password', lang)}
<FilledInput
label={t('login_password', lang)}
value={newUserPass}
onChange={(e) => setNewUserPass(e.target.value)}
className="w-full bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
type="text"
/>
<button onClick={handleCreateUser} className="w-full py-2 bg-primary text-on-primary rounded-full text-sm font-medium">
{t('create_btn', lang)}

View File

@@ -68,16 +68,7 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
exercises
} = tracker;
// We need activePlan from the hook or props. The hook returns 'plans' but not 'activePlan'.
// Actually useTracker takes activePlan as prop but doesn't return it.
// We should probably pass activePlan as a prop to this component directly from the parent.
// Let's assume the parent passes it or we modify the hook.
// For now, let's use the activePlan passed to the hook if possible, but the hook doesn't expose it.
// I will modify the hook to return activePlan or just accept it as prop here.
// The hook accepts activePlan as argument, so I can return it.
// Let's modify useTracker to return activePlan in the next step if needed, or just pass it here.
// Wait, I can't modify useTracker easily now without rewriting it.
// I'll pass activePlan as a prop to ActiveSessionView.
const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length;

View File

@@ -60,13 +60,15 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
icon={<Dumbbell size={10} />}
autoComplete="off"
type="text"
rightElement={
<button
onClick={() => setIsCreating(true)}
className="p-2 text-primary hover:bg-primary-container/20 rounded-full"
>
<Plus size={24} />
</button>
}
/>
<button
onClick={() => setIsCreating(true)}
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-primary hover:bg-primary-container/20 rounded-full z-10"
>
<Plus size={24} />
</button>
{showSuggestions && (
<div className="absolute top-full left-0 w-full bg-surface-container rounded-xl shadow-elevation-3 overflow-hidden z-20 mt-1 max-h-60 overflow-y-auto animate-in fade-in slide-in-from-top-2">
{filteredExercises.length > 0 ? (
@@ -163,8 +165,8 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
<button
onClick={onLogSet}
className={`w-full h-14 font-medium text-lg rounded-full shadow-elevation-2 hover:shadow-elevation-3 active:scale-[0.98] transition-all flex items-center justify-center gap-2 ${isSporadic && sporadicSuccess
? 'bg-green-500 text-white'
: 'bg-primary-container text-on-primary-container'
? 'bg-green-500 text-white'
: 'bg-primary-container text-on-primary-container'
}`}
>
{isSporadic && sporadicSuccess ? <CheckCircle size={24} /> : (isSporadic ? <Plus size={24} /> : <CheckCircle size={24} />)}

View File

@@ -183,6 +183,7 @@ export const useTracker = ({
const updateSelection = async () => {
if (selectedExercise) {
setBwPercentage(selectedExercise.bodyWeightPercentage ? selectedExercise.bodyWeightPercentage.toString() : '100');
setSearchQuery(selectedExercise.name);
const set = await getLastSetForExercise(userId, selectedExercise.id);
setLastSet(set);