Skip to content

Commit

Permalink
explore page
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasdavis committed Nov 19, 2024
1 parent bf3265f commit bc08f29
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 66 deletions.
188 changes: 129 additions & 59 deletions apps/registry/app/explore/ClientResumes.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { motion } from 'framer-motion';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';

export default function ClientResumes({
initialResumes,
Expand All @@ -11,9 +11,11 @@ export default function ClientResumes({
currentSearch,
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [searchTerm, setSearchTerm] = useState(currentSearch);
const [debouncedSearch, setDebouncedSearch] = useState(currentSearch);
const [isLoading, setIsLoading] = useState(false);

// Debounce search
useEffect(() => {
Expand All @@ -28,156 +30,224 @@ export default function ClientResumes({
useEffect(() => {
if (debouncedSearch === currentSearch) return;

setIsLoading(true);
const params = new URLSearchParams(searchParams);
if (debouncedSearch) {
params.set('search', debouncedSearch);
params.delete('page'); // Reset to first page on new search
} else {
params.delete('search');
}
router.push(`/explore?${params.toString()}`);
}, [debouncedSearch, router, searchParams, currentSearch]);
router.push(`${pathname}?${params.toString()}`);
}, [debouncedSearch, router, searchParams, currentSearch, pathname]);

const handlePageChange = (newPage) => {
setIsLoading(true);
const params = new URLSearchParams(searchParams);
params.set('page', newPage);
router.push(`/explore?${params.toString()}`);
router.push(`${pathname}?${params.toString()}`);
};

// Reset loading state when new data arrives
useEffect(() => {
setIsLoading(false);
}, [initialResumes]);

// Generate page numbers to show
const getPageNumbers = () => {
const pages = [];
const showEllipsis = totalPages > 7;

if (!showEllipsis) {
// Show all pages if total is 7 or less
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);

if (currentPage > 3) {
pages.push('...');
}

// Show pages around current page
for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) {

for (
let i = Math.max(2, currentPage - 1);
i <= Math.min(totalPages - 1, currentPage + 1);
i++
) {
pages.push(i);
}

if (currentPage < totalPages - 2) {
pages.push('...');
}

// Always show last page

if (totalPages > 1) {
pages.push(totalPages);
}
}

return pages;
};

return (
<div>
<div className="mb-6">
<div className="relative mb-6">
<input
type="text"
placeholder="Search resumes..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full p-2 border rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
className="w-full p-2 pl-10 border rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isLoading}
/>
<svg
className="absolute left-3 top-3 h-4 w-4 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
{isLoading && (
<div className="absolute right-3 top-3">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
</div>
)}
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{initialResumes.map((resume, index) => (
<AnimatePresence mode="wait">
{isLoading ? (
<motion.div
key={resume.username}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow"
key="loading"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="grid grid-cols-1 md:grid-cols-2 gap-6"
>
<div className="flex items-center space-x-4">
<img
src={resume.image}
alt={resume.name || 'Profile'}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h3 className="text-lg font-semibold">
<a
href={`/${resume.username}`}
className="hover:text-blue-600 transition-colors"
>
{resume.name || 'Anonymous'}
</a>
</h3>
<p className="text-gray-600">{resume.label || 'No title'}</p>
<p className="text-sm text-gray-500">
{resume.location?.city
? `${resume.location.city}, ${resume.location.countryCode}`
: 'Location not specified'}
</p>
{[...Array(6)].map((_, index) => (
<div
key={index}
className="bg-white p-6 rounded-lg shadow-md animate-pulse"
>
<div className="flex items-center space-x-4">
<div className="rounded-full bg-gray-200 h-16 w-16"></div>
<div className="flex-1">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="space-y-3 mt-4">
<div className="h-3 bg-gray-200 rounded"></div>
<div className="h-3 bg-gray-200 rounded w-5/6"></div>
</div>
</div>
</div>
</div>
</div>
))}
</motion.div>
))}
</div>
) : (
<motion.div
key="content"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="grid grid-cols-1 md:grid-cols-2 gap-6"
>
{initialResumes.map((resume, index) => (
<motion.div
key={resume.username}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow"
>
<div className="flex items-center space-x-4">
<img
src={resume.image}
alt={resume.name || 'Profile'}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h3 className="text-lg font-semibold">
<a
href={`/${resume.username}`}
className="hover:text-blue-600 transition-colors"
>
{resume.name || 'Anonymous'}
</a>
</h3>
<p className="text-gray-600">
{resume.label || 'No title'}
</p>
<p className="text-sm text-gray-500">
{resume.location?.city
? `${resume.location.city}, ${resume.location.countryCode}`
: 'Location not specified'}
</p>
</div>
</div>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>

{totalPages > 1 && (
<div className="mt-8 flex justify-center">
<nav className="flex items-center space-x-2" aria-label="Pagination">
<button
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
disabled={currentPage === 1 || isLoading}
className="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="First page"
>
«
</button>
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
disabled={currentPage === 1 || isLoading}
className="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Previous page"
>
</button>
{getPageNumbers().map((pageNum, index) => (

{getPageNumbers().map((pageNum, index) =>
pageNum === '...' ? (
<span key={`ellipsis-${index}`} className="px-3 py-2">...</span>
<span key={`ellipsis-${index}`} className="px-3 py-2">
...
</span>
) : (
<button
key={pageNum}
onClick={() => handlePageChange(pageNum)}
disabled={isLoading}
className={`px-3 py-2 rounded-md text-sm font-medium ${
currentPage === pageNum
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-50'
}`}
} disabled:opacity-50 disabled:cursor-not-allowed`}
aria-current={currentPage === pageNum ? 'page' : undefined}
>
{pageNum}
</button>
)
))}
)}

<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
disabled={currentPage === totalPages || isLoading}
className="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Next page"
>
</button>
<button
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
disabled={currentPage === totalPages || isLoading}
className="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Last page"
>
Expand Down
16 changes: 9 additions & 7 deletions apps/registry/app/explore/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co';
const supabaseKey = process.env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);

const ITEMS_PER_PAGE = 20;
const ITEMS_PER_PAGE = 100;

export const metadata = {
title: 'Explore JSON Resumes | JSON Resume Registry',
Expand All @@ -23,24 +23,26 @@ export const metadata = {
async function getResumes(page = 1, search = '') {
try {
// First get the total count
let countQuery = supabase.from('resumes').select('*', { count: 'exact', head: true });

let countQuery = supabase
.from('resumes')
.select('*', { count: 'exact', head: true });

if (search && search.trim() !== '') {
countQuery = countQuery.textSearch('resume', search.trim(), {
config: 'english',
type: 'websearch'
type: 'websearch',
});
}

const { count: totalCount } = await countQuery;

// Then get the actual data
let dataQuery = supabase.from('resumes').select('*');

if (search && search.trim() !== '') {
dataQuery = dataQuery.textSearch('resume', search.trim(), {
config: 'english',
type: 'websearch'
type: 'websearch',
});
}

Expand Down

0 comments on commit bc08f29

Please sign in to comment.