data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: update texT

dusk 0e46dc94 b4e91590

+417 -338
+46 -38
src/lib/bluesky.ts
··· 1 - import { env } from '$env/dynamic/private' 2 - import { Bot, type Post } from "@skyware/bot"; 3 - import { get, writable } from 'svelte/store' 1 + import { env } from '$env/dynamic/private'; 2 + import { Bot, type Post } from '@skyware/bot'; 3 + import { get, writable } from 'svelte/store'; 4 4 5 - const bskyClient = writable<null | Bot>(null) 5 + const bskyClient = writable<null | Bot>(null); 6 6 7 7 export const getBskyClient = async () => { 8 - let client = get(bskyClient) 9 - if (client === null) { 10 - client = await loginToBsky() 11 - bskyClient.set(client) 12 - } 13 - return client 14 - } 8 + let client = get(bskyClient); 9 + if (client === null) { 10 + client = await loginToBsky(); 11 + bskyClient.set(client); 12 + } 13 + return client; 14 + }; 15 15 16 16 const loginToBsky = async () => { 17 - const bot = new Bot({ service: "https://gaze.systems" }) 18 - await bot.login({ identifier: 'guestbook.gaze.systems', password: env.BSKY_PASSWORD ?? "" }) 19 - return bot 20 - } 17 + const bot = new Bot({ service: 'https://gaze.systems' }); 18 + await bot.login({ identifier: 'guestbook.gaze.systems', password: env.BSKY_PASSWORD ?? '' }); 19 + return bot; 20 + }; 21 21 22 - export const getUserPosts = async (did: string, count: number = 10, cursor: string | null = null) => { 23 - const client = await getBskyClient() 24 - let feedCursor: string | null | undefined = cursor; 25 - let posts: Post[] = [] 26 - // fetch requested amount of posts 27 - while (posts.length < count - 1 && (typeof feedCursor === "string" || feedCursor === null)) { 28 - let feedData = await client.getUserPosts( 29 - did, { limit: count, filter: 'posts_no_replies', cursor: feedCursor === null ? undefined : feedCursor } 30 - ) 31 - posts.push(...feedData.posts.filter((post) => post.author.did === did)) 32 - feedCursor = feedData.cursor 33 - } 34 - return { posts, cursor: feedCursor === null ? undefined : feedCursor } 35 - } 22 + export const getUserPosts = async ( 23 + did: string, 24 + count: number = 10, 25 + cursor: string | null = null 26 + ) => { 27 + const client = await getBskyClient(); 28 + let feedCursor: string | null | undefined = cursor; 29 + const posts: Post[] = []; 30 + // fetch requested amount of posts 31 + while (posts.length < count - 1 && (typeof feedCursor === 'string' || feedCursor === null)) { 32 + const feedData = await client.getUserPosts(did, { 33 + limit: count, 34 + filter: 'posts_no_replies', 35 + cursor: feedCursor === null ? undefined : feedCursor 36 + }); 37 + posts.push(...feedData.posts.filter((post) => post.author.did === did)); 38 + feedCursor = feedData.cursor; 39 + } 40 + return { posts, cursor: feedCursor === null ? undefined : feedCursor }; 41 + }; 36 42 37 - const lastPosts = writable<Post[]>([]) 43 + const lastPosts = writable<Post[]>([]); 38 44 39 45 export const updateLastPosts = async () => { 40 - try { 41 - const { posts } = await getUserPosts("did:plc:dfl62fgb7wtjj3fcbb72naae", 13) 42 - lastPosts.set(posts) 43 - } catch (err) { 44 - console.log(`can't update last posts ${err}`) 45 - } 46 - } 46 + try { 47 + const { posts } = await getUserPosts('did:plc:dfl62fgb7wtjj3fcbb72naae', 13); 48 + lastPosts.set(posts); 49 + } catch (err) { 50 + console.log(`can't update last posts ${err}`); 51 + } 52 + }; 47 53 48 - export const getLastPosts = () => { return get(lastPosts) } 54 + export const getLastPosts = () => { 55 + return get(lastPosts); 56 + };
+39 -31
src/lib/lastfm.ts
··· 1 - import { get, writable } from "svelte/store" 1 + import { get, writable } from 'svelte/store'; 2 2 3 - const GET_RECENT_TRACKS_ENDPOINT = "https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=yusdacra&api_key=da1911d405b5b37383e200b8f36ee9ec&format=json&limit=1" 3 + const GET_RECENT_TRACKS_ENDPOINT = 4 + 'https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=yusdacra&api_key=da1911d405b5b37383e200b8f36ee9ec&format=json&limit=1'; 4 5 5 6 type LastTrack = { 6 - name: string, 7 - artist: string, 8 - image: string | null, 9 - link: string, 10 - when: number, 11 - playing: boolean, 12 - } 13 - const lastTrack = writable<LastTrack | null>(null) 7 + name: string; 8 + artist: string; 9 + image: string | null; 10 + link: string; 11 + when: number; 12 + playing: boolean; 13 + }; 14 + const lastTrack = writable<LastTrack | null>(null); 14 15 15 16 export const lastFmUpdateNowPlaying = async () => { 16 - try { 17 - var resp = await (await fetch(GET_RECENT_TRACKS_ENDPOINT)).json() 18 - var track = resp.recenttracks.track[0] ?? null 19 - if (!((track['@attr'] ?? {}).nowplaying ?? null)) { 20 - throw "no nowplaying track found" 21 - } 22 - var data = { 23 - name: track.name, 24 - artist: track.artist['#text'], 25 - image: track.image[2]['#text'] ?? null, 26 - link: track.url, 27 - when: Date.now(), 28 - playing: true, 29 - } 30 - lastTrack.set(data) 31 - } catch(why) { 32 - console.log("could not fetch last fm: ", why) 33 - lastTrack.update((t) => { if (t !== null) { t.playing = false; } return t }) 34 - } 35 - } 17 + try { 18 + const resp = await (await fetch(GET_RECENT_TRACKS_ENDPOINT)).json(); 19 + const track = resp.recenttracks.track[0] ?? null; 20 + if (!((track['@attr'] ?? {}).nowplaying ?? null)) { 21 + throw 'no nowplaying track found'; 22 + } 23 + const data = { 24 + name: track.name, 25 + artist: track.artist['#text'], 26 + image: track.image[2]['#text'] ?? null, 27 + link: track.url, 28 + when: Date.now(), 29 + playing: true 30 + }; 31 + lastTrack.set(data); 32 + } catch (why) { 33 + console.log('could not fetch last fm: ', why); 34 + lastTrack.update((t) => { 35 + if (t !== null) { 36 + t.playing = false; 37 + } 38 + return t; 39 + }); 40 + } 41 + }; 36 42 37 - export const getNowPlaying = () => { return get(lastTrack) } 43 + export const getNowPlaying = () => { 44 + return get(lastTrack); 45 + };
+48 -41
src/lib/steam.ts
··· 1 - import { env } from "$env/dynamic/private"; 2 - import SGDB from "steamgriddb"; 3 - import { get, writable } from "svelte/store"; 1 + import { env } from '$env/dynamic/private'; 2 + import SGDB from 'steamgriddb'; 3 + import { get, writable } from 'svelte/store'; 4 4 5 - const STEAM_ID = "76561198106829949" 6 - const GET_PLAYER_SUMMARY_ENDPOINT = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${env.STEAM_API_KEY}&steamids=${STEAM_ID}&format=json` 5 + const STEAM_ID = '76561198106829949'; 6 + const GET_PLAYER_SUMMARY_ENDPOINT = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${env.STEAM_API_KEY}&steamids=${STEAM_ID}&format=json`; 7 7 8 8 type LastGame = { 9 - name: string, 10 - link: string, 11 - icon: string, 12 - pfp: string, 13 - when: number, 14 - playing: boolean, 15 - } 9 + name: string; 10 + link: string; 11 + icon: string; 12 + pfp: string; 13 + when: number; 14 + playing: boolean; 15 + }; 16 16 17 - const steamgriddbClient = writable<SGDB | null>(null) 18 - const lastGame = writable<LastGame | null>(null) 17 + const steamgriddbClient = writable<SGDB | null>(null); 18 + const lastGame = writable<LastGame | null>(null); 19 19 20 20 export const steamUpdateNowPlaying = async () => { 21 - var griddbClient = get(steamgriddbClient) 22 - if (griddbClient === null) { 23 - griddbClient = new SGDB(env.STEAMGRIDDB_API_KEY) 24 - steamgriddbClient.set(griddbClient) 25 - } 26 - try { 27 - var profile = (await (await fetch(GET_PLAYER_SUMMARY_ENDPOINT)).json()).response.players[0] 28 - if (!profile.gameid) { 29 - throw "no game is being played" 30 - } 31 - var icons = await griddbClient.getIconsBySteamAppId(profile.gameid, ['official', 'custom']) 32 - //console.log(icons) 33 - var game: LastGame = { 34 - name: profile.gameextrainfo, 35 - link: `https://store.steampowered.com/app/${profile.gameid}`, 36 - icon: icons[0].thumb.toString(), 37 - pfp: profile.avatarmedium, 38 - when: Date.now(), 39 - playing: true, 40 - } 41 - lastGame.set(game) 42 - } catch(why) { 43 - console.log("could not fetch steam: ", why) 44 - lastGame.update((t) => { if (t !== null) { t.playing = false; } return t }) 45 - } 46 - } 21 + let griddbClient = get(steamgriddbClient); 22 + if (griddbClient === null) { 23 + griddbClient = new SGDB(env.STEAMGRIDDB_API_KEY); 24 + steamgriddbClient.set(griddbClient); 25 + } 26 + try { 27 + const profile = (await (await fetch(GET_PLAYER_SUMMARY_ENDPOINT)).json()).response.players[0]; 28 + if (!profile.gameid) { 29 + throw 'no game is being played'; 30 + } 31 + const icons = await griddbClient.getIconsBySteamAppId(profile.gameid, ['official', 'custom']); 32 + //console.log(icons) 33 + const game: LastGame = { 34 + name: profile.gameextrainfo, 35 + link: `https://store.steampowered.com/app/${profile.gameid}`, 36 + icon: icons[0].thumb.toString(), 37 + pfp: profile.avatarmedium, 38 + when: Date.now(), 39 + playing: true 40 + }; 41 + lastGame.set(game); 42 + } catch (why) { 43 + console.log('could not fetch steam: ', why); 44 + lastGame.update((t) => { 45 + if (t !== null) { 46 + t.playing = false; 47 + } 48 + return t; 49 + }); 50 + } 51 + }; 47 52 48 - export const getLastGame = () => { return get(lastGame) } 53 + export const getLastGame = () => { 54 + return get(lastGame); 55 + };
+107 -95
src/lib/visits.ts
··· 1 - import { env } from "$env/dynamic/private"; 2 - import { scopeCookies } from "$lib"; 3 - import type { Cookies } from "@sveltejs/kit"; 4 - import { existsSync, readFileSync, writeFileSync } from "fs"; 5 - import { nanoid } from "nanoid"; 6 - import { get, writable } from "svelte/store"; 1 + import { env } from '$env/dynamic/private'; 2 + import { scopeCookies } from '$lib'; 3 + import type { Cookies } from '@sveltejs/kit'; 4 + import { existsSync, readFileSync, writeFileSync } from 'fs'; 5 + import { nanoid } from 'nanoid'; 6 + import { get, writable } from 'svelte/store'; 7 7 8 - const visitCountFile = `${env.WEBSITE_DATA_DIR}/visitcount` 9 - const visitCount = writable(parseInt(existsSync(visitCountFile) ? readFileSync(visitCountFile).toString() : '0')) 8 + const visitCountFile = `${env.WEBSITE_DATA_DIR}/visitcount`; 9 + const visitCount = writable( 10 + parseInt(existsSync(visitCountFile) ? readFileSync(visitCountFile).toString() : '0') 11 + ); 10 12 11 - type Visitor = { visits: number[] } 12 - const lastVisitors = writable<Map<string, Visitor>>(new Map()) 13 - const VISITOR_EXPIRY_SECONDS = 60 * 60 // an hour seems reasonable 13 + type Visitor = { visits: number[] }; 14 + const lastVisitors = writable<Map<string, Visitor>>(new Map()); 15 + const VISITOR_EXPIRY_SECONDS = 60 * 60; // an hour seems reasonable 14 16 15 17 export const incrementVisitCount = (request: Request, cookies: Cookies) => { 16 - let currentVisitCount = get(visitCount) 17 - // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) 18 - if (isBot(request)) { return currentVisitCount } 19 - const scopedCookies = scopeCookies(cookies, '/') 20 - // parse the last visit timestamp from cookies if it exists 21 - const visitedTimestamp = parseInt(scopedCookies.get('visitedTimestamp') || "0") 22 - // get unix timestamp 23 - const currentTime = Date.now() 24 - const timeSinceVisit = currentTime - visitedTimestamp 25 - // check if this is the first time a client is visiting or if an hour has passed since they last visited 26 - if (visitedTimestamp === 0 || timeSinceVisit > 1000 * 60 * 60 * 24) { 27 - // increment current and write to the store 28 - currentVisitCount += 1; visitCount.set(currentVisitCount) 29 - // update the cookie with the current timestamp 30 - scopedCookies.set('visitedTimestamp', currentTime.toString()) 31 - // write the visit count to a file so we can load it later again 32 - writeFileSync(visitCountFile, currentVisitCount.toString()) 33 - } 34 - return currentVisitCount 35 - } 18 + let currentVisitCount = get(visitCount); 19 + // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) 20 + if (isBot(request)) { 21 + return currentVisitCount; 22 + } 23 + const scopedCookies = scopeCookies(cookies, '/'); 24 + // parse the last visit timestamp from cookies if it exists 25 + const visitedTimestamp = parseInt(scopedCookies.get('visitedTimestamp') || '0'); 26 + // get unix timestamp 27 + const currentTime = Date.now(); 28 + const timeSinceVisit = currentTime - visitedTimestamp; 29 + // check if this is the first time a client is visiting or if an hour has passed since they last visited 30 + if (visitedTimestamp === 0 || timeSinceVisit > 1000 * 60 * 60 * 24) { 31 + // increment current and write to the store 32 + currentVisitCount += 1; 33 + visitCount.set(currentVisitCount); 34 + // update the cookie with the current timestamp 35 + scopedCookies.set('visitedTimestamp', currentTime.toString()); 36 + // write the visit count to a file so we can load it later again 37 + writeFileSync(visitCountFile, currentVisitCount.toString()); 38 + } 39 + return currentVisitCount; 40 + }; 36 41 37 42 export const addLastVisitor = (request: Request, cookies: Cookies) => { 38 - let visitors = get(lastVisitors) 39 - visitors = _addLastVisitor(visitors, request, cookies) 40 - lastVisitors.set(visitors) 41 - return visitors 42 - } 43 + let visitors = get(lastVisitors); 44 + visitors = _addLastVisitor(visitors, request, cookies); 45 + lastVisitors.set(visitors); 46 + return visitors; 47 + }; 43 48 44 49 export const getVisitorId = (cookies: Cookies) => { 45 - const scopedCookies = scopeCookies(cookies, '/') 46 - // parse the last visit timestamp from cookies if it exists 47 - return scopedCookies.get('visitorId') 48 - } 50 + const scopedCookies = scopeCookies(cookies, '/'); 51 + // parse the last visit timestamp from cookies if it exists 52 + return scopedCookies.get('visitorId'); 53 + }; 49 54 50 55 // 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) 51 56 const _addLastVisitor = (visitors: Map<string, Visitor>, request: Request, cookies: Cookies) => { 52 - const currentTime = Date.now() 53 - // filter out old entries 54 - visitors.forEach((visitor, id, map) => { 55 - if (currentTime - visitor.visits[0] > 1000 * VISITOR_EXPIRY_SECONDS) 56 - map.delete(id) 57 - else { 58 - visitor.visits = visitor.visits.filter((since) => { 59 - return currentTime - since < 1000 * VISITOR_EXPIRY_SECONDS 60 - }) 61 - map.set(id, visitor) 62 - } 63 - }) 64 - // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) 65 - if (isBot(request)) { return visitors } 66 - const scopedCookies = scopeCookies(cookies, '/') 67 - // parse the last visit timestamp from cookies if it exists 68 - let visitorId = scopedCookies.get('visitorId') || "" 69 - // if no such id exists, create one and assign it to the client 70 - if (! visitors.has(visitorId)) { 71 - visitorId = nanoid() 72 - scopedCookies.set('visitorId', visitorId) 73 - console.log(`new client visitor id ${visitorId}`) 74 - } 75 - // update the entry 76 - let visitorEntry = visitors.get(visitorId) || {visits: []} 77 - // put new visit in the front 78 - visitorEntry.visits = [currentTime].concat(visitorEntry.visits) 79 - visitors.set(visitorId, visitorEntry); 80 - return visitors 81 - } 57 + const currentTime = Date.now(); 58 + // filter out old entries 59 + visitors.forEach((visitor, id, map) => { 60 + if (currentTime - visitor.visits[0] > 1000 * VISITOR_EXPIRY_SECONDS) map.delete(id); 61 + else { 62 + visitor.visits = visitor.visits.filter((since) => { 63 + return currentTime - since < 1000 * VISITOR_EXPIRY_SECONDS; 64 + }); 65 + map.set(id, visitor); 66 + } 67 + }); 68 + // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) 69 + if (isBot(request)) { 70 + return visitors; 71 + } 72 + const scopedCookies = scopeCookies(cookies, '/'); 73 + // parse the last visit timestamp from cookies if it exists 74 + let visitorId = scopedCookies.get('visitorId') || ''; 75 + // if no such id exists, create one and assign it to the client 76 + if (!visitors.has(visitorId)) { 77 + visitorId = nanoid(); 78 + scopedCookies.set('visitorId', visitorId); 79 + console.log(`new client visitor id ${visitorId}`); 80 + } 81 + // update the entry 82 + const visitorEntry = visitors.get(visitorId) || { visits: [] }; 83 + // put new visit in the front 84 + visitorEntry.visits = [currentTime].concat(visitorEntry.visits); 85 + visitors.set(visitorId, visitorEntry); 86 + return visitors; 87 + }; 82 88 83 89 const isBot = (request: Request) => { 84 - const ua = request.headers.get('user-agent') 85 - return ua ? ua.toLowerCase().match(/(bot|crawl|spider|walk|fetch|scrap|proxy|image)/) !== null : true 86 - } 90 + const ua = request.headers.get('user-agent'); 91 + return ua 92 + ? ua.toLowerCase().match(/(bot|crawl|spider|walk|fetch|scrap|proxy|image)/) !== null 93 + : true; 94 + }; 87 95 88 96 export const notifyDarkVisitors = (url: URL, request: Request) => { 89 - fetch('https://api.darkvisitors.com/visits', { 90 - method: 'POST', 91 - headers: { 92 - authorization: `Bearer ${env.DARK_VISITORS_TOKEN}`, 93 - 'content-type': 'application/json', 94 - }, 95 - body: JSON.stringify({ 96 - request_path: url.pathname, 97 - request_method: request.method, 98 - request_headers: request.headers, 99 - }) 100 - }).catch((why) => { 101 - console.log("failed sending dark visitors analytics:", why) 102 - return null 103 - }).then(async (resp) => { 104 - if (resp !== null) { 105 - const msg = await resp.json() 106 - const host = `(${request.headers.get('host')} ${request.headers.get('x-real-ip')})` 107 - console.log(`sent visitor analytic to dark visitors: ${resp.statusText}; ${msg.message ?? ''}${host}`) 108 - } 109 - }) 110 - } 97 + fetch('https://api.darkvisitors.com/visits', { 98 + method: 'POST', 99 + headers: { 100 + authorization: `Bearer ${env.DARK_VISITORS_TOKEN}`, 101 + 'content-type': 'application/json' 102 + }, 103 + body: JSON.stringify({ 104 + request_path: url.pathname, 105 + request_method: request.method, 106 + request_headers: request.headers 107 + }) 108 + }) 109 + .catch((why) => { 110 + console.log('failed sending dark visitors analytics:', why); 111 + return null; 112 + }) 113 + .then(async (resp) => { 114 + if (resp !== null) { 115 + const msg = await resp.json(); 116 + const host = `(${request.headers.get('host')} ${request.headers.get('x-real-ip')})`; 117 + console.log( 118 + `sent visitor analytic to dark visitors: ${resp.statusText}; ${msg.message ?? ''}${host}` 119 + ); 120 + } 121 + }); 122 + };
+17 -6
src/lib/window.ts
··· 1 - import { writable } from "svelte/store"; 1 + /* eslint-disable no-useless-escape */ 2 + import { writable } from 'svelte/store'; 2 3 3 - export const highestZIndex = writable(0) 4 + export const highestZIndex = writable(0); 4 5 5 6 export const isMobile = () => { 6 - let check = false; 7 - (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor); 8 - return check; 9 - } 7 + let check = false; 8 + (function (a) { 9 + if ( 10 + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( 11 + a 12 + ) || 13 + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( 14 + a.slice(0, 4) 15 + ) 16 + ) 17 + check = true; 18 + })(navigator.userAgent || navigator.vendor); 19 + return check; 20 + };
+17 -17
src/routes/+layout.server.ts
··· 5 5 export const csr = true; 6 6 export const ssr = true; 7 7 export const prerender = false; 8 - export const trailingSlash = 'always'; 8 + export const trailingSlash = 'always'; 9 9 10 10 export async function load({ request, cookies, url }) { 11 - notifyDarkVisitors(url, request) // no await so it doesnt block load 11 + notifyDarkVisitors(url, request); // no await so it doesnt block load 12 12 13 - // block any requests if the user agent is disallowed by our robots txt 14 - if (await testUa(url.toString(), request.headers.get('user-agent') ?? "") === false) { 15 - throw error(403, "get a better user agent silly") 16 - } 13 + // block any requests if the user agent is disallowed by our robots txt 14 + if ((await testUa(url.toString(), request.headers.get('user-agent') ?? '')) === false) { 15 + throw error(403, 'get a better user agent silly'); 16 + } 17 17 18 - const lastVisitors = addLastVisitor(request, cookies) 19 - let recentVisitCount = 0 20 - for (const [_, visitor] of lastVisitors) { 21 - recentVisitCount += visitor.visits.length 22 - } 18 + const lastVisitors = addLastVisitor(request, cookies); 19 + let recentVisitCount = 0; 20 + for (const [, visitor] of lastVisitors) { 21 + recentVisitCount += visitor.visits.length; 22 + } 23 23 24 - return { 25 - route: url.pathname, 26 - visitCount: incrementVisitCount(request, cookies), 27 - lastVisitors, 28 - recentVisitCount, 29 - } 24 + return { 25 + route: url.pathname, 26 + visitCount: incrementVisitCount(request, cookies), 27 + lastVisitors, 28 + recentVisitCount 29 + }; 30 30 }
+38 -19
src/routes/+layout.svelte
··· 5 5 import '../styles/app.css'; 6 6 7 7 interface Props { 8 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 9 data: any; 9 10 children?: import('svelte').Snippet; 10 11 } ··· 73 74 <filter id="squiggly-{index}"> 74 75 <feTurbulence 75 76 id="turbulence" 76 - baseFrequency=0.03 77 + baseFrequency="0.03" 77 78 numOctaves="3" 78 79 result="noise" 79 80 seed={index} ··· 134 135 </defs> 135 136 </svg> 136 137 137 - <div class="md:h-[96vh] pb-[8vh] lg:px-[1vw] 2xl:px-[2vw] lg:pb-[3vh] lg:pt-[1vh] overflow-x-hidden [scrollbar-gutter:stable]"> 138 + <div 139 + class="md:h-[96vh] pb-[8vh] lg:px-[1vw] 2xl:px-[2vw] lg:pb-[3vh] lg:pt-[1vh] overflow-x-hidden [scrollbar-gutter:stable]" 140 + > 138 141 {@render children?.()} 139 142 </div> 140 143 ··· 152 155 {@const highlight = isRoute(item.href)} 153 156 <NavButton {highlight} {...item} /> 154 157 {#if doAddPostItem && menuIdx == 1} 155 - <NavButton highlight name={routeComponents[2]} href={data.route.slice(1)} iconUri='/icons/entry.webp'/> 158 + <NavButton 159 + highlight 160 + name={routeComponents[2]} 161 + href={data.route.slice(1)} 162 + iconUri="/icons/entry.webp" 163 + /> 156 164 {/if} 157 165 {/each} 158 166 <div class="hidden md:block grow"></div> ··· 163 171 </div> 164 172 <Tooltip> 165 173 {#snippet tooltipContent()} 166 - 167 - <p class="font-monospace"> 168 - <nobr>total visits = <span class="text-ralsei-green-light text-shadow-green">{data.visitCount.toString().padStart(9, ".")}</span></nobr> 169 - <nobr>uniq recent visits = <span class="text-ralsei-green-light text-shadow-green">{data.lastVisitors.size.toString().padStart(3, ".")}</span></nobr> 170 - </p> 171 - 172 - {/snippet} 173 - <div class="navbox"><p><span class="text-ralsei-green-light text-shadow-green">{data.recentVisitCount}</span> recent clicks</p></div> 174 + <p class="font-monospace"> 175 + <nobr 176 + >total visits = <span class="text-ralsei-green-light text-shadow-green" 177 + >{data.visitCount.toString().padStart(9, '.')}</span 178 + ></nobr 179 + > 180 + <nobr 181 + >uniq recent visits = <span class="text-ralsei-green-light text-shadow-green" 182 + >{data.lastVisitors.size.toString().padStart(3, '.')}</span 183 + ></nobr 184 + > 185 + </p> 186 + {/snippet} 187 + <div class="navbox"> 188 + <p> 189 + <span class="text-ralsei-green-light text-shadow-green">{data.recentVisitCount}</span> recent 190 + clicks 191 + </p> 192 + </div> 174 193 </Tooltip> 175 - {#if isRoute("entries") || isRoute("log")} 176 - <div class="navbox !gap-1"> 177 - rss: 178 - <a class="align-middle hover:underline" href="/entries/_rss">posts</a> 179 - / 180 - <a class="align-middle hover:underline" href="/log/_rss">log</a> 181 - </div> 194 + {#if isRoute('entries') || isRoute('log')} 195 + <div class="navbox !gap-1"> 196 + rss: 197 + <a class="align-middle hover:underline" href="/entries/_rss">posts</a> 198 + / 199 + <a class="align-middle hover:underline" href="/log/_rss">log</a> 200 + </div> 182 201 {/if} 183 202 </div> 184 203 </div> ··· 189 208 @apply flex gap-3 px-1.5 text-nowrap align-middle items-center text-center place-content-center border-ralsei-white border-4; 190 209 border-style: groove; 191 210 } 192 - </style> 211 + </style>
+105 -91
src/routes/+page.svelte
··· 3 3 import Note from '../components/note.svelte'; 4 4 import Window from '../components/window.svelte'; 5 5 import LatestStuff from './lateststuff.md'; 6 - import {renderDate, renderRelativeDate} from '$lib/dateFmt'; 6 + import { renderDate, renderRelativeDate } from '$lib/dateFmt'; 7 7 8 8 interface Props { 9 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 10 data: any; 10 11 } 11 12 ··· 16 17 <div class="flex flex-col gap-y-2 lg:gap-y-0 mx-auto"> 17 18 <Window title="readme?" iconUri="/icons/question.webp" removePadding> 18 19 <div class="flex flex-col p-1.5 gap-1.5 prose prose-ralsei prose-img:m-0 leading-none"> 19 - <div 20 - class="flex flex-row gap-3 mx-auto bg-ralsei-black/20 overflow-hidden" 21 - > 20 + <div class="flex flex-row gap-3 mx-auto bg-ralsei-black/20 overflow-hidden"> 22 21 {#each data.banners as bannerNo, index} 23 22 {@const hideIfMobile = index === data.banners.length - 1} 24 23 <img ··· 35 34 {/each} 36 35 </div> 37 36 <div class="flex flex-grow"> 38 - <div 39 - class="w-36 [padding:8px] place-content-center place-self-center bg-ralsei-black/20" 40 - > 41 - <img class="w-36 u-photo hover:invert transition-all [transition-duration:300ms]" src="/pfp-iojkqpwerojnasduijf.webp" alt="my character" title="hi ;)"/> 37 + <div class="w-36 [padding:8px] place-content-center place-self-center bg-ralsei-black/20"> 38 + <img 39 + class="w-36 u-photo hover:invert transition-all [transition-duration:300ms]" 40 + src="/pfp-iojkqpwerojnasduijf.webp" 41 + alt="my angelsona" 42 + title="that's me! my angelsona :3c" 43 + /> 42 44 </div> 43 45 <div 44 46 class="flex flex-row flex-grow place-content-center ml-1.5 [padding:8px] bg-ralsei-black/20" ··· 47 49 class="place-self-center m-0 mr-4 [padding-left:1em] sm:[padding-left:0.5em] leading-none marker:[content:'->'] [list-style-type:'->']" 48 50 > 49 51 <li class="[list-style-type:'->'] p-note">trying to do stuff</li> 50 - <li class="[list-style-type:'->'] p-note">is a thing that exists</li> 52 + <li class="[list-style-type:'->'] p-note" title="angelrobotdollpuppything"> 53 + is a thing (it/they) 54 + </li> 51 55 <li class="[list-style-type:'->']"> 52 56 <span class="p-category">software engineer</span>, 53 57 <span class="p-category">indie game dev</span> ··· 95 99 </div> 96 100 <div class="flex flex-col gap-y-2 lg:gap-y-0 mx-auto w-full md:w-fit place-items-end"> 97 101 <Window title="links!" iconUri="/icons/contact.webp"> 98 - <div class="prose prose-ralsei prose-ul:px-[0.9rem] prose-ul:leading-[1.1rem] prose-headings:leading-none"> 102 + <div 103 + class="prose prose-ralsei prose-ul:px-[0.9rem] prose-ul:leading-[1.1rem] prose-headings:leading-none" 104 + > 99 105 <ul> 100 106 <li>discord: yusdacra</li> 101 107 <li> ··· 135 141 </ul> 136 142 <h4>88x31</h4> 137 143 <div class="flex flex-row flex-wrap gap-1 prose-img:m-0"> 138 - <img src="/88x31.gif" alt="88x31 banner" title="midnight AND sunrise! woaw"/> 139 - <img src="/88x31_midnight.gif" alt="88x31 banner (midnight only)" title="it's midnight!"/> 140 - <img src="/88x31_sunrise.gif" alt="88x31 banner (sunrise only)" title="it's sunrise!"/> 144 + <img src="/88x31.gif" alt="88x31 banner" title="midnight AND sunrise! woaw" /> 145 + <img 146 + src="/88x31_midnight.gif" 147 + alt="88x31 banner (midnight only)" 148 + title="it's midnight!" 149 + /> 150 + <img src="/88x31_sunrise.gif" alt="88x31 banner (sunrise only)" title="it's sunrise!" /> 141 151 </div> 142 152 </div> 143 153 </Window> 144 154 <Window title="status" style="mt-auto" removePadding> 145 155 {#if data.lastNote} 146 - <div class="m-1.5 flex flex-col font-monospace text-sm"> 147 - <p 148 - class="prose prose-ralsei p-1 border-4 text-sm bg-ralsei-black" 149 - style="border-style: double double none double;" 150 - title={renderDate(data.lastNote.published)} 151 - > 152 - <a href="/entries">last log was…</a> 153 - published {renderRelativeDate(data.lastNote.published)}! 154 - </p> 155 - <div class="mt-0 p-1.5 border-4 border-double bg-ralsei-black min-w-full max-w-[40ch]"> 156 - <Note note={data.lastNote} onlyContent/> 156 + <div class="m-1.5 flex flex-col font-monospace text-sm"> 157 + <p 158 + class="prose prose-ralsei p-1 border-4 text-sm bg-ralsei-black" 159 + style="border-style: double double none double;" 160 + title={renderDate(data.lastNote.published)} 161 + > 162 + <a href="/entries">last log was…</a> 163 + published {renderRelativeDate(data.lastNote.published)}! 164 + </p> 165 + <div class="mt-0 p-1.5 border-4 border-double bg-ralsei-black min-w-full max-w-[40ch]"> 166 + <Note note={data.lastNote} onlyContent /> 167 + </div> 157 168 </div> 158 - </div> 159 169 {/if} 160 170 {#if data.lastTrack} 161 - <div class="flex flex-row gap-0.5 m-1.5 border-4 border-double bg-ralsei-black"> 162 - <!-- svelte-ignore a11y_missing_attribute --> 163 - {#if data.lastTrack.image} 171 + <div class="flex flex-row gap-0.5 m-1.5 border-4 border-double bg-ralsei-black"> 172 + <!-- svelte-ignore a11y_missing_attribute --> 173 + {#if data.lastTrack.image} 174 + <img 175 + class="border-4 w-[4.5rem] h-[4.5rem]" 176 + style="border-style: none double none none;" 177 + src={data.lastTrack.image} 178 + /> 179 + {:else} 180 + <img 181 + class="border-4 w-[4.5rem] h-[4.5rem] p-2" 182 + style="border-style: none double none none; image-rendering: pixelated;" 183 + src="/icons/cd_audio.webp" 184 + /> 185 + {/if} 186 + <div class="flex flex-col max-w-[40ch] p-2"> 187 + <p 188 + class="text-shadow-green text-ralsei-green-light text-sm text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 189 + > 190 + <span class="text-sm text-shadow-white text-ralsei-white" 191 + >{data.lastTrack.playing ? 'listening to' : 'listened to'}</span 192 + > 193 + <a 194 + title={data.lastTrack.name} 195 + href="https://www.last.fm/user/yusdacra" 196 + class="hover:underline motion-safe:hover:animate-squiggle">{data.lastTrack.name}</a 197 + > 198 + </p> 199 + <p 200 + class="text-shadow-pink text-ralsei-pink-regular text-sm text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 201 + > 202 + <span class="text-shadow-white text-ralsei-white">by</span> 203 + <span title={data.lastTrack.artist}>{data.lastTrack.artist}</span> 204 + </p> 205 + <p 206 + class="text-shadow-white text-ralsei-white text-xs text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 207 + > 208 + …{renderRelativeDate(data.lastTrack.when)} 209 + </p> 210 + </div> 211 + </div> 212 + {/if} 213 + {#if data.lastGame} 214 + <div class="flex flex-row m-1.5 border-4 border-double bg-ralsei-black"> 215 + <!-- svelte-ignore a11y_missing_attribute --> 164 216 <img 165 217 class="border-4 w-[4.5rem] h-[4.5rem]" 166 218 style="border-style: none double none none;" 167 - src={data.lastTrack.image} 219 + width="64" 220 + height="64" 221 + src={data.lastGame.icon} 168 222 /> 169 - {:else} 170 - <img 171 - class="border-4 w-[4.5rem] h-[4.5rem] p-2" 172 - style="border-style: none double none none; image-rendering: pixelated;" 173 - src="/icons/cd_audio.webp" 174 - /> 175 - {/if} 176 - <div class="flex flex-col max-w-[40ch] p-2"> 177 - <p 178 - class="text-shadow-green text-ralsei-green-light text-sm text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 179 - > 180 - <span class="text-sm text-shadow-white text-ralsei-white">{data.lastTrack.playing ? "listening to" : "listened to"}</span> 181 - <a 182 - title={data.lastTrack.name} 183 - href="https://www.last.fm/user/yusdacra" 184 - class="hover:underline motion-safe:hover:animate-squiggle">{data.lastTrack.name}</a 223 + <div class="flex flex-col max-w-[40ch] p-2 gap-0.5 overflow-hidden"> 224 + <p 225 + class="text-shadow-green text-ralsei-green-light text-sm text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 185 226 > 186 - </p> 187 - <p 188 - class="text-shadow-pink text-ralsei-pink-regular text-sm text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 189 - > 190 - <span class="text-shadow-white text-ralsei-white">by</span> 191 - <span title={data.lastTrack.artist}>{data.lastTrack.artist}</span> 192 - </p> 193 - <p 194 - class="text-shadow-white text-ralsei-white text-xs text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 195 - > 196 - …{renderRelativeDate(data.lastTrack.when)} 197 - </p> 198 - </div> 199 - </div> 200 - {/if} 201 - {#if data.lastGame} 202 - <div class="flex flex-row m-1.5 border-4 border-double bg-ralsei-black"> 203 - <!-- svelte-ignore a11y_missing_attribute --> 204 - <img 205 - class="border-4 w-[4.5rem] h-[4.5rem]" 206 - style="border-style: none double none none;" 207 - width="64" 208 - height="64" 209 - src={data.lastGame.icon} 210 - /> 211 - <div class="flex flex-col max-w-[40ch] p-2 gap-0.5 overflow-hidden"> 212 - <p 213 - class="text-shadow-green text-ralsei-green-light text-sm text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 214 - > 215 - <span class="text-sm text-shadow-white text-ralsei-white">{data.lastGame.playing ? "playing" : "played"}</span> 216 - <a title={data.lastGame.name} class="hover:underline" href={data.lastGame.link} 217 - >{data.lastGame.name}</a 227 + <span class="text-sm text-shadow-white text-ralsei-white" 228 + >{data.lastGame.playing ? 'playing' : 'played'}</span 229 + > 230 + <a title={data.lastGame.name} class="hover:underline" href={data.lastGame.link} 231 + >{data.lastGame.name}</a 232 + > 233 + </p> 234 + <p 235 + class="text-shadow-white text-ralsei-white text-xs text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 236 + > 237 + …{renderRelativeDate(data.lastGame.when)} 238 + </p> 239 + <!-- svelte-ignore a11y_missing_attribute --> 240 + <a 241 + href="https://steamcommunity.com/id/yusdacra" 242 + class="text-xs hover:underline text-shadow-green text-ralsei-green-light" 243 + ><img class="inline w-4" src={data.lastGame.pfp} /> 244 + <span class="align-middle">steam profile</span></a 218 245 > 219 - </p> 220 - <p 221 - class="text-shadow-white text-ralsei-white text-xs text-ellipsis text-nowrap overflow-hidden max-w-[30ch]" 222 - > 223 - …{renderRelativeDate(data.lastGame.when)} 224 - </p> 225 - <!-- svelte-ignore a11y_missing_attribute --> 226 - <a 227 - href="https://steamcommunity.com/id/yusdacra" 228 - class="text-xs hover:underline text-shadow-green text-ralsei-green-light" 229 - ><img class="inline w-4" src={data.lastGame.pfp} /> 230 - <span class="align-middle">steam profile</span></a 231 - > 246 + </div> 232 247 </div> 233 - </div> 234 248 {/if} 235 249 </Window> 236 250 </div>