Card DataCommunity Asset
- Product
- Provet Cloud
- Availability
- New Frontend
<script setup lang="ts">
const items = [
{ id: 1, name: 'Fluffy', species: 'Cat', status: 'Active' },
{ id: 2, name: 'Buddy', species: 'Dog', status: 'Active' },
{ id: 3, name: 'Charlie', species: 'Bird', status: 'Inactive' },
{ id: 4, name: 'Max', species: 'Dog', status: 'Active' },
{ id: 5, name: 'Luna', species: 'Cat', status: 'Active' },
]
</script>
<template>
<CardData class="n-margin-be-xxl" heading="Pet List">
<template #header-end>
<BaseButton size="s">Add Pet</BaseButton>
</template>
<template #content>
<DividedStack gap="none">
<div
v-for="item in items"
:key="item.id"
class="n-padding-m full-width"
>
<div
class="n-stack-horizontal n-stack-no-wrap n-gap-xl n-items-start n-justify-between"
>
<div>
<h4>{{ item.name }}</h4>
<p class="n-color-text-weaker">{{ item.species }}</p>
</div>
<div>
<nord-badge
:variant="item.status === 'Active' ? 'success' : 'neutral'"
>
{{ item.status }}
</nord-badge>
</div>
</div>
</div>
</DividedStack>
</template>
<template #footer>
<p class="n-color-text-weaker">Total: {{ items.length }} pets</p>
</template>
</CardData>
</template>
<script setup lang="ts">
import type { PaginationState } from '@tanstack/vue-table'
// Mock data
const allItems = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
name: `Pet ${i + 1}`,
species: ['Cat', 'Dog', 'Bird', 'Rabbit'][i % 4],
status: i % 3 === 0 ? 'Inactive' : 'Active',
description: `This is a description for pet ${i + 1}`,
}))
const searchQuery = ref('')
const paginationState = ref<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
// Filter items based on search
const filteredItems = computed(() => {
if (!searchQuery.value) {
return allItems
}
return allItems.filter(
(item) =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.species.toLowerCase().includes(searchQuery.value.toLowerCase()),
)
})
// Paginate filtered items
const paginatedItems = computed(() => {
const start = paginationState.value.pageIndex * paginationState.value.pageSize
const end = start + paginationState.value.pageSize
return filteredItems.value.slice(start, end)
})
const paginationMeta = computed<QueryPaginationMeta>(() => ({
count: filteredItems.value.length,
page: paginationState.value.pageIndex + 1,
pages: Math.ceil(filteredItems.value.length / paginationState.value.pageSize),
}))
const handlePageChange = (newPagination: PaginationState) => {
paginationState.value = newPagination
}
// Reset to first page when search changes
watch(searchQuery, () => {
paginationState.value.pageIndex = 0
})
</script>
<template>
<CardData class="n-margin-be-xxl">
<template #header>
<nord-stack gap="s">
<h2>Pet Directory (Pagination)</h2>
<p class="n-color-text-weaker n-font-weight n-text-size-s">
This demo shows how to apply pagination to a card data component.
</p>
</nord-stack>
</template>
<template #before-content>
<div class="n-padding-m">
<InputSearch
v-model="searchQuery"
placeholder="Search pets..."
label="Search pets..."
hide-label
size="s"
/>
</div>
</template>
<template #content>
<DividedStack gap="none">
<div
v-for="item in paginatedItems"
:key="item.id"
class="n-padding-m full-width"
>
<div
class="n-stack-horizontal n-stack-no-wrap n-gap-xl n-items-start n-justify-between"
>
<div>
<h4>{{ item.name }}</h4>
<p class="n-color-text-weaker">{{ item.species }}</p>
<p class="n-color-text-weak n-margin-bs-s">
{{ item.description }}
</p>
</div>
<div>
<nord-badge
:variant="item.status === 'Active' ? 'success' : 'neutral'"
>
{{ item.status }}
</nord-badge>
</div>
</div>
</div>
<div
v-if="!paginatedItems?.length"
class="n-text-align-center n-padding-l"
>
<p class="n-color-text-weaker">No pets found matching your search.</p>
</div>
</DividedStack>
</template>
<template #footer>
<BasePagination
v-model:page-index="paginationState.pageIndex"
v-model:page-size="paginationState.pageSize"
:pagination-meta
@page-change="handlePageChange"
/>
</template>
</CardData>
</template>
<script setup lang="ts">
const route = useRoute()
// Mock data
const allItems = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
name: `Pet ${i + 1}`,
species: ['Cat', 'Dog', 'Bird', 'Rabbit'][i % 4],
status: i % 3 === 0 ? 'Inactive' : 'Active',
description: `This is a description for pet ${i + 1}`,
}))
const searchQuery = computed({
get: () => (route.query.search as string) || '',
set: async (value: string) => {
await navigateTo({
query: {
...route.query,
search: value || undefined,
page: undefined, // Reset page when search changes
},
})
},
})
// Filter items based on search
const filteredItems = computed(() => {
if (!searchQuery.value) {
return allItems
}
return allItems.filter(
(item) =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.species.toLowerCase().includes(searchQuery.value.toLowerCase()),
)
})
// Paginate filtered items
const currentPage = computed(() => toNumber(route.query.page) || 1)
const pageSize = computed(() => toNumber(route.query.pageSize) || 10)
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredItems.value.slice(start, end)
})
const paginationMeta = computed<QueryPaginationMeta>(() => ({
count: filteredItems.value.length,
page: currentPage.value,
pages: Math.ceil(filteredItems.value.length / pageSize.value),
}))
</script>
<template>
<CardData class="n-margin-be-xxl">
<template #header>
<nord-stack gap="s">
<h2>Pet Directory (URL Sync)</h2>
<p class="n-color-text-weaker n-font-weight n-text-size-s">
This demo shows URL synchronization. Search and pagination state is
reflected in the URL.
</p>
</nord-stack>
</template>
<template #before-content>
<div class="n-padding-m">
<InputSearch
:model-value="searchQuery"
placeholder="Search pets..."
label="Search pets..."
hide-label
size="s"
@update:model-value="searchQuery = $event"
/>
</div>
</template>
<template #content>
<DividedStack gap="none">
<div
v-for="item in paginatedItems"
:key="item.id"
class="n-padding-m full-width"
>
<div
class="n-stack-horizontal n-stack-no-wrap n-gap-xl n-items-start n-justify-between"
>
<div>
<h4>{{ item.name }}</h4>
<p class="n-color-text-weaker">{{ item.species }}</p>
<p class="n-color-text-weak n-margin-bs-s">
{{ item.description }}
</p>
</div>
<div>
<nord-badge
:variant="item.status === 'Active' ? 'success' : 'neutral'"
>
{{ item.status }}
</nord-badge>
</div>
</div>
</div>
<div
v-if="!paginatedItems?.length"
class="n-text-align-center n-padding-l"
>
<p class="n-color-text-weaker">No pets found matching your search.</p>
</div>
</DividedStack>
</template>
<template #footer>
<BasePagination
v-if="Boolean(paginationMeta?.count)"
:pagination-meta
use-url-sync
/>
</template>
</CardData>
</template>
<script setup lang="ts">
const route = useRoute()
// Mock data
const allItems = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
name: `Pet ${i + 1}`,
species: ['Cat', 'Dog', 'Bird', 'Rabbit'][i % 4],
status: i % 3 === 0 ? 'Inactive' : 'Active',
description: `This is a description for pet ${i + 1}`,
}))
const searchQuery = computed({
get: () => (route.query['list-search'] as string) || '',
set: async (value: string) => {
await navigateTo({
query: {
...route.query,
'list-search': value || undefined,
'list-page': undefined, // Reset page when search changes
},
})
},
})
// Filter items based on search
const filteredItems = computed(() => {
if (!searchQuery.value) {
return allItems
}
return allItems.filter(
(item) =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.species.toLowerCase().includes(searchQuery.value.toLowerCase()),
)
})
// Paginate filtered items
const currentPage = computed(() => toNumber(route.query['list-page']) || 1)
const pageSize = computed(() => toNumber(route.query['list-pageSize']) || 10)
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredItems.value.slice(start, end)
})
const paginationMeta = computed(
(): QueryPaginationMeta => ({
count: filteredItems.value.length,
page: currentPage.value,
pages: Math.ceil(filteredItems.value.length / pageSize.value),
}),
)
</script>
<template>
<CardData class="n-margin-be-xxl">
<template #header>
<nord-stack gap="s">
<h2>Pet Directory (URL Sync + Filter ID)</h2>
<p class="n-color-text-weaker n-font-weight n-text-size-s">
This demo shows URL synchronization with filter ID prefix. All URL
parameters are prefixed with "list-" due to the filterId prop.
</p>
</nord-stack>
</template>
<template #before-content>
<div class="n-padding-m">
<InputSearch
:model-value="searchQuery"
placeholder="Search pets..."
label="Search pets..."
hide-label
size="s"
@update:model-value="searchQuery = $event"
/>
</div>
</template>
<template #content>
<DividedStack gap="none">
<div
v-for="item in paginatedItems"
:key="item.id"
class="n-padding-m full-width"
>
<div
class="n-stack-horizontal n-stack-no-wrap n-gap-xl n-items-start n-justify-between"
>
<div>
<h4>{{ item.name }}</h4>
<p class="n-color-text-weaker">{{ item.species }}</p>
<p class="n-color-text-weak n-margin-bs-s">
{{ item.description }}
</p>
</div>
<div>
<nord-badge
:variant="item.status === 'Active' ? 'success' : 'neutral'"
>
{{ item.status }}
</nord-badge>
</div>
</div>
</div>
<div
v-if="!paginatedItems?.length"
class="n-text-align-center n-padding-l"
>
<p class="n-color-text-weaker">No pets found matching your search.</p>
</div>
</DividedStack>
</template>
<template #footer>
<BasePagination
v-if="Boolean(paginationMeta?.count)"
:pagination-meta
use-url-sync
filter-id="list"
/>
</template>
</CardData>
</template>
<script setup lang="ts">
import type { PaginationState } from '@tanstack/vue-table'
// Mock data with many items to demonstrate scrolling
const allItems = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
name: `Pet ${i + 1}`,
species: ['Cat', 'Dog', 'Bird', 'Rabbit', 'Fish', 'Hamster'][i % 6],
status: i % 4 === 0 ? 'Inactive' : 'Active',
description: `This is pet ${i + 1} with some details about their care and status.`,
lastVisit: new Date(
Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000,
).toLocaleDateString(),
}))
const searchQuery = ref('')
const paginationState = ref<PaginationState>({
pageIndex: 0,
pageSize: 15,
})
// Filter items based on search
const filteredItems = computed(() => {
if (!searchQuery.value) {
return allItems
}
return allItems.filter(
(item) =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.species.toLowerCase().includes(searchQuery.value.toLowerCase()),
)
})
// Paginate filtered items
const paginatedItems = computed(() => {
const start = paginationState.value.pageIndex * paginationState.value.pageSize
const end = start + paginationState.value.pageSize
return filteredItems.value.slice(start, end)
})
const paginationMeta = computed(
(): QueryPaginationMeta => ({
count: filteredItems.value.length,
page: paginationState.value.pageIndex + 1,
pages: Math.ceil(
filteredItems.value.length / paginationState.value.pageSize,
),
}),
)
const handlePageChange = (newPagination: PaginationState) => {
paginationState.value = newPagination
}
// Reset to first page when search changes
watch(searchQuery, () => {
paginationState.value.pageIndex = 0
})
</script>
<template>
<CardData sticky-before-content class="sticky-card-demo n-margin-be-xxl">
<template #header>
<nord-stack gap="s">
<h2>Pet Directory (Sticky Scrolling)</h2>
<p class="n-color-text-weaker n-font-weight n-text-size-s">
This card has a fixed height with sticky scrolling. The header and
footer remain visible while content scrolls.
</p>
</nord-stack>
</template>
<template #before-content>
<div class="n-padding-m">
<InputSearch
v-model="searchQuery"
placeholder="Search pets..."
label="Search pets..."
hide-label
size="s"
/>
</div>
</template>
<template #content>
<DividedStack gap="none">
<div
v-for="item in paginatedItems"
:key="item.id"
class="n-padding-m full-width"
>
<div
class="n-stack-horizontal n-stack-no-wrap n-gap-xl n-items-start n-justify-between"
>
<div>
<h4>{{ item.name }}</h4>
<p class="n-color-text-weaker">{{ item.species }}</p>
<p class="n-color-text-weak n-margin-bs-s">
{{ item.description }}
</p>
<p class="n-color-text-weaker n-text-size-s">
Last visit: {{ item.lastVisit }}
</p>
</div>
<div>
<nord-badge
:variant="item.status === 'Active' ? 'success' : 'neutral'"
>
{{ item.status }}
</nord-badge>
</div>
</div>
</div>
<div
v-if="!paginatedItems?.length"
class="n-text-align-center n-padding-l"
>
<p class="n-color-text-weaker">No pets found matching your search.</p>
</div>
</DividedStack>
</template>
<template #footer>
<BasePagination
v-model:page-index="paginationState.pageIndex"
v-model:page-size="paginationState.pageSize"
:pagination-meta
:page-size-options="[10, 15, 25]"
@page-change="handlePageChange"
/>
</template>
</CardData>
</template>
Integration
This community asset is currently only available to use in the New Frontend for Provet Cloud.
Troubleshooting
If you experience any issues while using this community asset, please ask for support in the #vet-frontend Slack channel.