Skip to content

Instantly share code, notes, and snippets.

@CodeNinjaUG
Created September 15, 2024 15:57
Show Gist options
  • Save CodeNinjaUG/d2ffccbf727ff6a3836f0f5fa05dc087 to your computer and use it in GitHub Desktop.
Save CodeNinjaUG/d2ffccbf727ff6a3836f0f5fa05dc087 to your computer and use it in GitHub Desktop.
const CustomSelect = {
template: `
<div>
<div class="custom-selection" @click="toggleDropdown">
<div class="selected-pills">
<span v-for="(category, index) in selectedCategories" :key="index" class="small-pill">
{{ category }}
<button type="button" class="close-btn" @click.stop="removeCategory(category)">×</button>
</span>
</div>
<div v-if="dropdownVisible" class="dropdown-options">
<ul>
<li
v-for="subcategory in subcategories"
:key="subcategory.subcategory"
@click.stop="selectCategory(subcategory.subcategory)"
:class="{ selected: isSelected(subcategory.subcategory) }"
>
{{ subcategory.subcategory }}
</li>
</ul>
</div>
</div>
</div>
`,
props: {
thematic_id: {
type: String,
required: true
},
value: {
type: Array,
default: () => []
},
subcategories: {
type: Array,
required: true
}
},
data() {
return {
selectedCategories: [...this.value],
dropdownVisible: false,
};
},
watch: {
value(newValue) {
this.selectedCategories = [...newValue];
}
},
computed: {
filters() {
return this.selectedCategories.length;
},
filteredSubcategories() {
return this.subcategories.filter(sub => sub.thematic_area_id === this.thematic_id);
}
},
methods: {
toggleDropdown() {
this.dropdownVisible = !this.dropdownVisible;
},
selectCategory(category) {
const index = this.selectedCategories.indexOf(category);
if (index !== -1) {
this.selectedCategories = this.selectedCategories.filter((c) => c !== category);
} else {
this.selectedCategories.push(category);
}
this.emitChange();
},
removeCategory(category) {
this.selectedCategories = this.selectedCategories.filter((c) => c !== category);
this.emitChange();
},
isSelected(category) {
return this.selectedCategories.includes(category);
},
emitChange() {
this.$emit('change', this.selectedCategories); // Emit the updated categories
}
}
};
const app = Vue.createApp(
{
components: {
CustomSelect,
},
data()
{
return {
radius: 240,
details: details,
selectedElements: [],
items: thematic_areas,
elementtitle: "",
entity_url: entity_url,
thematic_map_id: map_id,
map_sub_categories: subcategories,
EntityModal: false,
hoveredElement: null,
fetchedsubs: [],
allSubcategories : [],
selectedSection: null,
SelectedLogoFile: null,
selectedCategory: null,
tempSelectedSubcategories: [],
selectedSubcategories:{},
subcategories: [],
checkedSubcategories: [],
logo_url: null,
email: "",
location: "",
telephone: "",
description: "",
organisation: "",
acronym: "",
youtube: "",
instagram: "",
twitter: "",
facebook: "",
linkedin: "",
searchTerm: "",
thematic_areas_arr: thematic_areas,
filteredentities: null,
showModal: false,
tableCurrentPage: 1,
tableItemsPerPage: 5,
currentPage: 1,
itemsPerPage: 4,
allentities: entities,
svgarray: phpData,
subcategories: subcategories,
svgElements: [],
searchExpanded: false,
windowWidth: window.innerWidth,
wasMobile: window.innerWidth <= 768,
showEntityModal: false, // Controls modal visibility
selectedEntity: {}, // Holds the selected entity's data
entities: [],
};
},
computed:
{
isMobile()
{
return this.windowWidth <= 768; // Threshold for mobile devices
},
groupedCategories()
{
const grouped = this.filteredElements
.flatMap((entity) => entity.subcategories)
.reduce((acc, category) =>
{
const
{
thematic_area_id,
thematic_area_name
} = category;
if (!acc[thematic_area_id])
{
acc[thematic_area_id] = {
thematic_area_id,
thematic_area_name,
subcategories: [],
};
}
acc[thematic_area_id].subcategories.push(
{
subcategory_id: category.subcategory_id,
subcategory_name: category.subcategory_name,
});
return acc;
},
{});
return Object.values(grouped);
},
filteredElements()
{
let result = this.allentities;
if (this.selectedElements.length > 0)
{
result = result.filter((entity) =>
{
return entity.thematic_areas.some((thematicArea) =>
{
return thematicArea.subcategories.some((subcategory) =>
{
return this.selectedElements.some(
(selectedElement) => subcategory.subcategory_id === selectedElement.subcategory_id
);
});
});
});
}
// Apply the subcategory filter from Select2 (for each thematic area)
Object.keys(this.selectedSubcategories).forEach((thematic_area_id) =>
{
const selectedSubcats = this.selectedSubcategories[thematic_area_id];
if (selectedSubcats && selectedSubcats.length > 0)
{
result = result.filter((entity) =>
entity.thematic_areas.some(
(thematicArea) =>
thematicArea.thematic_area_id == thematic_area_id &&
thematicArea.subcategories.some((subcategory) =>
selectedSubcats.includes(subcategory.subcategory_id)
)
)
);
}
});
// Apply search filter
if (this.searchTerm.trim() !== "")
{
const searchLower = this.searchTerm.toLowerCase().trim();
result = result.filter((entity) =>
entity.organisation.toLowerCase().includes(searchLower)
);
}
return result;
},
paginateElements()
{
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
return this.filteredElements.slice(startIndex, endIndex);
},
totalPages()
{
return Math.ceil(this.filteredElements.length / this.itemsPerPage);
},
angleStep()
{
return 180 / this.items.length; // Adjust angle for right half-circle
},
labelRadius()
{
return this.radius * 0.7; // Adjust label position
},
getOuterCirclePath()
{
const outerRadius = this.radius * 1.1; // Decreased radius for the outer circle
return `
M ${outerRadius} 0
A ${outerRadius} ${outerRadius} 0 0 1 ${-outerRadius} 0
`;
},
getExtraCirclePath()
{
const extraRadius = this.radius * 1.25; // Decreased radius for the extra circle
return `
M ${extraRadius} 0
A ${extraRadius} ${extraRadius} 0 0 1 ${-extraRadius} 0
`;
},
paginatedTableData()
{
const startIndex = (this.tableCurrentPage - 1) * this.tableItemsPerPage;
const endIndex = startIndex + this.tableItemsPerPage;
return this.filteredElements.slice(startIndex, endIndex);
},
totalTablePages()
{
return Math.ceil(this.filteredElements.length / this.tableItemsPerPage);
},
getResetPath()
{
const startX = this.radius * 0.3 * Math.cos(0);
const startY = this.radius * 0.3 * Math.sin(0);
const endX = this.radius * 0.3 * Math.cos(Math.PI);
const endY = this.radius * 0.3 * Math.sin(Math.PI);
return `
M ${startX} ${startY}
A ${this.radius * 0.3} ${this.radius * 0.3} 0 0 1 ${endX} ${endY}
L 0 0
Z
`;
},
getResetSpacePath()
{
const outerRadius = this.radius * 0.35; // Slightly larger radius for the white space
const startX = outerRadius * Math.cos(0);
const startY = outerRadius * Math.sin(0);
const endX = outerRadius * Math.cos(Math.PI);
const endY = outerRadius * Math.sin(Math.PI);
return `
M ${startX} ${startY}
A ${outerRadius} ${outerRadius} 0 0 1 ${endX} ${endY}
L ${this.radius * 0.3 * Math.cos(Math.PI)} ${this.radius * 0.3 * Math.sin(Math.PI)}
A ${this.radius * 0.3} ${this.radius * 0.3} 0 0 0 ${this.radius * 0.3 * Math.cos(0)} ${this.radius * 0.3 * Math.sin(0)}
Z
`;
},
},
methods:
{
openEntityModal(entity) {
this.selectedEntity = entity;
this.showEntityModal = true;
},
closeModal() {
this.showEntityModal = false;
},
updateSelectedSubcategories(thematic_area_id, event)
{
// console.log(thematic_area_id);
// const subcategory = this.selectedElements.find((sub) => sub.thematic_area_id === thematic_area_id);
// if (subcategory) {
// } else {
// this.selectedElements.push(
// {
// thematic_area_id : thematic_area_id
// }
// )
// }
//const selectedOptions = Array.from(event.target.selectedOptions).map((option) => option.value);
//this.$set(this.selectedSubcategories, thematic_area_id, selectedOptions);
this.filterEntities();
},
isSelected(thematic_area_id, subcategory_id)
{
return this.selectedSubcategories[thematic_area_id] &&
this.selectedSubcategories[thematic_area_id].includes(subcategory_id);
},
handleComputedValue(value) {
console.log(value);
},
getContrastingTextColor(hexColor)
{
// Convert hex color to RGB
const r = parseInt(hexColor.slice(1, 3), 16);
const g = parseInt(hexColor.slice(3, 5), 16);
const b = parseInt(hexColor.slice(5, 7), 16);
// Calculate brightness based on RGB values
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// If brightness is above the threshold (128), return black, else white
return brightness > 128 ? 'black' : 'white';
},
handleResize()
{
this.windowWidth = window.innerWidth;
const isCurrentlyMobile = this.windowWidth <= 768;
if (isCurrentlyMobile !== this.wasMobile)
{
this.wasMobile = isCurrentlyMobile;
window.location.reload();
}
},
updateCheckedSubcategories(item)
{
this.tempSelectedSubcategories.forEach((subcategoryId) =>
{
const subcategory = this.subcategories.find((sub) => sub.subcategory_id === subcategoryId);
if (subcategory && !this.checkedSubcategories.find((sub) => sub.subcategory_id === sub.subcategory_id))
{
this.checkedSubcategories.push(
{
subcategory_id: subcategory.subcategory_id,
subcategory: subcategory.subcategory,
thematic_area_id: subcategory.thematic_area_id,
thematic_area: subcategory.thematic_area,
});
}
});
this.checkedSubcategories = this.checkedSubcategories.filter((sub) => this.tempSelectedSubcategories.includes(sub.subcategory_id));
},
limitText(text, maxLength)
{
if (text.length > maxLength)
{
return text.slice(0, maxLength) + "...";
}
return text;
},
handleSearch()
{
this.currentPage = 1; // Reset to first page when searching
this.searchExpanded = false; // Hide the overlay once the search is performed
},
fetchAllSubcategories(category_id)
{
this.fetchedsubs = this.subcategories.filter((obj) => obj.thematic_area_id === category_id);
},
displayEntityModal()
{
this.EntityModal = true;
},
updateCheckedSubcategories()
{
this.selectedSubcategories.forEach((subcategoryId) =>
{
const subcategory = this.subcategories.find((sub) => sub.subcategory_id === subcategoryId);
if (subcategory && !this.checkedSubcategories.find((sub) => sub.subcategory_id === sub.subcategory_id))
{
this.checkedSubcategories.push(
{
subcategory_id: subcategory.subcategory_id,
subcategory: subcategory.subcategory,
thematic_area_id: subcategory.thematic_area_id,
thematic_area: subcategory.thematic_area,
});
}
});
this.checkedSubcategories = this.checkedSubcategories.filter((sub) => this.selectedSubcategories.includes(sub.subcategory_id));
},
showElementName(element)
{
this.elementtitle = element.subcategory;
},
ViewFilteredTable()
{
this.showModal = true;
this.tableCurrentPage = 1;
},
nextTablePage()
{
if (this.tableCurrentPage < this.totalTablePages)
{
this.tableCurrentPage++;
}
},
prevTablePage()
{
if (this.tableCurrentPage > 1)
{
this.tableCurrentPage--;
}
},
nextPage()
{
if (this.currentPage < this.totalPages)
{
this.currentPage++;
}
},
viewEntity(id)
{
const baseUrl = window.location.origin;
const newPath = `/view-entity/${id}`;
const url = new URL(newPath, baseUrl).href;
window.open(url, "_blank");
},
idExists(arr, id)
{
return arr.some((obj) => obj.id === id);
},
prevPage()
{
if (this.currentPage > 1)
{
this.currentPage--;
}
},
getPath(index)
{
const startAngle = index * this.angleStep;
const endAngle = startAngle + this.angleStep;
const startX = this.radius * Math.cos((startAngle * Math.PI) / 180);
const startY = this.radius * Math.sin((startAngle * Math.PI) / 180);
const endX = this.radius * Math.cos((endAngle * Math.PI) / 180);
const endY = this.radius * Math.sin((endAngle * Math.PI) / 180);
return `
M 0 0
L ${startX} ${startY}
A ${this.radius} ${this.radius} 0 0 1 ${endX} ${endY}
Z
`;
},
removeItem(index)
{
if (index === -1)
{
console.log("index doesnt exist");
}
else
{
this.selectedElements.splice(index, 1);
this.removeEntities(index);
}
},
toggleElementSelection(item)
{
const index = this.selectedElements.indexOf(item);
if (index === -1)
{
this.selectedElements.push(item);
}
else
{
this.selectedElements.splice(index, 1);
}
},
showSelected(selected)
{
const index = this.selectedElements.findIndex((elem) => elem.subcategory === selected.subcategory);
if (index === -1)
{
this.selectedElements.push(selected);
}
else
{
this.selectedElements.splice(index, 1);
this.removeEntities(index);
}
},
filterEntities()
{
this.filteredentities = this.filteredElements;
},
removeEntities(selectedElement)
{
this.filteredElements.forEach((entity) =>
{
entity.subcategories.forEach((subcategory) =>
{
if (subcategory.subcategory_id === selectedElement.subcategory_id && subcategory.subcategory_name === selectedElement.subcategory)
{
const index = this.filterEntities.findIndex((item) => item.entity_id === entity.id);
console.log(index);
if (index !== -1)
{
this.filterEntities.splice(index, 1); // Remove the entity from the result array
}
}
});
});
},
showHover(element, event)
{
console.log(element, event);
},
truncateText(text)
{
const maxLength = 10; // Example max length per line
const lines = [];
let currentLine = "";
text.split(" ").forEach((word) =>
{
if (currentLine.length + word.length + 1 <= maxLength)
{
currentLine += (currentLine ? " " : "") + word;
}
else
{
lines.push(currentLine);
currentLine = word;
}
});
if (currentLine)
{
lines.push(currentLine);
}
return lines;
},
submitForm()
{
const formData = new FormData();
formData.append("organisation", this.organisation);
formData.append("image", this.SelectedLogoFile);
formData.append("acronym", this.acronym);
formData.append("facebook", this.facebook);
formData.append("instagram", this.instagram);
formData.append("twitter", this.twitter);
formData.append("linkedin", this.linkedin);
formData.append("youtube", this.youtube);
formData.append("telephone", this.telephone);
formData.append("email", this.email);
formData.append("location", this.location);
formData.append("description", this.description);
formData.append("selectedCategory", this.selectedCategory);
formData.append("selectedSubcategories", JSON.stringify(this.selectedSubcategories));
axios
.post(entity_url, formData,
{
headers:
{
"Content-Type": "multipart/form-data",
},
})
.then((response) =>
{
if ((response.data.success = true))
{
this.organisation = "";
this.description = "";
this.selectedFile = null;
this.email = "";
this.location = "";
this.telephone = "";
this.acronym = "";
this.youtube = "";
this.instagram = "";
this.twitter = "";
this.facebook = "";
this.linkedin = "";
this.selectedCategory = null;
this.selectedSubcategories = [];
this.EntityModal = false;
alert("Entity saved successfully!");
}
else
{
alert("An error occurred while saving the entity.");
}
})
.catch((error) =>
{
alert("An error occurred while saving the entity.");
});
},
onFilelogoChange(event)
{
const file = event.target.files[0];
this.SelectedLogoFile = file;
this.logo_url = URL.createObjectURL(this.SelectedLogoFile);
},
handleClick(index, color, item)
{
this.svgElements = this.subcategories.filter((it) => it.thematic_area_id === item.thematic_area_id);
this.selectedSection = index; // Set selected section index
this.updateSVGElementsPosition(index, color);
},
updateSVGElementsPosition(index, color)
{
const outerRadius = this.radius * 1.2;
const numberOfElements = this.svgElements.length;
const angleIncrement = Math.PI / (numberOfElements * 2);
this.svgElements.forEach((element, idx) =>
{
const angle = angleIncrement * idx + Math.PI / 4;
element.x = outerRadius * Math.cos(angle);
element.y = outerRadius * Math.sin(angle);
element.angle = angle;
element.color = color;
});
},
reset()
{
this.selectedSection = null;
this.selectedElements = [];
this.svgElements.forEach((element) =>
{
element.x = 0;
element.y = 0;
element.color = "#FFFFFF";
});
},
exportFilteredDataAsCSV()
{
const columns = ["organisation", "accronym", "description", "thematic_areas", "contact details"];
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += columns.join(",") + "\n";
if (this.filteredElements && this.filteredElements.length > 0)
{
this.filteredElements.forEach((item) =>
{
let row = [];
columns.forEach((column) =>
{
if (column === "thematic_areas")
{
row.push(item[column] ? item[column].map((area) => area.thematic_area_name).join("; ") : "");
}
else if (column === "contact details")
{
let contactDetails = [
item.contactemail,
item.instagram,
item.twitter,
item.facebook,
item.youtube,
item.telephone,
item.location,
item.linkedin,
]
.filter(Boolean)
.join("; ");
row.push(`"${contactDetails.replace(/"/g, '""')}"`);
}
else
{
row.push(item[column] ? `"${item[column].toString().replace(/"/g, '""')}"` : "");
}
});
csvContent += row.join(",") + "\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "filtered_data.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log("CSV generation attempted");
}
else
{
console.error("No data available in this.filteredElements");
}
},
removeLogoImage()
{
this.SelectedLogoFile = null;
this.logo_url = null;
},
getSVGElementTransform(index)
{
const element = this.svgElements[index];
return `translate(${element.x},${element.y}) rotate(${(element.angle * 180) / Math.PI})`;
},
zoomIn(event)
{
event.currentTarget.style.transform = 'scale(1.05)';
},
zoomOut(event)
{
event.currentTarget.style.transform = 'scale(1)';
},
expandSearch()
{
this.searchExpanded = true;
},
shrinkSearch()
{
if (!this.searchTerm.trim())
{
this.searchExpanded = false; // Hide overlay if the input is empty and blurred
}
},
loadHtml2Canvas()
{
return new Promise((resolve, reject) =>
{
if (window.html2canvas)
{
// If html2canvas is already loaded, resolve the promise
resolve();
}
else
{
// Create a script element
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js";
script.onload = () => resolve(); // Resolve when script is loaded
script.onerror = () => reject(new Error('Failed to load html2canvas'));
document.head.appendChild(script); // Append the script to the head
}
});
},
generateCardImage(cardId)
{
this.loadHtml2Canvas()
.then(() =>
{
const cardElement = document.getElementById(cardId);
if (cardElement)
{
html2canvas(cardElement).then((canvas) =>
{
const link = document.createElement("a");
link.href = canvas.toDataURL("image/png");
link.download = "card.png";
link.click();
});
}
})
.catch((error) =>
{
console.error('Error loading html2canvas:', error);
});
},
mounted()
{
window.addEventListener("resize", this.handleResize);
this.handleResize(); // Ensure the initial state is set based on window size
this.$nextTick(() =>
{
this.initializeSelect2(); // Initialize Select2 after DOM is rendered
});
},
beforeUnmount()
{
window.removeEventListener("resize", this.handleResize);
},
},
watch:
{
selectedSubcategories(newVal, oldVal)
{
this.updateCheckedSubcategories();
},
windowWidth()
{
this.handleResize();
},
},
template: `
<!-- Modal structure with entity details -->
<div v-if="showEntityModal" class="modal d-block" tabindex="-1" @click.self="showEntityModal = false" role="dialog">
<div class="modal-dialog modal-lg" role="document" style="max-width: 1000px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" style="font-family: Helvetica, sans-serif; font-weight: bold; font-size: 20px;">
{{ selectedEntity.organisation }} Profile
</h5>
<a class="btn btn-sm btn-outline" @click="showEntityModal = false">
<em class="icon ni ni-cross"></em>
</a>
</div>
<div class="modal-body">
<div class="row" style="display: flex; flex-wrap: wrap;">
<!-- Left column for the avatar and contact details -->
<div class="col-xl-4 col-lg-4 col-md-12 mb-4">
<div class="card h-100" style="border: 1px solid #f0f0f0; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); background-color: #f8f9fa;">
<div class="card-inner-group text-center p-4">
<div class="card-inner">
<div class="user-card user-card-s2">
<div class="user-avatar lg bg-info rounded-circle d-flex justify-content-center align-items-center" style="width: 120px; height: 120px; overflow: hidden; margin: auto;">
<img v-if="selectedEntity.logo_url" :src="selectedEntity.logo_url" alt="Entity Logo" style="width: 100%; object-fit: cover;">
<span v-else style="color: white; font-size: 40px; font-weight: bold;">
{{ selectedEntity.organisation.slice(0, 2).toUpperCase() }}
</span>
</div>
<div class="user-info mt-3">
<div class="badge bg-light rounded-pill ucap">{{ selectedEntity.acronym }}</div>
<h5 class="mt-2 mb-1" style="font-size: 20px; font-weight: bold; font-family: Helvetica, sans-serif;">
{{ selectedEntity.organisation }}
</h5>
<span class="sub-text">{{ selectedEntity.contactemail }}</span>
</div>
</div>
</div>
<div class="card-inner card-inner-sm mt-3">
<ul class="btn-toolbar justify-center gx-2">
<li v-if="selectedEntity.facebook">
<a :href="selectedEntity.facebook" class="btn btn-trigger btn-icon text-info">
<em class="icon ni ni-facebook-fill"></em>
</a>
</li>
<li v-if="selectedEntity.twitter">
<a :href="selectedEntity.twitter" class="btn btn-trigger btn-icon text-info">
<em class="icon ni ni-twitter-round"></em>
</a>
</li>
<li v-if="selectedEntity.instagram">
<a :href="selectedEntity.instagram" class="btn btn-trigger btn-icon text-info">
<em class="icon ni ni-instagram-round"></em>
</a>
</li>
<li v-if="selectedEntity.linkedin">
<a :href="selectedEntity.linkedin" class="btn btn-trigger btn-icon text-info">
<em class="icon ni ni-linkedin-round"></em>
</a>
</li>
<li v-if="selectedEntity.youtube">
<a :href="selectedEntity.youtube" class="btn btn-trigger btn-icon text-info">
<em class="icon ni ni-youtube-round"></em>
</a>
</li>
<li v-if="selectedEntity.telephone">
<a :href="'tel:' + selectedEntity.telephone" class="btn btn-trigger btn-icon text-info">
<em class="icon ni ni-call-alt-fill"></em>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Right column for the description and thematic areas -->
<div class="col-xl-8 col-lg-8 col-md-12">
<div class="card h-100" style="border: 1px solid #f0f0f0; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); width: 100%;">
<div class="card-inner p-4">
<!-- Description Section -->
<div class="nk-block mb-4">
<h6 class="text-muted" style="font-weight: 600; font-family: Helvetica, sans-serif; font-size: 18px;">Description</h6>
<div class="media-content mt-2">
<p style="text-align:justify; color:black; line-height: 1.6;">{{ selectedEntity.description }}</p>
</div>
</div>
<!-- Thematic Areas and Subcategories Section -->
<div class="nk-block">
<h6 class="text-muted" style="font-weight: 600; font-family: Helvetica, sans-serif; font-size: 18px;">Categorisation</h6>
<!-- 2x2 Grid for Thematic Areas -->
<div class="row g-3 mt-3" style="display: flex; flex-wrap: wrap;">
<div v-for="thematic in selectedEntity.thematic_areas" :key="thematic.thematic_area_id" class="col-md-6 d-flex align-items-stretch" style="padding: 10px;">
<div class="card mb-3 border-light" style="background-color: #f8f9fa; border-radius: 5px; flex-grow: 1; height: 100%; padding: 5px;">
<div class="card-inner d-flex align-items-center p-3">
<div class="icon-circle icon-circle-lg bg-light text-info">
<em class="icon ni ni-layers"></em>
</div>
<div class="ms-3">
<div class="lead-text" style="font-weight: 600; font-family: Helvetica, sans-serif; ">{{ thematic.thematic_area_name }}</div>
<span v-for="(subcategory, index) in thematic.subcategories" :key="subcategory.subcategory_id" class="sub-text text-muted">
{{ subcategory.subcategory_name }}<span v-if="index < thematic.subcategories.length - 1">, </span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-info" @click="showEntityModal = false">Close</button>
</div>
</div>
</div>
</div>
<div v-if="EntityModal">
<div class="modal" @click.self="EntityModal = false" :style="{ display: EntityModal ? 'block' : 'none' }">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" style="font-family: Helvetica, sans-serif; font-weight: bold; font-size: 20px;">
Submit Entity to the Mapping
</h5>
<a href="#" class="close" @click="EntityModal = false" aria-label="Close">
<em class="icon ni ni-cross"></em>
</a>
</div>
<div class="modal-body">
<form method="post" @submit.prevent="submitForm()">
<div class="row gy-4">
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">Organisation Name</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-01" v-model="organisation" placeholder="Enter Organisation Name" required>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">Acronym</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-05" v-model="acronym" placeholder="Enter Acronym" required>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label"> Facebook </label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-03" v-model="facebook" placeholder="Enter Facebook" required>
</div>
</div>
<div class="form-group">
<label class="form-label">Instagram</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-04" v-model="instagram" placeholder="Enter Instagram" required>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">LinkedIn</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-03" v-model="linkedin" placeholder="Enter LinkedIn" required>
</div>
</div>
<div class="form-group">
<label class="form-label">Twitter</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-04" v-model="twitter" placeholder="Enter Twitter" required>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">Youtube</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-03" v-model="youtube" placeholder="Enter Youtube" required>
</div>
</div>
<div class="form-group">
<label class="form-label">Telephone</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-04" placeholder="Enter Telephone" v-model="telephone" required>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">Contact Email</label>
<div class="form-control-wrap">
<input type="email" class="form-control" id="default-03" placeholder="Enter Contact Email" v-model="email" required>
</div>
</div>
<div class="form-group">
<label class="form-label">Location Base / Office Location</label>
<div class="form-control-wrap">
<input type="text" class="form-control" id="default-04" placeholder="Enter Location Base" v-model="location" required>
</div>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label class="form-label" for="default-textarea">Upload Company Logo</label>
<div class="form-control-wrap">
<div v-if="!SelectedLogoFile" class="form-file">
<input type="file" @change="onFilelogoChange" class="form-file-input" id="customFile">
<label class="form-file-label" for="customFile">Choose file</label>
</div>
<div class="d-flex flex-column mt-3" v-if="logo_url">
<img class="h-40 w-40" v-if="logo_url" :src="logo_url" />
<a role="button" class="btn btn-xs btn-primary col-md-3 mt-4" @click="removeLogoImage">Remove Image</a>
</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">Select Categories </label>
<div class="form-control-select">
<select v-model="selectedCategory" @change="fetchAllSubcategories(selectedCategory)" class="form-select" id="default-07" aria-label="select example" required>
<option disabled value="">Please select a category</option>
<template v-for="item in thematic_areas_arr" :key="item.id">
<option :value="item.thematic_area_id"> {{ item.thematic_area }}</option>
</template>
</select>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">Select SubCategories </label>
<div class="form-control-select-multiple" style="max-height: 200px; overflow-y: auto;">
<div v-for="sub in fetchedsubs" :key="sub.subcategory_id">
<input
type="checkbox"
:id="'subcategory-' + sub.subcategory_id"
:value="sub.subcategory_id"
v-model="selectedSubcategories"
@change="updateCheckedSubcategories"
>
<label :for="'subcategory-' + sub.subcategory_id">{{ sub.subcategory }}</label>
</div>
</div>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<select class="form-select" multiple="" id="default-07" aria-label="select example">
<option value="option_select0"> Checked SubCategories </option>
<option v-for="subchecked in checkedSubcategories" :value="subchecked.subcategory_id"> {{ subchecked.subcategory }}</option>
</select>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label class="form-label" for="default-textarea">Organisation Description</label>
<div class="form-control-wrap">
<textarea v-model="description" class="form-control no-resize" id="default-textarea" required></textarea>
</div>
</div>
</div>
<div class="col-sm-6">
<button type="submit" name="submit" class="btn btn-info">Save Entities</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div v-if="showModal" class="modal d-block" tabindex="-1" @click.self="showModal = false" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" style="font-family: Helvetica, sans-serif; font-weight: bold; font-size: 20px;">
{{ details.map_name }} Data Table
</h5>
<a @click="exportFilteredDataAsCSV" class="modal-title btn btn-sm btn-info text-white">Export Csv</a>
<a class="btn btn-sm btn-outline" @click="showModal = false">
<em class="icon ni ni-cross"></em>
</a>
</div>
<div class="modal-body">
<table class="table table-striped">
<thead>
<tr>
<th>Organisation</th>
<th>Description</th>
<th>Category</th>
<th>Contact Details</th>
</tr>
</thead>
<tbody>
<tr v-for="item in paginatedTableData" :key="item.entity_id">
<td>{{ item.organisation }}</td>
<td>{{ limitText(item.description, 100) }}</td>
<td>
<div v-for="thematic in item.thematic_areas" :key="thematic.thematic_area_id">
<strong>{{ thematic.thematic_area_name }}:</strong>
<span>
<span v-for="(sub, index) in thematic.subcategories" :key="sub.subcategory_id">
{{ sub.subcategory_name }}<span v-if="index < thematic.subcategories.length - 1">, </span>
</span>
</span>
</div>
</td>
<td>
<div class="d-flex flex-column bd-highlight mb-3">
<div class="bd-highlight">{{ item.contactemail }}</div>
</div>
</td>
</tr>
</tbody>
</table>
<nav>
<ul class="pagination justify-content-center">
<li class="page-item" :class="{ disabled: tableCurrentPage === 1 }">
<a class="page-link" href="#" @click.prevent="prevTablePage">Previous</a>
</li>
<li class="page-item disabled">
<span class="page-link">Page {{ tableCurrentPage }} of {{ totalTablePages }}</span>
</li>
<li class="page-item" :class="{ disabled: tableCurrentPage === totalTablePages }">
<a class="page-link" href="#" @click.prevent="nextTablePage">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<div>
<!-- Mobile View -->
<div v-if="isMobile">
<!-- Map Name Heading -->
<div class="d-flex flex-column align-items-center mb-4">
<div class="nk-block-title page-title"
style="font-family: Helvetica; font-weight: bold; font-size: 30px; text-align: center;">
{{ details.map_name }}
</div>
</div>
<div class="d-flex">
<h5 style="font-family: Helvetica, sans-serif; font-size: 18px; font-weight: bold;"> Filters </h5>
(<span class="card-title">{{ selectedElements.length }}</span>)
</div>
<div class="row g-gs">
<!-- Loop through thematic areas from the items array -->
<div v-for="(item, index) in items" :key="index" class="col-sm-6">
<div class="form-group" style="margin-bottom: 1.5rem;">
<label class="form-label" style="font-size: 1.1rem; color: #333; margin-bottom: .5rem;">
{{ item.thematic_area }}
</label>
<div class="form-control-wrap">
<custom-select
:thematic_id="item.thematic_area_id"
:value="selectedSubcategories[item.thematic_area_id] || []"
:subcategories="subcategories.filter(sub => sub.thematic_area_id === item.thematic_area_id)"
@change="updateSelectedSubcategories(item.thematic_area_id, $event)"
/>
</div>
</div>
</div>
</div>
<!-- Search Bar -->
<div class="search-bar-container mb-3">
<input v-model="searchTerm" type="text" class="form-control search-input" placeholder="Search Organisation">
</div>
<!-- Buttons: Download CSV and Add Entity -->
<div class="d-flex justify-content-between mb-3">
<button @click="exportFilteredDataAsCSV()" class="btn btn-sm btn-success">Download as CSV</button>
<button @click="displayEntityModal()" class="btn btn-sm btn-outline-info">Add Entity</button>
</div>
<!-- Cards View -->
<div class="row d-flex justify-content-between" style="gap: 10px;">
<!-- Loop through paginated elements to create cards -->
<div class="col-lg-5 col-md-6 col-sm-12 mb-2" v-for="item in paginateElements" :key="item.id" style="display: flex;">
<a href="#"
@click.prevent="viewEntity(item.entity_id)"
class="card-link"
@mouseover="zoomIn"
@mouseout="zoomOut"
style="display: block; text-decoration: none; color: inherit; transition: transform 0.3s ease-in-out; width: 100%; height: 100%;">
<!-- Card Structure with Compact Layout -->
<div class="card card-bordered h-100"
style="transition: transform 0.3s ease-in-out; cursor: pointer; width: 350px; height: auto; display: flex; flex-direction: column; padding: 8px; box-sizing: border-box; position: relative;">
<!-- Card Inner Content -->
<div class="card-inner" style="flex: 1; padding: 5px; border-radius: 10px; overflow: hidden; padding-bottom: 40px;">
<!-- Adjust padding-bottom -->
<div class="project">
<!-- Project Head Section (Organization Name and Icon) -->
<div class="project-head d-flex justify-content-between align-items-center" style="margin-bottom: 5px; line-height: 1.5;">
<div class="project-title d-flex align-items-center flex-grow-1">
<!-- Organization Avatar -->
<div class="user-avatar sq lg bg-blue"
style="width: 60px; height: 60px; display: flex; justify-content: center; align-items: center;">
<img v-if="item.logo_url" :src="item.logo_url" :alt="item.organisation + ' logo'" style="width: 100%; height: 100%; object-fit: cover;">
<div v-else>
<span class="text-uppercase" style="color: white; font-weight: bold; font-size: 18px;">{{ item.organisation.slice(0,2).toUpperCase() }}</span>
</div>
</div>
<!-- Organization Name with Wrapping -->
<div class="project-info ml-2" style="flex-grow: 1;">
<h6 class="title"
style="white-space: normal; word-wrap: break-word; font-size: 16px; font-weight: bold; font-family: Helvetica; max-width: 100%; line-height: 1.5;">
{{ item.organisation }}
</h6>
</div>
</div>
<!-- Verified Status Button -->
<div class="dropdown flex-shrink-0">
<a href="#" class="btn btn-sm btn-icon" style="display: flex; align-items: center; color: green;">
<!-- Shield check icon -->
<em class="icon ni ni-shield-check" style="margin-right: 5px; font-size: 1.2rem; color: green;"></em>
</a>
</div>
</div>
<!-- Divider Line -->
<hr style="border: 0; border-top: 1px solid #ccc; margin: 10px 0;">
<!-- Thematic Areas and Subcategories with Reduced Spacing and line-height 1.5 -->
<div v-for="it in item.thematic_areas" :key="it.id" class="project-progress mt-1" style="line-height: 1.2; margin-bottom: 4px;">
<!-- Thematic Area Icon and Name -->
<span style="font-size: 12px; font-family: sans-serif; font-weight: 600; color: #333;">
{{ it.thematic_area_name }}:
</span>
<!-- Subcategories (starting on the same line, wrapping below as needed, with reduced space) -->
<span style="font-size: 12px; font-family: sans-serif; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">
<span v-for="(i, index) in it.subcategories" :key="index" style="margin-right: 4px;">
{{ i.subcategory_name }}<span v-if="index < it.subcategories.length - 1">, </span>
</span>
</span>
</div>
</div>
</div>
<!-- "Read More" Button Fixed at the Bottom Right -->
<div class="project-meta" style="position: absolute; bottom: 10px; right: 10px;">
<span class="badge badge-dim bg-blue text-white"
style="display: flex; align-items: center; padding: 5px 10px; font-size: 12px; border-radius: 5px; font-weight: bold; font-family: Helvetica;">
<em class="icon ni ni-arrow-right" style="margin-right: 5px; font-size: 1rem;"></em>
<span>Read More</span>
</span>
</div>
</div>
</a>
</div>
</div>
<!-- Pagination Controls -->
<nav>
<ul class="pagination d-flex justify-content-center">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<a @click.prevent="prevPage" href="#" class="page-link">Prev</a>
</li>
<li v-for="page in totalPages" :key="page" class="page-item" :class="{ active: currentPage === page }">
<a @click.prevent="goToPage(page)" href="#" class="page-link">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
<a @click.prevent="nextPage" href="#" class="page-link">Next</a>
</li>
</ul>
</nav>
</div>
<div v-else>
<div class="nk-content nk-content-fluid">
<div class="container-xl wide-xl">
<div class="nk-content-body">
<div class="nk-block-head nk-block-head-sm">
<!-- Centered Map Name -->
<div class="d-flex flex-column align-items-center mb-4">
<div class="nk-block-title page-title"
style="font-family: Helvetica; font-weight: bold; font-size: 30px; text-align: center;">
{{ details.map_name }}
</div>
</div>
<!-- Row for Search and Buttons aligned to the right -->
<div class="d-flex justify-content-between align-items-center">
<!-- Left space (to help center the map name above) -->
<div></div>
<!-- Search and Buttons -->
<div class="d-flex flex-row align-items-center gap-3" style="margin-left: auto;">
<!-- Modern Search Bar with Icon -->
<div class="search-bar-container"
:style="{
transition: 'width 0.3s ease-in-out',
width: searchExpanded ? '400px' : '240px',
position: 'relative',
zIndex: searchExpanded ? 1000 : 'auto', /* Keep search bar above the overlay */
}">
<input
v-model="searchTerm"
@input="handleSearch"
@focus="expandSearch"
@blur="shrinkSearch"
type="text"
class="form-control search-input"
placeholder="Search Organisation"
:style="{
padding: '10px 40px 10px 15px',
borderRadius: '25px',
border: '1px solid #ccc',
width: '100%',
transition: 'width 0.3s ease-in-out',
outline: 'none'
}">
<!-- Search Icon (inside the search bar) -->
<i class="ni ni-search search-icon"
@click="handleSearch"
style="
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
font-size: 18px;
color: #888;
"></i>
</div>
<!-- View Filtered Data Button -->
<button
style="width: 250px;"
@click="ViewFilteredTable()"
class="btn btn-sm btn-outline-info">View Filtered Data As Table</button>
<!-- Add Entity Button -->
<button @click="displayEntityModal()" href="#" class="btn btn-sm btn-outline-info d-none d-md-inline-flex">
<span>Add Entity</span>
</button>
</div>
</div>
<!-- Light Transparent Layer (visible when search bar is focused) -->
<div
v-if="searchExpanded"
@click="shrinkSearch"
class="screen-overlay"
style="
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.2);
z-index: 999;
transition: opacity 0.3s ease-in-out;
">
</div>
<!-- Inline CSS for flexbox layout -->
<style scoped>
.search-bar-container {
display: flex;
align-items: center;
position: relative;
}
.search-input {
width: 100%;
padding-left: 15px;
padding-right: 40px; /* Space for the search icon */
border-radius: 25px;
border: 1px solid #ccc;
transition: width 0.3s ease-in-out;
}
.search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
font-size: 18px;
color: #888;
}
.search-input:focus {
border-color: #007bff;
}
.screen-overlay {
z-index: 998; /* Below the search bar */
}
</style>
</div>
<div class="nk-block">
<div class="row g-gs">
<div class="col-xxl-5 col-lg-5">
<div class="d-flex">
<svg style="margin-left: -105px;" :width="radius * 2.5" :height="radius * 8"
:viewBox="\`0 0 \${radius * 2.5} \${radius * 3.5}\`">
<title class="btn btn-secondary" data-toggle="tooltip" data-placement="top" title="Tooltip on top"> {{ elementtitle }} </title>
<g :transform="\`translate(\${radius * 0.2}, \${radius * 0.2}) rotate(-90)\`">
<!-- Outer White Circle -->
<path class="outer-circle" :d="getOuterCirclePath" fill="none" stroke="#FFFFFF" stroke-width="30" />
<!-- Outer Blue Circle -->
<path class="extra-circle" :d="getExtraCirclePath" fill="none" stroke="#1A5276" stroke-width="15" />
<!-- Display SVG Elements on Click -->
<g v-if="selectedSection !== null">
<g v-for="(svgElement, index) in svgElements" :key="index">
<g class="cursor-pointer"
@mouseover="showElementName(svgElement)"
@click="showSelected(svgElement)"
:transform="getSVGElementTransform(index)">
<rect
:fill="!selectedElements.includes(svgElement) ? '#c3bdab' : svgElement.color"
width="30.27"
height="230.006"
rx="10"
transform="translate(266.6) rotate(90)">
</rect>
<ellipse
:fill="!selectedElements.includes(svgElement) ? '#c3bdab' : svgElement.color"
cx="12.5"
cy="12"
rx="12.5"
ry="12"
transform="translate(0 3)">
</ellipse>
<rect
:fill="!selectedElements.includes(svgElement) ? '#c3bdab' : svgElement.color"
width="30"
height="6"
transform="translate(12 12)">
</rect>
<text
fill="white"
transform="translate(48 23)"
font-size="18px">
<tspan x="0" y="0">{{ limitText(svgElement.subcategory, 20) }}</tspan>
</text>
</g>
</g>
</g>
<!-- Main Half Circles -->
<g v-for="(item, index) in items" :key="index">
<!-- Define the path for each thematic area (half circle) -->
<path
:d="getPath(index)"
:fill="item.color"
@click="handleClick(index, item.color, item)"
style="cursor: pointer;"
/>
<!-- Define text for the thematic area, rotating 90 degrees to the right -->
<text
font-size="20px"
font-weight="bold"
font-family="Helvetica"
:transform="\`translate(\${labelRadius * Math.cos((index * angleStep + angleStep / 2) * (Math.PI / 180))}, \${labelRadius * Math.sin((index * angleStep + angleStep / 2) * (Math.PI / 180))}) rotate(90)\`"
text-anchor="middle"
fill="#FFFFFF"
dy="0.4em"
@click="handleClick(index, item.color, item)"
style="cursor: pointer;">
<tspan
v-for="(line, idx) in truncateText(item.thematic_area)"
:x="0"
:dy="idx === 0 ? '0' : '1.2em'"
>
{{ line }}
</tspan>
</text>
</g>
<!-- Circular white space around reset button -->
<path class="reset-space" :d="getResetSpacePath" fill="#FFFFFF" />
<!-- Reset Button -->
<path class="reset-button" :d="getResetPath" fill="#2980B9" @click="reset" />
<text font-size="25px" class="reset-text" :x="0" :y="radius * 0.12" text-anchor="middle" fill="white" dy="0.1em"
@click="reset">
Reset
</text>
</g>
</svg>
<div class="d-flex col-xx-4 col-lg-4 flex-column">
<div class="text-center">
<span @click="reset()" class="btn btn-sm btn-danger mb-4 mt-4 text-center">
Clear All
</span>
</div>
<div class="d-flex">
<h5 style="font-family: Helvetica, sans-serif; font-size: 18px; font-weight: bold;"> Filters </h5>
(<span class="card-title">{{ selectedElements.length }}</span>)
</div>
<ul style="
max-height: calc(8 * 50px);
overflow-y: auto;
padding: 0;
margin: 0;
list-style-type: none;">
<div v-for="(item, index) in selectedElements" :key="index">
<li style="
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
max-width: 90%;
padding: 0.5rem 0.5rem 0.5rem 14px;
margin: 5px;
font-family: Tahoma;
font-size: 15px;
line-height: 20px;
border-radius: 10px;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
color: #fff;
padding-right: 40px;
box-sizing: border-box;"
class="d-flex" role="button"
:style="{ color: 'white', backgroundColor: item.color }">
<span :title="item.subcategory" style="white-space: normal;">
{{ item.subcategory }}
</span>
<svg
style="width: 20px;
height: 20px;
border-radius: 10px;
background-color: rgba(232, 232, 232, 0.8);
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #000;
font-weight: 700;
font-size: 20px;
cursor: pointer;"
@click="removeItem(index)"
xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</li>
</div>
</ul>
</div>
</div>
</div>
<div class="col-xxl-7 col-lg-6">
<div class="row">
<div v-if="paginateElements.length === 0">
<div style="border-radius: 15px; width: 100%; max-width: 600px; margin: auto; background-color: #fff; padding: 30px; transition: transform 0.3s ease-in-out; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center;">
<!-- Icon Section with Animation -->
<div style="width: 120px; height: 120px; background-color: #e0f7fa; border-radius: 50%; display: flex; justify-content: center; align-items: center; margin: 0 auto 20px auto; animation: pulse 1.5s infinite;">
<em class="ni ni-alert-circle" style="font-size: 56px; color: #00796b;"></em>
</div>
<!-- Heading -->
<h2 style="color: #333; font-weight: bold; margin-bottom: 10px; font-size: 24px;">No Results Found</h2>
<!-- Message -->
<span style="font-size: 16px; color: #6c757d;">Make sure everything is spelled correctly or try different keywords.</span>
<!-- Hover Effect -->
<div style="transition: transform 0.3s ease-in-out;">
</div>
<!-- Inline CSS for keyframes animation -->
<style>
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
</style>
</div>
</div>
<div id="app" class="container-fluid">
<!-- Main Card Section -->
<div class="row d-flex justify-content-between">
<!-- Loop through paginated elements to create cards -->
<div class="col-lg-5 col-md-6 col-sm-12 mb-3" v-for="item in paginateElements" :key="item.id">
<a href="#"
@click.prevent="openEntityModal(item)"
class="card-link"
@mouseover="zoomIn"
@mouseout="zoomOut"
style="display: block; text-decoration: none; color: inherit; transition: transform 0.3s ease-in-out; width: 100%; height: 100%;">
<!-- Card Structure with Compact Layout -->
<div class="card card-bordered h-100"
style="transition: transform 0.3s ease-in-out; cursor: pointer; display: flex; flex-direction: column; padding: 8px; box-sizing: border-box; position: relative;">
<!-- Card Inner Content -->
<div class="card-inner" style="flex: 1; padding: 5px; border-radius: 10px; overflow: hidden; padding-bottom: 40px;">
<div class="project">
<!-- Project Head Section (Organization Name and Icon) -->
<div class="project-head d-flex justify-content-between align-items-center" style="margin-bottom: 5px;">
<div class="project-title d-flex align-items-center flex-grow-1">
<!-- Organization Avatar -->
<div class="user-avatar sq lg bg-blue"
style="width: 60px; height: 60px; display: flex; justify-content: center; align-items: center;">
<img v-if="item.logo_url" :src="item.logo_url" :alt="item.organisation + ' logo'" style="width: 100%; height: 100%; object-fit: cover;">
<div v-else>
<span class="text-uppercase" style="color: white; font-weight: bold; font-size: 18px;">{{ item.organisation.slice(0,2).toUpperCase() }}</span>
</div>
</div>
<!-- Organization Name with Wrapping -->
<div class="project-info ml-2" style="flex-grow: 1;">
<h6 class="title"
style="white-space: normal; word-wrap: break-word; font-size: 16px; font-weight: bold;">
{{ item.organisation }}
</h6>
</div>
</div>
<!-- Verified Status Button -->
<div class="dropdown flex-shrink-0">
<a href="#" class="btn btn-sm btn-icon" style="display: flex; align-items: center; color: green;">
<em class="icon ni ni-shield-check" style="margin-right: 5px; font-size: 1.2rem;"></em>
</a>
</div>
</div>
<hr style="border: 0; border-top: 1px solid #ccc; margin: 10px 0;">
<!-- Thematic Areas and Subcategories with Reduced Spacing and line-height 1.5 -->
<div v-for="it in item.thematic_areas" :key="it.id" class="project-progress mt-1" style="line-height: 1.2; margin-bottom: 4px;">
<span style="font-size: 12px; font-weight: 600; color: #333;">{{ it.thematic_area_name }}:</span>
<span style="font-size: 12px; white-space: normal; overflow-wrap: break-word;">
<span v-for="(i, index) in it.subcategories" :key="index" style="margin-right: 4px;">
{{ i.subcategory_name }}<span v-if="index < it.subcategories.length - 1">, </span>
</span>
</span>
</div>
</div>
</div>
<!-- "Read More" Button Fixed at the Bottom Right -->
<div class="project-meta" style="position: absolute; bottom: 10px; right: 10px;">
<span class="badge badge-dim bg-blue text-white"
style="display: flex; align-items: center; padding: 5px 10px; font-size: 12px; border-radius: 5px; font-weight: bold;">
<em class="icon ni ni-arrow-right" style="margin-right: 5px; font-size: 1rem;"></em>
<span>Read More</span>
</span>
</div>
</div>
</a>
</div>
</div>
<!-- Pagination Controls -->
<nav>
<ul class="pagination d-flex justify-content-center">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<a @click.prevent="prevPage" class="page-link" href="#" tabindex="-1" aria-disabled="currentPage === 1">Prev</a>
</li>
<li v-for="page in totalPages" :key="page" class="page-item" :class="{ active: currentPage === page }" :aria-current="currentPage === page ? 'page' : false">
<a @click.prevent="goToPage(page)" class="page-link" href="#">
{{ page }} <span v-if="currentPage === page" class="visually-hidden">(current)</span>
</a>
</li>
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
<a @click.prevent="nextPage" class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`,
}).mount("#app");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment