Новые мелькие изменения
This commit is contained in:
parent
9f14235135
commit
b407e85846
3 changed files with 66 additions and 50 deletions
1
.genkit/traces_idx/genkit.metadata
Normal file
1
.genkit/traces_idx/genkit.metadata
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":"1.21.0"}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Component } from 'solid-js';
|
import type { Component } from "solid-js";
|
||||||
import { Show } from 'solid-js';
|
import { Show, Match, Switch } from "solid-js";
|
||||||
import { FiChevronLeft, FiChevronRight } from 'solid-icons/fi';
|
import { FiChevronLeft, FiChevronRight } from "solid-icons/fi"; // Убедитесь, что иконки установлены
|
||||||
import type { PaginationProps } from '@globalInterfaces';
|
import type { PaginationProps } from "@globalInterfaces";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: PaginationProps;
|
page: PaginationProps;
|
||||||
|
|
@ -17,41 +17,59 @@ const Pagination: Component<Props> = (props) => {
|
||||||
class="mt-12 flex items-center justify-center gap-6 border-t border-neutral-200 pt-8 dark:border-neutral-800"
|
class="mt-12 flex items-center justify-center gap-6 border-t border-neutral-200 pt-8 dark:border-neutral-800"
|
||||||
aria-label="Навигация по страницам"
|
aria-label="Навигация по страницам"
|
||||||
>
|
>
|
||||||
{/* Кнопка "Назад" */}
|
<Switch>
|
||||||
<a
|
<Match when={isPrevDisabled()}>
|
||||||
href={props.page.url.prev}
|
<button
|
||||||
classList={{
|
disabled
|
||||||
'flex items-center justify-center h-10 w-10 rounded-full border transition-all duration-300': true,
|
class="flex h-10 w-10 cursor-not-allowed items-center justify-center rounded-full border border-neutral-300 bg-neutral-100 text-neutral-400 transition-all duration-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-600"
|
||||||
'border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-600': isPrevDisabled(),
|
aria-label="Предыдущая страница недоступна"
|
||||||
'border-neutral-300 bg-white text-neutral-700 shadow-sm hover:bg-neutral-50 hover:shadow-md active:scale-95 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-700': !isPrevDisabled(),
|
>
|
||||||
}}
|
<FiChevronLeft class="h-5 w-5" />
|
||||||
aria-disabled={isPrevDisabled()}
|
</button>
|
||||||
aria-label="Перейти на предыдущую страницу"
|
</Match>
|
||||||
>
|
<Match when={!isPrevDisabled()}>
|
||||||
<FiChevronLeft class="h-5 w-5" />
|
<a
|
||||||
</a>
|
href={props.page.url.prev}
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full border border-neutral-300 bg-white text-neutral-700 shadow-sm transition-all duration-300 hover:bg-neutral-50 hover:shadow-md active:scale-95 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
|
||||||
|
aria-label="Перейти на предыдущую страницу"
|
||||||
|
>
|
||||||
|
<FiChevronLeft class="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
{/* Индикатор текущей страницы */}
|
{/* Индикатор текущей страницы */}
|
||||||
<span class="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
<span class="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||||
Страница <span class="font-semibold text-blue-600 dark:text-blue-400">{props.page.currentPage}</span> из {props.page.lastPage}
|
Страница{" "}
|
||||||
|
<span class="font-semibold text-blue-600 dark:text-blue-400">
|
||||||
|
{props.page.currentPage}
|
||||||
|
</span>{" "}
|
||||||
|
из {props.page.lastPage}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Кнопка "Вперед" */}
|
<Switch>
|
||||||
<a
|
<Match when={isNextDisabled()}>
|
||||||
href={props.page.url.next}
|
<button
|
||||||
classList={{
|
disabled
|
||||||
'flex items-center justify-center h-10 w-10 rounded-full border transition-all duration-300': true,
|
class="flex h-10 w-10 cursor-not-allowed items-center justify-center rounded-full border border-neutral-300 bg-neutral-100 text-neutral-400 transition-all duration-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-600"
|
||||||
'border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-600': isNextDisabled(),
|
aria-label="Следующая страница недоступна"
|
||||||
'border-neutral-300 bg-white text-neutral-700 shadow-sm hover:bg-neutral-50 hover:shadow-md active:scale-95 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-700': !isNextDisabled(),
|
>
|
||||||
}}
|
<FiChevronRight class="h-5 w-5" />
|
||||||
aria-disabled={isNextDisabled()}
|
</button>
|
||||||
aria-label="Перейти на следующую страницу"
|
</Match>
|
||||||
>
|
<Match when={!isNextDisabled()}>
|
||||||
<FiChevronRight class="h-5 w-5" />
|
<a
|
||||||
</a>
|
href={props.page.url.next}
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full border border-neutral-300 bg-white text-neutral-700 shadow-sm transition-all duration-300 hover:bg-neutral-50 hover:shadow-md active:scale-95 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
|
||||||
|
aria-label="Перейти на следующую страницу"
|
||||||
|
>
|
||||||
|
<FiChevronRight class="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</nav>
|
</nav>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Pagination;
|
export default Pagination;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// src/pages/api/search.json.ts
|
// src/pages/api/search.json.ts
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { pb } from '@lib/pocketbase';
|
import { pb } from "@lib/pocketbase";
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ url }): Promise<Response> => {
|
export const GET: APIRoute = async ({ url }): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const query = url.searchParams.get('q')?.trim() || '';
|
const query = url.searchParams.get("q")?.trim() || "";
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return new Response(JSON.stringify([]), {
|
return new Response(JSON.stringify([]), {
|
||||||
|
|
@ -13,19 +13,17 @@ export const GET: APIRoute = async ({ url }): Promise<Response> => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. САНИТИЗАЦИЯ: Экранируем кавычки, чтобы запрос не сломал синтаксис фильтра PB
|
// УЛУЧШЕНИЕ: Используем параметризованный фильтр для безопасности и надежности.
|
||||||
// Если пользователь введет: React "Hero", мы превратим это в: React \"Hero\"
|
// PocketBase SDK автоматически экранирует значения, предотвращая ошибки.
|
||||||
const safeQuery = query.replace(/"/g, '\\"');
|
const filterString = `isActive = true && (title ~ {:query} || description ~ {:query} || content ~ {:query})`;
|
||||||
|
|
||||||
// 2. СБОРКА ФИЛЬТРА: Собираем строку вручную (это самый надежный способ)
|
const result = await pb.collection("posts").getList(1, 15, {
|
||||||
// Мы ищем совпадения в заголовке, описании ИЛИ контенте
|
|
||||||
const filterString = `isActive = true && (title ~ "${safeQuery}" || description ~ "${safeQuery}" || content ~ "${safeQuery}")`;
|
|
||||||
|
|
||||||
const result = await pb.collection('posts').getList(1, 15, {
|
|
||||||
filter: filterString,
|
filter: filterString,
|
||||||
sort: '-publishDate',
|
// Передаем значение query в качестве параметра
|
||||||
|
filterParams: { query },
|
||||||
|
sort: "-publishDate",
|
||||||
// Запрашиваем только нужные поля (без content, чтобы не грузить сеть)
|
// Запрашиваем только нужные поля (без content, чтобы не грузить сеть)
|
||||||
fields: 'id,title,description,slug',
|
fields: "id,title,description,slug",
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchData = result.items.map((post) => ({
|
const searchData = result.items.map((post) => ({
|
||||||
|
|
@ -39,17 +37,16 @@ export const GET: APIRoute = async ({ url }): Promise<Response> => {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Cache-Control": "public, max-age=60"
|
"Cache-Control": "public, max-age=60",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Логируем ошибку подробно, чтобы видеть причину в консоли
|
// Логируем ошибку подробно, чтобы видеть причину в консоли
|
||||||
console.error('Search API error:', error);
|
console.error("Search API error:", error);
|
||||||
|
|
||||||
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
return new Response(JSON.stringify({ error: "Internal server error" }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue