Skip to content

Commit bf7ae6d

Browse files
feat: add profile option in local server (#369)
1 parent de117ce commit bf7ae6d

File tree

2 files changed

+231
-10
lines changed

2 files changed

+231
-10
lines changed

app/src/main/assets/index.html

Lines changed: 170 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,28 @@
6868
<h1 class="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-primary-600 to-secondary-600 bg-clip-text text-transparent mb-2">
6969
Deepr
7070
</h1>
71-
<p class="text-gray-600 text-lg">Your Personal Link Manager</p>
71+
<p class="text-gray-600 text-lg mb-4">Your Personal Link Manager</p>
72+
73+
<!-- Profile Selector -->
74+
<div class="flex items-center justify-center gap-3">
75+
<label for="profileSelect" class="text-sm font-medium text-gray-700">Profile:</label>
76+
<select
77+
id="profileSelect"
78+
class="px-4 py-2 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 text-gray-800 bg-white"
79+
>
80+
<option value="1">Default</option>
81+
</select>
82+
<button
83+
id="addProfileBtn"
84+
class="bg-gradient-to-r from-primary-500 to-secondary-500 hover:from-primary-600 hover:to-secondary-600 text-white px-4 py-2 rounded-xl font-medium transition-all duration-200 transform hover:scale-105 flex items-center gap-1"
85+
title="Add New Profile"
86+
>
87+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
88+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
89+
</svg>
90+
New Profile
91+
</button>
92+
</div>
7293
</div>
7394
</header>
7495

@@ -265,11 +286,51 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Loading links...</h3>
265286
</div>
266287
</div>
267288

289+
<!-- Add Profile Modal -->
290+
<div id="addProfileModal" class="fixed inset-0 z-50 hidden">
291+
<div class="absolute inset-0 bg-black bg-opacity-50" onclick="closeAddProfileModal()"></div>
292+
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-2xl shadow-2xl p-6 w-full max-w-md">
293+
<h3 class="text-xl font-semibold text-gray-800 mb-4">Create New Profile</h3>
294+
<form id="addProfileForm">
295+
<div class="mb-4">
296+
<label for="newProfileName" class="block text-sm font-medium text-gray-700 mb-2">
297+
Profile Name
298+
</label>
299+
<input
300+
type="text"
301+
id="newProfileName"
302+
placeholder="Enter profile name..."
303+
required
304+
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 text-gray-800"
305+
>
306+
</div>
307+
<div class="flex gap-3 justify-end">
308+
<button
309+
type="button"
310+
onclick="closeAddProfileModal()"
311+
class="px-6 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-xl transition-all duration-200"
312+
>
313+
Cancel
314+
</button>
315+
<button
316+
type="submit"
317+
id="createProfileBtn"
318+
class="bg-gradient-to-r from-primary-500 to-secondary-500 hover:from-primary-600 hover:to-secondary-600 text-white px-6 py-2 rounded-xl font-semibold transition-all duration-200 transform hover:scale-105"
319+
>
320+
Create Profile
321+
</button>
322+
</div>
323+
</form>
324+
</div>
325+
</div>
326+
268327
<script>
269328
// Global variables to store all links and available tags
270329
let allLinks = [];
271330
let availableTags = []; // Now contains {id, name, count} objects
272331
let selectedTags = []; // Now contains {id, name} objects
332+
let profiles = []; // Contains {id, name, createdAt} objects
333+
let selectedProfileId = 1; // Default profile
273334

274335
// Toast notification system
275336
function showToast(message, type = 'success') {
@@ -320,6 +381,103 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Loading links...</h3>
320381
}
321382
}
322383

384+
// Load profiles from API
385+
async function loadProfiles() {
386+
try {
387+
const response = await fetch('/api/profiles');
388+
profiles = await response.json();
389+
populateProfileSelect();
390+
} catch (error) {
391+
console.error('Error loading profiles:', error);
392+
profiles = [];
393+
}
394+
}
395+
396+
// Populate profile dropdown
397+
function populateProfileSelect() {
398+
const profileSelect = document.getElementById('profileSelect');
399+
profileSelect.innerHTML = profiles.map(profile => `
400+
<option value="${profile.id}" ${profile.id === selectedProfileId ? 'selected' : ''}>
401+
${escapeHtml(profile.name)}
402+
</option>
403+
`).join('');
404+
}
405+
406+
// Handle profile selection change
407+
function onProfileChange() {
408+
const profileSelect = document.getElementById('profileSelect');
409+
selectedProfileId = parseInt(profileSelect.value, 10);
410+
loadLinks();
411+
showToast(`Switched to profile: ${profiles.find(p => p.id === selectedProfileId)?.name || 'Unknown'}`, 'info');
412+
}
413+
414+
// Open add profile modal
415+
function openAddProfileModal() {
416+
document.getElementById('addProfileModal').classList.remove('hidden');
417+
document.getElementById('newProfileName').focus();
418+
}
419+
420+
// Close add profile modal
421+
function closeAddProfileModal() {
422+
document.getElementById('addProfileModal').classList.add('hidden');
423+
document.getElementById('newProfileName').value = '';
424+
}
425+
426+
// Create new profile
427+
async function createProfile(event) {
428+
event.preventDefault();
429+
430+
const nameInput = document.getElementById('newProfileName');
431+
const button = document.getElementById('createProfileBtn');
432+
const name = nameInput.value.trim();
433+
434+
if (!name) {
435+
showToast('Please enter a profile name', 'error');
436+
return;
437+
}
438+
439+
const originalText = button.textContent;
440+
button.innerHTML = `
441+
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white inline" fill="none" viewBox="0 0 24 24">
442+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
443+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
444+
</svg>
445+
Creating...
446+
`;
447+
button.disabled = true;
448+
449+
try {
450+
const response = await fetch('/api/profiles', {
451+
method: 'POST',
452+
headers: {
453+
'Content-Type': 'application/json',
454+
},
455+
body: JSON.stringify({ name })
456+
});
457+
458+
if (response.ok) {
459+
showToast(`Profile "${name}" created successfully!`, 'success');
460+
closeAddProfileModal();
461+
await loadProfiles();
462+
// Select the newly created profile
463+
const newProfile = profiles.find(p => p.name === name);
464+
if (newProfile) {
465+
selectedProfileId = newProfile.id;
466+
populateProfileSelect();
467+
loadLinks();
468+
}
469+
} else {
470+
throw new Error('Failed to create profile');
471+
}
472+
} catch (error) {
473+
console.error('Error creating profile:', error);
474+
showToast('Failed to create profile. Please try again.', 'error');
475+
} finally {
476+
button.textContent = originalText;
477+
button.disabled = false;
478+
}
479+
}
480+
323481
// Tag management functions
324482
function addSelectedTag(tagName) {
325483
// Check if tag is already selected
@@ -612,7 +770,7 @@ <h3 class="text-lg font-semibold text-gray-800 line-clamp-2 flex-1">
612770

613771
async function loadLinks() {
614772
try {
615-
const response = await fetch('/api/links');
773+
const response = await fetch(`/api/links?profileId=${selectedProfileId}`);
616774
const links = await response.json();
617775

618776
allLinks = links;
@@ -665,7 +823,8 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Error loading links</h3>
665823
link: urlInput.value,
666824
name: nameInput.value,
667825
notes: notesInput.value,
668-
tags: selectedTags // Send tags in {id, name} format
826+
tags: selectedTags, // Send tags in {id, name} format
827+
profileId: selectedProfileId
669828
})
670829
});
671830

@@ -738,6 +897,11 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Error loading links</h3>
738897
document.getElementById('sortOrder').addEventListener('click', toggleSortOrder);
739898
document.getElementById('clearFilters').addEventListener('click', clearAllFilters);
740899

900+
// Profile controls
901+
document.getElementById('profileSelect').addEventListener('change', onProfileChange);
902+
document.getElementById('addProfileBtn').addEventListener('click', openAddProfileModal);
903+
document.getElementById('addProfileForm').addEventListener('submit', createProfile);
904+
741905
// Tag input functionality
742906
const tagInput = document.getElementById('linkTags');
743907
const debouncedShowSuggestions = debounce((input) => showSuggestions(input), 200);
@@ -768,7 +932,9 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Error loading links</h3>
768932

769933
// Initialize
770934
updateSortOrderIcon();
771-
loadLinks();
935+
loadProfiles().then(() => {
936+
loadLinks();
937+
});
772938
loadAvailableTags();
773939
});
774940
</script>

app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,52 @@ open class LocalServerRepositoryImpl(
142142
}
143143
}
144144

145+
get("/api/profiles") {
146+
try {
147+
val profiles = deeprQueries.getAllProfiles().executeAsList()
148+
val response =
149+
profiles.map { profile ->
150+
ProfileResponse(
151+
id = profile.id,
152+
name = profile.name,
153+
createdAt = profile.createdAt,
154+
)
155+
}
156+
call.respond(HttpStatusCode.OK, response)
157+
} catch (e: Exception) {
158+
Log.e("LocalServer", "Error getting profiles", e)
159+
call.respond(
160+
HttpStatusCode.InternalServerError,
161+
ErrorResponse("Error getting profiles: ${e.message}"),
162+
)
163+
}
164+
}
165+
166+
post("/api/profiles") {
167+
try {
168+
val request = call.receive<AddProfileRequest>()
169+
deeprQueries.insertProfile(request.name)
170+
call.respond(
171+
HttpStatusCode.Created,
172+
SuccessResponse("Profile created successfully"),
173+
)
174+
} catch (e: Exception) {
175+
Log.e("LocalServer", "Error creating profile", e)
176+
call.respond(
177+
HttpStatusCode.InternalServerError,
178+
ErrorResponse("Error creating profile: ${e.message}"),
179+
)
180+
}
181+
}
182+
145183
get("/api/links") {
146184
try {
185+
val profileId =
186+
call.request.queryParameters["profileId"]?.toLongOrNull() ?: 1L
147187
val links =
148188
deeprQueries
149189
.getLinksAndTags(
150-
1L, // Default profile
190+
profileId,
151191
"",
152192
"",
153193
"",
@@ -192,11 +232,13 @@ open class LocalServerRepositoryImpl(
192232
val request = call.receive<AddLinkRequest>()
193233
// Insert the link without tags first
194234
accountViewModel.insertAccount(
195-
request.link,
196-
request.name,
197-
false,
198-
request.tags.map { it.toDbTag() },
199-
request.notes,
235+
link = request.link,
236+
name = request.name,
237+
executed = false,
238+
tagsList = request.tags.map { it.toDbTag() },
239+
notes = request.notes,
240+
thumbnail = "",
241+
profileId = request.profileId,
200242
)
201243
call.respond(
202244
HttpStatusCode.Created,
@@ -469,6 +511,19 @@ data class AddLinkRequest(
469511
val name: String,
470512
val notes: String = "",
471513
val tags: List<TagData> = emptyList(),
514+
val profileId: Long = 1L,
515+
)
516+
517+
@Serializable
518+
data class ProfileResponse(
519+
val id: Long,
520+
val name: String,
521+
val createdAt: String,
522+
)
523+
524+
@Serializable
525+
data class AddProfileRequest(
526+
val name: String,
472527
)
473528

474529
@Serializable

0 commit comments

Comments
 (0)