diff --git a/frontend/src/app/etiketler/page.tsx b/frontend/src/app/etiketler/page.tsx
index 16bb109a..4c77fd9c 100644
--- a/frontend/src/app/etiketler/page.tsx
+++ b/frontend/src/app/etiketler/page.tsx
@@ -79,8 +79,8 @@ export default async function Tags({ searchParams }: TagsPageProps) {
-
-
+
+
@@ -92,7 +92,11 @@ export default async function Tags({ searchParams }: TagsPageProps) {
{/* Pagination */}
{hashtagsResponse.meta && (
-
+
)}
diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx
index 6c496b83..cad9d48c 100644
--- a/frontend/src/components/Pagination.tsx
+++ b/frontend/src/components/Pagination.tsx
@@ -5,9 +5,11 @@ import type { PaginationMeta } from "@/types/hashtag.types";
interface PaginationProps {
meta: PaginationMeta;
baseUrl?: string;
+ /** Optional query params to preserve in page links (e.g. { search: "foo" }) */
+ queryParams?: Record;
}
-const Pagination: React.FC = ({ meta, baseUrl = "/etiketler" }) => {
+const Pagination: React.FC = ({ meta, baseUrl = "/etiketler", queryParams }) => {
const { page, totalPages } = meta;
if (totalPages <= 1) {
@@ -16,6 +18,11 @@ const Pagination: React.FC = ({ meta, baseUrl = "/etiketler" })
const getPageUrl = (pageNum: number) => {
const params = new URLSearchParams();
+ if (queryParams) {
+ Object.entries(queryParams).forEach(([key, value]) => {
+ if (value != null && value !== '') params.set(key, value);
+ });
+ }
params.set('page', pageNum.toString());
return `${baseUrl}?${params.toString()}`;
};
diff --git a/frontend/src/services/hashtagService.ts b/frontend/src/services/hashtagService.ts
index 1dc3e3ba..743e9848 100644
--- a/frontend/src/services/hashtagService.ts
+++ b/frontend/src/services/hashtagService.ts
@@ -7,23 +7,51 @@ import type {
PaginationMeta
} from "@/types/hashtag.types";
+/** Backend list meta shape (currentPage, limit) */
+interface BackendListMeta {
+ total: number;
+ skip?: number;
+ limit: number;
+ currentPage: number;
+ totalPages: number;
+}
+
/**
* Transform API hashtag response to frontend hashtag format
*/
const transformHashtagResponse = (apiHashtag: HashtagApiResponse): Hashtag => {
- const slug = apiHashtag.name
- .toLowerCase()
- .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
- .replace(/\s+/g, '-') // Replace spaces with hyphens
- .trim();
+ const slug =
+ apiHashtag.slug ??
+ apiHashtag.name
+ .toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, '')
+ .replace(/\s+/g, '-')
+ .trim();
return {
id: apiHashtag.id,
name: apiHashtag.name,
slug,
- postCount: apiHashtag.postCount || 0,
- icon: '', // Default empty icon - could be enhanced with emoji or icon mapping
- description: `#${apiHashtag.name} etiketli ${apiHashtag.postCount || 0} yazı`
+ postCount: apiHashtag.postCount ?? 0,
+ icon: '',
+ description: `#${apiHashtag.name} etiketli ${apiHashtag.postCount ?? 0} yazı`,
+ };
+};
+
+/** Normalize backend list meta to frontend PaginationMeta */
+const normalizeListMeta = (
+ meta: BackendListMeta | undefined,
+ page: number,
+ limit: number
+): PaginationMeta => {
+ if (!meta) {
+ return { page: 1, pageSize: limit, totalPages: 0, total: 0 };
+ }
+ return {
+ page: meta.currentPage ?? page,
+ pageSize: meta.limit ?? limit,
+ totalPages: meta.totalPages ?? 0,
+ total: meta.total ?? 0,
};
};
@@ -36,37 +64,38 @@ export const getAllHashtags = async (
options: CacheOptions = { next: { revalidate: 43200 } } // 12 hours cache by default
): Promise => {
try {
- // Calculate skip from page number (backend expects skip, not page)
const skip = (page - 1) * limit;
-
- const response = await getData(
+ const response = await getData(
'hashtags',
- false, // summaryOnly
+ false,
limit,
page,
skip,
options
);
- // Transform the API response to frontend format
- const transformedData = response.data.map(transformHashtagResponse);
+ const transformedData = (response.data ?? []).map(transformHashtagResponse);
+ const meta = normalizeListMeta(
+ response.meta as BackendListMeta | undefined,
+ page,
+ limit
+ );
+
+ if (response.error) {
+ return { data: [], meta, message: response.userMessage ?? response.message ?? 'Error fetching hashtags' };
+ }
return {
data: transformedData,
- meta: response.meta,
- message: response.message
+ meta,
+ message: response.message ?? 'Hashtags retrieved successfully',
};
} catch (error) {
console.error('Error in getAllHashtags:', error);
return {
data: [],
- meta: {
- page: 1,
- pageSize: limit,
- totalPages: 0,
- total: 0
- },
- message: 'Error fetching hashtags'
+ meta: { page: 1, pageSize: limit, totalPages: 0, total: 0 },
+ message: 'Error fetching hashtags',
};
}
};
@@ -102,47 +131,71 @@ export const getHashtagById = async (
};
/**
- * Search hashtags by name
+ * Search hashtags by name via GET /hashtags/search
+ * Backend expects: term, skip, limit. Returns { data, count, message }.
*/
export const searchHashtags = async (
query: string,
page: number = 1,
- limit: number = 20
+ limit: number = 50
): Promise => {
try {
- // Note: This would require a search endpoint in the backend
- // For now, we'll fetch all and filter on frontend (not ideal for production)
- const allHashtags = await getAllHashtags(1, 100); // Fetch more for filtering
-
- const filteredData = allHashtags.data.filter(hashtag =>
- hashtag.name.toLowerCase().includes(query.toLowerCase())
- );
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL;
+ if (!apiUrl) {
+ throw new Error('API URL is not defined');
+ }
+
+ const term = query.trim();
+ if (!term) {
+ return {
+ data: [],
+ meta: { page: 1, pageSize: limit, totalPages: 0, total: 0 },
+ message: 'Search term is required',
+ };
+ }
- // Simple pagination on filtered results
- const startIndex = (page - 1) * limit;
- const paginatedData = filteredData.slice(startIndex, startIndex + limit);
+ const skip = (page - 1) * limit;
+ const params = new URLSearchParams({
+ term: encodeURIComponent(term),
+ skip: String(skip),
+ limit: String(limit),
+ });
+ const url = `${apiUrl}/hashtags/search?${params.toString()}`;
+ const response = await fetch(url, { next: { revalidate: 60 } });
+ const json = await response.json();
+
+ if (!response.ok) {
+ const message = json?.message ?? 'Error searching hashtags';
+ return {
+ data: [],
+ meta: { page: 1, pageSize: limit, totalPages: 0, total: 0 },
+ message,
+ };
+ }
+
+ const rawData = json.data ?? [];
+ const count = Number(json.count) ?? 0;
+ const transformedData = rawData.map((item: HashtagApiResponse) =>
+ transformHashtagResponse(item)
+ );
+ const totalPages = limit > 0 ? Math.ceil(count / limit) : 0;
return {
- data: paginatedData,
+ data: transformedData,
meta: {
page,
pageSize: limit,
- totalPages: Math.ceil(filteredData.length / limit),
- total: filteredData.length
+ totalPages,
+ total: count,
},
- message: 'Search results'
+ message: json.message ?? 'Search results',
};
} catch (error) {
console.error('Error searching hashtags:', error);
return {
data: [],
- meta: {
- page: 1,
- pageSize: limit,
- totalPages: 0,
- total: 0
- },
- message: 'Error searching hashtags'
+ meta: { page: 1, pageSize: limit, totalPages: 0, total: 0 },
+ message: 'Error searching hashtags',
};
}
};
diff --git a/frontend/src/types/hashtag.types.ts b/frontend/src/types/hashtag.types.ts
index ce8af3d8..3abae8ac 100644
--- a/frontend/src/types/hashtag.types.ts
+++ b/frontend/src/types/hashtag.types.ts
@@ -7,6 +7,7 @@ export interface HashtagBase {
// Backend API response from hashtag service
export interface HashtagApiResponse extends HashtagBase {
+ slug?: string;
postCount?: number;
}