add event management
This commit is contained in:
parent
e07bd83174
commit
961cf1d9c7
5 changed files with 666 additions and 0 deletions
412
src/components/auth/EventAuth.ts
Normal file
412
src/components/auth/EventAuth.ts
Normal file
|
@ -0,0 +1,412 @@
|
|||
import PocketBase from "pocketbase";
|
||||
import yaml from "js-yaml";
|
||||
import configYaml from "../../data/storeConfig.yaml?raw";
|
||||
|
||||
// Configuration type definitions
|
||||
interface Config {
|
||||
api: {
|
||||
baseUrl: string;
|
||||
};
|
||||
ui: {
|
||||
messages: {
|
||||
event: {
|
||||
saving: string;
|
||||
success: string;
|
||||
error: string;
|
||||
deleting: string;
|
||||
deleteSuccess: string;
|
||||
deleteError: string;
|
||||
messageTimeout: number;
|
||||
};
|
||||
};
|
||||
defaults: {
|
||||
pageSize: number;
|
||||
sortField: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Parse YAML configuration with type
|
||||
const config = yaml.load(configYaml) as Config;
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
event_id: string;
|
||||
event_name: string;
|
||||
event_code: string;
|
||||
registered_users: string; // JSON string
|
||||
points_to_reward: number;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
collectionId: string;
|
||||
collectionName: string;
|
||||
}
|
||||
|
||||
interface AuthElements {
|
||||
eventList: HTMLTableSectionElement;
|
||||
eventSearch: HTMLInputElement;
|
||||
searchEvents: HTMLButtonElement;
|
||||
addEvent: HTMLButtonElement;
|
||||
eventEditor: HTMLDialogElement;
|
||||
editorEventId: HTMLInputElement;
|
||||
editorEventName: HTMLInputElement;
|
||||
editorEventCode: HTMLInputElement;
|
||||
editorStartDate: HTMLInputElement;
|
||||
editorEndDate: HTMLInputElement;
|
||||
editorPointsToReward: HTMLInputElement;
|
||||
saveEventButton: HTMLButtonElement;
|
||||
}
|
||||
|
||||
export class EventAuth {
|
||||
private pb: PocketBase;
|
||||
private elements: AuthElements;
|
||||
private cachedEvents: Event[] = [];
|
||||
|
||||
constructor() {
|
||||
this.pb = new PocketBase(config.api.baseUrl);
|
||||
this.elements = this.getElements();
|
||||
this.init();
|
||||
}
|
||||
|
||||
private getElements(): AuthElements {
|
||||
const eventList = document.getElementById("eventList") as HTMLTableSectionElement;
|
||||
const eventSearch = document.getElementById("eventSearch") as HTMLInputElement;
|
||||
const searchEvents = document.getElementById("searchEvents") as HTMLButtonElement;
|
||||
const addEvent = document.getElementById("addEvent") as HTMLButtonElement;
|
||||
const eventEditor = document.getElementById("eventEditor") as HTMLDialogElement;
|
||||
const editorEventId = document.getElementById("editorEventId") as HTMLInputElement;
|
||||
const editorEventName = document.getElementById("editorEventName") as HTMLInputElement;
|
||||
const editorEventCode = document.getElementById("editorEventCode") as HTMLInputElement;
|
||||
const editorStartDate = document.getElementById("editorStartDate") as HTMLInputElement;
|
||||
const editorEndDate = document.getElementById("editorEndDate") as HTMLInputElement;
|
||||
const editorPointsToReward = document.getElementById("editorPointsToReward") as HTMLInputElement;
|
||||
const saveEventButton = document.getElementById("saveEventButton") as HTMLButtonElement;
|
||||
|
||||
if (
|
||||
!eventList ||
|
||||
!eventSearch ||
|
||||
!searchEvents ||
|
||||
!addEvent ||
|
||||
!eventEditor ||
|
||||
!editorEventId ||
|
||||
!editorEventName ||
|
||||
!editorEventCode ||
|
||||
!editorStartDate ||
|
||||
!editorEndDate ||
|
||||
!editorPointsToReward ||
|
||||
!saveEventButton
|
||||
) {
|
||||
throw new Error("Required DOM elements not found");
|
||||
}
|
||||
|
||||
return {
|
||||
eventList,
|
||||
eventSearch,
|
||||
searchEvents,
|
||||
addEvent,
|
||||
eventEditor,
|
||||
editorEventId,
|
||||
editorEventName,
|
||||
editorEventCode,
|
||||
editorStartDate,
|
||||
editorEndDate,
|
||||
editorPointsToReward,
|
||||
saveEventButton,
|
||||
};
|
||||
}
|
||||
|
||||
private getRegisteredUsersCount(registeredUsers: string): number {
|
||||
try {
|
||||
if (!registeredUsers) return 0;
|
||||
const users = JSON.parse(registeredUsers);
|
||||
return Array.isArray(users) ? users.length : 0;
|
||||
} catch (err) {
|
||||
console.warn("Failed to parse registered_users:", err);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchEvents(searchQuery: string = "") {
|
||||
try {
|
||||
// Only fetch from API if we don't have cached data
|
||||
if (this.cachedEvents.length === 0) {
|
||||
const records = await this.pb.collection("events").getList(1, config.ui.defaults.pageSize, {
|
||||
sort: config.ui.defaults.sortField,
|
||||
});
|
||||
this.cachedEvents = records.items as Event[];
|
||||
}
|
||||
|
||||
// Filter cached data based on search query
|
||||
let filteredEvents = this.cachedEvents;
|
||||
if (searchQuery) {
|
||||
const terms = searchQuery.toLowerCase().split(" ").filter(term => term.length > 0);
|
||||
if (terms.length > 0) {
|
||||
filteredEvents = this.cachedEvents.filter(event => {
|
||||
return terms.every(term =>
|
||||
(event.event_name?.toLowerCase().includes(term) ||
|
||||
event.event_id?.toLowerCase().includes(term) ||
|
||||
event.event_code?.toLowerCase().includes(term))
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { eventList } = this.elements;
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
if (filteredEvents.length === 0) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td colspan="8" class="text-center py-4">
|
||||
${searchQuery ? "No events found matching your search." : "No events found."}
|
||||
</td>
|
||||
`;
|
||||
fragment.appendChild(row);
|
||||
} else {
|
||||
filteredEvents.forEach((event) => {
|
||||
const row = document.createElement("tr");
|
||||
const startDate = event.start_date ? new Date(event.start_date).toLocaleString() : "N/A";
|
||||
const endDate = event.end_date ? new Date(event.end_date).toLocaleString() : "N/A";
|
||||
const registeredCount = this.getRegisteredUsersCount(event.registered_users);
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="block lg:table-cell">
|
||||
<!-- Mobile View -->
|
||||
<div class="lg:hidden space-y-2">
|
||||
<div class="font-medium">${event.event_name || "N/A"}</div>
|
||||
<div class="text-sm opacity-70">Event ID: ${event.event_id || "N/A"}</div>
|
||||
<div class="text-sm opacity-70">Code: ${event.event_code || "N/A"}</div>
|
||||
<div class="text-sm opacity-70">Start: ${startDate}</div>
|
||||
<div class="text-sm opacity-70">End: ${endDate}</div>
|
||||
<div class="text-sm opacity-70">Points: ${event.points_to_reward || 0}</div>
|
||||
<div class="text-sm opacity-70">Registered: ${registeredCount}</div>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<button class="btn btn-ghost btn-xs edit-event" data-event-id="${event.id}">
|
||||
<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>
|
||||
<button class="btn btn-ghost btn-xs text-error delete-event" data-event-id="${event.id}">
|
||||
<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="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop View -->
|
||||
<span class="hidden lg:block">${event.event_name || "N/A"}</span>
|
||||
</td>
|
||||
<td class="hidden lg:table-cell">${event.event_id || "N/A"}</td>
|
||||
<td class="hidden lg:table-cell">${event.event_code || "N/A"}</td>
|
||||
<td class="hidden lg:table-cell">${startDate}</td>
|
||||
<td class="hidden lg:table-cell">${endDate}</td>
|
||||
<td class="hidden lg:table-cell">${event.points_to_reward || 0}</td>
|
||||
<td class="hidden lg:table-cell">${registeredCount}</td>
|
||||
<td class="hidden lg:table-cell">
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost btn-xs edit-event" data-event-id="${event.id}">
|
||||
<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>
|
||||
<button class="btn btn-ghost btn-xs text-error delete-event" data-event-id="${event.id}">
|
||||
<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="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
fragment.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
eventList.innerHTML = "";
|
||||
eventList.appendChild(fragment);
|
||||
|
||||
// Setup event listeners for edit and delete buttons
|
||||
const editButtons = eventList.querySelectorAll(".edit-event");
|
||||
editButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const eventId = (button as HTMLButtonElement).dataset.eventId;
|
||||
if (eventId) {
|
||||
this.handleEventEdit(eventId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deleteButtons = eventList.querySelectorAll(".delete-event");
|
||||
deleteButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const eventId = (button as HTMLButtonElement).dataset.eventId;
|
||||
if (eventId) {
|
||||
this.handleEventDelete(eventId);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch events:", err);
|
||||
const { eventList } = this.elements;
|
||||
eventList.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-error">
|
||||
Failed to fetch events. Please try again.
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleEventEdit(eventId: string) {
|
||||
try {
|
||||
const event = await this.pb.collection("events").getOne(eventId);
|
||||
const {
|
||||
eventEditor,
|
||||
editorEventId,
|
||||
editorEventName,
|
||||
editorEventCode,
|
||||
editorStartDate,
|
||||
editorEndDate,
|
||||
editorPointsToReward,
|
||||
saveEventButton,
|
||||
} = this.elements;
|
||||
|
||||
// Populate the form
|
||||
editorEventId.value = event.event_id || "";
|
||||
editorEventName.value = event.event_name || "";
|
||||
editorEventCode.value = event.event_code || "";
|
||||
editorStartDate.value = event.start_date ? new Date(event.start_date).toISOString().slice(0, 16) : "";
|
||||
editorEndDate.value = event.end_date ? new Date(event.end_date).toISOString().slice(0, 16) : "";
|
||||
editorPointsToReward.value = event.points_to_reward?.toString() || "0";
|
||||
|
||||
// Store the event ID for saving
|
||||
saveEventButton.dataset.eventId = eventId;
|
||||
|
||||
// Disable event_id field for existing events
|
||||
editorEventId.disabled = true;
|
||||
|
||||
// Show the dialog
|
||||
eventEditor.showModal();
|
||||
} catch (err) {
|
||||
console.error("Failed to load event for editing:", err);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleEventSave() {
|
||||
const {
|
||||
eventEditor,
|
||||
editorEventId,
|
||||
editorEventName,
|
||||
editorEventCode,
|
||||
editorStartDate,
|
||||
editorEndDate,
|
||||
editorPointsToReward,
|
||||
saveEventButton,
|
||||
} = this.elements;
|
||||
|
||||
const eventId = saveEventButton.dataset.eventId;
|
||||
const isNewEvent = !eventId;
|
||||
|
||||
try {
|
||||
const eventData: Record<string, any> = {
|
||||
event_name: editorEventName.value,
|
||||
event_code: editorEventCode.value,
|
||||
start_date: editorStartDate.value,
|
||||
end_date: editorEndDate.value,
|
||||
points_to_reward: parseInt(editorPointsToReward.value) || 0,
|
||||
};
|
||||
|
||||
// Only set registered_users for new events
|
||||
if (isNewEvent) {
|
||||
eventData.event_id = editorEventId.value;
|
||||
eventData.registered_users = "[]";
|
||||
}
|
||||
|
||||
if (isNewEvent) {
|
||||
await this.pb.collection("events").create(eventData);
|
||||
} else {
|
||||
await this.pb.collection("events").update(eventId, eventData);
|
||||
}
|
||||
|
||||
// Close the dialog and refresh the table
|
||||
eventEditor.close();
|
||||
this.cachedEvents = []; // Clear cache to force refresh
|
||||
this.fetchEvents();
|
||||
} catch (err) {
|
||||
console.error("Failed to save event:", err);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleEventDelete(eventId: string) {
|
||||
if (confirm("Are you sure you want to delete this event?")) {
|
||||
try {
|
||||
await this.pb.collection("events").delete(eventId);
|
||||
this.cachedEvents = []; // Clear cache to force refresh
|
||||
this.fetchEvents();
|
||||
} catch (err) {
|
||||
console.error("Failed to delete event:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
// Initial fetch
|
||||
this.fetchEvents();
|
||||
|
||||
// Search functionality
|
||||
const handleSearch = () => {
|
||||
const searchQuery = this.elements.eventSearch.value.trim();
|
||||
this.fetchEvents(searchQuery);
|
||||
};
|
||||
|
||||
// Real-time search
|
||||
this.elements.eventSearch.addEventListener("input", handleSearch);
|
||||
|
||||
// Search button click handler
|
||||
this.elements.searchEvents.addEventListener("click", handleSearch);
|
||||
|
||||
// Add event button
|
||||
this.elements.addEvent.addEventListener("click", () => {
|
||||
const { eventEditor, editorEventId, saveEventButton } = this.elements;
|
||||
|
||||
// Clear the form
|
||||
this.elements.editorEventId.value = "";
|
||||
this.elements.editorEventName.value = "";
|
||||
this.elements.editorEventCode.value = "";
|
||||
this.elements.editorStartDate.value = "";
|
||||
this.elements.editorEndDate.value = "";
|
||||
this.elements.editorPointsToReward.value = "0";
|
||||
|
||||
// Enable event_id field for new events
|
||||
editorEventId.disabled = false;
|
||||
|
||||
// Clear the event ID to indicate this is a new event
|
||||
saveEventButton.dataset.eventId = "";
|
||||
|
||||
// Show the dialog
|
||||
eventEditor.showModal();
|
||||
});
|
||||
|
||||
// Event editor dialog
|
||||
const { eventEditor, saveEventButton } = this.elements;
|
||||
|
||||
// Close dialog when clicking outside
|
||||
eventEditor.addEventListener("click", (e) => {
|
||||
if (e.target === eventEditor) {
|
||||
eventEditor.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Save event button
|
||||
saveEventButton.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.handleEventSave();
|
||||
});
|
||||
}
|
||||
}
|
96
src/components/store/EventEditor.astro
Normal file
96
src/components/store/EventEditor.astro
Normal file
|
@ -0,0 +1,96 @@
|
|||
<!-- Event Editor Dialog -->
|
||||
<dialog id="eventEditor" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Event Details</h3>
|
||||
<form class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEventId">
|
||||
<span class="label-text">Event ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="editorEventId"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEventName">
|
||||
<span class="label-text">Event Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="editorEventName"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEventCode">
|
||||
<span class="label-text">Event Code</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="editorEventCode"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorStartDate">
|
||||
<span class="label-text">Start Date & Time</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="editorStartDate"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEndDate">
|
||||
<span class="label-text">End Date & Time</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="editorEndDate"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorPointsToReward">
|
||||
<span class="label-text">Points to Reward</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="editorPointsToReward"
|
||||
class="input input-bordered w-full"
|
||||
min="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick="eventEditor.close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
id="saveEventButton"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
82
src/components/store/EventManagement.astro
Normal file
82
src/components/store/EventManagement.astro
Normal file
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
import EventEditor from "./EventEditor.astro";
|
||||
---
|
||||
|
||||
<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">Event Management</h2>
|
||||
<div class="flex flex-col lg:flex-row gap-2">
|
||||
<div class="form-control w-full">
|
||||
<input
|
||||
type="text"
|
||||
id="eventSearch"
|
||||
placeholder="Search events..."
|
||||
class="input input-bordered input-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 w-full lg:w-auto">
|
||||
<button id="searchEvents" 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="addEvent"
|
||||
class="btn btn-primary 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="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span class="lg:hidden ml-2">Add Event</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="hidden lg:table-header-group">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Event ID</th>
|
||||
<th>Event Code</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Points</th>
|
||||
<th>Registered</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="eventList" class="divide-y">
|
||||
<!-- Event entries will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<EventEditor />
|
||||
|
||||
<script>
|
||||
import { EventAuth } from "../auth/EventAuth";
|
||||
new EventAuth();
|
||||
</script>
|
67
src/components/store/MemberManagement.astro
Normal file
67
src/components/store/MemberManagement.astro
Normal file
|
@ -0,0 +1,67 @@
|
|||
<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">
|
||||
<thead class="hidden lg:table-header-group">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Member ID</th>
|
||||
<th>Points</th>
|
||||
<th>Resume</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="resumeList" class="divide-y">
|
||||
<!-- Resume entries will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -53,6 +53,15 @@ ui:
|
|||
deleteError: Failed to delete resume. Please try again.
|
||||
messageTimeout: 3000
|
||||
|
||||
event:
|
||||
saving: Saving event...
|
||||
success: Event saved successfully!
|
||||
error: Failed to save event. Please try again.
|
||||
deleting: Deleting event...
|
||||
deleteSuccess: Event deleted successfully!
|
||||
deleteError: Failed to delete event. Please try again.
|
||||
messageTimeout: 3000
|
||||
|
||||
auth:
|
||||
loginError: Failed to start authentication
|
||||
notSignedIn: Not signed in
|
||||
|
|
Loading…
Reference in a new issue