Construindo um Hub de Atividades Dinâmico com Flutter para Plataformas OpenHarmony

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.

Tags: Flutter OpenHarmony UI/UX StateManagement MobileDevelopment

Publicado em 7-22 06:54