feat: atalhos de navegação Q/E e correções no i18n

This commit is contained in:
2026-06-03 16:55:01 +01:00
parent 0a4de43635
commit 1930190c8d
10 changed files with 700 additions and 314 deletions

View File

@@ -69,6 +69,7 @@ export default function App() {
const [defaultPage, setDefaultPage] = useState('dashboard');
const [weatherData, setWeatherData] = useState(null);
const [isPrivate, setIsPrivate] = useState(false);
const [shortcutsEnabled, setShortcutsEnabled] = useState(false);
const [userStatus, setUserStatus] = useState('online');
// Estado da Comunidade
@@ -228,6 +229,26 @@ export default function App() {
saveUserSetting('isPrivate', newVal);
};
const handleShortcutsToggle = (newVal) => {
setShortcutsEnabled(newVal);
saveUserSetting('shortcutsEnabled', newVal);
};
const handleNavShortcut = (direction) => {
const navItems = ['dashboard', 'closet', 'wishlist', 'laundry', 'outfits', 'planner', 'community', 'settings'];
const currentIndex = navItems.indexOf(view);
if (currentIndex === -1) return;
let nextIndex;
if (direction === 'up') {
nextIndex = currentIndex === 0 ? navItems.length - 1 : currentIndex - 1;
} else {
nextIndex = currentIndex === navItems.length - 1 ? 0 : currentIndex + 1;
}
setView(navItems[nextIndex]);
if (window.innerWidth < 768) setSidebarOpen(false);
};
const toggleStatus = (e) => {
e.stopPropagation();
e.preventDefault();
@@ -272,6 +293,23 @@ export default function App() {
}
};
useEffect(() => {
if (!shortcutsEnabled) return;
const handleKeyDown = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key.toLowerCase() === 'q') {
handleNavShortcut('up');
} else if (e.key.toLowerCase() === 'e') {
handleNavShortcut('down');
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [shortcutsEnabled, view]);
useEffect(() => {
if (editingItem && editingItem.color) {
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
@@ -415,6 +453,7 @@ export default function App() {
setDefaultPage(data.settings.defaultPage === 'planning' ? 'planner' : data.settings.defaultPage);
}
if (data.settings.isPrivate !== undefined) setIsPrivate(data.settings.isPrivate);
if (data.settings.shortcutsEnabled !== undefined) setShortcutsEnabled(data.settings.shortcutsEnabled);
if (data.settings.status !== undefined) setUserStatus(data.settings.status);
}
}
@@ -1435,6 +1474,11 @@ export default function App() {
<span className="text-3xl font-black tracking-tighter italic">MyCloset</span>
</button>
{shortcutsEnabled && (
<button onClick={() => handleNavShortcut('up')} className="w-full flex items-center justify-center gap-2 p-2 bg-gray-100 dark:bg-gray-800 rounded-xl mb-3 text-xs font-bold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
<span className="px-2 py-1 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">Q</span> {t('up')}
</button>
)}
<nav className="flex-1 space-y-3">
{[
{ id: 'dashboard', label: t('dashboard'), icon: LayoutDashboard },
@@ -1459,6 +1503,11 @@ export default function App() {
</button>
))}
</nav>
{shortcutsEnabled && (
<button onClick={() => handleNavShortcut('down')} className="w-full flex items-center justify-center gap-2 p-2 bg-gray-100 dark:bg-gray-800 rounded-xl mt-3 text-xs font-bold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
<span className="px-2 py-1 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">E</span> {t('down')}
</button>
)}
<div className="mt-auto pt-10 border-t border-inherit">
<div onClick={() => setView('profile')} className="w-full flex items-center gap-4 mb-5 md:mb-8 px-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800 py-3 rounded-2xl transition-all cursor-pointer">
@@ -2539,29 +2588,29 @@ export default function App() {
<div className="space-y-6 text-inherit bg-gray-50 dark:bg-gray-800/50 p-6 rounded-3xl">
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Data de Nascimento</p>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('dob')}</p>
<p className="font-bold">{selectedUserProfile?.dob ? new Date(selectedUserProfile.dob).toLocaleDateString() : 'Não especificada'}</p>
</div>
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Localidade</p>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('location')}</p>
<p className="font-bold">{selectedUserProfile?.location || selectedCommunityUser.location || 'Não especificada'}</p>
</div>
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Bio</p>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('bio')}</p>
<p className="font-bold opacity-80">{selectedUserProfile?.bio || 'Sem biografia'}</p>
</div>
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Peças Registadas</p>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('registeredPieces')}</p>
<p className="text-2xl font-black">{selectedUserClothes.length}</p>
</div>
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Outfits Criados</p>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('createdOutfits')}</p>
<p className="text-2xl font-black">{selectedUserLooks.length}</p>
</div>
</div>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Data de Registo da Conta</p>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('accountRegistrationDate')}</p>
<p className="font-bold">{selectedUserProfile?.createdAt ? new Date(selectedUserProfile.createdAt).toLocaleDateString() : 'Desconhecida'}</p>
</div>
</div>
@@ -2676,10 +2725,21 @@ export default function App() {
<div className={`w-6 h-6 rounded-full bg-white absolute top-1 transition-all ${notificationsEnabled ? 'left-7' : 'left-1'}`}></div>
</button>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-bold text-inherit flex items-center gap-2">{t('keyboardShortcuts')}</p>
<p className="text-[10px] uppercase tracking-widest opacity-50 text-inherit">{t('enableNavigationQE')}</p>
</div>
<button onClick={() => handleShortcutsToggle(!shortcutsEnabled)} className={`w-14 h-8 rounded-full transition-colors relative ${shortcutsEnabled ? 'bg-primary-600' : 'bg-gray-200'}`}>
<div className={`w-6 h-6 rounded-full bg-white absolute top-1 transition-all ${shortcutsEnabled ? 'left-7' : 'left-1'}`}></div>
</button>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<p className="font-bold text-inherit flex items-center gap-2">Notificação do Outfit Diário</p>
<p className="text-[10px] uppercase tracking-widest opacity-50 text-inherit">Receber notificação com o outfit planeado à hora marcada</p>
<p className="font-bold text-inherit flex items-center gap-2">{t('dailyOutfitNotification')}</p>
<p className="text-[10px] uppercase tracking-widest opacity-50 text-inherit">{t('receiveNotificationAtScheduledTime')}</p>
</div>
<div className="flex items-center gap-4 w-full sm:w-auto justify-between sm:justify-end">
{dailyOutfitNotifEnabled && (

View File

@@ -35,6 +35,16 @@ export const translations = {
todayIn: "Hoje em Portugal",
weatherUpdate: "22°C - Ensolarado",
weatherCurrentAvg: "{current}°C Atual • Média {avg}°C",
registeredPieces: "Peças Registadas",
createdOutfits: "Outfits Criados",
accountRegistrationDate: "Data de Registo da Conta",
keyboardShortcuts: "Atalhos de Teclado",
enableNavigationQE: "Ativar navegação com Q e E",
dailyOutfitNotification: "Notificação do Outfit Diário",
receiveNotificationAtScheduledTime: "Receber notificação com o outfit planeado à hora marcada",
up: "Cima",
down: "Baixo",
weatherForecastDesc: "O dia de hoje tem máximas de {max}°C e mínimas de {min}°C.",
weatherMsg: "Está um dia fantástico! Recomendamos as tuas peças leves. Que tal um visual casual com as tuas sapatilhas favoritas?",
exploreSuggestions: "Explorar Sugestões",
@@ -252,6 +262,16 @@ export const translations = {
todayIn: "Today in Portugal",
weatherUpdate: "22°C - Sunny",
weatherCurrentAvg: "{current}°C Current • Average {avg}°C",
registeredPieces: "Registered Pieces",
createdOutfits: "Created Outfits",
accountRegistrationDate: "Account Registration Date",
keyboardShortcuts: "Keyboard Shortcuts",
enableNavigationQE: "Enable navigation with Q and E",
dailyOutfitNotification: "Daily Outfit Notification",
receiveNotificationAtScheduledTime: "Receive notification with the planned outfit at the scheduled time",
up: "Up",
down: "Down",
weatherForecastDesc: "Today has highs of {max}°C and lows of {min}°C.",
weatherMsg: "It's a fantastic day! We recommend your light pieces. How about a casual look with your favorite sneakers?",
exploreSuggestions: "Explore Suggestions",
@@ -469,6 +489,16 @@ export const translations = {
todayIn: "Hoy en Portugal",
weatherUpdate: "22°C - Soleado",
weatherCurrentAvg: "{current}°C Actual • Media {avg}°C",
registeredPieces: "Piezas Registradas",
createdOutfits: "Outfits Creados",
accountRegistrationDate: "Fecha de Registro de la Cuenta",
keyboardShortcuts: "Atajos de Teclado",
enableNavigationQE: "Habilitar navegación con Q y E",
dailyOutfitNotification: "Notificación del Outfit Diario",
receiveNotificationAtScheduledTime: "Recibir notificación con el outfit planeado a la hora programada",
up: "Arriba",
down: "Abajo",
weatherForecastDesc: "El día de hoy tiene máximas de {max}°C y mínimas de {min}°C.",
weatherMsg: "¡Es un día fantástico! Recomendamos tus piezas ligeras. ¿Qué tal un look casual con tus zapatillas favoritas?",
exploreSuggestions: "Explorar Sugerencias",
@@ -686,6 +716,16 @@ export const translations = {
todayIn: "Aujourd'hui au Portugal",
weatherUpdate: "22°C - Ensoleillé",
weatherCurrentAvg: "{current}°C Actuel • Moyenne {avg}°C",
registeredPieces: "Pièces Enregistrées",
createdOutfits: "Outfits Créés",
accountRegistrationDate: "Date d'Inscription du Compte",
keyboardShortcuts: "Raccourcis Clavier",
enableNavigationQE: "Activer la navigation avec Q et E",
dailyOutfitNotification: "Notification de Tenue Quotidienne",
receiveNotificationAtScheduledTime: "Recevoir une notification avec la tenue prévue à l'heure programmée",
up: "Haut",
down: "Bas",
weatherForecastDesc: "Aujourd'hui a des maximales de {max}°C et des minimales de {min}°C.",
weatherMsg: "C'est une journée fantastique ! Nous recommandons vos pièces légères. Que diriez-vous d'un look décontracté avec vos baskets préférées ?",
exploreSuggestions: "Explorer les Suggestions",
@@ -903,6 +943,16 @@ export const translations = {
todayIn: "Heute in Portugal",
weatherUpdate: "22°C - Sonnig",
weatherCurrentAvg: "{current}°C Aktuell • Durchschnitt {avg}°C",
registeredPieces: "Registrierte Stücke",
createdOutfits: "Erstellte Outfits",
accountRegistrationDate: "Konto-Registrierungsdatum",
keyboardShortcuts: "Tastenkombinationen",
enableNavigationQE: "Navigation mit Q und E aktivieren",
dailyOutfitNotification: "Tägliche Outfit-Benachrichtigung",
receiveNotificationAtScheduledTime: "Benachrichtigung mit dem geplanten Outfit zur geplanten Zeit erhalten",
up: "Oben",
down: "Unten",
weatherForecastDesc: "Der heutige Tag hat Höchstwerte von {max}°C und Tiefstwerte von {min}°C.",
weatherMsg: "Es ist ein fantastischer Tag! Wir empfehlen leichte Stücke. Wie wäre es mit einem lässigen Look mit deinen Lieblings-Sneakern?",
exploreSuggestions: "Vorschläge entdecken",