O centro de atividades é um componente crucial para engajar usuários e promover ofertas especiais em aplicativos modernos. Ele serve como um portal para descontos, recompensas por pontos e promoções por tempo limitado, visando aumentar a interação do usuário e a fidelidade à plataforma. Este guia detalha a criação de uma página robusta para um hub de atividades, incluindo a exibição de um carrossel de banners, uma lista de eventos, detalhes de atividades, e funcionalidades de contagem regressiva.
Estrutura Básica da Página
A interface do hub de atividades é organizada com um carrossel rotativo no topo, destacando os eventos principais. Abaixo, uma lista exibe todos os eventos disponíveis. Cada cartão de evento apresenta o título, uma breve descrição, o prazo final e um botão de ação. Cores temáticas distintas são usadas para diferenciar os tipos de eventos, facilitando a identificação rápida pelo usuário.
import 'package:flutter/material.dart';
import 'dart:async'; // Para temporizadores (Timer)
/// Página do Hub de Atividades - Exibe diversas promoções e eventos da plataforma
/// Suporta carrossel, lista de eventos, detalhes de eventos e contagem regressiva.
class HubAtividadesPage extends StatefulWidget {
const HubAtividadesPage({super.key});
@override
State<HubAtividadesPage> createState() => _HubAtividadesPageState();
}
A utilização de um StatefulWidget para HubAtividadesPage é essencial, pois permite gerenciar estados dinâmicos, como a reprodução automática do carrossel e as atualizações da contagem regressiva. O StatefulWidget oferece métodos de ciclo de vida como initState para iniciar temporizadores e dispose para liberar recursos, garantindo a performance e prevenindo vazamentos de memória. Através do método setState, a interface pode ser re-renderizada para exibir atualizações em tempo real.
Variáveis de Estado e Inicialização de Dados
class _HubAtividadesPageState extends State<HubAtividadesPage> {
static const Color _corPrincipal = Color(0xFF6B4EFF);
final PageController _controladorCarrossel = PageController();
int _indiceCarrosselAtual = 0;
Timer? _temporizadorCarrossel;
Timer? _temporizadorContagemRegressiva;
int _segundosRestantesOferta = 72000; // Por exemplo, 20 horas
// ... (dados e outros métodos)
Definimos variáveis de estado importantes: um PageController para o carrossel, o índice da página atual, e instâncias de Timer para a automação do carrossel e a contagem regressiva. _segundosRestantesOferta é inicializado com um valor para simular uma oferta por tempo limitado. _controladorCarrossel é instanciado no nível da classe para garantir consistência. A cor _corPrincipal estabelece o esquema visual padrão do aplicativo.
Dados do Carrossel de Banners
final List<Map<String, dynamic>> _dadosCarrossel = [
{'id': 'banner01', 'titulo': 'Ofertas de Verão', 'subtitulo': 'Descontos de até 50%', 'cor': Color(0xFFE53935), 'icone': Icons.local_fire_department},
{'id': 'banner02', 'titulo': 'Indique um Amigo', 'subtitulo': 'Ganhe 150 pontos para cada indicação', 'cor': Color(0xFF43A047), 'icone': Icons.group_add},
{'id': 'banner03', 'titulo': 'Primeira Compra', 'subtitulo': 'R$ 25 de desconto no seu primeiro pedido', 'cor': Color(0xFFFF9800), 'icone': Icons.attach_money},
{'id': 'banner04', 'titulo': 'Vantagens Premium', 'subtitulo': 'Benefícios exclusivos para membros VIP', 'cor': Color(0xFF6B4EFF), 'icone': Icons.card_membership},
];
A lista _dadosCarrossel armazena as informações para os banners. Cada item inclui um identificador, título, subtítulo, uma cor de destaque e um ícone. O uso de cores distintas (veremlho para promoções, verde para indicações, laranja para novos usuários, roxo para membros) melhora a experiência do usuário, permitindo uma rápida categorização visual. Em uma aplicação real, esses dados seriam dinamicamente carregados de um serviço de backend.
Dados da Lista de Eventos
final List<Map<String, dynamic>> _listaEventos = [
{'id': 'evento01', 'titulo': 'Pacote de Boas-Vindas', 'descricao': 'Novos usuários recebem cupom de R$30 e desconto adicional de R$20 na primeira equipe.', 'expiraEm': '28/02/2025', 'tipo': 'Desconto', 'status': 'Ativo', 'participantes': 15800, 'emDestaque': true},
{'id': 'evento02', 'titulo': 'Fim de Semana Insano', 'descricao': '20% de desconto em todas as formações de equipe de sexta a domingo, sem limite.', 'expiraEm': 'Permanente', 'tipo': 'Promoção', 'status': 'Ativo', 'participantes': 9200, 'emDestaque': true},
{'id': 'evento03', 'titulo': 'Convidar e Ganhar', 'descricao': 'Convide amigos e ambos ganham 60 pontos, ilimitado!', 'expiraEm': 'Permanente', 'tipo': 'Pontos', 'status': 'Ativo', 'participantes': 7100, 'emDestaque': false},
{'id': 'evento04', 'titulo': 'Revisor de Roteiros', 'descricao': 'Envie avaliações de roteiros e concorra a sessões grátis.', 'expiraEm': '31/01/2025', 'tipo': 'Concurso', 'status': 'Ativo', 'participantes': 3500, 'emDestaque': false},
{'id': 'evento05', 'titulo': 'Recompensa Diária', 'descricao': 'Faça check-in por 7 dias consecutivos e receba um pacote de prêmios.', 'expiraEm': 'Permanente', 'tipo': 'Check-in', 'status': 'Ativo', 'participantes': 17000, 'emDestaque': true},
];
A lista _listaEventos armazena os dados dos eventos. Cada entrada inclui título, descrição, prazo, tipo de evento, status, número de participantes e um indicador de destaque. O campo 'tipo' permite aplicar etiquetas coloridas distintas, e 'emDestaque' sinaliza eventos populares para atrair mais atenção. Em um cenário real, esses dados seriam provenientes de uma API.
Gerenciamento do Ciclo de Vida
@override
void initState() {
super.initState();
_iniciarReproducaoAutomaticaCarrossel();
_iniciarContagemRegressiva();
}
@override
void dispose() {
_temporizadorCarrossel?.cancel();
_temporizadorContagemRegressiva?.cancel();
_controladorCarrossel.dispose();
super.dispose();
}
No método initState, inicializamos os temporizadores para o carrossel e a contagem regressiva. O método dispose é vital para cancelar todos os temporizadores ativos e liberar o PageController, prevenindo vazamentos de memória e otimizando o desempenho do aplicativo. Esta é uma prática recomendada no desenvolvimento Flutter para gerenciar recursos corretamente.
Métodos de Temporizador
void _iniciarReproducaoAutomaticaCarrossel() {
_temporizadorCarrossel = Timer.periodic(const Duration(seconds: 5), (timer) {
if (_controladorCarrossel.hasClients) {
int proximaPagina = (_indiceCarrosselAtual + 1) % _dadosCarrossel.length;
_controladorCarrossel.animateToPage(proximaPagina, duration: const Duration(milliseconds: 400), curve: Curves.easeOut);
}
});
}
void _iniciarContagemRegressiva() {
_temporizadorContagemRegressiva = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_segundosRestantesOferta > 0) {
setState(() => _segundosRestantesOferta--);
} else {
_temporizadorContagemRegressiva?.cancel(); // Para o temporizador quando a contagem chega a zero
}
});
}
O método _iniciarReproducaoAutomaticaCarrossel usa um Timer.periodic para avançar o carrossel a cada 5 segundos, com uma animação suave via animateToPage. A verificação hasClients garante que o controlador esteja anexado a um PageView. O método _iniciarContagemRegressiva decrementa _segundosRestantesOferta a cada segundo e invoca setState para atualizar a UI, criando o efeito de contagem regressiva em tempo real. Uma vez que o tempo se esgota, o temporizador é cancelado.
Formatação da Contagem Regressiva e Construção da Página
String _formatarTempoRestante(int segundos) {
int horas = segundos ~/ 3600;
int minutos = (segundos % 3600) ~/ 60;
int segs = segundos % 60;
return '${horas.toString().padLeft(2, '0')}:${minutos.toString().padLeft(2, '0')}:${segs.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
backgroundColor: _corPrincipal,
elevation: 0,
title: const Text('Hub de Atividades', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.receipt_long, color: Colors.white),
onPressed: () => _exibirHistoricoParticipacao(context),
)
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_construirSecaoCarrossel(context),
_construirAreaContagemRegressiva(),
_construirEntradasRapidas(context),
_construirListagemEventos(context),
],
),
),
);
}
O método _formatarTempoRestante converte segundos em um formato "HH:MM:SS" com preenchimento de zeros. O método build estrutura a página usando um Scaffold. A AppBar inclui um título centralizado e um botão de histórico de participação. O corpo da página utiliza um SingleChildScrollView, permitindo que todo o conteúdo seja rolado, e organiza as seções em uma Column: carrossel, contagem regressiva, entradas rápidas e a lista de eventos.
Implementação do Carrossel de Banners
Widget _construirSecaoCarrossel(BuildContext context) {
return Container(
height: 190,
child: Stack(
children: [
PageView.builder(
controller: _controladorCarrossel,
onPageChanged: (index) => setState(() => _indiceCarrosselAtual = index),
itemCount: _dadosCarrossel.length,
itemBuilder: (context, index) {
final banner = _dadosCarrossel[index];
return GestureDetector(
onTap: () => _aoTocarCarrossel(context, banner),
child: Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [banner['cor'], (banner['cor'] as Color).withOpacity(0.7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: (banner['cor'] as Color).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Stack(
children: [
Positioned(
right: -20,
bottom: -20,
child: Icon(banner['icone'] as IconData, size: 130, color: Colors.white.withOpacity(0.15)),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(banner['titulo'], style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(banner['subtitulo'], style: const TextStyle(color: Colors.white70, fontSize: 14)),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
child: Text('Participar Agora', style: TextStyle(color: banner['cor'] as Color, fontWeight: FontWeight.bold)),
),
],
),
),
],
),
),
);
},
),
Positioned(
bottom: 20,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_dadosCarrossel.length, (index) =>
Container(
width: _indiceCarrosselAtual == index ? 20 : 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: _indiceCarrosselAtual == index ? Colors.white : Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
)),
),
),
],
),
);
}
O carrossel é construído com PageView.builder, permitindo a rolagem e a criação sob demanda dos banners. Cada banner é envolto em um GestureDetector para capturar toques. A aparência visual é aprimorada com LinearGradient para o fundo, bordas arredondadas e sombras sutis, além de um ícone translúcido como adorno. Indicadores na parte inferior do carrossel mostram a página atual, com um formato mais largo para o índice ativo. O Stack é utilizado para sobrepor o carrossel e os indicadores de página, enquanto o Padding e o margin garantem espaçamento adequado.
Seção de Contagem Regressiva e Entradas Rápidas
A seção de contagem regressiva, com seu fundo gradiente vibrante, gera um senso de urgência, essencial para ofertas relâmpago. A exibição clara do tempo restante incentiva a ação imediata do usuário. As entradas rápidas oferecem acesso direto às funcionalidades mais comuns, como cupons e check-in, melhorando a navegabilidade e a experiência geral do usuário.
Widget _construirAreaContagemRegressiva() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFFFF7A7A), Color(0xFFFF9C62)]),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.timer, color: Colors.white, size: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Oferta Relâmpago', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('Próxima oferta em: ${_formatarTempoRestante(_segundosRestantesOferta)}', style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
child: const Text('Ver Ofertas', style: TextStyle(color: Color(0xFFFF7A7A), fontWeight: FontWeight.bold)),
),
],
),
);
}
Widget _construirEntradasRapidas(BuildContext context) {
final entradas = [
{'icone': Icons.local_offer, 'titulo': 'Meus Cupons', 'cor': Colors.deepOrange},
{'icone': Icons.calendar_today, 'titulo': 'Check-in Diário', 'cor': Colors.green},
{'icone': Icons.share, 'titulo': 'Convidar Amigos', 'cor': Colors.blueAccent},
{'icone': Icons.workspace_premium, 'titulo': 'Clube VIP', 'cor': _corPrincipal},
];
return Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: entradas.map((entrada) {
return GestureDetector(
onTap: () => _aoTocarEntradaRapida(context, entrada['titulo'] as String),
child: Column(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: (entrada['cor'] as Color).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: Icon(entrada['icone'] as IconData, color: entrada['cor'] as Color, size: 24),
),
const SizedBox(height: 8),
Text(entrada['titulo'] as String, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
],
),
);
}).toList(),
),
);
}
A seção de contagem regressiva utiliza um layout Row com um gradiente de cor vibrante, ícone, texto e um botão de ação. Expanded garante que o texto ocupe o espaço necessário. As entradas rápidas são renderizadas como uma Row de Columns, cada uma com um GestureDetector. Ícones coloridos com fundos translúcidos e bordas arredondadas são criados para cada entrada, utilizando map e toList para uma construção eficiente da UI.
Listagem de Eventos e Cartões
A listagem de eventos é o ponto central de exibição das promoções. A funcionalidade de filtro na parte superior permite aos usuários encontrar rapidamente eventos de interesse. Os cartões de evento são cuidadosamente projetados com cabeçalhos gradientes temáticos e áreas de conteúdo em branco, garantindo que cada evento seja visualmente atraente e informativo.
Widget _construirListagemEventos(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Todos os Eventos', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
TextButton(
onPressed: () => _exibirOpcoesFiltro(context),
child: Row(
children: [
Text('Filtrar', style: TextStyle(color: Colors.grey[600])),
Icon(Icons.filter_list, size: 16, color: Colors.grey[600]),
],
),
),
],
),
const SizedBox(height: 8),
..._listaEventos.map((evento) => _construirCartaoEvento(context, evento)).toList(),
],
),
);
}
Widget _construirCartaoEvento(BuildContext context, Map<String, dynamic> evento) {
Color tipoCor = _obterCorPorTipoEvento(evento['tipo']);
return GestureDetector(
onTap: () => _exibirDetalhesEvento(context, evento),
child: Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2)),
],
),
child: Column(
children: [
Container(
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [tipoCor, tipoCor.withOpacity(0.7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
),
child: Stack(
children: [
Positioned(
right: -10,
top: -10,
child: Icon(_obterIconePorTipoEvento(evento['tipo']), size: 80, color: Colors.white.withOpacity(0.2)),
),
if (evento['emDestaque'] == true)
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(4)),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, size: 12, color: Colors.white),
SizedBox(width: 4),
Text('Popular', style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
],
),
),
),
Center(child: Text(evento['titulo'], style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold))),
],
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(evento['descricao'], style: TextStyle(color: Colors.grey[700], fontSize: 14), maxLines: 2, overflow: TextOverflow.ellipsis),
const SizedBox(height: 12),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: tipoCor.withOpacity(0.1), borderRadius: BorderRadius.circular(4)),
child: Text(evento['tipo'], style: TextStyle(color: tipoCor, fontSize: 11, fontWeight: FontWeight.w500)),
),
const SizedBox(width: 8),
Icon(Icons.people, size: 14, color: Colors.grey[500]),
const SizedBox(width: 4),
Text('${_formatarNumeroParticipantes(evento['participantes'])} participantes', style: TextStyle(color: Colors.grey[600], fontSize: 11)),
const Spacer(),
Icon(Icons.date_range, size: 14, color: Colors.grey[500]),
const SizedBox(width: 4),
Text(evento['expiraEm'], style: TextStyle(color: Colors.grey[600], fontSize: 11)),
],
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _confirmarParticipacaoEvento(context, evento),
style: ElevatedButton.styleFrom(
backgroundColor: tipoCor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Quero Participar!', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
],
),
),
],
),
),
);
}
A lista de eventos é construída dinamicamente. Cada item é um cartão (Container) com um cabeçalho que utiliza um LinearGradient baseado no tipo do evento, junto a um ícone decorativo e um marcador de "Popular" para eventos em destaque. O corpo do cartão detalha a descrição, tipo, número de participantes (formatado por _formatarNumeroParticipantes) e prazo, finalizando com um botão de "Quero Participar!". Stack e Positioned são usados para o layout do cabeçalho, enquanto Row e Spacer organizam as informações no corpo do cartão, e ElevatedButton fornece a interação principal.
Métodos Auxiliares e Manipulação de Eventos
Para manter o código limpo e reutilizável, métodos auxiliares são empregados para mapear tipos de eventos a cores e ícones específicos. A função _formatarNumeroParticipantes converte grandes números para um formato mais legível. A manipulação de eventos de toque é realizada por meio de callbacks que disparam navegação ou exibição de mensagens.
Color _obterCorPorTipoEvento(String tipo) {
switch (tipo) {
case 'Desconto': return Colors.redAccent;
case 'Promoção': return Colors.orange;
case 'Pontos': return Colors.green;
case 'Concurso': return _corPrincipal;
case 'Check-in': return Colors.teal;
case 'Conquista': return Colors.amber;
case 'Flash': return Colors.pink;
case 'Membro': return Colors.indigo;
default: return _corPrincipal;
}
}
IconData _obterIconePorTipoEvento(String tipo) {
switch (tipo) {
case 'Desconto': return Icons.local_offer;
case 'Promoção': return Icons.discount;
case 'Pontos': return Icons.star_half;
case 'Concurso': return Icons.emoji_events;
case 'Check-in': return Icons.check_circle_outline;
case 'Conquista': return Icons.trophy;
case 'Flash': return Icons.flash_on;
case 'Membro': return Icons.diamond;
default: return Icons.event;
}
}
String _formatarNumeroParticipantes(int numero) {
if (numero >= 10000) return '${(numero / 10000).toStringAsFixed(1).replaceAll('.', ',')}K'; // '1.2K'
if (numero >= 1000) return '${(numero / 1000).toStringAsFixed(1).replaceAll('.', ',')}mil'; // '1.2mil'
return '$numero';
}
void _aoTocarCarrossel(BuildContext context, Map<String, dynamic> banner) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PaginaDetalheEvento(eventoId: banner['id'])));
}
void _aoTocarEntradaRapida(BuildContext context, String titulo) {
switch (titulo) {
case 'Meus Cupons':
Navigator.push(context, MaterialPageRoute(builder: (context) => PaginaCupons()));
break;
case 'Check-in Diário':
_exibirDialogoCheckin(context);
break;
case 'Convidar Amigos':
Navigator.push(context, MaterialPageRoute(builder: (context) => PaginaConvidarAmigos()));
break;
case 'Clube VIP':
Navigator.push(context, MaterialPageRoute(builder: (context) => PaginaClubeVIP()));
break;
}
}
void _confirmarParticipacaoEvento(BuildContext context, Map<String, dynamic> evento) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Você se juntou ao evento "${evento['titulo']}"!'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
Os métodos _obterCorPorTipoEvento e _obterIconePorTipoEvento utilizam uma estrutura switch para associar tipos de evento a cores e ícones correspondentes, o que facilita a adição de novas categorias no futuro. _formatarNumeroParticipantes oferece uma representação mais compacta para grandes números. Os métodos de tratamento de toque (_aoTocarCarrossel, _aoTocarEntradaRapida) usam Navigator.push para navegação padrão e _confirmarParticipacaoEvento exibe uma SnackBar para feedback ao usuário.
Implementação de Diálogos e Páginas de Detalhes
Para interações específicas como check-in ou visualização de detalhes, são utilizados diálogos e painéis modais. Estes componentes garantem que o usuário receba informações de forma clara e interativa, sem desviar completamente da página principal. A interface de check-in é projetada para ser visualmente intuitiva, mostrando o progresso do usuário através de um calendário.
void _exibirDialogoCheckin(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle_outline, color: Colors.green, size: 60),
const SizedBox(height: 16),
const Text('Check-in Realizado!', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('Você ganhou 10 pontos! Sequência de 3 dias.', style: TextStyle(color: Colors.grey[600])),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(7, (index) {
bool jaMarcado = index < 3; // Exemplo: dias 1, 2, 3 marcados
bool eHoje = index == 2; // Exemplo: 3º dia é hoje
return Column(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: jaMarcado ? _corPrincipal : Colors.grey[200],
shape: BoxShape.circle,
border: eHoje ? Border.all(color: Colors.amber, width: 2) : null,
),
child: Icon(jaMarcado ? Icons.check : Icons.circle_outlined, color: jaMarcado ? Colors.white : Colors.grey[400], size: 20),
),
const SizedBox(height: 4),
Text('Dia ${index + 1}', style: TextStyle(fontSize: 10, color: jaMarcado ? _corPrincipal : Colors.grey)),
],
);
}),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: _corPrincipal,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Entendido', style: TextStyle(color: Colors.white)),
),
),
],
),
),
);
},
);
}
void _exibirDetalhesEvento(BuildContext context, Map<String, dynamic> evento) {
Color tipoCor = _obterCorPorTipoEvento(evento['tipo']);
showModalBottomSheet(
context: context,
isScrollControlled: true, // Permite que a sheet ocupe mais da tela
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
builder: (BuildContext sheetContext) {
return Container(
height: MediaQuery.of(sheetContext).size.height * 0.75, // Ocupa 75% da altura da tela
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(evento['titulo'], style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Row(
children: [
Icon(Icons.calendar_today, size: 16, color: Colors.grey[600]),
const SizedBox(width: 6),
Text('Expira em: ${evento['expiraEm']}', style: TextStyle(color: Colors.grey[700])),
const Spacer(),
Icon(Icons.people_alt, size: 16, color: Colors.grey[600]),
const SizedBox(width: 6),
Text('${_formatarNumeroParticipantes(evento['participantes'])} participantes', style: TextStyle(color: Colors.grey[700])),
],
),
const Divider(height: 30),
const Text('Descrição Completa', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(evento['descricao'], style: TextStyle(color: Colors.grey[800], height: 1.6)),
// Adicione mais detalhes aqui, se necessário
],
),
),
),
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black12, width: 0.5)),
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(sheetContext).pop(); // Fecha o bottom sheet
_confirmarParticipacaoEvento(context, evento);
},
style: ElevatedButton.styleFrom(
backgroundColor: tipoCor,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Entrar no Evento', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
),
),
),
],
),
);
},
);
}
void _exibirHistoricoParticipacao(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
builder: (BuildContext sheetContext) {
return Container(
height: MediaQuery.of(sheetContext).size.height * 0.6,
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
),
const Padding(
padding: EdgeInsets.all(16),
child: Text('Meu Histórico', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: 5, // Exemplo de 5 itens no histórico
itemBuilder: (context, index) => Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.grey[50], borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(color: _corPrincipal.withOpacity(0.1), borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.emoji_events, color: _corPrincipal, size: 20),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Evento de Verão #${index + 1}', style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('Participação em: 15/07/2024', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(4)),
child: const Text('Concluído', style: TextStyle(color: Colors.green, fontSize: 11)),
),
],
),
),
),
),
],
),
);
},
);
}
void _exibirOpcoesFiltro(BuildContext context) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
builder: (BuildContext sheetContext) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Filtrar por Tipo', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: ['Todos', 'Desconto', 'Promoção', 'Pontos', 'Check-in', 'Flash', 'Membro']
.map((tipo) => FilterChip(
label: Text(tipo),
selected: tipo == 'Todos', // Estado de seleção inicial
selectedColor: _corPrincipal.withOpacity(0.2),
onSelected: (selected) {
Navigator.of(sheetContext).pop(); // Fecha o bottom sheet
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Filtro aplicado: $tipo'),
duration: const Duration(seconds: 1),
),
);
// Lógica para aplicar o filtro na lista principal
},
))
.toList(),
),
const SizedBox(height: 20),
],
),
);
},
);
}
}
// Classes fictícias para navegação
class PaginaDetalheEvento extends StatelessWidget {
final String eventoId;
const PaginaDetalheEvento({required this.eventoId, super.key});
@override
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Detalhes $eventoId')), body: Center(child: Text('Conteúdo detalhado do evento $eventoId')));
}
class PaginaCupons extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: const Text('Meus Cupons')), body: const Center(child: Text('Lista de Cupons')));
}
class PaginaConvidarAmigos extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: const Text('Convidar Amigos')), body: const Center(child: Text('Compartilhe com seus amigos!')));
}
class PaginaClubeVIP extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: const Text('Clube VIP')), body: const Center(child: Text('Benefícios Exclusivos')));
}
O método _exibirDialogoCheckin apresenta um Dialog com um calendário de check-in visualmente atraente. Dias já marcados são preenchidos com a cor principal, e o dia atual é destacado com uma borda dourada. _exibirDetalhesEvento e _exibirHistoricoParticipacao utilizam showModalBottomSheet para exibir painéis deslizantes na parte inferior da tela, que contêm informações detalhadas ou registros de participação, com um indicador de arrasto no topo. A lógica de navegação foi alterada para utilizar Navigator.push, e ScaffoldMessenger.of(context).showSnackBar para mensagens de feedback. O método _exibirOpcoesFiltro utiliza FilterChips dentro de um Wrap para permitir aos usuários selecionar tipos de eventos, proporcionando uma interface de filtro amigável e interativa.