diff --git a/.gitignore b/.gitignore
index bc2cdd3..0e517dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ node_modules
_site
.DS_Store
src/_data/views.json
+src/_data/likes.json
diff --git a/docker-compose.yml b/docker-compose.yml
index 6f7c7c1..f208baf 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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: {}
diff --git a/src/_includes/post.njk b/src/_includes/post.njk
index 536889f..2f9e907 100644
--- a/src/_includes/post.njk
+++ b/src/_includes/post.njk
@@ -21,9 +21,23 @@ layout: "layout.njk"
...
+ |
+
+
+ 0
+
+
+
{{ content | safe }}
+
diff --git a/src/css/style.css b/src/css/style.css
index c07b0ca..332c3db 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -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);
+}
diff --git a/src/js/like-button.js b/src/js/like-button.js
new file mode 100644
index 0000000..1864e11
--- /dev/null
+++ b/src/js/like-button.js
@@ -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();
+});
\ No newline at end of file
diff --git a/src/server.js b/src/server.js
index ace6419..cb3be7d 100644
--- a/src/server.js
+++ b/src/server.js
@@ -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) => {