Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions frontend/src/app/etiketler/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ export default async function Tags({ searchParams }: TagsPageProps) {
<div className="w-full mx-2 mb-4">
<EtiketlerSearch className="max-w-md" />
</div>
<Button variant="secondary" children="Takipteki Etiketler" />
<Button variant="secondary" children="Gizli Etiketler" />
<Button variant="secondary" children="Takipteki" />
<Button variant="secondary" children="Gizli" />
</div>
</div>

Expand All @@ -92,7 +92,11 @@ export default async function Tags({ searchParams }: TagsPageProps) {

{/* Pagination */}
{hashtagsResponse.meta && (
<Pagination meta={hashtagsResponse.meta} baseUrl="/etiketler" />
<Pagination
meta={hashtagsResponse.meta}
baseUrl="/etiketler"
queryParams={searchQuery ? { search: searchQuery } : undefined}
/>
)}
</div>
</div>
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
}

const Pagination: React.FC<PaginationProps> = ({ meta, baseUrl = "/etiketler" }) => {
const Pagination: React.FC<PaginationProps> = ({ meta, baseUrl = "/etiketler", queryParams }) => {
const { page, totalPages } = meta;

if (totalPages <= 1) {
Expand All @@ -16,6 +18,11 @@ const Pagination: React.FC<PaginationProps> = ({ 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()}`;
};
Expand Down
145 changes: 99 additions & 46 deletions frontend/src/services/hashtagService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

Expand All @@ -36,37 +64,38 @@ export const getAllHashtags = async (
options: CacheOptions = { next: { revalidate: 43200 } } // 12 hours cache by default
): Promise<HashtagServiceResponse> => {
try {
// Calculate skip from page number (backend expects skip, not page)
const skip = (page - 1) * limit;

const response = await getData<HashtagApiResponse, PaginationMeta>(
const response = await getData<HashtagApiResponse, BackendListMeta>(
'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',
};
}
};
Expand Down Expand Up @@ -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<HashtagServiceResponse> => {
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',
};
}
};
1 change: 1 addition & 0 deletions frontend/src/types/hashtag.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface HashtagBase {

// Backend API response from hashtag service
export interface HashtagApiResponse extends HashtagBase {
slug?: string;
postCount?: number;
}

Expand Down
Loading