265 lines
12 KiB
JavaScript
265 lines
12 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import TopicItems from "./TopicItem";
|
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
|
import { Button } from "./ui/button";
|
|
import Loader from "./ui/loader";
|
|
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
|
|
|
export default function TopicCreation() {
|
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
|
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
|
|
const [topics, setTopics] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [pagination, setPagination] = useState({
|
|
current_page: 1,
|
|
last_page: 1,
|
|
per_page: 10,
|
|
total: 0
|
|
});
|
|
|
|
// Get current page from URL or default to 1
|
|
const getCurrentPage = () => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const page = parseInt(params.get('page')) || 1;
|
|
return Math.max(1, Math.min(page, pagination.last_page));
|
|
};
|
|
|
|
// Fetch topics data
|
|
const fetchTopics = async (page, search = '') => {
|
|
setLoading(true);
|
|
try {
|
|
let url = `${PUBLIC_TOPIC_API_URL}?query=get-all-topics&page=${page}`;
|
|
|
|
if (search) {
|
|
url += `&search=${encodeURIComponent(search)}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setTopics(data.data || []);
|
|
setPagination(prev => ({
|
|
...data.pagination,
|
|
current_page: Math.min(data.pagination.current_page, data.pagination.last_page)
|
|
}));
|
|
setSearchTerm(data.search_term || '');
|
|
} catch (err) {
|
|
setError(err.message);
|
|
console.error('Fetch error:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Handle page change
|
|
const handlePageChange = (newPage) => {
|
|
const validatedPage = Math.max(1, Math.min(newPage, pagination.last_page));
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.set('page', validatedPage);
|
|
window.history.pushState({}, '', `?${params.toString()}`);
|
|
fetchTopics(validatedPage, searchTerm);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
// Handle search
|
|
const handleSearch = (e) => {
|
|
e.preventDefault();
|
|
const newSearchTerm = e.target.elements.search.value.trim();
|
|
setSearchTerm(newSearchTerm);
|
|
|
|
// Reset to page 1 when searching
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.set('page', 1);
|
|
if (newSearchTerm) {
|
|
params.set('search', newSearchTerm);
|
|
} else {
|
|
params.delete('search');
|
|
}
|
|
window.history.pushState({}, '', `?${params.toString()}`);
|
|
fetchTopics(1, newSearchTerm);
|
|
};
|
|
|
|
// Initial load and URL change handling
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const initialSearch = params.get('search') || '';
|
|
setSearchTerm(initialSearch);
|
|
fetchTopics(getCurrentPage(), initialSearch);
|
|
|
|
const handlePopState = () => {
|
|
const newParams = new URLSearchParams(window.location.search);
|
|
const newSearch = newParams.get('search') || '';
|
|
setSearchTerm(newSearch);
|
|
fetchTopics(getCurrentPage(), newSearch);
|
|
};
|
|
|
|
window.addEventListener('popstate', handlePopState);
|
|
return () => {
|
|
window.removeEventListener('popstate', handlePopState);
|
|
};
|
|
}, []);
|
|
|
|
// ... (keep existing authLoading and authError checks)
|
|
|
|
return (
|
|
<>
|
|
{isLoggedIn && (
|
|
<>
|
|
<div className="container mx-auto flex justify-end gap-x-4 my-4">
|
|
<Button className="hidden lg:block" onClick={() => window.location.href = '/topic/new'} variant="outline">Create New</Button>
|
|
<Button onClick={() => window.location.href = '/topic/my-topic'} variant="outline">My Creation</Button>
|
|
</div>
|
|
<button onClick={() => window.location.href = '/topic/new'} className="fixed z-10 bottom-20 lg:bottom-10 right-0 lg:right-10 rounded-full w-16 h-16 bg-[#6d9e37]">
|
|
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#6d9e37] opacity-75 duration-[1s,15s]"></span>
|
|
<span class="relative inline-flex w-16 h-16 rounded-full bg-[#6d9e37]">
|
|
<svg className="border-[2px] border-[#fff] rounded-full" fill="#FFFFFF" viewBox="-5.76 -5.76 35.52 35.52" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" d="M12.3023235,7.94519388 L4.69610276,15.549589 C4.29095108,15.9079238 4.04030835,16.4092335 4,16.8678295 L4,20.0029438 L7.06398288,20.004826 C7.5982069,19.9670062 8.09548693,19.7183782 8.49479322,19.2616227 L16.0567001,11.6997158 L12.3023235,7.94519388 Z M13.7167068,6.53115006 L17.4709137,10.2855022 L19.8647941,7.89162181 C19.9513987,7.80501747 20.0000526,7.68755666 20.0000526,7.56507948 C20.0000526,7.4426023 19.9513987,7.32514149 19.8647932,7.23853626 L16.7611243,4.13485646 C16.6754884,4.04854589 16.5589355,4 16.43735,4 C16.3157645,4 16.1992116,4.04854589 16.1135757,4.13485646 L13.7167068,6.53115006 Z M16.43735,2 C17.0920882,2 17.7197259,2.26141978 18.1781068,2.7234227 L21.2790059,5.82432181 C21.7406843,6.28599904 22.0000526,6.91216845 22.0000526,7.56507948 C22.0000526,8.21799052 21.7406843,8.84415992 21.2790068,9.30583626 L9.95750718,20.6237545 C9.25902448,21.4294925 8.26890003,21.9245308 7.1346,22.0023295 L2,22.0023295 L2,21.0023295 L2.00324765,16.7873015 C2.08843822,15.7328366 2.57866679,14.7523321 3.32649633,14.0934196 L14.6953877,2.72462818 C15.1563921,2.2608295 15.7833514,2 16.43735,2 Z"></path> </g></svg>
|
|
</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{loading && !topics.length ? (
|
|
<Loader />
|
|
) : error ? (
|
|
<div className="error-message p-4 bg-red-100 text-red-700 rounded">Error loading topics: {error}</div>
|
|
) : (
|
|
<>
|
|
<TopicItems topics={topics} title="SoliconPin Topics" description={topicPageDesc} onSearch={handleSearch} searchTerm={searchTerm}/>
|
|
|
|
{pagination.last_page > 1 && (
|
|
<div className="flex flex-col justify-between items-center mt-8 gap-4">
|
|
<div className="text-sm text-gray-600">
|
|
Showing {(pagination.current_page - 1) * pagination.per_page + 1}-
|
|
{Math.min(pagination.current_page * pagination.per_page, pagination.total)} of {pagination.total} topics
|
|
{searchTerm && (
|
|
<span className="ml-2">matching "{searchTerm}"</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{
|
|
pagination.current_page > 1 && (
|
|
<>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handlePageChange(1)}
|
|
disabled={pagination.current_page <= 1}
|
|
className="hidden sm:inline-flex"
|
|
>
|
|
First
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handlePageChange(pagination.current_page - 1)}
|
|
disabled={pagination.current_page <= 1}
|
|
>
|
|
Previous
|
|
</Button>
|
|
</>
|
|
)
|
|
}
|
|
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
{generatePageNumbers(pagination.current_page, pagination.last_page).map((page, i) => (
|
|
page === '...' ? (
|
|
<span key={i} className="px-2">...</span>
|
|
) : (
|
|
<Button
|
|
key={i}
|
|
variant={page === pagination.current_page ? "default" : "outline"}
|
|
onClick={() => handlePageChange(page)}
|
|
className="min-w-10"
|
|
>
|
|
{page}
|
|
</Button>
|
|
)
|
|
))}
|
|
</div>
|
|
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handlePageChange(pagination.current_page + 1)}
|
|
disabled={pagination.current_page >= pagination.last_page}
|
|
>
|
|
Next
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handlePageChange(pagination.last_page)}
|
|
disabled={pagination.current_page >= pagination.last_page}
|
|
className="hidden sm:inline-flex"
|
|
>
|
|
Last
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Helper function to generate smart page numbers
|
|
function generatePageNumbers(currentPage, lastPage) {
|
|
const pages = [];
|
|
const maxVisible = 5; // Maximum visible page numbers
|
|
|
|
if (lastPage <= maxVisible) {
|
|
for (let i = 1; i <= lastPage; i++) {
|
|
pages.push(i);
|
|
}
|
|
} else {
|
|
// Always show first page
|
|
pages.push(1);
|
|
|
|
// Calculate start and end of middle pages
|
|
let start = Math.max(2, currentPage - 1);
|
|
let end = Math.min(lastPage - 1, currentPage + 1);
|
|
|
|
// Adjust if we're at the beginning
|
|
if (currentPage <= 3) {
|
|
end = maxVisible - 2;
|
|
}
|
|
|
|
// Adjust if we're at the end
|
|
if (currentPage >= lastPage - 2) {
|
|
start = lastPage - (maxVisible - 2);
|
|
}
|
|
|
|
// Add ellipsis if needed
|
|
if (start > 2) {
|
|
pages.push('...');
|
|
}
|
|
|
|
// Add middle pages
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i);
|
|
}
|
|
|
|
// Add ellipsis if needed
|
|
if (end < lastPage - 1) {
|
|
pages.push('...');
|
|
}
|
|
|
|
// Always show last page
|
|
pages.push(lastPage);
|
|
}
|
|
|
|
return pages;
|
|
} |