new: implement like counter for posts
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules
|
|||||||
_site
|
_site
|
||||||
.DS_Store
|
.DS_Store
|
||||||
src/_data/views.json
|
src/_data/views.json
|
||||||
|
src/_data/likes.json
|
||||||
|
@@ -14,8 +14,8 @@ services:
|
|||||||
- 3000:3000
|
- 3000:3000
|
||||||
volumes:
|
volumes:
|
||||||
# Persist the view count data in a named volume.
|
# Persist the view count data in a named volume.
|
||||||
- views_data:/app/_data
|
- data:/app/_data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
views_data: null
|
data: null
|
||||||
networks: {}
|
networks: {}
|
||||||
|
@@ -21,9 +21,23 @@ layout: "layout.njk"
|
|||||||
<i class="fas fa-eye mr-2"></i>
|
<i class="fas fa-eye mr-2"></i>
|
||||||
<span class="view-count" data-view-count data-slug="{{ page.fileSlug }}">...</span>
|
<span class="view-count" data-view-count data-slug="{{ page.fileSlug }}">...</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
</article>
|
</article>
|
||||||
|
<script src="/js/like-button.js"></script>
|
||||||
<script src="/js/view-counter.js"></script>
|
<script src="/js/view-counter.js"></script>
|
||||||
<div id="bottom"></div>
|
<div id="bottom"></div>
|
||||||
|
@@ -182,3 +182,18 @@ article ol {
|
|||||||
article li {
|
article li {
|
||||||
margin-bottom: 0.5rem;
|
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);
|
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 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);
|
fs.ensureFileSync(dbPath);
|
||||||
const initialData = fs.readFileSync(dbPath, 'utf8');
|
const initialData = fs.readFileSync(dbPath, 'utf8');
|
||||||
if (initialData.length === 0) {
|
if (initialData.length === 0) {
|
||||||
fs.writeJsonSync(dbPath, {});
|
fs.writeJsonSync(dbPath, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fs.ensureFileSync(likesDbPath);
|
||||||
|
const initialLikesData = fs.readFileSync(likesDbPath, 'utf8');
|
||||||
|
if (initialLikesData.length === 0) {
|
||||||
|
fs.writeJsonSync(likesDbPath, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
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
|
// POST: Increment the view count for a specific post slug
|
||||||
app.post('/api/views/:slug', async (req, res) => {
|
app.post('/api/views/:slug', async (req, res) => {
|
||||||
try {
|
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) ---
|
// --- 404 Handler (Catch-All) ---
|
||||||
// This MUST be the last route or middleware added.
|
// This MUST be the last route or middleware added.
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
Reference in New Issue
Block a user