I was thinking of something fun and interactive to add to my portfolio, and a clickable counter that visitors could use to leave a little "I was here" message felt like a great idea. It’s simple, engaging, and a great way to make your website feel a bit more personal.
If this sounds like something you'd like to create, this guide will walk you through it step by step. Let’s dive in!
1. Setting Up JSONBin
We need a place to store the total number of clicks and the IP addresses of visitors who have clicked (so they can’t click more than once). For this, we’ll use JSONBin.io, a free online service that lets you store and retrieve JSON data.
Follow these steps to set it up:
- Go to JSONBin.io and sign up for a free account.
- Create a new bin with this initial data:
{
"totalClicks": 0,
"clickedIPs": []
}
- After saving, copy the Bin ID from the bin details page.
- Go to your account settings and generate an API Key.
- Replace
YOUR_BIN_ID
andYOUR_API_KEY
in the JavaScript code with your actual Bin ID and API Key.
This will act as our backend storage, keeping track of how many times the button has been clicked and by whom.
2. The HTML: Building the Counter
Now, let’s set up the HTML structure for our view counter. We'll use a simple button with an eye icon to make it visually engaging.
<div class="eye-counter">
<button class="eye-button" aria-label="View counter">
<svg viewBox="0 0 24 24" width="24" height="24" class="eye-icon">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
</svg>
<span class="view-count">0</span>
</button>
</div>
3. The CSS: Styling the Counter
To make our counter look clean and centered, we’ll add some simple styles:
.eye-counter {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.eye-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 20px;
background-color: rgba(255, 255, 255, 0.1);
color: #333;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.eye-button:hover {
transform: scale(1.05);
background-color: rgba(255, 255, 255, 0.2);
}
.eye-icon {
fill: currentColor;
}
.view-count {
font-size: 1rem;
font-weight: bold;
}
4. Bringing It to Life with JavaScript
Now for the main event, making the counter work.
Here’s the JavaScript, broken down into functions to keep it simple and easy to follow:
Step 1: Retrieving the Visitor’s IP Address
Before we start counting clicks, we need a way to identify unique visitors. The best way to do this is by getting each visitor's IP address (a unique identifier assigned to every device connected to the internet).
Here’s the function that does that:
// Function to get the visitor's IP address
async function getVisitorIP() {
try {
const response = await fetch('https://api.ipify.org?format=json');
const data = await response.json();
return data.ip;
} catch (error) {
console.error('Error fetching IP:', error);
return null;
}
}
Explanation
-
We make a request to
https://api.ipify.org?format=json
- This is an API that returns your device’s public IP address in JSON format
- Example response:
{ "ip": "192.168.1.1" }
-
We wait for the API to respond using
await response.json()
- This converts the response into an object we can use in our code.
-
We return the IP address (
data.ip
)- If the request is successful, the function will return something like
"192.168.1.1"
.
- If the request is successful, the function will return something like
-
Error Handling
- If there’s an issue (like no internet connection), we log an error and return
null
instead of breaking the code.
- If there’s an issue (like no internet connection), we log an error and return
By using this function, we can uniquely track visitors.
Step 2: Fetching and Updating the Click Count
Now that we have a way to identify unique visitors, we need a place to store the number of clicks and the IP addresses of visitors who have already clicked.
For this, we’ll use our JSONBin already created
Fetching the Click Count
const BIN_URL = 'https://api.jsonbin.io/v3/b/YOUR_BIN_ID';
const API_KEY = 'YOUR_API_KEY';
async function fetchBinData() {
const response = await fetch(BIN_URL, { headers: { 'X-Master-Key': API_KEY } });
const result = await response.json();
return result.record;
}
Explanation
-
BIN_URL
– This is the unique URL where our data is stored in JSONBin. -
API_KEY
– A secret key that allows us to access and modify the data. -
We send a request to JSONBin using
fetch()
- The
headers
section includes the API key for authentication. - If the request is successful, we get back an object like this:
{ "totalClicks": 25, "clickedIPs": ["192.168.1.1", "203.0.113.5"] }
- The
-
We return
result.record
- This extracts and returns the stored click count and IP addresses.
Updating the Click Count
Now, we need a function to update the stored data when someone clicks the counter.
async function updateBinData(data) {
await fetch(BIN_URL, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Master-Key': API_KEY
},
body: JSON.stringify(data)
});
}
Explanation
- We use
fetch()
to send a request to JSONBin. - The
method: 'PUT'
part tells JSONBin to replace the existing data. - We convert the new data into JSON format using
JSON.stringify(data)
. - If successful, the click count and IP list get updated.
With these two functions, we can now fetch the existing counter value and update it when needed.
Step 3: Handling Click Events
We need to:
- Check if the visitor has already clicked (using their IP).
- If not, increase the count and update JSONBin.
- Prevent multiple clicks from the same visitor.
Here’s the full implementation:
document.addEventListener('DOMContentLoaded', async () => {
const eyeButton = document.querySelector('.eye-button');
const viewCount = document.querySelector('.view-count');
const visitorIP = await getVisitorIP();
if (!visitorIP) {
eyeButton.classList.add('disabled');
return;
}
const binData = await fetchBinData();
viewCount.textContent = binData.totalClicks;
if (binData.clickedIPs.includes(visitorIP)) {
eyeButton.classList.add('disabled');
}
eyeButton.addEventListener('click', async () => {
if (!eyeButton.classList.contains('disabled')) {
binData.totalClicks += 1;
binData.clickedIPs.push(visitorIP);
await updateBinData(binData);
viewCount.textContent = binData.totalClicks;
eyeButton.classList.add('disabled');
}
});
});
Explanation
-
Wait for the page to load
- The
DOMContentLoaded
event ensures our code only runs after the page has fully loaded.
- The
-
Get the visitor’s IP address
- We call
getVisitorIP()
to determine if the visitor is unique.
- We call
-
If the IP can’t be retrieved, disable the button
- This prevents errors in case the IP-fetching API fails.
-
Fetch stored click data from JSONBin
- The function
fetchBinData()
retrieves the current number of clicks and IPs that have already clicked.
- The function
-
Update the display with the current click count
- We set
viewCount.textContent = binData.totalClicks;
so users see the latest number.
- We set
-
Check if the visitor has already clicked
- If their IP exists in
binData.clickedIPs
, we disable the button so they can’t click again.
- If their IP exists in
-
Handle the click event
- If the visitor hasn’t clicked before, we:
- Increase the count (
binData.totalClicks += 1
). - Add their IP to the list (
binData.clickedIPs.push(visitorIP)
). - Save the updated data to JSONBin (
await updateBinData(binData)
). - Update the displayed number of clicks.
- Disable the button to prevent another click.
- Increase the count (
- If the visitor hasn’t clicked before, we:
Final Thoughts
By following these structured steps, we ensure:
- Visitors can only click once(tracked by IP).
- Clicks are stored and updated in real-time using JSONBin.
- The counter remains interactive and visually engaging.
That’s it! You’ve built a fun and interactive view counter. It’s a simple way to engage visitors and add a touch of personality to your site.
Feel free to visit my portfolio to see it in action
Top comments (0)