Skip to content

Commit cf29d3e

Browse files
committed
Add contact list page with search and pagination
- Implemented `/dashboard/contacts` route with a searchable, paginated contact list using Svelte. - Added `contactList` function in `ContactApi.js` for fetching filtered contacts. - Integrated smooth animations for search form toggling. - Styled page with Tailwind CSS and Font Awesome for enhanced UI experience.
1 parent 73e3d57 commit cf29d3e

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed

src/lib/api/ContactApi.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,20 @@ export const contactCreate = async (token, {first_name, last_name, email, phone}
1313
phone
1414
})
1515
})
16+
}
17+
18+
export const contactList = async (token, {name, email, phone, page}) => {
19+
const url = new URL(`${import.meta.env.VITE_URL_API}/contacts`)
20+
if (name) url.searchParams.append('name', name)
21+
if (email) url.searchParams.append('email', email)
22+
if (phone) url.searchParams.append('phone', phone)
23+
if (page) url.searchParams.append('page', page)
24+
25+
return await fetch(url, {
26+
method: 'GET',
27+
headers: {
28+
'Accept': 'application/json',
29+
'Authorization': token
30+
}
31+
})
1632
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<script>
2+
import {onMount} from "svelte";
3+
import {contactList} from "$lib/api/ContactApi.js";
4+
import {alertError} from "$lib/alert.js";
5+
6+
const token = localStorage.getItem('token');
7+
const search = $state({
8+
name: "",
9+
email: "",
10+
phone: "",
11+
page: 1
12+
})
13+
let totalPage = $state(0)
14+
let pages = $derived.by(() => {
15+
const data = [];
16+
for (let i = 1; i <= totalPage; i++) {
17+
data.push(i);
18+
}
19+
return data;
20+
})
21+
let contacts = $state([])
22+
23+
async function handlePageChange(value) {
24+
search.page = value;
25+
await fetchContacts();
26+
}
27+
28+
async function handleSearch(e) {
29+
e.preventDefault();
30+
search.page = 1;
31+
32+
await fetchContacts();
33+
}
34+
35+
async function fetchContacts() {
36+
const response = await contactList(token, search)
37+
const responseBody = await response.json()
38+
console.log(responseBody)
39+
40+
if (response.status === 200) {
41+
contacts = responseBody.data;
42+
totalPage = responseBody.paging.total_page;
43+
} else {
44+
await alertError(responseBody.errors);
45+
}
46+
}
47+
48+
function toggleSearchForm() {
49+
const toggleButton = document.getElementById('toggleSearchForm');
50+
const searchFormContent = document.getElementById('searchFormContent');
51+
const toggleIcon = document.getElementById('toggleSearchIcon');
52+
53+
// Add transition for smooth animation
54+
searchFormContent.style.transition = 'max-height 0.3s ease-in-out, opacity 0.3s ease-in-out, margin 0.3s ease-in-out';
55+
searchFormContent.style.overflow = 'hidden';
56+
searchFormContent.style.maxHeight = '0px';
57+
searchFormContent.style.opacity = '0';
58+
searchFormContent.style.marginTop = '0';
59+
60+
toggleButton.addEventListener('click', function () {
61+
if (searchFormContent.style.maxHeight !== '0px') {
62+
// Hide the form
63+
searchFormContent.style.maxHeight = '0px';
64+
searchFormContent.style.opacity = '0';
65+
searchFormContent.style.marginTop = '0';
66+
toggleIcon.classList.remove('fa-chevron-up');
67+
toggleIcon.classList.add('fa-chevron-down');
68+
} else {
69+
// Show the form
70+
searchFormContent.style.maxHeight = searchFormContent.scrollHeight + 'px';
71+
searchFormContent.style.opacity = '1';
72+
searchFormContent.style.marginTop = '1rem';
73+
toggleIcon.classList.remove('fa-chevron-down');
74+
toggleIcon.classList.add('fa-chevron-up');
75+
}
76+
});
77+
}
78+
79+
onMount(async () => {
80+
toggleSearchForm();
81+
await fetchContacts();
82+
})
83+
</script>
84+
85+
<div class="flex items-center mb-6">
86+
<i class="fas fa-users text-blue-400 text-2xl mr-3"></i>
87+
<h1 class="text-2xl font-bold text-white">My Contacts</h1>
88+
</div>
89+
90+
<!-- Search form -->
91+
<div class="bg-gray-800 bg-opacity-80 rounded-xl shadow-custom border border-gray-700 p-6 mb-8 animate-fade-in">
92+
<div class="flex items-center justify-between mb-4">
93+
<div class="flex items-center">
94+
<i class="fas fa-search text-blue-400 mr-3"></i>
95+
<h2 class="text-xl font-semibold text-white">Search Contacts</h2>
96+
</div>
97+
<button type="button" id="toggleSearchForm"
98+
class="text-gray-300 hover:text-white hover:bg-gray-700 p-2 rounded-full focus:outline-none transition-all duration-200">
99+
<i class="fas fa-chevron-down text-lg" id="toggleSearchIcon"></i>
100+
</button>
101+
</div>
102+
<div id="searchFormContent" class="mt-4">
103+
<form onsubmit={handleSearch}>
104+
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
105+
<div>
106+
<label for="search_name" class="block text-gray-300 text-sm font-medium mb-2">Name</label>
107+
<div class="relative">
108+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
109+
<i class="fas fa-user text-gray-500"></i>
110+
</div>
111+
<input type="text" id="search_name" name="search_name"
112+
class="w-full pl-10 pr-3 py-3 bg-gray-700 bg-opacity-50 border border-gray-600 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
113+
placeholder="Search by name" bind:value={search.name}>
114+
</div>
115+
</div>
116+
<div>
117+
<label for="search_email" class="block text-gray-300 text-sm font-medium mb-2">Email</label>
118+
<div class="relative">
119+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
120+
<i class="fas fa-envelope text-gray-500"></i>
121+
</div>
122+
<input type="text" id="search_email" name="search_email"
123+
class="w-full pl-10 pr-3 py-3 bg-gray-700 bg-opacity-50 border border-gray-600 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
124+
placeholder="Search by email" bind:value={search.email}>
125+
</div>
126+
</div>
127+
<div>
128+
<label for="search_phone" class="block text-gray-300 text-sm font-medium mb-2">Phone</label>
129+
<div class="relative">
130+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
131+
<i class="fas fa-phone text-gray-500"></i>
132+
</div>
133+
<input type="text" id="search_phone" name="search_phone"
134+
class="w-full pl-10 pr-3 py-3 bg-gray-700 bg-opacity-50 border border-gray-600 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
135+
placeholder="Search by phone" bind:value={search.phone}>
136+
</div>
137+
</div>
138+
</div>
139+
<div class="mt-5 text-right">
140+
<button type="submit"
141+
class="px-5 py-3 bg-gradient text-white rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-all duration-200 font-medium shadow-lg transform hover:-translate-y-0.5">
142+
<i class="fas fa-search mr-2"></i> Search
143+
</button>
144+
</div>
145+
</form>
146+
</div>
147+
</div>
148+
149+
<!-- Contact cards grid -->
150+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
151+
<!-- Create New Contact Card -->
152+
<div class="bg-gray-800 bg-opacity-80 rounded-xl shadow-custom overflow-hidden border-2 border-dashed border-gray-700 card-hover animate-fade-in">
153+
<a href="/dashboard/contacts/create" class="block p-6 h-full">
154+
<div class="flex flex-col items-center justify-center h-full text-center">
155+
<div class="w-20 h-20 bg-gradient rounded-full flex items-center justify-center mb-5 shadow-lg transform transition-transform duration-300 hover:scale-110">
156+
<i class="fas fa-user-plus text-3xl text-white"></i>
157+
</div>
158+
<h2 class="text-xl font-semibold text-white mb-3">Create New Contact</h2>
159+
<p class="text-gray-300">Add a new contact to your list</p>
160+
</div>
161+
</a>
162+
</div>
163+
164+
{#each contacts as contact (contact.id)}
165+
<div class="bg-gray-800 bg-opacity-80 rounded-xl shadow-custom border border-gray-700 overflow-hidden card-hover animate-fade-in">
166+
<div class="p-6">
167+
<a href="/dashboard/contacts/{contact.id}"
168+
class="block cursor-pointer hover:bg-gray-700 rounded-lg transition-all duration-200 p-3">
169+
<div class="flex items-center mb-3">
170+
<div class="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center mr-3 shadow-md">
171+
<i class="fas fa-user text-white"></i>
172+
</div>
173+
<h2 class="text-xl font-semibold text-white hover:text-blue-300 transition-colors duration-200">
174+
{contact.first_name} {contact.last_name}
175+
</h2>
176+
</div>
177+
<div class="space-y-3 text-gray-300 ml-2">
178+
<p class="flex items-center">
179+
<i class="fas fa-user-tag text-gray-500 w-6"></i>
180+
<span class="font-medium w-24">First Name:</span>
181+
<span>{contact.first_name}</span>
182+
</p>
183+
<p class="flex items-center">
184+
<i class="fas fa-user-tag text-gray-500 w-6"></i>
185+
<span class="font-medium w-24">Last Name:</span>
186+
<span>{contact.last_name}</span>
187+
</p>
188+
<p class="flex items-center">
189+
<i class="fas fa-envelope text-gray-500 w-6"></i>
190+
<span class="font-medium w-24">Email:</span>
191+
<span>{contact.email}</span>
192+
</p>
193+
<p class="flex items-center">
194+
<i class="fas fa-phone text-gray-500 w-6"></i>
195+
<span class="font-medium w-24">Phone:</span>
196+
<span>{contact.phone}</span>
197+
</p>
198+
</div>
199+
</a>
200+
<div class="mt-4 flex justify-end space-x-3">
201+
<a href="/dashboard/contacts/{contact.id}/edit"
202+
class="px-4 py-2 bg-gradient text-white rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-all duration-200 font-medium shadow-md flex items-center">
203+
<i class="fas fa-edit mr-2"></i> Edit
204+
</a>
205+
<button class="px-4 py-2 bg-gradient-to-r from-red-600 to-red-500 text-white rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-all duration-200 font-medium shadow-md flex items-center">
206+
<i class="fas fa-trash-alt mr-2"></i> Delete
207+
</button>
208+
</div>
209+
</div>
210+
</div>
211+
{/each}
212+
213+
</div>
214+
215+
<!-- Pagination -->
216+
<div class="mt-10 flex justify-center">
217+
<nav class="flex items-center space-x-3 bg-gray-800 bg-opacity-80 rounded-xl shadow-custom border border-gray-700 p-3 animate-fade-in">
218+
{#if search.page > 1}
219+
<a href="#" onclick={() => handlePageChange(search.page -1)}
220+
class="px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-all duration-200 flex items-center">
221+
<i class="fas fa-chevron-left mr-2"></i> Previous
222+
</a>
223+
{/if}
224+
{#each pages as page (page)}
225+
{#if page === search.page}
226+
<a href="#" onclick={() => handlePageChange(page)}
227+
class="px-4 py-2 bg-gradient text-white rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-all duration-200 font-medium shadow-md">
228+
{page}
229+
</a>
230+
{:else}
231+
<a href="#" onclick={() => handlePageChange(page)}
232+
class="px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-all duration-200">
233+
{page}
234+
</a>
235+
{/if}
236+
{/each}
237+
{#if search.page < totalPage}
238+
<a href="#" onclick={() => handlePageChange(search.page + 1)}
239+
class="px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-all duration-200 flex items-center">
240+
Next <i class="fas fa-chevron-right ml-2"></i>
241+
</a>
242+
{/if}
243+
</nav>
244+
</div>

0 commit comments

Comments
 (0)