Massive bug fix. Clear button added into fields.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
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 { X, Dumbbell, User, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, Percent } from 'lucide-react';
|
||||||
import { ExerciseDef, ExerciseType, Language } from '../types';
|
import { ExerciseDef, ExerciseType, Language } from '../types';
|
||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
@@ -82,6 +83,7 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
|
|||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
autocapitalize="words"
|
autocapitalize="words"
|
||||||
|
onBlur={() => setNewName(toTitleCase(newName))}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-xs text-error mt-2 ml-3">{error}</p>
|
<p className="text-xs text-error mt-2 ml-3">{error}</p>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
interface FilledInputProps {
|
interface FilledInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onClear?: () => void;
|
||||||
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -13,9 +15,19 @@ interface FilledInputProps {
|
|||||||
inputMode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal";
|
inputMode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal";
|
||||||
autocapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
|
autocapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
|
||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
|
rightElement?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilledInput: React.FC<FilledInputProps> = ({ label, value, onChange, onFocus, onBlur, type = "number", icon, autoFocus, step, inputMode, autocapitalize, autoComplete }) => (
|
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">
|
<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">
|
<label className="absolute top-2 left-4 text-[10px] font-medium text-on-surface-variant flex items-center gap-1">
|
||||||
{icon} {label}
|
{icon} {label}
|
||||||
@@ -25,7 +37,7 @@ const FilledInput: React.FC<FilledInputProps> = ({ label, value, onChange, onFoc
|
|||||||
step={step}
|
step={step}
|
||||||
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
|
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
className="w-full pt-6 pb-2 px-4 bg-transparent text-2xl text-on-surface focus:outline-none placeholder-transparent"
|
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"
|
placeholder="0"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -34,7 +46,22 @@ const FilledInput: React.FC<FilledInputProps> = ({ label, value, onChange, onFoc
|
|||||||
autoCapitalize={autocapitalize}
|
autoCapitalize={autocapitalize}
|
||||||
autoComplete={autoComplete}
|
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>
|
||||||
);
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default FilledInput;
|
export default FilledInput;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface HistoryProps {
|
|||||||
const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSession, lang }) => {
|
const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSession, lang }) => {
|
||||||
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
|
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
|
||||||
|
|
||||||
|
|
||||||
const calculateSessionWork = (session: WorkoutSession) => {
|
const calculateSessionWork = (session: WorkoutSession) => {
|
||||||
@@ -86,6 +87,18 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
|
|||||||
if (deletingId && onDeleteSession) {
|
if (deletingId && onDeleteSession) {
|
||||||
onDeleteSession(deletingId);
|
onDeleteSession(deletingId);
|
||||||
setDeletingId(null);
|
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
|
// Find the session and set up for deletion
|
||||||
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
|
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
|
||||||
if (parentSession) {
|
if (parentSession) {
|
||||||
setEditingSession(JSON.parse(JSON.stringify(parentSession)));
|
setDeletingSetInfo({ sessionId: parentSession.id, setId: set.id });
|
||||||
setDeletingId(set.id); // Use set ID for deletion
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* DELETE CONFIRMATION DIALOG (MD3) */}
|
{/* 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="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">
|
<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>
|
<h3 className="text-xl font-normal text-on-surface mb-2">
|
||||||
<p className="text-sm text-on-surface-variant mb-8">{t('delete_confirm', lang)}</p>
|
{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">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeletingId(null)}
|
onClick={() => {
|
||||||
|
setDeletingId(null);
|
||||||
|
setDeletingSetInfo(null);
|
||||||
|
}}
|
||||||
className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5"
|
className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5"
|
||||||
>
|
>
|
||||||
{t('cancel', lang)}
|
{t('cancel', lang)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import FilledInput from './FilledInput';
|
||||||
import { login, changePassword } from '../services/auth';
|
import { login, changePassword } from '../services/auth';
|
||||||
import { User, Language } from '../types';
|
import { User, Language } from '../types';
|
||||||
import { Dumbbell, ArrowRight, Lock, Mail, Globe } from 'lucide-react';
|
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>
|
<p className="text-sm text-on-surface-variant mb-6">{t('change_pass_desc', language)}</p>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
|
<FilledInput
|
||||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('change_pass_new', language)}</label>
|
label={t('change_pass_new', language)}
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="w-full bg-transparent text-lg text-on-surface focus:outline-none pt-1"
|
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
type="password"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
onClick={handleChangePassword}
|
onClick={handleChangePassword}
|
||||||
className="w-full py-3 bg-primary text-on-primary rounded-full font-medium"
|
className="w-full py-3 bg-primary text-on-primary rounded-full font-medium"
|
||||||
@@ -104,33 +102,23 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
|||||||
|
|
||||||
<form onSubmit={handleLogin} className="w-full max-w-sm space-y-6">
|
<form onSubmit={handleLogin} className="w-full max-w-sm space-y-6">
|
||||||
<div className="space-y-4">
|
<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">
|
<FilledInput
|
||||||
<div className="flex items-center px-4 pt-4">
|
label={t('login_email', language)}
|
||||||
<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}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
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"
|
type="email"
|
||||||
placeholder="user@gymflow.ai"
|
icon={<Mail size={16} />}
|
||||||
|
inputMode="email"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="group bg-surface-container-high rounded-t-lg border-b border-outline-variant focus-within:border-primary transition-colors">
|
<FilledInput
|
||||||
<div className="flex items-center px-4 pt-4">
|
label={t('login_password', language)}
|
||||||
<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}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
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"
|
type="password"
|
||||||
|
icon={<Lock size={16} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <div className="text-error text-sm text-center bg-error-container/10 p-2 rounded-lg">{error}</div>}
|
{error && <div className="text-error text-sm text-center bg-error-container/10 p-2 rounded-lg">{error}</div>}
|
||||||
|
|
||||||
|
|||||||
@@ -6,30 +6,15 @@ import { getPlans, savePlan, deletePlan, getExercises, saveExercise } from '../s
|
|||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
import { generateId } from '../utils/uuid';
|
import { generateId } from '../utils/uuid';
|
||||||
|
|
||||||
|
import FilledInput from './FilledInput';
|
||||||
|
import { toTitleCase } from '../utils/text';
|
||||||
|
|
||||||
interface PlansProps {
|
interface PlansProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
onStartPlan: (plan: WorkoutPlan) => void;
|
onStartPlan: (plan: WorkoutPlan) => void;
|
||||||
lang: Language;
|
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: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||||
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
|
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
@@ -195,6 +180,8 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
|||||||
placeholder={t('plan_name_ph', lang)}
|
placeholder={t('plan_name_ph', lang)}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
autoCapitalize="words"
|
||||||
|
onBlur={() => setName(toTitleCase(name))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -305,6 +292,8 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
|||||||
onChange={(e: any) => setNewExName(e.target.value)}
|
onChange={(e: any) => setNewExName(e.target.value)}
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
autocapitalize="words"
|
||||||
|
onBlur={() => setNewExName(toTitleCase(newExName))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getExercises, saveExercise } from '../services/storage';
|
|||||||
import { getWeightHistory, logWeight } from '../services/weight';
|
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 { 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 ExerciseModal from './ExerciseModal';
|
||||||
|
import FilledInput from './FilledInput';
|
||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
import Snackbar from './Snackbar';
|
import Snackbar from './Snackbar';
|
||||||
|
|
||||||
@@ -375,16 +376,14 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
<Plus size={16} /> {t('create_exercise', lang)}
|
<Plus size={16} /> {t('create_exercise', lang)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
<FilledInput
|
||||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('filter_by_name', lang) || 'Filter by name'}</label>
|
label={t('filter_by_name', lang) || 'Filter by name'}
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={exerciseNameFilter}
|
value={exerciseNameFilter}
|
||||||
onChange={(e) => setExerciseNameFilter(e.target.value)}
|
onChange={(e: any) => setExerciseNameFilter(e.target.value)}
|
||||||
placeholder={t('type_to_filter', lang) || 'Type to filter...'}
|
icon={<i className="hidden" />} // No icon needed or maybe use Search icon? Profile doesn't import Search. I'll omit icon if optional.
|
||||||
className="w-full bg-transparent text-on-surface focus:outline-none"
|
type="text"
|
||||||
|
autoFocus={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<label className="text-xs text-on-surface-variant">{t('show_archived', lang)}</label>
|
<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 */}
|
{/* Create User */}
|
||||||
<div className="space-y-3 mb-6">
|
<div className="space-y-3 mb-6">
|
||||||
<h4 className="text-xs font-medium text-on-surface-variant">{t('create_user', lang)}</h4>
|
<h4 className="text-xs font-medium text-on-surface-variant">{t('create_user', lang)}</h4>
|
||||||
<input
|
<FilledInput
|
||||||
type="email"
|
label="Email"
|
||||||
placeholder="Email"
|
|
||||||
value={newUserEmail}
|
value={newUserEmail}
|
||||||
onChange={(e) => setNewUserEmail(e.target.value)}
|
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
|
<FilledInput
|
||||||
type="text"
|
label={t('login_password', lang)}
|
||||||
placeholder={t('login_password', lang)}
|
|
||||||
value={newUserPass}
|
value={newUserPass}
|
||||||
onChange={(e) => setNewUserPass(e.target.value)}
|
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">
|
<button onClick={handleCreateUser} className="w-full py-2 bg-primary text-on-primary rounded-full text-sm font-medium">
|
||||||
{t('create_btn', lang)}
|
{t('create_btn', lang)}
|
||||||
|
|||||||
@@ -68,16 +68,7 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
|||||||
exercises
|
exercises
|
||||||
} = tracker;
|
} = 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;
|
const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length;
|
||||||
|
|
||||||
|
|||||||
@@ -60,13 +60,15 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
|
|||||||
icon={<Dumbbell size={10} />}
|
icon={<Dumbbell size={10} />}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
rightElement={
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCreating(true)}
|
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"
|
className="p-2 text-primary hover:bg-primary-container/20 rounded-full"
|
||||||
>
|
>
|
||||||
<Plus size={24} />
|
<Plus size={24} />
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{showSuggestions && (
|
{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">
|
<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 ? (
|
{filteredExercises.length > 0 ? (
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export const useTracker = ({
|
|||||||
const updateSelection = async () => {
|
const updateSelection = async () => {
|
||||||
if (selectedExercise) {
|
if (selectedExercise) {
|
||||||
setBwPercentage(selectedExercise.bodyWeightPercentage ? selectedExercise.bodyWeightPercentage.toString() : '100');
|
setBwPercentage(selectedExercise.bodyWeightPercentage ? selectedExercise.bodyWeightPercentage.toString() : '100');
|
||||||
|
setSearchQuery(selectedExercise.name);
|
||||||
const set = await getLastSetForExercise(userId, selectedExercise.id);
|
const set = await getLastSetForExercise(userId, selectedExercise.id);
|
||||||
setLastSet(set);
|
setLastSet(set);
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -25,7 +25,13 @@ router.get('/', async (req: any, res) => {
|
|||||||
try {
|
try {
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
const sessions = await prisma.workoutSession.findMany({
|
const sessions = await prisma.workoutSession.findMany({
|
||||||
where: { userId },
|
where: {
|
||||||
|
userId,
|
||||||
|
OR: [
|
||||||
|
{ endTime: { not: null } },
|
||||||
|
{ type: 'QUICK_LOG' }
|
||||||
|
]
|
||||||
|
},
|
||||||
include: { sets: { include: { exercise: true } } },
|
include: { sets: { include: { exercise: true } } },
|
||||||
orderBy: { startTime: 'desc' }
|
orderBy: { startTime: 'desc' }
|
||||||
});
|
});
|
||||||
@@ -505,7 +511,6 @@ router.patch('/active/set/:setId', async (req: any, res) => {
|
|||||||
height: height !== undefined ? (height ? parseFloat(height) : null) : undefined,
|
height: height !== undefined ? (height ? parseFloat(height) : null) : undefined,
|
||||||
bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined,
|
bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined,
|
||||||
side: side !== undefined ? side : undefined,
|
side: side !== undefined ? side : undefined,
|
||||||
note: note !== undefined ? note : undefined,
|
|
||||||
},
|
},
|
||||||
include: { exercise: true }
|
include: { exercise: true }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,10 +7,15 @@ const prisma = new PrismaClient();
|
|||||||
async function backup() {
|
async function backup() {
|
||||||
try {
|
try {
|
||||||
console.log('Starting backup...');
|
console.log('Starting backup...');
|
||||||
const sporadicSets = await prisma.sporadicSet.findMany();
|
// Backup Quick Log sessions and their sets (formerly SporadicSets)
|
||||||
|
const quickLogSessions = await prisma.workoutSession.findMany({
|
||||||
|
where: { type: 'QUICK_LOG' },
|
||||||
|
include: { sets: true }
|
||||||
|
});
|
||||||
|
|
||||||
const backupPath = path.join(__dirname, '../../sporadic_backup.json');
|
const backupPath = path.join(__dirname, '../../sporadic_backup.json');
|
||||||
fs.writeFileSync(backupPath, JSON.stringify(sporadicSets, null, 2));
|
fs.writeFileSync(backupPath, JSON.stringify(quickLogSessions, null, 2));
|
||||||
console.log(`Backed up ${sporadicSets.length} sporadic sets to ${backupPath}`);
|
console.log(`Backed up ${quickLogSessions.length} quick log sessions to ${backupPath}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Backup failed:', error);
|
console.error('Backup failed:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ const translations = {
|
|||||||
sets_count: 'Sets',
|
sets_count: 'Sets',
|
||||||
finished: 'Finished',
|
finished: 'Finished',
|
||||||
delete_workout: 'Delete workout?',
|
delete_workout: 'Delete workout?',
|
||||||
|
delete_set: 'Delete set?',
|
||||||
delete_confirm: 'This action cannot be undone.',
|
delete_confirm: 'This action cannot be undone.',
|
||||||
|
delete_set_confirm: 'Are you sure you want to delete this set?',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
@@ -258,7 +260,9 @@ const translations = {
|
|||||||
sets_count: 'Сетов',
|
sets_count: 'Сетов',
|
||||||
finished: 'Завершено',
|
finished: 'Завершено',
|
||||||
delete_workout: 'Удалить тренировку?',
|
delete_workout: 'Удалить тренировку?',
|
||||||
|
delete_set: 'Удалить подход?',
|
||||||
delete_confirm: 'Это действие нельзя отменить.',
|
delete_confirm: 'Это действие нельзя отменить.',
|
||||||
|
delete_set_confirm: 'Вы уверены, что хотите удалить этот подход?',
|
||||||
delete: 'Удалить',
|
delete: 'Удалить',
|
||||||
edit: 'Редактировать',
|
edit: 'Редактировать',
|
||||||
save: 'Сохранить',
|
save: 'Сохранить',
|
||||||
|
|||||||
5
utils/text.ts
Normal file
5
utils/text.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const toTitleCase = (str: string): string => {
|
||||||
|
return str.replace(/\w\S*/g, (txt) => {
|
||||||
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user