<aside> <img src="/icons/arrow-northeast_gray.svg" alt="/icons/arrow-northeast_gray.svg" width="40px" />
Índice
</aside>
<aside> <img src="/icons/notion_green.svg" alt="/icons/notion_green.svg" width="40px" />
Conteúdos
Anatomia de um componente .vue
Interpolação e reatividade com ref e computed
Diretivas: v-bind, v-on, v-if, v-for e v-model
Criando o projeto Gumball Verse
Primeira requisição com useFetch
Layout, base do projeto e nuxt.config.ts
Consumindo a API nas listagens
Páginas de detalhes com rotas dinâmicas
Tratando o erro 404 com createError
Títulos das páginas com useHead
Filtros, paginação e busca com debounce
Desafio: título e favicon dinâmicos
</aside>
Chegamos às páginas mais completas do projeto: as listagens de lugares e de personagens. Nelas, vamos juntar praticamente tudo o que vimos até agora: computed, useFetch, props, emits, v-if, v-for e useHead, criando filtros, paginação e busca controlados pela URL.
Em vez de guardar o filtro, a busca e a página atual em variáveis soltas, vamos guardar tudo isso na URL, usando os query params. Por exemplo:
/locations?type=school&search=elmore&page=2
Isso traz várias vantagens:
computed, sempre que a URL muda, o useFetch busca os dados de novo sozinho.Atualize o arquivo app/pages/locations/index.vue:
<script setup>
definePageMeta({
scrollToTop: true,
});
const route = useRoute();
const router = useRouter();
const page = computed(() => Number(route.query.page) || 1);
const type = computed(() => route.query.type);
const search = computed(() => route.query.search);
const { data, error } = await useFetch('https://gumball-api-server.vercel.app/locations', {
query: { page, type, search, limit: 12, sort: 'id' },
});
const totalItems = computed(() => data.value?.meta.totalItems ?? 0);
const types = [
{ value: 'town', label: 'Cidade' },
{ value: 'residence', label: 'Residência' },
{ value: 'school', label: 'Escola' },
{ value: 'school-facility', label: 'Espaço escolar' },
{ value: 'shop', label: 'Loja' },
{ value: 'restaurant', label: 'Restaurante' },
{ value: 'business', label: 'Comércio' },
{ value: 'public-service', label: 'Serviço público' },
{ value: 'leisure', label: 'Lazer' },
{ value: 'transport', label: 'Transporte' },
{ value: 'nature', label: 'Natureza' },
{ value: 'other-realm', label: 'Outra dimensão' },
];
let searchTimeout;
function handleSearch(value) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
router.replace({ query: { type: type.value, search: value || undefined } });
}, 400);
}
useHead({
title: 'Lugares | Gumball Verse',
});
</script>
<template>
<div>
<PageHero
color="yellow"
eyebrow="Mapa de Elmore"
title="Todos os lugares"
lead="Da casa dos Watterson às dimensões mais bizarras: todos os cantos do universo Gumball."
/>
<section class="section section--list">
<div class="container">
<ListToolbar
placeholder="Buscar lugar pelo nome…"
:count="`${totalItems} ${totalItems === 1 ? 'lugar' : 'lugares'}`"
:search="search"
@search="handleSearch"
/>
<FilterChips>
<FilterChip :to="{ query: { search } }" :active="!type">Todos</FilterChip>
<FilterChip
v-for="currentType in types"
:key="currentType.value"
:to="{ query: { search, type: currentType.value } }"
:active="type === currentType.value"
>
{{ currentType.label }}
</FilterChip>
</FilterChips>
<StateMessage v-if="error">
Não foi possível carregar os lugares. Tente novamente mais tarde.
</StateMessage>
<StateMessage v-else-if="!data?.data.length">
Nenhum lugar encontrado. Tente outro termo de busca ou remova os filtros.
</StateMessage>
<LocationsGrid v-else :locations="data.data" />
<Pagination
v-if="data?.meta.totalPages > 1"
:page="page"
:total-pages="data.meta.totalPages"
/>
</div>
</section>
</div>
</template>
useRoute lê a rota atual (incluindo os query params), e o useRouter permite alterar a rota pelo código;route.query. O Number(...) || 1 garante que, se não houver página na URL, começamos na página 1;page, type e search como estão, sem .value. Assim, o useFetch acompanha as mudanças e refaz a requisição sozinho. O limit: 12 define quantos itens vêm por página e o sort: 'id' ordena pelo id;meta da resposta. O ?? define 0 como valor padrão enquanto não há dados;