new: implement like counter for posts
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules
|
||||
_site
|
||||
.DS_Store
|
||||
src/_data/views.json
|
||||
src/_data/likes.json
|
||||
|
@@ -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: {}
|
||||
|
@@ -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>
|
||||
|
@@ -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
57
src/js/like-button.js
Normal 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();
|
||||
});
|
@@ -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) => {
|
||||
|
Reference in New Issue
Block a user