Skip to content

Lading Page do projeto Eco-Play: Um incentivo a sustentabilidade e reciclagem através da gamificação!

Notifications You must be signed in to change notification settings

Victor-Lis/EcoPlay-Web

Repository files navigation

EcoPlay | Web

Lading Page do projeto EcoPlay, que tem sido o meu principal projeto nos últimos tempos, justamente por ser meu Trabalho de Conclusão de Curso ou TCC


SPA - Single Page Application

Tive a ideia de para causar uma experiência diferente para os usuários trazer uma aplicação em uma tela só (exceto pela tela de histórico)


Sistema de Achievements

O grande detalhe da aplicação é que, um dos incentivos do projeto é a gamificação, para trazer isso para parte web fiz algo similar a um sistem a de "conquista" nos jogos onde após o usuário passar de uma sessão ele "libera" a possibilidade de scrollar de volta para aquela tela como se fosse uma consquista.

Eco-Play.1.mp4

Funções de navegação

Essas funções são a base para o sistema em formato S.P.A. e além disso para o sistema de achievements


getBottom

Essa é a função base para descobrir o ponto mais inferior de um elemento exibido na tela. Basta passar o #id do elemento:

  src/utils/navigation.ts
  const getBottom = (id: string) => {
    const element = document.getElementById(id);
    return (element?.offsetTop || 0) + (element?.clientHeight || 0);
  };

isMoreDown

Essa função utiliza da função interior para descobrir se a posição atual do "client"(mouse) é mais inferior que o elemento determinado. Mais uma vez basta passar o #id do elemento:

A utilização dessa função nessa aplicação é para liberar o achievement do elemento, ou seja, caso o sistema envie o #id de um elemento para essa função e ela retorne true, então o achievement desse elemento é liberado.

  src/utils/navigation.ts
  export const isMoreDown = (id: string) => {
    return window.scrollY > getDown(id);
  };

getTop

Diferente da função getBottom e como próprio nome sugere, ao invés de pegar a parte debaixo do elemento ela é responsável por pegar a parte de cima do elemento. Seguindo o padrão basta passar o #id:

  src/utils/navigation.ts
  const getTop = (id: string) => {
    const element = document.getElementById(id);
    return (element?.offsetTop || 0);
  };

isMoreUp

Essa função serve para, com base na última função, verificar se o client/usuário está acima do elemento em questão. Novamente basta passar o #id do elemento:

Já essa função tem uma aplicação um pouco diferente da "isMoreDown", nesse caso ela serve para verificar se o usuário estivá imediatamente acima do elemento em questão, ao usar o guia de scroll, se a função retornar true, o usuário será enviado diretamente para o próximo elemento:

  src/utils/navigation.ts
  export const isMoreUp = (id: string) => {
    return window.scrollY < getTop(id);
  };

goTo

Essa é função mais simples em relação a todas acima, ela só navega até o elemento em questão:

  src/utils/navigation.ts
  export const goTo = (id: string) => {
    document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });
  };

Conexão com Firebase para recebimento dos dados

  src/utils/firebaseConfig.ts
import { initializeApp } from "firebase/app";
import { getDatabase, ref } from "firebase/database";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_APIKEY,
  authDomain: process.env.NEXT_PUBLIC_AUTHDOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_DATABASEURL,
  projectId: process.env.NEXT_PUBLIC_PROJECTID,
  storageBucket: process.env.NEXT_PUBLIC_STORAGEBUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_MESSAGINGSENDERID,
  appId: process.env.NEXT_PUBLIC_APPID,
  measurementId: process.env.NEXT_PUBLIC_MEASUREMENTID
}

export const app = initializeApp(firebaseConfig);
  
export const database = getDatabase(app);

export const dataRef = ref(database, "/");
export const tampinhasRef = ref(database, "/tampinhas");
export const totalRef = ref(database, '/total')

Recebendo e repassando os dados

Nesse trecho os dados são pegos através do websocket do firebase que é o onValue.

Ao receber esses dados eles passam uma função chamada "formatCapByDate", que torna os dados mais usáveis

Esse método foi escolhido por mim justamente por ser um websocket e portanto se manter sempre atualizado quanto a base de dados

  src/providers/CapContext.tsx
