feat: redesign sidebar with corporate branding and ProsApp logo

- Logo: network image in white card + tagline
- Sidebar: deep navy gradient (#0D1B3E) with blue accent active states
- MenuItem: left accent bar indicator, cleaner hover, better contrast
- Mode toggle: gradient button matching brand colors
- TextSeparator: uppercase label with divider line

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-27 07:40:08 -05:00
co-authored by Claude Sonnet 4.6
parent c8e5d481ed
commit 77f44ac43a
4 changed files with 200 additions and 162 deletions
+102 -103
View File
@@ -9,8 +9,6 @@ import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/shared/widgets/menu_item.dart'; import 'package:prosapp_web_app/ui/shared/widgets/menu_item.dart';
import 'package:prosapp_web_app/ui/shared/widgets/text_separator.dart'; import 'package:prosapp_web_app/ui/shared/widgets/text_separator.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:prosapp_web_app/ui/shared/widgets/logo.dart'; import 'package:prosapp_web_app/ui/shared/widgets/logo.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -29,169 +27,167 @@ class Sidebar extends StatelessWidget {
final professionalProvider = Provider.of<ProfessionalProvider>(context); final professionalProvider = Provider.of<ProfessionalProvider>(context);
return Container( return Container(
width: 200, width: 220,
height: double.infinity, height: double.infinity,
decoration: buildBoxDecoration(), decoration: buildBoxDecoration(),
child: ListView( child: ListView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
children: [ children: [
const Logo(), const Logo(),
const SizedBox(height: 50), const SizedBox(height: 8),
if (professionalProvider.isProModeActive) ...[ if (professionalProvider.isProModeActive) ...[
const TextSeparator(text: 'Profesional'), const TextSeparator(text: 'Profesional'),
MenuItem( MenuItem(
isActive: isActive: sideMenuProvider.currentPage == Flurorouter.profileRoute ||
sideMenuProvider.currentPage == Flurorouter.profileRoute || sideMenuProvider.currentPage == Flurorouter.professionalProfileRoute,
sideMenuProvider.currentPage == text: 'Perfil',
Flurorouter.professionalProfileRoute, icon: Icons.person_outlined,
text: 'Perfil', onPressed: () {
icon: Icons.person_outlined, if (professionalProvider.isProModeActive) {
onPressed: () { navigateTo(Flurorouter.professionalProfileRoute);
if (professionalProvider.isProModeActive) { } else {
navigateTo(Flurorouter.professionalProfileRoute); navigateTo(Flurorouter.profileRoute);
} else {
navigateTo(Flurorouter.profileRoute);
}
} }
},
// () => navigateTo(Flurorouter.profileRoute), ),
),
MenuItem( MenuItem(
isActive: sideMenuProvider.currentPage == isActive: sideMenuProvider.currentPage == Flurorouter.professionalServicesRequestsRoute,
Flurorouter.professionalServicesRequestsRoute,
text: 'Mis solicitudes', text: 'Mis solicitudes',
icon: Icons.room_service_outlined, icon: Icons.room_service_outlined,
onPressed: () => { onPressed: () {
Provider.of<ServicesProvider>(context, listen: false) Provider.of<ServicesProvider>(context, listen: false).clearServices();
.clearServices(), navigateTo(Flurorouter.professionalServicesRequestsRoute);
navigateTo(Flurorouter.professionalServicesRequestsRoute)
}, },
), ),
MenuItem( MenuItem(
isActive: sideMenuProvider.currentPage == isActive: sideMenuProvider.currentPage == Flurorouter.professionalServicesRoute,
Flurorouter.professionalServicesRoute,
text: 'Mis servicios', text: 'Mis servicios',
icon: Icons.list, icon: Icons.list_alt_outlined,
onPressed: () => { onPressed: () {
Provider.of<ServicesProvider>(context, listen: false) Provider.of<ServicesProvider>(context, listen: false).clearServices();
.clearServices(), navigateTo(Flurorouter.professionalServicesRoute);
navigateTo(Flurorouter.professionalServicesRoute)
}, },
), ),
MenuItem( MenuItem(
isActive: sideMenuProvider.currentPage == isActive: sideMenuProvider.currentPage == Flurorouter.professionalServicesHistoryRoute,
Flurorouter.professionalServicesHistoryRoute,
text: 'Historial', text: 'Historial',
icon: Icons.restore_outlined, icon: Icons.restore_outlined,
onPressed: () => { onPressed: () {
Provider.of<ServicesProvider>(context, listen: false) Provider.of<ServicesProvider>(context, listen: false).clearServices();
.clearServices(), navigateTo(Flurorouter.professionalServicesHistoryRoute);
navigateTo(Flurorouter.professionalServicesHistoryRoute)
}, },
), ),
MenuItem( MenuItem(
text: 'Calendario', text: 'Calendario',
icon: Icons.calendar_month_outlined, icon: Icons.calendar_month_outlined,
onPressed: () => onPressed: () => navigateTo(Flurorouter.professionalCalendarRoute),
navigateTo(Flurorouter.professionalCalendarRoute),
), ),
] else ...[ ] else ...[
const TextSeparator(text: 'Usuario'), const TextSeparator(text: 'Usuario'),
MenuItem( MenuItem(
isActive: isActive: sideMenuProvider.currentPage == Flurorouter.profileRoute,
sideMenuProvider.currentPage == Flurorouter.profileRoute,
text: 'Perfil', text: 'Perfil',
icon: Icons.person_outlined, icon: Icons.person_outlined,
onPressed: () => navigateTo(Flurorouter.profileRoute), onPressed: () => navigateTo(Flurorouter.profileRoute),
), ),
MenuItem( MenuItem(
isActive: isActive: sideMenuProvider.currentPage == Flurorouter.dashboardRoute,
sideMenuProvider.currentPage == Flurorouter.dashboardRoute,
text: 'Solicitar', text: 'Solicitar',
icon: Icons.room_service_outlined, icon: Icons.room_service_outlined,
onPressed: () => navigateTo(Flurorouter.dashboardRoute), onPressed: () => navigateTo(Flurorouter.dashboardRoute),
), ),
MenuItem( MenuItem(
isActive: isActive: sideMenuProvider.currentPage == Flurorouter.userServicesRoute,
sideMenuProvider.currentPage == Flurorouter.userServicesRoute,
text: 'Mis servicios', text: 'Mis servicios',
icon: Icons.list, icon: Icons.list_alt_outlined,
onPressed: () => { onPressed: () {
Provider.of<ServicesProvider>(context, listen: false) Provider.of<ServicesProvider>(context, listen: false).clearServices();
.clearServices(), navigateTo(Flurorouter.userServicesRoute);
navigateTo(Flurorouter.userServicesRoute)
}, },
), ),
MenuItem( MenuItem(
isActive: sideMenuProvider.currentPage == isActive: sideMenuProvider.currentPage == Flurorouter.userServicesHistoryRoute,
Flurorouter.userServicesHistoryRoute,
text: 'Historial', text: 'Historial',
icon: Icons.restore_outlined, icon: Icons.restore_outlined,
onPressed: () => { onPressed: () {
Provider.of<ServicesProvider>(context, listen: false) Provider.of<ServicesProvider>(context, listen: false).clearServices();
.clearServices(), navigateTo(Flurorouter.userServicesHistoryRoute);
navigateTo(Flurorouter.userServicesHistoryRoute)
}, },
), ),
MenuItem( MenuItem(
isActive: isActive: sideMenuProvider.currentPage == Flurorouter.supportRoute,
sideMenuProvider.currentPage == Flurorouter.supportRoute,
text: 'Soporte', text: 'Soporte',
icon: Icons.support_agent_outlined, icon: Icons.support_agent_outlined,
onPressed: () => navigateTo(Flurorouter.supportRoute), onPressed: () => navigateTo(Flurorouter.supportRoute),
), ),
], ],
Material(
color: Colors.blueAccent,
child: InkWell(
onTap: () {
if (authProvider.user?.picture == '' ||
authProvider.user?.city == '' ||
authProvider.user?.name == null ||
authProvider.user?.name == '' ||
authProvider.user?.email == '') {
NotificationsService.showSnackBarError(
'Primero completa tu perfil');
navigateTo(Flurorouter.profileRoute);
return; const SizedBox(height: 16),
} Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
if (authProvider.user?.proState == ProState.active) { child: MouseRegion(
professionalProvider.toggleProMode(); cursor: SystemMouseCursors.click,
child: GestureDetector(
if (professionalProvider.isProModeActive) { onTap: () {
navigateTo(Flurorouter.professionalServicesRequestsRoute); if (authProvider.user?.name == null ||
} else { authProvider.user?.name == '') {
navigateTo(Flurorouter.dashboardRoute); NotificationsService.showSnackBarError('Primero completa tu perfil');
navigateTo(Flurorouter.profileRoute);
return;
} }
} else { if (authProvider.user?.proState == ProState.active) {
navigateTo(Flurorouter.requestProfessionalRoute); professionalProvider.toggleProMode();
} if (professionalProvider.isProModeActive) {
}, navigateTo(Flurorouter.professionalServicesRequestsRoute);
child: Padding( } else {
padding: const EdgeInsets.symmetric( navigateTo(Flurorouter.dashboardRoute);
horizontal: 30, }
vertical: 10, } else {
), navigateTo(Flurorouter.requestProfessionalRoute);
child: MouseRegion( }
cursor: SystemMouseCursors.click, },
child: Text( child: Container(
'Modo ${professionalProvider.isProModeActive ? 'Usuario' : 'Profesional'}', padding: const EdgeInsets.symmetric(vertical: 11),
style: GoogleFonts.roboto( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.8)), gradient: const LinearGradient(
colors: [Color(0xFF42A4EF), Color(0xFF1565C0)],
),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
professionalProvider.isProModeActive
? Icons.person_outlined
: Icons.work_outline,
color: Colors.white,
size: 16,
),
const SizedBox(width: 8),
Text(
'Modo ${professionalProvider.isProModeActive ? 'Usuario' : 'Profesional'}',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
), ),
), ),
), ),
), ),
), ),
const SizedBox(height: 50), const SizedBox(height: 24),
const TextSeparator(text: 'Salir'), const TextSeparator(text: 'Sesion'),
MenuItem( MenuItem(
text: 'Cerrar sesión', text: 'Cerrar sesion',
icon: Icons.exit_to_app_outlined, icon: Icons.exit_to_app_outlined,
onPressed: () => authProvider.logout(), onPressed: () => authProvider.logout(),
), ),
const SizedBox(height: 20),
], ],
), ),
); );
@@ -199,15 +195,18 @@ class Sidebar extends StatelessWidget {
BoxDecoration buildBoxDecoration() => const BoxDecoration( BoxDecoration buildBoxDecoration() => const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [ colors: [
Color(0xff092044), Color(0xFF0D1B3E),
Color(0xff092042), Color(0xFF0A1628),
], ],
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black26, color: Colors.black38,
blurRadius: 10, blurRadius: 12,
offset: Offset(2, 0),
), ),
], ],
); );
+28 -12
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class Logo extends StatelessWidget { class Logo extends StatelessWidget {
const Logo({super.key}); const Logo({super.key});
@@ -7,21 +6,38 @@ class Logo extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: EdgeInsets.only(top: 30), padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 20),
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Image.asset('logo.png', width: 50, height: 50), Container(
SizedBox(width: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
Text( decoration: BoxDecoration(
'Prosapp',
style: GoogleFonts.montserratAlternates(
fontSize: 20,
fontWeight: FontWeight.w200,
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Image.network(
'https://www.prosapp.co/img/logo_prosapp.png',
height: 32,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Text(
'ProsApp',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF42A4EF),
),
),
),
),
const SizedBox(height: 8),
const Text(
'Conecta con profesionales',
style: TextStyle(
color: Color(0xFF8BAFD4),
fontSize: 10,
letterSpacing: 0.3,
), ),
), ),
SizedBox(width: 10),
], ],
), ),
); );
+52 -38
View File
@@ -1,5 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
const _kAccent = Color(0xFF42A4EF);
const _kTextActive = Colors.white;
const _kTextInactive = Color(0xFF8BAFD4);
const _kIconActive = _kAccent;
const _kIconInactive = Color(0xFF5A7A9B);
class MenuItem extends StatefulWidget { class MenuItem extends StatefulWidget {
final String text; final String text;
@@ -24,45 +29,54 @@ class _MenuItemState extends State<MenuItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedContainer( final active = widget.isActive;
duration: const Duration(milliseconds: 300), final highlight = active || isHovered;
color: isHovered
? Colors.white.withOpacity(0.1) return MouseRegion(
: widget.isActive cursor: active ? SystemMouseCursors.basic : SystemMouseCursors.click,
? Colors.white.withOpacity(0.1) onEnter: (_) => setState(() => isHovered = true),
: Colors.transparent, onExit: (_) => setState(() => isHovered = false),
child: Material( child: GestureDetector(
color: Colors.transparent, onTap: active ? null : () => widget.onPressed(),
child: InkWell( child: AnimatedContainer(
onTap: widget.isActive ? null : () => widget.onPressed(), duration: const Duration(milliseconds: 200),
child: Padding( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
padding: const EdgeInsets.symmetric( decoration: BoxDecoration(
horizontal: 30, color: highlight
vertical: 10, ? const Color(0xFF42A4EF).withOpacity(0.12)
), : Colors.transparent,
child: MouseRegion( borderRadius: BorderRadius.circular(10),
cursor: widget.isActive ),
? SystemMouseCursors.basic child: Row(
: SystemMouseCursors.click, children: [
onEnter: (_) => setState(() => isHovered = true), AnimatedContainer(
onExit: (_) => setState(() => isHovered = false), duration: const Duration(milliseconds: 200),
child: Row( width: 3,
crossAxisAlignment: CrossAxisAlignment.center, height: 36,
children: [ decoration: BoxDecoration(
Icon( color: active ? _kAccent : Colors.transparent,
widget.icon, borderRadius: const BorderRadius.only(
color: Colors.white.withOpacity(0.3), topRight: Radius.circular(3),
bottomRight: Radius.circular(3),
), ),
const SizedBox(width: 10), ),
Text(
widget.text,
style: GoogleFonts.roboto(
color: Colors.white.withOpacity(0.8),
),
),
],
), ),
), const SizedBox(width: 12),
Icon(
widget.icon,
color: active ? _kIconActive : _kIconInactive,
size: 20,
),
const SizedBox(width: 10),
Text(
widget.text,
style: TextStyle(
color: active ? _kTextActive : _kTextInactive,
fontSize: 13,
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
),
),
],
), ),
), ),
), ),
+18 -9
View File
@@ -1,20 +1,29 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class TextSeparator extends StatelessWidget { class TextSeparator extends StatelessWidget {
final String text; final String text;
const TextSeparator({super.key, required this.text}); const TextSeparator({super.key, required this.text});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Padding(
padding: EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.fromLTRB(20, 16, 20, 4),
margin: EdgeInsets.only(bottom: 5), child: Row(
child: Text( children: [
text, Text(
style: GoogleFonts.roboto( text.toUpperCase(),
color: Colors.white.withOpacity(0.3), fontSize: 12), style: const TextStyle(
color: Color(0xFF4A6F8A),
fontSize: 10,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
const SizedBox(width: 8),
const Expanded(
child: Divider(color: Color(0xFF1E3A52), thickness: 1, height: 1),
),
],
), ),
); );
} }