Massive bug fix. Clear button added into fields.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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} />)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -25,7 +25,13 @@ router.get('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const sessions = await prisma.workoutSession.findMany({
|
||||
where: { userId },
|
||||
where: {
|
||||
userId,
|
||||
OR: [
|
||||
{ endTime: { not: null } },
|
||||
{ type: 'QUICK_LOG' }
|
||||
]
|
||||
},
|
||||
include: { sets: { include: { exercise: true } } },
|
||||
orderBy: { startTime: 'desc' }
|
||||
});
|
||||
@@ -505,7 +511,6 @@ router.patch('/active/set/:setId', async (req: any, res) => {
|
||||
height: height !== undefined ? (height ? parseFloat(height) : null) : undefined,
|
||||
bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined,
|
||||
side: side !== undefined ? side : undefined,
|
||||
note: note !== undefined ? note : undefined,
|
||||
},
|
||||
include: { exercise: true }
|
||||
});
|
||||
|
||||
@@ -7,10 +7,15 @@ const prisma = new PrismaClient();
|
||||
async function backup() {
|
||||
try {
|
||||
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');
|
||||
fs.writeFileSync(backupPath, JSON.stringify(sporadicSets, null, 2));
|
||||
console.log(`Backed up ${sporadicSets.length} sporadic sets to ${backupPath}`);
|
||||
fs.writeFileSync(backupPath, JSON.stringify(quickLogSessions, null, 2));
|
||||
console.log(`Backed up ${quickLogSessions.length} quick log sessions to ${backupPath}`);
|
||||
} catch (error) {
|
||||
console.error('Backup failed:', error);
|
||||
} finally {
|
||||
|
||||
@@ -92,7 +92,9 @@ const translations = {
|
||||
sets_count: 'Sets',
|
||||
finished: 'Finished',
|
||||
delete_workout: 'Delete workout?',
|
||||
delete_set: 'Delete set?',
|
||||
delete_confirm: 'This action cannot be undone.',
|
||||
delete_set_confirm: 'Are you sure you want to delete this set?',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
@@ -258,7 +260,9 @@ const translations = {
|
||||
sets_count: 'Сетов',
|
||||
finished: 'Завершено',
|
||||
delete_workout: 'Удалить тренировку?',
|
||||
delete_set: 'Удалить подход?',
|
||||
delete_confirm: 'Это действие нельзя отменить.',
|
||||
delete_set_confirm: 'Вы уверены, что хотите удалить этот подход?',
|
||||
delete: 'Удалить',
|
||||
edit: 'Редактировать',
|
||||
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