Public Access
1
0

new: implement like counter for posts

This commit is contained in:
2025-08-25 17:23:35 -04:00
parent d5f7d68c42
commit bf6b72769b
6 changed files with 146 additions and 4 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ node_modules
_site
.DS_Store
src/_data/views.json
src/_data/likes.json

View File

@@ -14,8 +14,8 @@ services:
- 3000:3000
volumes:
# Persist the view count data in a named volume.
- views_data:/app/_data
- data:/app/_data
restart: unless-stopped
volumes:
views_data: null
data: null
networks: {}

View File

@@ -21,9 +21,23 @@ layout: "layout.njk"
<i class="fas fa-eye mr-2"></i>
<span class="view-count" data-view-count data-slug="{{ page.fileSlug }}">...</span>
</div>
<span class="hidden md:inline mx-2 text-gray-600">|</span>
<div class="like-wrapper">
<i class="fas fa-heart like-icon mr-2" data-like-button data-slug="{{ page.fileSlug }}"></i>
<span class="like-count" data-like-count data-slug="{{ page.fileSlug }}">0</span>
</div>
<!--- Comments Section -->
<!---
<span class="hidden md:inline mx-2 text-gray-600">|</span>
<div class="flex items-center">
<i class="fas fa-comments mr-2"></i>
<a href="#disqus_thread">Comments</a>
</div>
-->
</div>
<br/>
{{ content | safe }}
</article>
<script src="/js/like-button.js"></script>
<script src="/js/view-counter.js"></script>
<div id="bottom"></div>

View File

@@ -182,3 +182,18 @@ article ol {
article li {
margin-bottom: 0.5rem;
}
.like-icon {
cursor: pointer;
transition: color 0.2s ease-in-out;
}
.like-icon:hover {
color: #fff;
transform: scale(1.15);
}
.like-icon.liked {
cursor: pointer;
color: #e2264d;
}
.like-icon.liked:hover {
transform: scale(1.15);
}

57
src/js/like-button.js Normal file
View File

@@ -0,0 +1,57 @@
document.addEventListener('DOMContentLoaded', () => {
const likeIcon = document.querySelector('[data-like-button]');
if (!likeIcon) return;
const slug = likeIcon.dataset.slug;
const likeCountSpan = document.querySelector(`[data-like-count][data-slug="${slug}"]`);
const storageKey = `liked-${slug}`;
// Function to update the UI based on liked state and count
const updateUI = (isLiked, count) => {
if (likeCountSpan) {
likeCountSpan.textContent = count;
}
if (isLiked) {
likeIcon.classList.add('liked');
} else {
likeIcon.classList.remove('liked');
}
};
// Fetch and display the initial like count and set the initial state
const getInitialState = async () => {
try {
const response = await fetch(`/api/likes/${slug}`);
if (!response.ok) return;
const data = await response.json();
const isLiked = localStorage.getItem(storageKey) === 'true';
updateUI(isLiked, data.count || 0);
} catch (error) {
console.error('Error fetching likes:', error);
}
};
// Add click event listener to the icon
likeIcon.addEventListener('click', async () => {
const isCurrentlyLiked = likeIcon.classList.contains('liked');
const method = isCurrentlyLiked ? 'DELETE' : 'POST';
try {
const response = await fetch(`/api/likes/${slug}`, { method });
if (!response.ok) return;
const data = await response.json();
const isNowLiked = !isCurrentlyLiked;
// Update localStorage to remember the user's choice
localStorage.setItem(storageKey, isNowLiked);
// Update the button and count on the page
updateUI(isNowLiked, data.count);
} catch (error) {
console.error('Error submitting like/unlike:', error);
}
});
// Load the initial state when the page loads
getInitialState();
});

View File

@@ -8,16 +8,24 @@ const port = 3000;
app.set('trust proxy', true);
// Path to the file where view counts will be stored
// Path to the file where metadata counts will be stored
const dbPath = path.join(__dirname, '_data', 'views.json');
const likesDbPath = path.join(__dirname, '_data', 'likes.json');
// Ensure the data directory and the views.json file exist
// Ensure the data directory and the .json files exist
fs.ensureFileSync(dbPath);
const initialData = fs.readFileSync(dbPath, 'utf8');
if (initialData.length === 0) {
fs.writeJsonSync(dbPath, {});
}
fs.ensureFileSync(likesDbPath);
const initialLikesData = fs.readFileSync(likesDbPath, 'utf8');
if (initialLikesData.length === 0) {
fs.writeJsonSync(likesDbPath, {});
}
app.use(cors());
app.use(express.json());
@@ -39,6 +47,20 @@ app.get('/api/views/:slug', async (req, res) => {
}
});
// GET: Fetch the like count for a specific post slug
app.get('/api/likes/:slug', async (req, res) => {
try {
const { slug } = req.params;
const likes = await fs.readJson(likesDbPath);
const count = likes[slug] || 0;
res.json({ count });
} catch (error) {
console.error('Error reading like count:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// POST: Increment the view count for a specific post slug
app.post('/api/views/:slug', async (req, res) => {
try {
@@ -53,6 +75,39 @@ app.post('/api/views/:slug', async (req, res) => {
}
});
// POST: Increment the like count for a specific post slug
app.post('/api/likes/:slug', async (req, res) => {
try {
const { slug } = req.params;
const likes = await fs.readJson(likesDbPath);
likes[slug] = (likes[slug] || 0) + 1;
await fs.writeJson(likesDbPath, likes);
res.json({ count: likes[slug] });
} catch (error) {
console.error('Error updating like count:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// DELETE: Decrement the like count for a specific post slug (unlike)
app.delete('/api/likes/:slug', async (req, res) => {
try {
const { slug } = req.params;
const likes = await fs.readJson(likesDbPath);
// Ensure the count doesn't go below zero
if (likes[slug] > 0) {
likes[slug] -= 1;
} else {
likes[slug] = 0;
}
await fs.writeJson(likesDbPath, likes);
res.json({ count: likes[slug] });
} catch (error) {
console.error('Error updating like count:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// --- 404 Handler (Catch-All) ---
// This MUST be the last route or middleware added.
app.use((req, res, next) => {