"use client";
import type { ReactNode } from "react";
import type { CapType } from "@/@types/CapType";

import { ref, onValue } from "firebase/database";
import { createContext, useEffect, useState } from "react";

import { dataRef, tampinhasRef } from '@/utils/firebaseConfig'
import { YearType } from "@/@types/YearType";
import { formatCapByDate } from "@/utils/formatCapByDate";

interface CapContextData {
  total: number;
  tampinhas: CapType[]
  formattedTampinhas: YearType[]
}

interface CapValType {
  data: string,
  hora: string
}

export const CapContext = createContext({} as CapContextData);

export const CapProvider = ({ children }: { children: ReactNode }) => {
  const [total, setTotal] = useState<number>(0)
  const [tampinhas, setTampinhas] = useState<CapType[]>([])
  const [formattedTampinhas, setFormattedTampinhas] = useState<YearType[]>([])

  useEffect(() => {
    const unsubscribe = onValue(tampinhasRef, (snapshot) => {
      const tampinhasVal = snapshot.val()
      if(!tampinhasVal) return
      const keys = Object.keys(tampinhasVal) || []
      const values = Object.values(snapshot.val() as CapValType[]) || []

      setTotal(keys.length)

      const data: CapType[] = []
      keys?.map((key, i) => {
        data.push({
          key,
          dia: values[i].data.slice(0, 2),
          mes: values[i].data.slice(3, 5),
          ano: values[i].data.slice(6, 10),
          hora: values[i].hora,
        })
      })
      
      setTampinhas(data)

      const formattedData = formatCapByDate({caps: data})
      setFormattedTampinhas(formattedData)
    });

    return () => unsubscribe();
  }, []);

  return (
    <CapContext.Provider value={{ total, tampinhas, formattedTampinhas }}>
      {children}
    </CapContext.Provider>
  );
};

formatCapByDate

Essa é a função mais complexo do projeto com facilidade, tentei faze-la da maneira mais completa e otimizada, com certeza é possível melhorar ela, inclusive se você acha que pode fazer uma função melhor otimizada e quis tentar, eu agradeceria qualquer ajuda

  src/utils/formatCapByDate.ts
import type { YearType } from "@/@types/YearType";
import type { CapType } from "@/@types/CapType";

function getTime(hora: string){
  const horas = Number.parseInt(hora.slice(0, 2)); // Pega as horas (0 a 1)
  const minutos = Number.parseInt(hora.slice(3, 5)); // Pega os minutos (3 a 4)
  const segundos = Number.parseInt(hora.slice(6, 8)); // Pega os segundos (6 a 7)

  const time = horas * 3600 + minutos * 60 + segundos;

  return time;
}

function formatCapByDayHour({days}:{days: CapType[]}){
  const dates: string[] = []
  days.map((day) => {
    if (!dates.includes(day.dia)) dates.push(day.dia);
  });
  const formattedDate: any[] = []
  dates.map((date) => {
    const tampinhas = days.filter((day) => day.dia === date)
    tampinhas.sort((a,b) => getTime(b.hora)-getTime(a.hora))
    formattedDate.push({
      dia: date,
      tampinhas,
    })
  }) 

  return formattedDate
}

export function formatCapByDate({ caps }: { caps: CapType[] }): any[] {
  const anos: any[] = [];
  caps.map((cap) => {
    if (!anos.includes(cap.ano)) anos.unshift(cap.ano);
  });

  const result: YearType[] = [];
  formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 9) || []})
  anos.map((ano) => {
    result.push({
      ano,
      meses: [
        {
          mes: "Janeiro",
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 1 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Fevereiro", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 2 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Março", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 3 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Abril",
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 4 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Maio", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 5 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Junho", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 6 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Julho",
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 7 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Agosto", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 8 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Setembro", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 9 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Outubro",
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 10 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Novembro", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 11 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
        {
          mes: "Dezembro", 
          dias: formatCapByDayHour({days: caps.filter((cap) => Number.parseInt(cap.mes) === 12 && cap.ano === ano).sort((a,b) => Number.parseInt(b.dia)-Number.parseInt(a.dia))}),
        },
      ],
    });
  });

  result.sort((a,b) => Number.parseInt(b.ano)-Number.parseInt(a.ano))
  return result;
}

