
- Separated the components for pocketbase - Removed the entire profile page temporarily and only added back basic user profile - Fixed many components dependencies
502 lines
18 KiB
Text
502 lines
18 KiB
Text
---
|
|
// Import the majors list
|
|
import allMajors from "../../data/allUCSDMajors.txt?raw";
|
|
import yaml from "js-yaml";
|
|
import textConfig from "../../config/text.yml?raw";
|
|
import profileConfig from "../../config/profileConfig.yaml?raw";
|
|
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
|
|
|
|
const majorsList: string[] = allMajors
|
|
.split("\n")
|
|
.filter((major: string) => major.trim())
|
|
.sort((a, b) => a.localeCompare(b)); // Sort alphabetically
|
|
|
|
// Parse configurations
|
|
const text = yaml.load(textConfig) as any;
|
|
const profile = yaml.load(profileConfig) as any;
|
|
const pbConfig = yaml.load(pocketbaseConfig) as any;
|
|
---
|
|
|
|
<div
|
|
class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 lg:gap-0 mb-4"
|
|
>
|
|
<h2 class="text-2xl font-bold">Member Management</h2>
|
|
<div class="flex flex-col lg:flex-row gap-2">
|
|
<div class="form-control w-full">
|
|
<input
|
|
type="text"
|
|
id="resumeSearch"
|
|
placeholder="Search users..."
|
|
class="input input-bordered input-sm w-full"
|
|
/>
|
|
</div>
|
|
<div class="flex gap-2 w-full lg:w-auto">
|
|
<button id="searchResumes" class="btn btn-sm flex-1 lg:flex-none">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
<span class="lg:hidden ml-2">Search</span>
|
|
</button>
|
|
<button
|
|
id="refreshResumes"
|
|
class="btn btn-ghost btn-sm flex-1 lg:flex-none"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-5 w-5"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
|
clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span class="lg:hidden ml-2">Refresh</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table
|
|
class="table table-zebra w-full [&_tr]:border-b [&_tr]:border-base-200 text-center"
|
|
>
|
|
<thead class="hidden lg:table-header-group">
|
|
<tr>
|
|
<th class="text-center">Name</th>
|
|
<th class="text-center">Email</th>
|
|
<th class="text-center">Member ID</th>
|
|
<th class="text-center">Major</th>
|
|
<th class="text-center">Grad Year</th>
|
|
<th class="text-center">Points</th>
|
|
<th class="text-center">Resume</th>
|
|
<th class="text-center">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="resumeList" class="divide-y divide-base-200">
|
|
<!-- Resume entries will be populated here -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Add Profile Editor Dialog -->
|
|
<dialog id="profileEditor" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg mb-4">Edit User Profile</h3>
|
|
<form method="dialog" class="space-y-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Name</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="editorName"
|
|
class="input input-bordered"
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Email</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="editorEmail"
|
|
class="input input-bordered"
|
|
readonly
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">IEEE Member ID</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="editorMemberId"
|
|
class="input input-bordered"
|
|
placeholder="00000000"
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Major</span>
|
|
</label>
|
|
<select id="editorMajor" class="select select-bordered w-full">
|
|
<option value="">Select major</option>
|
|
{
|
|
majorsList.map((major) => (
|
|
<option value={major.trim()}>{major.trim()}</option>
|
|
))
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Graduation Year</span>
|
|
</label>
|
|
<select
|
|
id="editorGradYear"
|
|
class="select select-bordered w-full"
|
|
>
|
|
<option value="">Select graduation year</option>
|
|
{
|
|
Array.from({ length: 6 }, (_, i) => {
|
|
const year = new Date().getFullYear() + i;
|
|
return <option value={year}>{year}</option>;
|
|
})
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Points</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="editorPoints"
|
|
class="input input-bordered"
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Events Attended</span>
|
|
</label>
|
|
<textarea
|
|
id="editorEvents"
|
|
class="textarea textarea-bordered"
|
|
readonly></textarea>
|
|
</div>
|
|
<div class="modal-action">
|
|
<button
|
|
class="btn"
|
|
onclick="document.getElementById('profileEditor').close()"
|
|
>Cancel</button
|
|
>
|
|
<button id="saveProfileButton" class="btn btn-primary"
|
|
>Save Changes</button
|
|
>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</dialog>
|
|
|
|
<script is:inline define:vars={{ majorsList }}>
|
|
// Make majorsList available globally
|
|
window.majorsList = majorsList;
|
|
</script>
|
|
|
|
<script>
|
|
import PocketBase from "pocketbase";
|
|
import type { RecordModel } from "pocketbase";
|
|
import yaml from "js-yaml";
|
|
import textConfig from "../../config/text.yml?raw";
|
|
import profileConfig from "../../config/profileConfig.yaml?raw";
|
|
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
|
|
|
|
// Define interface for user data structure
|
|
interface UserData {
|
|
id: string;
|
|
avatar: string;
|
|
collectionId: string;
|
|
collectionName: string;
|
|
created: string;
|
|
email: string;
|
|
emailVisibility: boolean;
|
|
events_attended: string[];
|
|
graduation_year: number;
|
|
last_login: string;
|
|
major: string;
|
|
member_id: string;
|
|
member_type: string;
|
|
name: string;
|
|
points: number;
|
|
resume: string[];
|
|
updated: string;
|
|
verified: boolean;
|
|
}
|
|
|
|
// Parse YAML configuration
|
|
const text = yaml.load(textConfig) as any;
|
|
const profile = yaml.load(profileConfig) as any;
|
|
const pbConfig = yaml.load(pocketbaseConfig) as any;
|
|
const pb = new PocketBase(pbConfig.api.baseUrl);
|
|
|
|
// Get DOM elements
|
|
const resumeList = document.getElementById("resumeList");
|
|
const searchInput = document.getElementById(
|
|
"resumeSearch"
|
|
) as HTMLInputElement;
|
|
const searchButton = document.getElementById("searchResumes");
|
|
const refreshButton = document.getElementById("refreshResumes");
|
|
|
|
// Function to format date
|
|
function formatDate(dateStr: string): string {
|
|
return new Date(dateStr).toLocaleString();
|
|
}
|
|
|
|
// Make handleEditUser globally available
|
|
window.handleEditUser = function (user: UserData) {
|
|
console.log("Edit user data:", user);
|
|
const profileEditor = document.getElementById(
|
|
"profileEditor"
|
|
) as HTMLDialogElement;
|
|
const editorName = document.getElementById(
|
|
"editorName"
|
|
) as HTMLInputElement;
|
|
const editorEmail = document.getElementById(
|
|
"editorEmail"
|
|
) as HTMLInputElement;
|
|
const editorMemberId = document.getElementById(
|
|
"editorMemberId"
|
|
) as HTMLInputElement;
|
|
const editorMajor = document.getElementById(
|
|
"editorMajor"
|
|
) as HTMLSelectElement;
|
|
const editorGradYear = document.getElementById(
|
|
"editorGradYear"
|
|
) as HTMLSelectElement;
|
|
const editorPoints = document.getElementById(
|
|
"editorPoints"
|
|
) as HTMLInputElement;
|
|
const editorEvents = document.getElementById(
|
|
"editorEvents"
|
|
) as HTMLTextAreaElement;
|
|
const saveProfileButton = document.getElementById(
|
|
"saveProfileButton"
|
|
) as HTMLButtonElement;
|
|
|
|
// Check if all required elements exist
|
|
if (
|
|
!profileEditor ||
|
|
!editorName ||
|
|
!editorEmail ||
|
|
!editorMemberId ||
|
|
!editorMajor ||
|
|
!editorGradYear ||
|
|
!editorPoints ||
|
|
!editorEvents ||
|
|
!saveProfileButton
|
|
) {
|
|
console.error("Missing form elements:", {
|
|
profileEditor: !!profileEditor,
|
|
editorName: !!editorName,
|
|
editorEmail: !!editorEmail,
|
|
editorMemberId: !!editorMemberId,
|
|
editorMajor: !!editorMajor,
|
|
editorGradYear: !!editorGradYear,
|
|
editorPoints: !!editorPoints,
|
|
editorEvents: !!editorEvents,
|
|
saveProfileButton: !!saveProfileButton,
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Populate the form with user data
|
|
editorName.value = user.name || "";
|
|
editorEmail.value = user.email || "";
|
|
editorMemberId.value = user.member_id || "";
|
|
editorMajor.value = user.major?.trim() || ""; // Trim to remove any newlines
|
|
editorGradYear.value = user.graduation_year?.toString() || "";
|
|
editorPoints.value = user.points?.toString() || "0";
|
|
editorEvents.value = Array.isArray(user.events_attended)
|
|
? user.events_attended.join(", ")
|
|
: "No events attended";
|
|
saveProfileButton.dataset.userId = user.id;
|
|
|
|
// Show the dialog
|
|
profileEditor.showModal();
|
|
} catch (err) {
|
|
console.error("Error populating form:", err);
|
|
}
|
|
};
|
|
|
|
// Function to render user row
|
|
function renderUserRow(user: RecordModel) {
|
|
const userData = user as unknown as UserData;
|
|
const escapedUser = JSON.stringify(userData)
|
|
.replace(/'/g, "\\'")
|
|
.replace(/"/g, """);
|
|
return `
|
|
<tr class="hover:bg-base-200">
|
|
<td class="text-center">
|
|
<div class="font-medium">${userData.name || "N/A"}</div>
|
|
</td>
|
|
<td class="text-center">${userData.email || "N/A"}</td>
|
|
<td class="text-center">${userData.member_id || "N/A"}</td>
|
|
<td class="text-center">${userData.major?.trim() || "N/A"}</td>
|
|
<td class="text-center">${userData.graduation_year || "N/A"}</td>
|
|
<td class="text-center">${userData.points || "0"}</td>
|
|
<td class="text-center">
|
|
${
|
|
userData.resume && userData.resume.length > 0
|
|
? `
|
|
<button class="btn btn-ghost btn-xs" onclick="window.open('${pb.files.getUrl(userData, userData.resume[0])}', '_blank')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
|
|
</svg>
|
|
View
|
|
</button>
|
|
`
|
|
: "No Resume"
|
|
}
|
|
</td>
|
|
<td class="text-center">
|
|
<button class="btn btn-ghost btn-xs" onclick="handleEditUser(${escapedUser})">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
|
</svg>
|
|
Edit
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// Function to handle saving profile changes
|
|
async function handleProfileSave() {
|
|
const profileEditor = document.getElementById(
|
|
"profileEditor"
|
|
) as HTMLDialogElement;
|
|
const editorName = document.getElementById(
|
|
"editorName"
|
|
) as HTMLInputElement;
|
|
const editorMemberId = document.getElementById(
|
|
"editorMemberId"
|
|
) as HTMLInputElement;
|
|
const editorMajor = document.getElementById(
|
|
"editorMajor"
|
|
) as HTMLSelectElement;
|
|
const editorGradYear = document.getElementById(
|
|
"editorGradYear"
|
|
) as HTMLSelectElement;
|
|
const editorPoints = document.getElementById(
|
|
"editorPoints"
|
|
) as HTMLInputElement;
|
|
const saveProfileButton = document.getElementById(
|
|
"saveProfileButton"
|
|
) as HTMLButtonElement;
|
|
const userId = saveProfileButton.dataset.userId;
|
|
|
|
if (!userId) {
|
|
console.error("No user ID found for saving");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append("name", editorName.value);
|
|
formData.append("member_id", editorMemberId.value);
|
|
formData.append("major", editorMajor.value);
|
|
formData.append("graduation_year", editorGradYear.value);
|
|
formData.append("points", editorPoints.value);
|
|
|
|
await pb.collection("users").update(userId, formData);
|
|
profileEditor.close();
|
|
// Refresh the user list
|
|
loadUsers();
|
|
} catch (err) {
|
|
console.error("Failed to save user profile:", err);
|
|
}
|
|
}
|
|
|
|
// Function to load users
|
|
async function loadUsers(searchQuery = "") {
|
|
if (!resumeList) return;
|
|
|
|
try {
|
|
// Show loading state
|
|
resumeList.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="text-center py-4">
|
|
<span class="loading loading-spinner loading-md"></span>
|
|
Loading users...
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
// Fetch users with filter if search query exists
|
|
const filter = searchQuery
|
|
? `name ~ "${searchQuery}" || email ~ "${searchQuery}" || member_id ~ "${searchQuery}"`
|
|
: "";
|
|
|
|
const users = await pb.collection("users").getList(1, 50, {
|
|
filter,
|
|
sort: "-created",
|
|
});
|
|
|
|
// Update table
|
|
if (users.items.length === 0) {
|
|
resumeList.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="text-center py-4">
|
|
No users found
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
resumeList.innerHTML = users.items.map(renderUserRow).join("");
|
|
} catch (error) {
|
|
console.error("Failed to load users:", error);
|
|
resumeList.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="text-center py-4 text-error">
|
|
Failed to load users. Please try again.
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Add event listeners
|
|
if (searchButton && searchInput) {
|
|
searchButton.addEventListener("click", () => {
|
|
loadUsers(searchInput.value);
|
|
});
|
|
|
|
searchInput.addEventListener("keypress", (e) => {
|
|
if (e.key === "Enter") {
|
|
loadUsers(searchInput.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (refreshButton) {
|
|
refreshButton.addEventListener("click", () => {
|
|
if (searchInput) searchInput.value = "";
|
|
loadUsers();
|
|
});
|
|
}
|
|
|
|
// Add event listener for save button
|
|
const saveProfileButton = document.getElementById("saveProfileButton");
|
|
if (saveProfileButton) {
|
|
saveProfileButton.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
handleProfileSave();
|
|
});
|
|
}
|
|
|
|
// Initial load
|
|
loadUsers();
|
|
</script>
|
|
|
|
<script>
|
|
declare global {
|
|
interface Window {
|
|
handleEditUser: (user: any) => void;
|
|
majorsList: string[];
|
|
}
|
|
}
|
|
</script>
|