{"id":45056348,"url":"https://github.com/pedro-git-projects/puc-mvp-software-architecture-frontend","last_synced_at":"2026-02-19T09:43:16.141Z","repository":{"id":245492620,"uuid":"818381814","full_name":"pedro-git-projects/puc-mvp-software-architecture-frontend","owner":"pedro-git-projects","description":null,"archived":false,"fork":false,"pushed_at":"2024-06-24T21:05:19.000Z","size":244,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-06-25T19:44:07.410Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pedro-git-projects.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-21T18:13:08.000Z","updated_at":"2024-06-24T21:05:22.000Z","dependencies_parsed_at":"2024-06-24T19:07:52.549Z","dependency_job_id":"ab3c2b05-9706-445c-9e09-e061a79d1c5c","html_url":"https://github.com/pedro-git-projects/puc-mvp-software-architecture-frontend","commit_stats":null,"previous_names":["pedro-git-projects/puc-mvp-software-architecture-frontend"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pedro-git-projects/puc-mvp-software-architecture-frontend","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pedro-git-projects%2Fpuc-mvp-software-architecture-frontend","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pedro-git-projects%2Fpuc-mvp-software-architecture-frontend/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pedro-git-projects%2Fpuc-mvp-software-architecture-frontend/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pedro-git-projects%2Fpuc-mvp-software-architecture-frontend/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pedro-git-projects","download_url":"https://codeload.github.com/pedro-git-projects/puc-mvp-software-architecture-frontend/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pedro-git-projects%2Fpuc-mvp-software-architecture-frontend/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29609525,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T06:47:36.664Z","status":"ssl_error","status_checked_at":"2026-02-19T06:45:47.551Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-19T09:43:15.459Z","updated_at":"2026-02-19T09:43:16.132Z","avatar_url":"https://github.com/pedro-git-projects.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Projeto MPV em Arquitetura de Software (Songboxd)\n\nEste é o frontend do MVP em Arquitetura de Software para a pós-graduação em engenharia de software da PUC-RJ desenvolvido com Next.js. A aplicação consome uma API própria de persistência hospedada [aqui](https://github.com/pedro-git-projects/puc-mvp-software-architecture-persistence) e as APIs públicas e gratuitas [Music Brainz](https://musicbrainz.org/doc/MusicBrainz_API) e [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive/API).\n\n## Tabela de Conteúdos\n\n1. [Pré-requisitos](#pré-requisitos)\n2. [Estrutura do Projeto](#estrutura-do-projeto)\n3. [Configuração](#configuração)\n4. [Desenvolvimento](#desenvolvimento)\n5. [Uso](#uso)\n6. [Dockerfile](#dockerfile)\n7. [docker-compose.yml](#docker-composeyml)\n8. [Critérios Avaliativos](#critérios-avaliativos)\n9. [Vídeo](#vídeo)\n\n## Pré-requisitos\n\nAntes de começar, você precisará ter as seguintes ferramentas instaladas em sua máquina:\n\n- [Docker](https://www.docker.com/get-started)\n- [Docker Compose](https://docs.docker.com/compose/install/)\n\n## Estrutura do Projeto\n\n- `Dockerfile`: Define a imagem Docker para a aplicação.\n- `docker-compose.yml`: Define os serviços Docker para desenvolvimento.\n- `docker-compose.prod.yml`: Define os serviços Docker para produção.\n- `.dockerignore`: Arquivos e diretórios a serem ignorados pelo Docker.\n\n## Configuração\n\n1. Clone o repositório:\n\n   ```bash\n   git clone https://github.com/pedro-git-projects/puc-mvp-software-architecture-frontend.git\n   cd puc-mvp-software-architecture-frontend\n   ```\n\n## Desenvolvimento\n\nPara rodar a aplicação em modo de desenvolvimento, use o Docker Compose:\n\n1. Construa a imagem Docker:\n\n   ```bash\n   docker-compose build\n   ```\n\n2. Inicie a aplicação:\n\n   ```bash\n   docker-compose up\n   ```\n\nA aplicação estará disponível em [http://localhost:3000](http://localhost:3000).\n\nLembre-se de ter subido também a outra API que faz parte do projeto, hospedada [aqui](https://github.com/pedro-git-projects/puc-mvp-software-architecture-persistence)\n\n## Uso\n\n1. **Abrir a Aplicação**:\n   - Inicie a aplicação no seu navegador.\n\n2. **Cadastro**:\n   - Clique no botão para se cadastrar.\n   - Preencha o formulário de criação de conta com seus dados.\n   - Clique em \"Criar Conta\".\n\n3. **Login**:\n   - Após criar a conta, faça login usando suas credenciais.\n   \n4. **Barra de Pesquisa**:\n   - Após fazer login, você terá acesso à barra de pesquisa do site.\n   - Digite o nome de um álbum ou artista na barra de pesquisa.\n   - Clique na lupa para realizar a pesquisa.\n   - Você verá uma lista dos álbuns relacionados à sua pesquisa.\n\n5. **Salvando e removendo favoritos**\n    - Clique no coração em outline no card para adicionar um álbum aos favoritos.\n    - Clique em um coração sólido no card para remover um álbum dos favoritos.\n\n6. **Viusualizado a lista de favoritos**\n    - Clique em \"favoritos\" na navbar para navegar até /favoritos \n    - Uma lista com todos os seus items favoritados será renderizada.\n\n7. **Perfil**:\n   - Navegue até a seção de perfil.\n   - Aqui você pode ver o seu email registrado.\n   - Você também pode optar por deletar sua conta.\n\nNo trabalho, é possível buscar álbuns através do nome, artista ou músicas, assim como favoritar álbuns. As informações dos álbuns são buscadas do MusicBrainz, enquanto as capas dos álbuns são obtidas do Cover Art Archive, que depende do ID fornecido pelo MusicBrainz. Autenticação e autorização também foram implementadas através de tokens.\n\n## Dockerfile\n\nO `Dockerfile` utilizado para a construção da imagem Docker:\n\n```Dockerfile\n# Stage 1: Base\nFROM node:22-alpine AS base\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\n\n# Stage 2: Build\nFROM base AS build\nRUN npm run build\n\n# Stage 3: Production\nFROM node:18-alpine AS production\nWORKDIR /app\nCOPY --from=build /app ./\nEXPOSE 3000\nCMD [\"npm\", \"start\"]\n\n# Stage 4: Development\nFROM base AS development\nWORKDIR /app\nEXPOSE 3000\nCMD [\"npm\", \"run\", \"dev\"]\n```\n\n## docker-compose.yml\n\nArquivo `docker-compose.yml` para desenvolvimento:\n\n```yaml\nservices:\n  nextjs-app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - '3000:3000'\n    volumes:\n      - .:/app\n      - /app/node_modules\n    environment:\n      NODE_ENV: development\n    command: [\"npm\", \"run\", \"dev\"]\n```\n\n## Critérios Avaliativos\n\n- \"A interface do usuário deve fazer chamadas a pelo menos 5 rotas diferentes.\"\n\n\n\n| Rota                                                                                                    | Componente            | Serviço                   | Método | Propósito                                                                                               |\n|---------------------------------------------------------------------------------------------------------|-----------------------|---------------------------|--------|---------------------------------------------------------------------------------------------------------|\n| `https://musicbrainz.org/ws/2/release?query=artist:${artist}%20AND%20release:${album}\u0026fmt=json`         | `AlbumSearch`         | MusicBrainz               | GET    | Buscar informações de álbuns de um artista específico. No caso é utilizada para pegar o ID que pode ser usado para buscar a arte do álbum em uma segunda API.                                     |\n| `https://coverartarchive.org/release/${releaseId}/front`                                                | `AlbumCover`          | Cover Art Archive         | GET    | Obter a url da imagem da capa de um lançamento específico.                                                      |\n| `https://musicbrainz.org/ws/2/release?query=artist:${query.artist}%20AND%20release:${query.album}\u0026fmt=json` | `SearchResults`       | MusicBrainz               | GET    | Buscar informações de álbuns de um artista específico com base em uma consulta do usuário. |\n| `http://localhost:8000/token`                                                                           | `AuthProvider`        | Servidor desenvolvido por mim | POST   | Obter o token de autenticação para login do usuário.                                                     |\n| `http://localhost:8000/users/`                                                                          | `SignupForm`         | Servidor desenvolvido por mim | POST    | Criar um novo usuário.                                                             |\n| `http://localhost:8000/users/me`                                                                        | `Profile`             | Servidor desenvolvido por mim | DELETE | Deletar a conta do usuário logado.                                                            |\n| `http://localhost:8000/users/me/`                                                                       | `Profile`             | Servidor desenvolvido por mim | GET    | Obter o perfil do usuário logado.                                                             |\n| `http://localhost:8000/users/me/favorites`                                                              | `Favorites`           | Servidor desenvolvido por mim | GET    | Obter a lista de álbuns favoritos do usuário logado.                                                             |\n| `http://localhost:8000/users/me/favorites`                                                              | `FavoriteAlbumCard`   | Servidor desenvolvido por mim | POST   | Adicionar um álbum à lista de favoritos do usuário logado.                                                             |\n| `http://localhost:8000/users/me/favorites/${release.album_id}`                                          | `FavoriteAlbumCard`   | Servidor desenvolvido por mim | DELETE | Remover um álbum da lista de favoritos do usuário logado.                                                             |\n| `/api/search?album=${album}`                                                                            | `SearchInput`         | Next.js Server Action     | GET    | Buscar informações de álbuns de um artista específico com base no nome do álbum fornecido pelo usuário.                                                             |\n\n\n- \"Uso de uma API externa pública e que ofereça um serviço não pago. Apresentar na documentação a componente principal a API externa que será utilizada, deixando claro informações como: licença de uso (se aplicável), cadastro (se necessário) e rotas que foram utilizados.\"\n\nComo visto na tabela acima foram utilizadas duas APIs externas, **Music Brainz** e **Cover Art Archive**. Aqui estão os componentes:\n\n```tsx\nexport const fetchAlbumData = async (artist: string, album: string) =\u003e {\n  const response = await fetch(\n    `https://musicbrainz.org/ws/2/release?query=artist:${artist}%20AND%20release:${album}\u0026fmt=json`,\n  );\n  const data = await response.json();\n  if (data.releases \u0026\u0026 data.releases.length \u003e 0) {\n    const releaseId = data.releases[0].id;\n    const coverArtUrl = `https://coverartarchive.org/release/${releaseId}/front`;\n    return {\n      name: data.releases[0].title,\n      artist: data.releases[0][\"artist-credit\"][0].name,\n      coverArtUrl,\n    };\n  }\n  return null;\n};\n```\n\n```tsx\nexport const fetchAlbums = async (queries: Query[]): Promise\u003cAlbum[]\u003e =\u003e {\n  const albums = [];\n  for (const query of queries) {\n    const response = await fetch(\n      `https://musicbrainz.org/ws/2/release?query=artist:${query.artist}%20AND%20release:${query.album}\u0026fmt=json`,\n    );\n    const data = await response.json();\n    if (data.releases \u0026\u0026 data.releases.length \u003e 0) {\n      const releaseId = data.releases[0].id;\n      const coverArtUrl = `https://coverartarchive.org/release/${releaseId}/front-250`;\n      albums.push({\n        name: data.releases[0].title,\n        artist: data.releases[0][\"artist-credit\"][0].name,\n        coverArtUrl,\n      });\n    }\n  }\n  return albums;\n};\n```\n\n```tsx\n  const handleSubmit = async (event: React.FormEvent) =\u003e {\n    event.preventDefault();\n    setError(\"\");\n    setSuccess(\"\");\n\n    try {\n      const response = await fetch(\"http://localhost:8000/users/\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({\n          email,\n          password,\n        }),\n      });\n\n      if (!response.ok) {\n        const errorData = await response.json();\n        throw new Error(errorData.detail || \"Falha ao criar conta\");\n      }\n\n      setSuccess(\"Conta criada com sucesso\");\n    } catch (error: any) {\n      console.error(\"Signup failed:\", error);\n      setError(error.message || \"Falha ao criar conta\");\n    }\n  };\n\n```\n\n```tsx\nexport async function deleteUserAccount() {\n  const token = localStorage.getItem(\"token\");\n  const response = await fetch(\"http://localhost:8000/users/me\", {\n    method: \"DELETE\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${token}`,\n    },\n  });\n\n  if (!response.ok) {\n    throw new Error(\"Failed to delete user account\");\n  }\n\n  return response.json();\n}\n```\n\n```tsx\nexport async function fetchUserProfile() {\n  const token = localStorage.getItem(\"token\");\n  const response = await fetch(\"http://localhost:8000/users/me/\", {\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${token}`,\n    },\n  });\n\n  if (!response.ok) {\n    throw new Error(\"Failed to fetch user profile\");\n  }\n\n  return response.json();\n}\n```\n\n```\n  const handleToggleFavorite = async () =\u003e {\n    if (!isAuthenticated) {\n      alert(\"Você precisa estar logado para salvar nos favoritos\");\n      return;\n    }\n\n    try {\n      const token = localStorage.getItem(\"token\");\n      const url = isFavorite\n        ? `http://localhost:8000/users/me/favorites/${release.id}`\n        : \"http://localhost:8000/users/me/favorites\";\n      const method = isFavorite ? \"DELETE\" : \"POST\";\n      const body = !isFavorite\n        ? JSON.stringify({\n            album_id: release.id,\n            album_name: release.title || release.album_name,\n            artist_name: release[\"artist-credit\"]\n              ? release[\"artist-credit\"][0].name\n              : release.artist_name,\n            cover_art_url: coverArtUrl,\n          })\n        : null;\n\n      const response = await fetch(url, {\n        method,\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Authorization: `Bearer ${token}`,\n        },\n        body,\n      });\n\n      if (response.ok) {\n        setIsFavorite(!isFavorite);\n      } else {\n        console.error(\"Error toggling favorite:\", response.statusText);\n      }\n    } catch (error) {\n      console.error(\"Error toggling favorite:\", error);\n    }\n  };\n```\n\n```tsx\nexport default function ResultsPage() {\n  const { isAuthenticated } = useAuth();\n  const [releases, setReleases] = useState\u003cRelease[]\u003e(() =\u003e {\n    try {\n      const item = window.sessionStorage.getItem(\"searchResults\");\n      return item ? JSON.parse(item) : [];\n    } catch (error) {\n      console.error(error);\n      return [];\n    }\n  });\n  const [favorites, setFavorites] = useState\u003cPythonRelease[]\u003e([]);\n  const [isMounted, setIsMounted] = useState\u003cboolean\u003e(false);\n\n  useEffect(() =\u003e {\n    setIsMounted(true);\n\n    if (isAuthenticated) {\n      const fetchFavorites = async () =\u003e {\n        try {\n          const token = localStorage.getItem(\"token\");\n          const response = await fetch(\n            \"http://localhost:8000/users/me/favorites\",\n            {\n              headers: {\n                Authorization: `Bearer ${token}`,\n              },\n            },\n          );\n\n          if (response.ok) {\n            const data = await response.json();\n            setFavorites(data);\n          } else {\n            console.error(\"Error fetching favorites:\", response.statusText);\n          }\n        } catch (error) {\n          console.error(\"Error fetching favorites:\", error);\n        }\n      };\n\n      fetchFavorites();\n    }\n  }, [isAuthenticated]);\n\n  useEffect(() =\u003e {\n    const handleStorageUpdate = () =\u003e {\n      try {\n        const item = window.sessionStorage.getItem(\"searchResults\");\n        setReleases(item ? JSON.parse(item) : []);\n      } catch (error) {\n        console.error(error);\n      }\n    };\n\n    window.addEventListener(\"storageUpdate\", handleStorageUpdate);\n    return () =\u003e {\n      window.removeEventListener(\"storageUpdate\", handleStorageUpdate);\n    };\n  }, []);\n\n  if (!isMounted) {\n    return (\n      \u003cdiv className=\"flex items-center justify-center min-h-screen bg-gray-100 p-4\"\u003e\n        \u003cdiv className=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 lg:gap-8 w-full max-w-6xl\"\u003e\n          {Array.from({ length: 8 }).map((_, index) =\u003e (\n            \u003cSkeletonCard key={index} /\u003e\n          ))}\n        \u003c/div\u003e\n      \u003c/div\u003e\n    );\n  }\n\n  return (\n    \u003cdiv className=\"bg-white py-24 sm:py-32\"\u003e\n      \u003cdiv className=\"mx-auto max-w-7xl px-6 lg:px-8\"\u003e\n        \u003cdiv className=\"mx-auto max-w-2xl lg:mx-0\"\u003e\n          \u003ch2 className=\"text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl\"\u003e\n            Resultados\n          \u003c/h2\u003e\n          \u003cp className=\"mt-2 text-lg leading-8 text-gray-600\"\u003e\n            Clique no coração para favoritar ou remover dos favoritos.\n          \u003c/p\u003e\n        \u003c/div\u003e\n        \u003cdiv className=\"mx-auto mt-10 max-w-2xl border-t border-gray-200 pt-10 sm:mt-16 sm:pt-16 lg:mx-0 lg:max-w-none\"\u003e\n          {releases.length \u003e 0 ? (\n            \u003cdiv className=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-8 lg:gap-8 w-full\"\u003e\n              {releases.map((release: Release) =\u003e (\n                \u003cAlbumCard\n                  key={release.id}\n                  release={release}\n                  favorites={favorites}\n                /\u003e\n              ))}\n            \u003c/div\u003e\n          ) : (\n            \u003ch2 className=\"text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl\"\u003e\n              Nenhum resultado encontrado para o termo pesquisado.\n            \u003c/h2\u003e\n          )}\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n```tsx\nasync function fetchWithRetry(\n  url: string,\n  options: RequestInit,\n  retries: number = 3,\n): Promise\u003cResponse\u003e {\n  for (let i = 0; i \u003c retries; i++) {\n    try {\n      const response = await fetch(url, options);\n      if (response.ok) {\n        return response;\n      }\n      console.warn(`Attempt ${i + 1} failed: ${response.statusText}`);\n    } catch (error) {\n      console.warn(`Attempt ${i + 1} error: ${error}`);\n      if (i === retries - 1) {\n        throw error;\n      }\n    }\n  }\n  throw new Error(\"All retry attempts failed\");\n}\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const album = searchParams.get(\"album\");\n\n  if (!album) {\n    console.error(\"Album name is required\");\n    return NextResponse.json(\n      { error: \"Nome do álbum é obrigatório\" },\n      { status: 400 },\n    );\n  }\n\n  try {\n    const response = await fetchWithRetry(\n      `https://musicbrainz.org/ws/2/release/?query=release:${album}\u0026fmt=json`,\n      {\n        headers: {\n          \"User-Agent\": \"Songboxd/1.0 (pedro.coding.contact@gmail.com)\",\n        },\n      },\n    );\n\n    const data = await response.json();\n\n    if (!response.ok) {\n      console.error(\"Error fetching from MusicBrainz:\", data);\n      return NextResponse.json({ error: data }, { status: response.status });\n    }\n\n    return NextResponse.json(data.releases);\n  } catch (error) {\n    console.error(\"Internal server error:\", error);\n    return NextResponse.json(\n      { error: \"Falha ao buscar dados do MusicBrainz\" },\n      { status: 500 },\n    );\n  }\n}\n```\n\n\n- \"Será permitido a utilização de bibliotecas ou frameworks baseadas em Javascript, como o React, Next, e outras; Será permitido também o uso de bibliotecas de componentes, como o Material UI, Bootstrap, e outras\" \n\nA aplicação é desenvolvida com NextJS 14 e Tailwind.\n\n- \"É fortemente recomendado produzir uma imagem (fluxograma) ilustrando a arquitetura da aplicação desenvolvida.\"\n\n![Diagrama do Projeto](./diagram.png)\n\n\n## Vídeo\n\n A demonstração em vídeo está hospedada em: [YouTube](https://youtu.be/KvMT9ThhVQQ)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpedro-git-projects%2Fpuc-mvp-software-architecture-frontend","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpedro-git-projects%2Fpuc-mvp-software-architecture-frontend","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpedro-git-projects%2Fpuc-mvp-software-architecture-frontend/lists"}