Antes de passar pela função (Formato)

[
    {
        "key": "-O71HBW5tTumRZkuzAPv",
        "dia": "17",
        "mes": "02",
        "ano": "2023",
        "hora": "20:19:37"
    },
    {
        "key": "-O76ynPfX9imPle3eSLL",
        "dia": "18",
        "mes": "09",
        "ano": "2024",
        "hora": "22:52:37"
    },
    {
        "key": "-O770joo-eJ50zsbb6Qf",
        "dia": "18",
        "mes": "09",
        "ano": "2024",
        "hora": "23:05:28"
    },
]

Depois de passar pela função (Formato)

[
    {
        "ano": "2024",
        "meses": [
            {
                "mes": "Janeiro",
                "dias": []
            },
            {
                "mes": "Fevereiro",
                "dias": []
            },
            {
                "mes": "Março",
                "dias": []
            },
            {
                "mes": "Abril",
                "dias": []
            },
            {
                "mes": "Maio",
                "dias": []
            },
            {
                "mes": "Junho",
                "dias": []
            },
            {
                "mes": "Julho",
                "dias": []
            },
            {
                "mes": "Agosto",
                "dias": []
            },
            {
                "mes": "Setembro",
                "dias": [
                    {
                        "dia": "21",
                        "tampinhas": [
                            {
                                "key": "-O7HHpKCxwY4vn0LRQD7",
                                "dia": "21",
                                "mes": "09",
                                "ano": "2024",
                                "hora": "01:56:19"
                            },
                        ]
                    },
                    {
                        "dia": "20",
                        "tampinhas": [
                            {
                                "key": "-O7H6v5CQKl37-e5zLKz",
                                "dia": "20",
                                "mes": "09",
                                "ano": "2024",
                                "hora": "22:08:40"
                            },
                        ]
                    },
                    {
                        "dia": "18",
                        "tampinhas": [
                            {
                                "key": "-O770joo-eJ50zsbb6Qf",
                                "dia": "18",
                                "mes": "09",
                                "ano": "2024",
                                "hora": "23:05:28"
                            },
                        ]
                    }
                ]
            },
            {
                "mes": "Outubro",
                "dias": []
            },
            {
                "mes": "Novembro",
                "dias": []
            },
            {
                "mes": "Dezembro",
                "dias": []
            }
        ]
    },
]

Porque formatar os dados?

Para ser possível alcançar a UI que me interessava no TCC era necessário que os dados mudassem completamente de formato, além de que eu gostaria de evitar ao máximo situações de loading para meus usuário, da forma que os dados ficam ao final, é possível que o usuário escolha "qual parte dos dados ver" sem necessitar de um refresh ou puxar os dados novamente


Tela com a exibição dos dados

Eco-Play.2.mp4

API

Foi criado um rota de API para receber requisições do protótipo fisíco e então salvar os dados, assim tornando mais leve o lado mais "limitado" que seria o hardware

  src/app/api/tampinha/route.ts
import { NextResponse } from "next/server";

import { tampinhasRef } from "@/utils/firebaseConfig";
import { push } from "firebase/database";

const isSmallerThenTen = (n: number) => (n < 10 ? `0${n}` : n);

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const senha = searchParams.get("senha");
  try {
    if (senha == process.env.PASSWORD) {
      const today = new Date();
      await push(tampinhasRef, {
        data: `${isSmallerThenTen(today.getDate())}/${isSmallerThenTen(today.getMonth() + 1)}/${today.getFullYear()}`,
        hora: `${isSmallerThenTen(today.getHours())}:${isSmallerThenTen(today.getMinutes())}:${isSmallerThenTen(today.getSeconds())}`,
      });
      return NextResponse.json({ status: 200 });
    }else {
      throw new Error("Senha incorreta")
    }
  } catch (error: any) {
    return NextResponse.json({ error: error?.message ? error.message : "Tampinha não salva!" }, { status: 400 });
  }
}


Relacionados

Autores

About

Lading Page do projeto Eco-Play: Um incentivo a sustentabilidade e reciclagem através da gamificação!

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages