BambitTestTask/pages/index.vue
2025-11-17 18:40:04 +03:00

278 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="index-page">
<div class="index-page__search">
<UInput
:model-value="search"
size="xl"
class="w-full"
placeholder="Поиск..."
:loading="postsIsLoading || usersIsLoading"
@update:model-value="updateSearch"
/>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr
v-for="headerGroup in table?.getHeaderGroups()"
:key="headerGroup?.id"
>
<th
v-for="header in headerGroup?.headers"
:key="header?.id"
:colSpan="header?.colSpan"
:style="header.column.columnDef.meta?.style"
@click="header?.column?.getToggleSortingHandler()?.($event)"
>
<FlexRender
v-if="!header?.isPlaceholder"
:render="header?.column?.columnDef?.header"
:props="header?.getContext()"
/>
{{ { asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in table?.getRowModel()?.rows"
:key="row?.id"
>
<td
v-for="cell in row?.getVisibleCells()"
:key="cell?.id"
:style="cell.column.columnDef.meta?.style"
>
<FlexRender
:render="cell?.column?.columnDef?.cell"
:props="cell?.getContext()"
/>
</td>
</tr>
</tbody>
</table>
</div>
<UModal v-model:open="open" :title="selectedUser?.email">
<template #body>
<div>
<div>Имя: {{ selectedUser?.name }}</div>
<div>Логин: {{ selectedUser?.username }}</div>
<div>Электронная почта: {{ selectedUser?.email }}</div>
<div>Телефон: {{ selectedUser?.phone }}</div>
<a :href="`https://${selectedUser?.website}`" target="_blank">Веб-сайт: <span class="title-cell">{{ selectedUser?.website }}</span></a>
<div>Название компании: {{ selectedUser?.company?.name }}</div>
<div>Адрес: {{ `${selectedUser?.address?.street} ${selectedUser?.address?.suite} ${selectedUser?.address?.city}` }}</div>
</div>
</template>
</UModal>
</div>
</template>
<script setup lang="ts">
import { UTooltip } from '#components'
import {
createColumnHelper,
FlexRender,
getCoreRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { useDebounceFn, useMediaQuery } from '@vueuse/core'
import { useGetPosts, useGetUsers } from '~/api/queries'
const isMobile = useMediaQuery('(max-width: 1280px)')
const open = ref(false)
const selectedUser = ref<User | null>(null)
const search = ref('')
const updateSearch = useDebounceFn((value: string) => {
search.value = value
}, 500)
const { data: posts, isLoading: postsIsLoading } = useGetPosts(search)
const { data: users, isLoading: usersIsLoading } = useGetUsers()
const tableData = computed(() =>
posts?.value?.map(post => ({
userId: post?.userId,
userEmail: users?.value?.find(user => user?.id === post?.userId)?.email,
id: post?.id,
title: post?.title,
body: post?.body,
})))
const columnHelper = createColumnHelper<Posts>()
const columns = [
columnHelper.accessor('id', {
header: 'ID',
footer: props => props?.column?.id,
meta: { style: 'text-align: center' },
}),
columnHelper.accessor('title', {
header: 'Заголовок',
cell: info => isMobile.value
? info.getValue()
: h(UTooltip, { text: info.getValue(),
}, {
default: () => h('span', { class: 'ellipsis' }, truncate(info.getValue()),
),
}),
}),
columnHelper.accessor('userEmail', {
header: 'Автор',
cell: info => h(
'span',
{ class: 'title-cell', onClick: () => openPost(info.row.original) },
info.getValue(),
),
meta: { style: 'text-align: center' },
}),
columnHelper.accessor('body', {
header: 'Контент',
cell: info => isMobile.value
? info.getValue()
: h(UTooltip, {
text: info.getValue(),
}, {
default: () => h('span', { class: 'ellipsis' }, truncate(info.getValue())),
}),
}),
]
const table = useVueTable({
get data() {
return tableData
},
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
function openPost(post: Post) {
selectedUser.value = users?.value?.find(user => user?.id === post?.userId)
open.value = true
}
function truncate(text: string, max = 15) {
return text.length > max ? `${text.slice(0, max)}` : text
}
</script>
<style lang="scss" scoped>
@use '~/assets/scss/utils' as *;
.index-page {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 600px;
margin-inline: auto;
padding-bottom: 40px;
position: relative;
&__search {
position: fixed;
top: 48px;
left: 50%;
transform: translateX(-50%);
max-width: 600px;
width: 100%;
z-index: 1000;
background: var(--ui-bg-base);
@include mobile {
padding-inline: 10px;
}
}
}
/* -------------------- TABLE WRAPPER -------------------- */
.table-wrapper {
margin-top: calc(40px + 10px);
border-radius: 14px;
overflow: auto;
max-height: 600px;
scrollbar-width: thin;
}
table {
min-width: 100%;
border-collapse: separate;
border-spacing: 0;
background: var(--ui-bg-elevated);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
white-space: nowrap;
}
/* -------------------- HEADER -------------------- */
thead th {
background: var(--ui-bg-accented);
color: var(--ui-text);
padding: 12px 14px;
border-bottom: 1px solid var(--ui-border);
user-select: none;
transition: background 0.15s ease;
}
thead th:hover {
background: var(--ui-bg-accented-hover);
}
/* -------------------- BODY -------------------- */
tbody tr {
height: 50px;
transition: background 0.2s ease;
}
tbody tr:nth-child(odd) {
background: var(--ui-bg-subtle);
}
tbody tr:hover {
background: var(--ui-bg-accented);
}
tbody td {
padding: 12px 14px;
font-size: 14px;
border-bottom: 1px solid var(--ui-border-subtle);
}
tbody tr:last-child td {
border-bottom: none;
}
/* -------------------- SORT ICON -------------------- */
th {
position: relative;
}
th .sort-icon {
margin-left: 6px;
opacity: 0.7;
font-size: 12px;
}
/* -------------------- CLICKABLE USER EMAIL CELL -------------------- */
.title-cell {
cursor: pointer;
text-decoration: underline;
color: var(--ui-primary);
font-weight: 500;
}
.title-cell:hover {
color: var(--ui-primary-hover);
}
</style>