Compare commits
No commits in common. "e04784d407614d5e88b1b4c590fafae3fbf8818c" and "98af218a353ffb0538b3a2ff8838b52c5d1ec351" have entirely different histories.
e04784d407
...
98af218a35
@ -1 +0,0 @@
|
|||||||
VITE_MY_API_BASE_URL=https://jsonplaceholder.typicode.com
|
|
||||||
52
.github/workflows/gh-pages.yml
vendored
52
.github/workflows/gh-pages.yml
vendored
@ -1,52 +0,0 @@
|
|||||||
name: Deploy to GitHub Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: pages
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: 'yarn'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Generate static site
|
|
||||||
run: yarn generate
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: ./.output/public
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
|
|
||||||
|
|
||||||
41
.github/workflows/nuxtjs.yml
vendored
41
.github/workflows/nuxtjs.yml
vendored
@ -1,41 +0,0 @@
|
|||||||
# https://github.com/actions/deploy-pages#usage
|
|
||||||
name: Deploy to GitHub Pages
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: corepack enable
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
# Pick your own package manager and build script
|
|
||||||
- run: npm install
|
|
||||||
- run: npx nuxt build --preset github_pages
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: ./.output/public
|
|
||||||
# Deployment job
|
|
||||||
deploy:
|
|
||||||
# Add a dependency to the build job
|
|
||||||
needs: build
|
|
||||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
|
||||||
permissions:
|
|
||||||
pages: write # to deploy to Pages
|
|
||||||
id-token: write # to verify the deployment originates from an appropriate source
|
|
||||||
# Deploy to the github_pages environment
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
# Specify runner + deployment step
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
41
.gitignore
vendored
41
.gitignore
vendored
@ -1,41 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules
|
|
||||||
.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# nuxt
|
|
||||||
*.log*
|
|
||||||
.nuxt
|
|
||||||
.nitro
|
|
||||||
.cache
|
|
||||||
.output
|
|
||||||
.env
|
|
||||||
dist
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# turbo
|
|
||||||
.turbo
|
|
||||||
out
|
|
||||||
|
|
||||||
.idea
|
|
||||||
.npmrc
|
|
||||||
@ -4,7 +4,4 @@ yarn
|
|||||||
|
|
||||||
yarn dev
|
yarn dev
|
||||||
|
|
||||||
<img width="846" height="1181" alt="image" src="https://github.com/user-attachments/assets/85f08dce-d4fc-4b7f-bbda-6dc3420df7b7" />
|
https://wp.koptilnya.xyz/wp-admin/edit.php?post_type=product
|
||||||
<img width="846" height="1157" alt="image" src="https://github.com/user-attachments/assets/cdbc5bac-7eec-4d23-9494-cc58253b84e0" />
|
|
||||||
<img width="864" height="1178" alt="image" src="https://github.com/user-attachments/assets/005ddcc0-a5d9-45d0-ac95-95f5e87b89f5" />
|
|
||||||
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { httpClient } from '../httpClient'
|
|
||||||
|
|
||||||
export const getPosts = (search: string) =>
|
|
||||||
httpClient<Posts>('/api/posts', {
|
|
||||||
method: 'GET',
|
|
||||||
query: { search },
|
|
||||||
})
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import { httpClient } from '../httpClient'
|
|
||||||
|
|
||||||
export const getUsers = () =>
|
|
||||||
httpClient<Users>('/api/users')
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './getPosts'
|
|
||||||
export * from './getUsers'
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { useToast } from '#imports'
|
|
||||||
|
|
||||||
const showToast = (description: string) => {
|
|
||||||
if (!import.meta.client)
|
|
||||||
return
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
toast.add({
|
|
||||||
title: 'Uh oh! Something went wrong.',
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const httpClient = $fetch.create({
|
|
||||||
onResponseError({ request, response }) {
|
|
||||||
const message = response?._data?.statusMessage ?? 'Something went wrong'
|
|
||||||
|
|
||||||
showToast(message)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './useGetPosts'
|
|
||||||
export * from './useGetUsers'
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
|
||||||
import { getPosts } from '~/api/endpoints'
|
|
||||||
|
|
||||||
export const useGetPosts = (search: MaybeRef<string>) => {
|
|
||||||
return useQuery<Posts>({
|
|
||||||
queryKey: ['get-posts', search],
|
|
||||||
queryFn: () => getPosts(unref(search)),
|
|
||||||
staleTime: Infinity,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
|
||||||
import { getUsers } from '~/api/endpoints'
|
|
||||||
|
|
||||||
export const useGetUsers = () => {
|
|
||||||
return useQuery<Users>({
|
|
||||||
queryKey: ['get-users'],
|
|
||||||
queryFn: () => getUsers(),
|
|
||||||
staleTime: Infinity,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
11
app.vue
11
app.vue
@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UApp>
|
|
||||||
<NuxtLayout>
|
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</UApp>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use '@/assets/scss/main' as *;
|
|
||||||
</style>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@import "@nuxt/ui";
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
* {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(128, 128, 128, 0.5) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
@mixin mobile {
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,196 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="tableContainerRef"
|
|
||||||
class="table-wrapper"
|
|
||||||
:style="{ height, width }"
|
|
||||||
>
|
|
||||||
<div :style="{ height: `${totalSize}px` }">
|
|
||||||
<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,
|
|
||||||
{ width: `${header?.getSize()}px` },
|
|
||||||
]"
|
|
||||||
@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 :style="{ height: `${totalSize}px` }">
|
|
||||||
<tr
|
|
||||||
v-for="vRow in virtualRows"
|
|
||||||
:ref="measureElement"
|
|
||||||
:key="rows[vRow?.index]?.id"
|
|
||||||
:data-index=" vRow?.index"
|
|
||||||
:style="{ transform: `translateY(${vRow?.start}px)` }"
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
v-for="cell in rows[vRow?.index]?.getVisibleCells()"
|
|
||||||
:key="cell?.id"
|
|
||||||
:style="[
|
|
||||||
cell?.column?.columnDef.meta?.style,
|
|
||||||
{ width: `${cell?.column?.getSize()}px` },
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<FlexRender
|
|
||||||
:render="cell?.column?.columnDef?.cell"
|
|
||||||
:props="cell?.getContext()"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { ColumnDef } from '@tanstack/vue-table'
|
|
||||||
import { FlexRender, getCoreRowModel, getSortedRowModel, useVueTable } from '@tanstack/vue-table'
|
|
||||||
import { useVirtualizer } from '@tanstack/vue-virtual'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
tableData: any[]
|
|
||||||
columns: ColumnDef<any>[]
|
|
||||||
height: string
|
|
||||||
width: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const table = useVueTable({
|
|
||||||
get data() {
|
|
||||||
return props?.tableData
|
|
||||||
},
|
|
||||||
columns: props?.columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const rows = computed(() => table?.getRowModel()?.rows)
|
|
||||||
const tableContainerRef = ref<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
const rowVirtualizerOptions = computed(() => {
|
|
||||||
return {
|
|
||||||
count: rows?.value?.length,
|
|
||||||
estimateSize: () => 50,
|
|
||||||
getScrollElement: () => tableContainerRef?.value,
|
|
||||||
overscan: 5,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)
|
|
||||||
|
|
||||||
const virtualRows = computed(() => rowVirtualizer?.value?.getVirtualItems())
|
|
||||||
const totalSize = computed(() => rowVirtualizer?.value?.getTotalSize())
|
|
||||||
|
|
||||||
function measureElement(el?: Element) {
|
|
||||||
if (!el) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rowVirtualizer?.value?.measureElement(el)
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
/* -------------------- TABLE WRAPPER -------------------- */
|
|
||||||
.table-wrapper {
|
|
||||||
position: relative;
|
|
||||||
margin-top: calc(40px + 10px);
|
|
||||||
border-radius: 14px;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
height: 500px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
display: grid;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
background: var(--ui-bg-accented);
|
|
||||||
}
|
|
||||||
|
|
||||||
thead tr {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------- BODY -------------------- */
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
display: grid;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
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: 10px 12px;
|
|
||||||
border-bottom: 1px solid var(--ui-border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="wrapper" class="table-short-text" v-bind="attrs">
|
|
||||||
<UTooltip v-if="isMobile" :text="text" :open="open">
|
|
||||||
{{ truncate(text, maxLength) }}
|
|
||||||
<Icon
|
|
||||||
v-if="!hideIcon"
|
|
||||||
name="heroicons:information-circle"
|
|
||||||
@click="open = !open"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
|
|
||||||
<UTooltip v-else :text="text">
|
|
||||||
<span>{{ truncate(text, maxLength) }}</span>
|
|
||||||
</UTooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onClickOutside, useMediaQuery } from '@vueuse/core'
|
|
||||||
|
|
||||||
defineProps<{ text: string, maxLength: number, hideIcon: boolean }>()
|
|
||||||
const attrs = useAttrs()
|
|
||||||
|
|
||||||
const wrapper = ref<HTMLElement | null>(null)
|
|
||||||
const open = ref(false)
|
|
||||||
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1280px)')
|
|
||||||
|
|
||||||
onClickOutside(wrapper, () => {
|
|
||||||
open.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
function truncate(text: string, max = 15) {
|
|
||||||
return text?.length > max ? `${text?.slice(0, max)}…` : text
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{ selectedUser: User | null }>()
|
|
||||||
const open = defineModel('modelValue', { type: Boolean, default: false })
|
|
||||||
</script>
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import antfu from '@antfu/eslint-config'
|
|
||||||
|
|
||||||
export default antfu({
|
|
||||||
rules: {
|
|
||||||
'antfu/top-level-function': 'off',
|
|
||||||
},
|
|
||||||
formatters: {
|
|
||||||
css: true,
|
|
||||||
},
|
|
||||||
overrides: {
|
|
||||||
vue: {
|
|
||||||
'vue/block-order': ['error', {
|
|
||||||
order: ['template', 'script', 'style'],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="layout">
|
|
||||||
<header class="header bg-default">
|
|
||||||
<div class="header__container">
|
|
||||||
<h1>BambitTestTask</h1>
|
|
||||||
|
|
||||||
<UColorModeSwitch />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main">
|
|
||||||
<UContainer class="container">
|
|
||||||
<slot />
|
|
||||||
</UContainer>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&__container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin-inline: auto;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
padding-inline: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__container h1 {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__container > *:last-child {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
--ui-container: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
margin-top: calc(32px + 20px);
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
padding-inline: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
||||||
import { createResolver } from '@nuxt/kit'
|
|
||||||
|
|
||||||
const { resolve } = createResolver(import.meta.url)
|
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
|
||||||
ssr: false,
|
|
||||||
compatibilityDate: '2025-05-15',
|
|
||||||
devtools: { enabled: true },
|
|
||||||
modules: [
|
|
||||||
'@nuxt/ui',
|
|
||||||
'@nuxt/image',
|
|
||||||
'@nuxt/icon',
|
|
||||||
'@nuxt/fonts',
|
|
||||||
'@nuxtjs/i18n',
|
|
||||||
[
|
|
||||||
'@vee-validate/nuxt',
|
|
||||||
{
|
|
||||||
autoImports: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
css: ['~/assets/css/main.css', '~/assets/scss/main.scss'],
|
|
||||||
i18n: {
|
|
||||||
locales: [
|
|
||||||
{ code: 'en', name: 'English', file: 'en.json' },
|
|
||||||
{ code: 'ru', name: 'Русский', file: 'ru.json' },
|
|
||||||
],
|
|
||||||
defaultLocale: 'ru',
|
|
||||||
strategy: 'prefix_except_default',
|
|
||||||
detectBrowserLanguage: false,
|
|
||||||
},
|
|
||||||
nitro: {
|
|
||||||
preset: 'github-pages',
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
baseURL: '/BambitTestTask',
|
|
||||||
head: {
|
|
||||||
meta: [
|
|
||||||
{
|
|
||||||
name: 'viewport',
|
|
||||||
content: 'width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, user-scalable=no',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
vite: {
|
|
||||||
css: {
|
|
||||||
preprocessorOptions: {
|
|
||||||
scss: {
|
|
||||||
additionalData: `@use "${resolve('./assets/scss/mixins')}" as *;`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
16419
package-lock.json
generated
16419
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "nuxt-app",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "nuxt build",
|
|
||||||
"dev": "nuxt dev",
|
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
|
||||||
"postinstall": "nuxt prepare",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"lint:fix": "eslint . --fix",
|
|
||||||
"typegen": "node .typegen"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@nuxt/content": "^3.7.1",
|
|
||||||
"@nuxt/fonts": "0.11.4",
|
|
||||||
"@nuxt/icon": "1.15.0",
|
|
||||||
"@nuxt/image": "1.10.0",
|
|
||||||
"@nuxt/ui": "^4.0.1",
|
|
||||||
"@nuxtjs/i18n": "^10.0.4",
|
|
||||||
"@tanstack/vue-query": "^5.75.5",
|
|
||||||
"@tanstack/vue-query-devtools": "^5.87.1",
|
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
|
||||||
"@tanstack/vue-virtual": "^3.13.12",
|
|
||||||
"@vee-validate/nuxt": "^4.15.1",
|
|
||||||
"@vueuse/core": "^13.1.0",
|
|
||||||
"axios": "^1.12.2",
|
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"decimal.js": "^10.5.0",
|
|
||||||
"maska": "^3.2.0",
|
|
||||||
"nuxt": "^4.1.3",
|
|
||||||
"typescript": "^5.6.3",
|
|
||||||
"vue": "^3.5.17",
|
|
||||||
"vue-router": "^4.5.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@antfu/eslint-config": "^4.13.2",
|
|
||||||
"@nuxt/devtools": "latest",
|
|
||||||
"eslint": "^9.27.0",
|
|
||||||
"eslint-plugin-format": "^1.0.1",
|
|
||||||
"sass": "^1.71.0"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
||||||
}
|
|
||||||
140
pages/index.vue
140
pages/index.vue
@ -1,140 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
|
||||||
<UiTable :table-data="tableData" :columns="columns" height="600px" width="600px" />
|
|
||||||
|
|
||||||
<UserModal v-model:model-value="open" :selected-user="selectedUser" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { UiTableShortText } from '#components'
|
|
||||||
import {
|
|
||||||
createColumnHelper,
|
|
||||||
} from '@tanstack/vue-table'
|
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
|
||||||
import { useGetPosts, useGetUsers } from '~/api/queries'
|
|
||||||
import UserModal from '~/components/modals/UserModal.vue'
|
|
||||||
import UiTable from '~/components/UiTable.vue'
|
|
||||||
|
|
||||||
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: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
size: 50,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('title', {
|
|
||||||
header: 'Заголовок',
|
|
||||||
cell: info => h(UiTableShortText, {
|
|
||||||
text: info.getValue(),
|
|
||||||
maxLength: 10,
|
|
||||||
}),
|
|
||||||
size: 150,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('userEmail', {
|
|
||||||
header: 'Автор',
|
|
||||||
cell: info =>
|
|
||||||
h(UiTableShortText, {
|
|
||||||
text: info.getValue(),
|
|
||||||
hideIcon: true,
|
|
||||||
class: 'title-cell',
|
|
||||||
onClick: () => openPost(info.row.original),
|
|
||||||
}),
|
|
||||||
meta: {
|
|
||||||
style: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
size: 170,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('body', {
|
|
||||||
header: 'Контент',
|
|
||||||
cell: info => h(UiTableShortText, {
|
|
||||||
text: info.getValue(),
|
|
||||||
maxLength: 18,
|
|
||||||
}),
|
|
||||||
size: 230,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
function openPost(post: Post) {
|
|
||||||
selectedUser.value = users?.value?.find(user => user?.id === post?.userId)
|
|
||||||
open.value = true
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//userEmail
|
|
||||||
.title-cell {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
color: var(--ui-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-cell:hover {
|
|
||||||
color: var(--ui-primary-hover);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import type {
|
|
||||||
DehydratedState,
|
|
||||||
VueQueryPluginOptions,
|
|
||||||
} from '@tanstack/vue-query'
|
|
||||||
import { defineNuxtPlugin, useState } from '#imports'
|
|
||||||
import {
|
|
||||||
dehydrate,
|
|
||||||
hydrate,
|
|
||||||
QueryClient,
|
|
||||||
VueQueryPlugin,
|
|
||||||
} from '@tanstack/vue-query'
|
|
||||||
|
|
||||||
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxt) => {
|
|
||||||
const vueQueryState = useState<DehydratedState | null>('vue-query')
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
retry: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const options: VueQueryPluginOptions = { queryClient }
|
|
||||||
|
|
||||||
nuxt.vueApp.use(VueQueryPlugin, options)
|
|
||||||
|
|
||||||
if (import.meta.server) {
|
|
||||||
nuxt.hooks.hook('app:rendered', () => {
|
|
||||||
vueQueryState.value = dehydrate(queryClient)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.client) {
|
|
||||||
nuxt.hooks.hook('app:created', () => {
|
|
||||||
hydrate(queryClient, vueQueryState.value)
|
|
||||||
|
|
||||||
nuxt.vueApp.use(VueQueryDevtools, {
|
|
||||||
initialIsOpen: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { defineEventHandler, getQuery } from 'h3'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
try {
|
|
||||||
const { search } = getQuery(event)
|
|
||||||
const apiUrl = process.env.VITE_MY_API_BASE_URL!
|
|
||||||
|
|
||||||
return await $fetch<Posts>(`${apiUrl}/posts?title_like=${search}`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept-Language': 'ru-RU',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
return sendError(event, createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: `Не удалось получить посты: ${error}`,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { createError, defineEventHandler, sendError } from 'h3'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
try {
|
|
||||||
const apiUrl = process.env.VITE_MY_API_BASE_URL!
|
|
||||||
|
|
||||||
return await $fetch<Users>(`${apiUrl}/users`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept-Language': 'ru-RU',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
return sendError(event, createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: `Не удалось получить пользователей: ${error}`,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export interface Post {
|
|
||||||
userId: number
|
|
||||||
id: number
|
|
||||||
title: string
|
|
||||||
body: string
|
|
||||||
}
|
|
||||||
export type Posts = Post[]
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
export interface UserCompany {
|
|
||||||
name: string
|
|
||||||
catchPhrase: string
|
|
||||||
bs: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserAddressGeo {
|
|
||||||
lat: string
|
|
||||||
lng: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserAddress {
|
|
||||||
street: string
|
|
||||||
suite: string
|
|
||||||
city: string
|
|
||||||
zipcode: string
|
|
||||||
geo: UserAddressGeo
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
address: UserAddress
|
|
||||||
phone: string
|
|
||||||
website: string
|
|
||||||
company: UserCompany
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Users = User[]
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
// tailwind.config.ts
|
|
||||||
import type { Config } from 'tailwindcss'
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
content: [], // Nuxt сам заполняет content автоматически
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Inter', 'sans-serif'], // если используешь @nuxt/fonts или свою кастомную типографику
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
|
||||||
"extends": "./.nuxt/tsconfig.json",
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user