story-az-card-kit
Version:
A flexible and customizable story card component with multiple layouts
372 lines (315 loc) • 9.86 kB
JavaScript
/**
* StoryCardManager - A utility class for managing multiple story cards
*/
export class StoryCardManager {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.stories = options.stories || [];
this.currentStories = [...this.stories];
this.options = {
layout: 'grid',
theme: 'default',
columns: 3,
gap: 'medium',
itemsPerPage: 12,
currentPage: 1,
sortBy: 'date',
sortOrder: 'desc',
filterTags: [],
searchQuery: '',
...options
};
this.render();
}
/**
* Add a new story to the collection
*/
addStory(story) {
this.stories.push(story);
this.currentStories = [...this.stories];
this.render();
}
/**
* Remove a story from the collection
*/
removeStory(index) {
this.stories.splice(index, 1);
this.currentStories = [...this.stories];
this.render();
}
/**
* Update a story in the collection
*/
updateStory(index, updatedStory) {
this.stories[index] = { ...this.stories[index], ...updatedStory };
this.currentStories = [...this.stories];
this.render();
}
/**
* Filter stories by tags
*/
filterByTags(tags) {
this.options.filterTags = Array.isArray(tags) ? tags : [tags];
this.applyFilters();
}
/**
* Search stories by title and description
*/
search(query) {
this.options.searchQuery = query.toLowerCase();
this.applyFilters();
}
/**
* Sort stories by a specific field
*/
sortBy(field, order = 'desc') {
this.options.sortBy = field;
this.options.sortOrder = order;
this.applyFilters();
}
/**
* Go to a specific page
*/
goToPage(page) {
this.options.currentPage = Math.max(1, Math.min(page, this.getTotalPages()));
this.render();
}
/**
* Get the next page
*/
nextPage() {
this.goToPage(this.options.currentPage + 1);
}
/**
* Get the previous page
*/
previousPage() {
this.goToPage(this.options.currentPage - 1);
}
/**
* Get total number of pages
*/
getTotalPages() {
return Math.ceil(this.currentStories.length / this.options.itemsPerPage);
}
/**
* Get current page stories
*/
getCurrentPageStories() {
const start = (this.options.currentPage - 1) * this.options.itemsPerPage;
const end = start + this.options.itemsPerPage;
return this.currentStories.slice(start, end);
}
/**
* Apply all filters and sorting
*/
applyFilters() {
let filtered = [...this.stories];
// Apply tag filtering
if (this.options.filterTags.length > 0) {
filtered = filtered.filter(story =>
story.tags && story.tags.some(tag =>
this.options.filterTags.includes(tag)
)
);
}
// Apply search filtering
if (this.options.searchQuery) {
filtered = filtered.filter(story =>
(story.title && story.title.toLowerCase().includes(this.options.searchQuery)) ||
(story.description && story.description.toLowerCase().includes(this.options.searchQuery)) ||
(story.author && story.author.toLowerCase().includes(this.options.searchQuery))
);
}
// Apply sorting
filtered.sort((a, b) => {
let aValue = a[this.options.sortBy];
let bValue = b[this.options.sortBy];
if (this.options.sortBy === 'date') {
aValue = new Date(aValue || 0);
bValue = new Date(bValue || 0);
} else {
aValue = String(aValue || '').toLowerCase();
bValue = String(bValue || '').toLowerCase();
}
if (this.options.sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
this.currentStories = filtered;
this.options.currentPage = 1; // Reset to first page when filtering
this.render();
}
/**
* Render the current state
*/
render() {
if (!this.container) return;
// Clear container
this.container.innerHTML = '';
// Create grid container
const grid = document.createElement('div');
const gridClass = `story-card-grid story-card-grid--${this.options.columns}-cols story-card-grid--gap-${this.options.gap}`;
grid.className = gridClass;
// Add story cards
const currentStories = this.getCurrentPageStories();
currentStories.forEach(story => {
const card = this.createStoryCard(story);
grid.appendChild(card);
});
this.container.appendChild(grid);
// Add pagination if needed
if (this.getTotalPages() > 1) {
this.renderPagination();
}
// Add info about current state
this.renderInfo();
}
/**
* Create a single story card
*/
createStoryCard(story) {
const card = document.createElement('div');
card.className = `story-card story-card--${this.options.layout} story-card--${this.options.theme}`;
card.setAttribute('role', 'button');
card.setAttribute('tabindex', '0');
// Add image
if (story.image) {
const imageContainer = document.createElement('div');
imageContainer.className = 'story-card__image';
const img = document.createElement('img');
img.src = story.image;
img.alt = story.title || '';
img.loading = 'lazy';
imageContainer.appendChild(img);
card.appendChild(imageContainer);
}
// Add content
const content = document.createElement('div');
content.className = 'story-card__content';
if (story.title) {
const title = document.createElement('h3');
title.className = 'story-card__title';
title.textContent = story.title;
content.appendChild(title);
}
if (story.description) {
const description = document.createElement('p');
description.className = 'story-card__description';
description.textContent = story.description;
content.appendChild(description);
}
if (story.author || story.date) {
const meta = document.createElement('div');
meta.className = 'story-card__meta';
if (story.author) {
const author = document.createElement('span');
author.className = 'story-card__author';
author.textContent = story.author;
meta.appendChild(author);
}
if (story.date) {
const date = document.createElement('time');
date.className = 'story-card__date';
date.setAttribute('datetime', story.date);
date.textContent = new Date(story.date).toLocaleDateString();
meta.appendChild(date);
}
content.appendChild(meta);
}
if (story.tags && story.tags.length > 0) {
const tags = document.createElement('div');
tags.className = 'story-card__tags';
story.tags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'story-card__tag';
tagElement.textContent = tag;
tags.appendChild(tagElement);
});
content.appendChild(tags);
}
card.appendChild(content);
// Add click handler
card.addEventListener('click', () => {
if (this.options.onCardClick) {
this.options.onCardClick(story);
}
});
return card;
}
/**
* Render pagination controls
*/
renderPagination() {
const pagination = document.createElement('div');
pagination.className = 'story-card-pagination';
const totalPages = this.getTotalPages();
const currentPage = this.options.currentPage;
// Previous button
if (currentPage > 1) {
const prevBtn = document.createElement('button');
prevBtn.textContent = '‹ Previous';
prevBtn.className = 'story-card-pagination__btn';
prevBtn.addEventListener('click', () => this.previousPage());
pagination.appendChild(prevBtn);
}
// Page numbers
for (let i = 1; i <= totalPages; i++) {
const pageBtn = document.createElement('button');
pageBtn.textContent = i;
pageBtn.className = `story-card-pagination__btn ${i === currentPage ? 'story-card-pagination__btn--active' : ''}`;
pageBtn.addEventListener('click', () => this.goToPage(i));
pagination.appendChild(pageBtn);
}
// Next button
if (currentPage < totalPages) {
const nextBtn = document.createElement('button');
nextBtn.textContent = 'Next ›';
nextBtn.className = 'story-card-pagination__btn';
nextBtn.addEventListener('click', () => this.nextPage());
pagination.appendChild(nextBtn);
}
this.container.appendChild(pagination);
}
/**
* Render info about current state
*/
renderInfo() {
const info = document.createElement('div');
info.className = 'story-card-info';
const totalStories = this.currentStories.length;
const currentPage = this.options.currentPage;
const itemsPerPage = this.options.itemsPerPage;
const start = (currentPage - 1) * itemsPerPage + 1;
const end = Math.min(currentPage * itemsPerPage, totalStories);
info.textContent = `Showing ${start}-${end} of ${totalStories} stories`;
if (this.options.searchQuery) {
info.textContent += ` matching "${this.options.searchQuery}"`;
}
if (this.options.filterTags.length > 0) {
info.textContent += ` filtered by tags: ${this.options.filterTags.join(', ')}`;
}
this.container.appendChild(info);
}
/**
* Get current state
*/
getState() {
return {
stories: this.stories,
currentStories: this.currentStories,
options: { ...this.options },
totalPages: this.getTotalPages(),
currentPageStories: this.getCurrentPageStories()
};
}
/**
* Update options and re-render
*/
updateOptions(newOptions) {
this.options = { ...this.options, ...newOptions };
this.render();
}
}