feat: use last visitors to show recent clicks

This commit is contained in:
dusk 2025-01-16 13:16:01 +03:00
parent da8485f20f
commit dc04c39bd3
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
5 changed files with 47 additions and 23 deletions

View File

@ -59,7 +59,7 @@
class=" class="
window-titlebar p-1 border-ralsei-white border-8 window-titlebar p-1 border-ralsei-white border-8
bg-gradient-to-l from-ralsei-pink-neon to-ralsei-black to-75% bg-gradient-to-l from-ralsei-pink-neon to-ralsei-black to-75%
{!isOnMobile ? "cursor-move" : ""} uppercase {!isOnMobile ? "cursor-move" : ""}
" "
style="border-style: hidden hidden ridge hidden;" style="border-style: hidden hidden ridge hidden;"
> >

View File

@ -2,15 +2,15 @@ import { env } from "$env/dynamic/private";
import { scopeCookies } from "$lib"; import { scopeCookies } from "$lib";
import type { Cookies } from "@sveltejs/kit"; import type { Cookies } from "@sveltejs/kit";
import { existsSync, readFileSync, writeFileSync } from "fs"; import { existsSync, readFileSync, writeFileSync } from "fs";
import { nanoid } from "nanoid";
import { get, writable } from "svelte/store"; import { get, writable } from "svelte/store";
const visitCountFile = `${env.WEBSITE_DATA_DIR}/visitcount` const visitCountFile = `${env.WEBSITE_DATA_DIR}/visitcount`
const visitCount = writable(parseInt(existsSync(visitCountFile) ? readFileSync(visitCountFile).toString() : '0')); const visitCount = writable(parseInt(existsSync(visitCountFile) ? readFileSync(visitCountFile).toString() : '0'))
type Visitor = { since: number } type Visitor = { count: number, since: number }
const lastVisitors = writable<Visitor[]>([]); const lastVisitors = writable<Map<string, Visitor>>(new Map())
const MAX_VISITORS = 10 const VISITOR_EXPIRY_SECONDS = 60 * 60 * 1
const VISITOR_EXPIRY_SECONDS = 60 * 30 // half an hour is reasonable
export const incrementVisitCount = (request: Request, cookies: Cookies) => { export const incrementVisitCount = (request: Request, cookies: Cookies) => {
let currentVisitCount = get(visitCount) let currentVisitCount = get(visitCount)
@ -35,31 +35,45 @@ export const incrementVisitCount = (request: Request, cookies: Cookies) => {
} }
export const addLastVisitor = (request: Request, cookies: Cookies) => { export const addLastVisitor = (request: Request, cookies: Cookies) => {
let visitors = get(lastVisitors)
console.log(visitors)
visitors = _addLastVisitor(visitors, request, cookies)
lastVisitors.set(visitors)
return visitors
}
// why not use this for incrementVisitCount? cuz i wanna have separate visit counts (one per hour and one per day, per hour being recent visitors)
const _addLastVisitor = (visitors: Map<string, Visitor>, request: Request, cookies: Cookies) => {
const currentTime = Date.now() const currentTime = Date.now()
let visitors = get(lastVisitors).filter( // filter out old entries
(value) => { return currentTime - value.since > 1000 * VISITOR_EXPIRY_SECONDS } visitors = new Map(
visitors.entries().filter(
([_, value]) =>
{ return currentTime - value.since < 1000 * VISITOR_EXPIRY_SECONDS }
)
) )
// check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots)
if (isBot(request)) { return visitors } if (isBot(request)) { return visitors }
const scopedCookies = scopeCookies(cookies, '/') const scopedCookies = scopeCookies(cookies, '/')
// parse the last visit timestamp from cookies if it exists // parse the last visit timestamp from cookies if it exists
const visitorTimestamp = parseInt(scopedCookies.get('visitorTimestamp') || "0") let visitorId = scopedCookies.get('visitorId') || ""
// get unix timestamp // if no such id exists, create one and assign it to the client
const timeSinceVisit = currentTime - visitorTimestamp if (! visitors.has(visitorId)) {
// check if this is the first time a client is visiting or if an hour has passed since they last visited visitorId = nanoid()
if (visitorTimestamp === 0 || timeSinceVisit > 1000 * VISITOR_EXPIRY_SECONDS) { scopedCookies.set('visitorId', visitorId)
visitors.push({ since: currentTime }) console.log(`new client id ${visitorId}`)
if (visitors.length > MAX_VISITORS) { visitors.shift() }
// update the cookie with the current timestamp
scopedCookies.set('visitorTimestamp', currentTime.toString())
} }
// update the entry
let visitorEntry = visitors.get(visitorId) || {count: 0, since: 0}
visitorEntry.count += 1
visitorEntry.since = currentTime
visitors.set(visitorId, visitorEntry);
return visitors return visitors
} }
const isBot = (request: Request) => { const isBot = (request: Request) => {
const ua = request.headers.get('user-agent') const ua = request.headers.get('user-agent')
return ua ? ua.toLowerCase().match(/(bot|crawl|spider|walk|fetch|scrap|proxy|image)/) !== null : true return ua ? ua.toLowerCase().match(/(bot|crawl|spider|walk|fetch|scrap|proxy|image)/) !== null : true
} }
export const notifyDarkVisitors = (url: URL, request: Request) => { export const notifyDarkVisitors = (url: URL, request: Request) => {

View File

@ -1,5 +1,5 @@
import { testUa } from '$lib/robots.js'; import { testUa } from '$lib/robots.js';
import { incrementVisitCount, notifyDarkVisitors } from '$lib/visits.js'; import { addLastVisitor, incrementVisitCount, notifyDarkVisitors } from '$lib/visits.js';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
export const csr = true; export const csr = true;
@ -18,5 +18,6 @@ export async function load({ request, cookies, url }) {
return { return {
route: url.pathname, route: url.pathname,
visitCount: incrementVisitCount(request, cookies), visitCount: incrementVisitCount(request, cookies),
lastVisitors: addLastVisitor(request, cookies),
} }
} }

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import getTitle from '$lib/getTitle'; import getTitle from '$lib/getTitle';
import type { Visitor } from 'svelte/types/compiler/interfaces';
import NavButton from '../components/navButton.svelte'; import NavButton from '../components/navButton.svelte';
import Tooltip from '../components/tooltip.svelte'; import Tooltip from '../components/tooltip.svelte';
import Window from '../components/window.svelte'; import Window from '../components/window.svelte';
@ -41,6 +42,10 @@
$: title = getTitle(data.route); $: title = getTitle(data.route);
$: recentVisitCount = data.lastVisitors.values().reduce(
(total, visitor) => { return total + visitor.count; }, 0
)
const svgSquiggles = [[2], [3], [2], [3], [1]]; const svgSquiggles = [[2], [3], [2], [3], [1]];
</script> </script>
@ -140,7 +145,7 @@
<slot /> <slot />
</div> </div>
<nav class="w-full min-h-[5vh] max-h-[6vh] fixed bottom-0 z-[999] bg-ralsei-black overflow-visible uppercase"> <nav class="w-full min-h-[5vh] max-h-[6vh] fixed bottom-0 z-[999] bg-ralsei-black overflow-visible">
<div <div
class=" class="
max-w-full max-h-fit p-1 z-[999] max-w-full max-h-fit p-1 z-[999]
@ -160,14 +165,17 @@
<div class="hidden md:block grow" /> <div class="hidden md:block grow" />
<div class="navbox"> <div class="navbox">
<a title="previous site" class="hover:underline" href="https://xn--sr8hvo.ws/previous"></a> <a title="previous site" class="hover:underline" href="https://xn--sr8hvo.ws/previous"></a>
<a class="hover:underline" href="https://xn--sr8hvo.ws">IndieWeb 🕸💍</a> <a class="hover:underline" href="https://xn--sr8hvo.ws">indieweb 🕸💍</a>
<a title="next site" class="hover:underline" href="https://xn--sr8hvo.ws/next"></a> <a title="next site" class="hover:underline" href="https://xn--sr8hvo.ws/next"></a>
</div> </div>
<Tooltip> <Tooltip>
<svelte:fragment slot="tooltipContent"> <svelte:fragment slot="tooltipContent">
<img class="min-w-64" style="image-rendering: crisp-edges pixelated;" alt="visits" src="https://count.getloli.com/@yusdacrawebsite?name=yusdacrawebsitetest&theme=booru-lewd&padding=5&offset=0&align=center&scale=1&pixelated=1&darkmode=0&num={data.visitCount}"/> <p class="font-monospace">
<nobr>total visits = <span class="text-ralsei-green-light text-shadow-green">{data.visitCount.toString().padStart(10, "0")}</span></nobr>
<nobr>unique recent visits = <span class="text-ralsei-green-light text-shadow-green">{data.lastVisitors.size.toString().padStart(2, "0")}</span></nobr>
</p>
</svelte:fragment> </svelte:fragment>
<div class="navbox"><p><span class="text-ralsei-green-light text-shadow-green">{data.visitCount}</span> visit(s)</p></div> <div class="navbox"><p><span class="text-ralsei-green-light text-shadow-green">{recentVisitCount}</span> recent clicks</p></div>
</Tooltip> </Tooltip>
{#if isRoute("entries") || isRoute("log")} {#if isRoute("entries") || isRoute("log")}
<div class="navbox !gap-1"> <div class="navbox !gap-1">

View File

@ -19,6 +19,7 @@
<img <img
width="150" width="150"
height="20" height="20"
title="banners from https://blinkies.cafe/"
alt="banner" alt="banner"
class=" class="
{hideIfMobile ? 'hidden' : ''} sm:inline w-[150px] [height:20px] {hideIfMobile ? 'hidden' : ''} sm:inline w-[150px] [height:20px]