Новые мелькие изменения

This commit is contained in:
Web-serfer 2026-04-15 00:44:37 +05:00
parent 9f14235135
commit b407e85846
3 changed files with 66 additions and 50 deletions

View file

@ -0,0 +1 @@
{"version":"1.21.0"}

View file

@ -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,38 +17,56 @@ 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>
<Match when={isPrevDisabled()}>
<button
disabled
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"
aria-label="Предыдущая страница недоступна"
>
<FiChevronLeft class="h-5 w-5" />
</button>
</Match>
<Match when={!isPrevDisabled()}>
<a <a
href={props.page.url.prev} href={props.page.url.prev}
classList={{ 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"
'flex items-center justify-center h-10 w-10 rounded-full border transition-all duration-300': true,
'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(),
'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(),
}}
aria-disabled={isPrevDisabled()}
aria-label="Перейти на предыдущую страницу" aria-label="Перейти на предыдущую страницу"
> >
<FiChevronLeft class="h-5 w-5" /> <FiChevronLeft class="h-5 w-5" />
</a> </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>
<Match when={isNextDisabled()}>
<button
disabled
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"
aria-label="Следующая страница недоступна"
>
<FiChevronRight class="h-5 w-5" />
</button>
</Match>
<Match when={!isNextDisabled()}>
<a <a
href={props.page.url.next} href={props.page.url.next}
classList={{ 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"
'flex items-center justify-center h-10 w-10 rounded-full border transition-all duration-300': true,
'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(),
'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(),
}}
aria-disabled={isNextDisabled()}
aria-label="Перейти на следующую страницу" aria-label="Перейти на следующую страницу"
> >
<FiChevronRight class="h-5 w-5" /> <FiChevronRight class="h-5 w-5" />
</a> </a>
</Match>
</Switch>
</nav> </nav>
</Show> </Show>
); );

View file

@ -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" },
}); });
} }
}; };