<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

Introdução

Introdução ao Vue.js

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

Componentes e props

Slots, emits e ciclo de vida

Introdução ao Nuxt.js

Criando o projeto Gumball Verse

Primeira requisição com useFetch

Rotas a partir de arquivos

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

Conclusão e próximos passos

</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.


A ideia: a URL é a fonte da verdade

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:


Listagem de lugares

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>

Entendendo o script