1
0
Fork 0
forked from nin0/website

Compare commits

...
Sign in to create a new pull request.

49 commits

Author SHA1 Message Date
058139a8d7 selectively show ass card 2025-06-25 16:37:15 -04:00
49803ba398 fix firefox assness 2025-06-25 16:34:38 -04:00
7271006f49 add dark reader lock 2025-06-25 15:56:10 -04:00
sadan
96194316d3
use a static file for your avatar
this way, others can link to it statically and always have an up to date
copy of your avatar
2025-06-25 15:51:12 -04:00
sadan
f584041619
add a package manager field
with older pnpm versions, pnpm will error and exit because of
onlyBuiltDependencies in pnpm-workspace.yaml

this results in you being unable to run any commands (help, version,
etc...) without pnpm erroring and exiting
2025-06-25 15:38:48 -04:00
f118b8b11c
add subtext 2025-05-10 07:38:49 -04:00
995dca803c
add idiot mode 2025-04-22 19:22:32 -04:00
b567a81a22
fix building 2025-04-22 19:22:24 -04:00
cd4495767f
.FUCKYEAH 2025-04-21 16:38:10 -04:00
315fd85117
fix heart size 2025-04-21 15:24:50 -04:00
7dbaa56b81
loved tracks 2025-04-21 15:23:16 -04:00
83c4444c8e
prettier time 2025-04-21 08:42:18 -04:00
77a85e1611
crusty titlebar 2025-04-21 08:39:13 -04:00
e98811546b
implement "someone is on the website and i pause my song" 2025-04-21 08:18:06 -04:00
11517475ed
this website no longer hates TVs 2025-04-21 08:15:58 -04:00
83cb45f9f8
Vencord Virus 2025-04-21 08:09:58 -04:00
6c9d093c8c
typed lastfm things 2025-04-21 06:42:14 -05:00
99ba9cef4b
oops 2025-04-20 23:53:33 -05:00
0bb40ff89f
remove react 2025-04-20 23:52:17 -05:00
darwinx64
98412adab9
fixes
Signed-off-by: darwinx64 <tiramisyuz@proton.me>
2025-04-20 14:51:33 -05:00
8ed804a5e6
fix 2025-04-16 13:34:24 -04:00
80d0b4992c
p 2 2025-04-16 13:32:59 -04:00
e439cde473
better spotify card 2025-04-16 13:32:49 -04:00
74f2fd5ec3
add border 2025-03-15 22:24:32 -04:00
cf292c95f1
make pfp light up 2025-03-15 22:17:35 -04:00
4268b094f7
a 2025-03-15 22:13:10 -04:00
6015db1f4f
internal corporate restructuring 2025-03-15 22:03:19 -04:00
22251c3b2b
remove pgp 2025-03-14 23:27:13 -04:00
74fd12d199
formatting 2025-03-14 23:27:04 -04:00
60358d32d6
various insanities 2025-03-14 23:23:27 -04:00
fd44e9866e
update dependencies 2025-03-14 22:38:54 -04:00
4ee9a3723c
add webfinger 2025-03-04 13:50:55 -05:00
651987661f
added my PGP key 2024-09-02 18:24:55 -04:00
3c83b759a8 Merge branch 'astro' of https://git.nin0.dev/nin0/website into astro 2024-07-30 09:01:49 -04:00
08d9fd1ffb Added .dockerignore 2024-07-30 09:01:42 -04:00
e194941b5c Added license 2024-07-29 21:06:59 +00:00
8da480329b dockerfile testing time 2024-07-29 16:52:47 -04:00
9b58091459 added buttons 2024-07-29 16:48:24 -04:00
6d7b18dc01 move mainwindow to component 2024-07-29 16:30:20 -04:00
0ee0b19d16 better mobile 2024-07-29 16:29:17 -04:00
b47f03ff2a added nojs warning 2024-07-29 16:23:17 -04:00
fc7959737f ?remind 120h STOP USING console.log FOR LOGGING SHIT 2024-07-29 16:23:10 -04:00
eb07a77584 made window draggable 2024-07-29 16:14:46 -04:00
161e28d5f3 added socials 2024-07-29 16:10:40 -04:00
af797ba515 Added presence 2024-07-29 15:37:29 -04:00
6f6c9f229f added own header 2024-07-29 13:42:01 -04:00
f9b8428d2b made window 2024-07-29 13:10:58 -04:00
2e77b84df3 initial commit 2024-07-29 10:55:50 -04:00
bfc0c43b12 rip 2024-07-29 10:44:44 -04:00
66 changed files with 6997 additions and 1886 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
.git
info/
node_modules/
public/
src/

138
.gitignore vendored
View file

@ -1,130 +1,24 @@
# Logs
logs
*.log
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
# environment variables
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.env.production
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# macOS-specific files
.DS_Store
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# jetbrains setting folder
.idea/

18
.prettierrc.json Normal file
View file

@ -0,0 +1,18 @@
{
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
],
"tabWidth": 4,
"useTabs": true,
"singleQuote": false,
"jsxSingleQuote": false,
"semi": true,
"arrowParens": "avoid",
"trailingComma": "none"
}

4
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

4
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}

View file

@ -1,4 +1,8 @@
FROM lipanski/docker-static-website:latest
# Copy your static files
COPY . .
FROM node:lts-alpine3.17
WORKDIR /usr/src/app
COPY ["package.json", "pnpm-lock.yaml", "dist/*", "./"]
RUN pnpm i
EXPOSE 4321
RUN chown -R node /usr/src/app
USER node
CMD ["pnpm", "start"]

View file

@ -1,3 +1,6 @@
Copyright (c) 2024 nin0dev (https://github.com/nin0-dev, https://codeberg.org/nin0dev, https://git.nin0.dev/nin0, https://nin0.dev)
Usage and reproduction is prohibited.
All rights reserved.
The Rust Bad License
If you fork this and include The Rust Programming Language files in any way, shape, or form; you will be liable for damages ranging from 10-20 thousand dollars per file.
Otherwise, usage and reproduction is prohibited.
All rights reserved.

47
README.md Normal file
View file

@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

3
astro.config.mjs Normal file
View file

@ -0,0 +1,3 @@
import { defineConfig } from 'astro/config';
export default defineConfig({});

View file

@ -1,60 +0,0 @@
import pluginJs from "@eslint/js";
import stylisticJs from "@stylistic/eslint-plugin-js";
import globals from "globals";
export default [
{files: ["**/*.{js,mjs,cjs,ts}"],},
{languageOptions: { globals: globals.node }},
pluginJs.configs.recommended,
{
ignores: ["dist/*", "**/jquery.js"]
},
{
plugins: {
"@stylistic/js": stylisticJs,
},
rules: {
"yoda": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"prefer-destructuring": ["error", {
"VariableDeclarator": { "array": false, "object": true },
"AssignmentExpression": { "array": false, "object": false }
}],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"dot-notation": "error",
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"semi": [2, "always"],
"@stylistic/js/indent": ["error", 4],
"@stylistic/js/quotes": [2, "double", { "avoidEscape": true }]
}
},
{
files: ["public/**/*.js"],
rules: {
"no-undef": "off", // due to being separate files eslint goes insane
"@typescript-eslint/no-unused-vars": ["off"] // noone cares lol
}
}
];

View file

@ -1,23 +1,21 @@
{
"name": "website",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "echo \"Error: no test specified\" && exit 1"
},
"author": "nin0dev",
"license": "All Rights Reserved",
"dependencies": {
"@stylistic/eslint-plugin-js": "^2.4.0",
"ejs": "^3.1.10",
"eslint-plugin-simple-import-sort": "^12.1.1",
"express": "^4.19.2",
"nodemon": "^3.1.4"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
"eslint": "9.x",
"globals": "^15.8.0"
}
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^5.7.4",
"color.js": "^1.2.0",
"sharp": "^0.34.1"
},
"devDependencies": {
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1"
},
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
}

4060
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

4
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,4 @@
onlyBuiltDependencies:
- esbuild
- sharp
- workerd

View file

@ -0,0 +1,9 @@
{
"subject": "acct:personal@nin0.dev",
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://auth.nin0.dev"
}
]
}

BIN
public/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View file

@ -1,81 +0,0 @@
:root {
--online-color: #23a55a;
--idle-color: #f0b232;
--dnd-color: #f23f43;
--offline-color: #80848e;
}
body {
padding: 30px;
font-family: "Segoe UI", "Roboto", sans-serif !important;
background-color: #56a0d1;
}
#main-window {
max-width: 600px;
}
@media (pointer:coarse) {
body {
overflow: scroll !important;
}
#main-window {
margin-right: 10px;
margin-bottom: 10px;
}
}
.window-body {
padding: 10px;
}
#header {
display: flex;
}
#header h3 {
font-weight: 400;
margin-left: 20px;
margin-top: auto;
margin-bottom: auto;
}
#pfp {
width: 90px;
height: 90px;
border-radius: 6px;
box-shadow: 10px black;
border-color: var(--offline-color);
border-style: solid;
border-width: 2px;
}
a {
color: blue;
}
li {
padding: 2px;
}
#presence img {
width: 30px;
height: 30px;
}
#presence-content {
margin-top: auto;
margin-bottom: auto;
margin-left: 10px;
}
#presence {
display: flex;
}
#bottom-actions {
display: flex;
align-items: end;
justify-content: right;
}
body {
overflow: hidden;
}
* {
/* no this is not to protect my content or whatever. this is just to make draggable windows work in a non deranged way */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -1,56 +0,0 @@
// wow i love being fake programmer
// source: https://jams.hackclub.com/batch/webOS/part-3
function dragElement(element) {
// Step 2: Set up variables to keep track of the element's position.
var initialX = 0;
var initialY = 0;
var currentX = 0;
var currentY = 0;
// Step 3: Check if there is a special header element associated with the draggable element.
if (document.getElementById("title-bar")) {
// Step 4: If present, assign the `dragMouseDown` function to the header's `onmousedown` event.
// This allows you to drag the window around by its header.
document.getElementById("title-bar").onmousedown = startDragging;
} else {
// Step 5: If not present, assign the function directly to the draggable element's `onmousedown` event.
// This allows you to drag the window by holding down anywhere on the window.
element.onmousedown = startDragging;
}
// Step 6: Define the `startDragging` function to capture the initial mouse position and set up event listeners.
function startDragging(e) {
e = e || window.event;
e.preventDefault();
// Step 7: Get the mouse cursor position at startup.
initialX = e.clientX;
initialY = e.clientY;
// Step 8: Set up event listeners for mouse movement (`elementDrag`) and mouse button release (`closeDragElement`).
document.onmouseup = stopDragging;
document.onmousemove = dragElement;
}
// Step 9: Define the `elementDrag` function to calculate the new position of the element based on mouse movement.
function dragElement(e) {
e = e || window.event;
e.preventDefault();
// Step 10: Calculate the new cursor position.
currentX = initialX - e.clientX;
currentY = initialY - e.clientY;
initialX = e.clientX;
initialY = e.clientY;
// Step 11: Update the element's new position by modifying its `top` and `left` CSS properties.
console.log(element.offsetTop);
console.log(currentX);
element.style.top = (element.offsetTop - currentY) + "px";
element.style.left = (element.offsetLeft - currentX) + "px";
}
// Step 12: Define the `stopDragging` function to stop tracking mouse movement by removing the event listeners.
function stopDragging() {
document.onmouseup = null;
document.onmousemove = null;
console.log(element.offsetTop - currentY);
console.log(element.offsetLeft - currentX);
}
}

View file

@ -1,69 +0,0 @@
const shouldLog = false;
function log(content) {
if (shouldLog) console.log(content);
}
function onUpdate(data) {
// set status
log(data);
const pfp = document.getElementById("pfp");
switch(data.discord_status) {
case "online":
pfp.style.borderColor = "var(--online-color)";
break;
case "idle":
pfp.style.borderColor = "var(--idle-color)";
break;
case "dnd":
pfp.style.borderColor = "var(--dnd-color)";
break;
case "offline":
pfp.style.borderColor = "var(--offline-color)";
break;
}
// set presence
log(data.activities);
let listening = false;
let content = "";
data.activities.forEach(presence => {
if(presence.application_id === "463151177836658699" && presence.assets.small_text !== "Paused") { // premid
listening = true;
artist = presence.state.substring(0, presence.state.indexOf(" -"));
if (artist === "") {
artist = presence.state;
}
content = `Listening to ${presence.details} - ${artist}`;
}
if(presence.application_id === "1108588077900898414") { // vencord lastfm
listening = true;
content = `Listening to ${presence.details} - ${presence.state}`;
}
if(presence.application_id === "1054951789318909972") { // vendetta lastfm
listening = true;
content = `Listening to ${presence.details} - ${presence.state}`;
}
if(presence.id === "spotify:1") { // built in spotify hooluy shit normal presence)
listening = true;
content = `Listening to ${presence.details} - ${presence.state}`;
}
if(presence.type === 0 && presence.application_id !== "463151177836658699" && presence.application_id !== "1108588077900898414") { // generic playing status that isn't vencord lastfm or premid
listening = false;
content = `Playing ${presence.name}`;
}
});
document.getElementById("presence").style.display = content === "" ? "none": "flex";
document.getElementById("presence-content").innerText = content;
document.getElementById("presence-icon").src = listening ? "img/music.ico" : "img/game.ico";
}
LanyardWrapper.connectWebSocket("886685857560539176", onUpdate)
.catch(err => {
console.error(err);
});
// eslint-disable-next-line no-unused-vars
function showCredits() {
document.getElementById("credits").style.display = "block";
document.getElementById("credits-button").style.display = "none";
}
document.addEventListener("DOMContentLoaded", () => {
dragElement(document.getElementById("main-window"));
});

View file

@ -1,239 +0,0 @@
// oneko.js: https://github.com/adryd325/oneko.js
(function oneko() {
const isReducedMotion =
window.matchMedia("(prefers-reduced-motion: reduce)") === true ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches === true;
if (isReducedMotion) return;
const nekoEl = document.createElement("div");
let nekoPosX = 32;
let nekoPosY = 32;
let mousePosX = 0;
let mousePosY = 0;
let frameCount = 0;
let idleTime = 0;
let idleAnimation = null;
let idleAnimationFrame = 0;
const nekoSpeed = 10;
const spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "fixed";
nekoEl.style.pointerEvents = "none";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
nekoEl.style.zIndex = Number.MAX_VALUE;
let nekoFile = "./oneko.gif";
const curScript = document.currentScript;
if (curScript && curScript.dataset.cat) {
nekoFile = curScript.dataset.cat;
}
nekoEl.style.backgroundImage = `url(${nekoFile})`;
document.body.appendChild(nekoEl);
document.addEventListener("mousemove", function (event) {
mousePosX = event.clientX;
mousePosY = event.clientY;
});
window.requestAnimationFrame(onAnimationFrame);
}
let lastFrameTimestamp;
function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM
if (!nekoEl.isConnected) {
return;
}
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp;
frame();
}
window.requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) {
const sprite = spriteSets[name][frame % spriteSets[name].length];
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime += 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) === 0 &&
idleAnimation == null
) {
const avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame += 1;
}
function frame() {
frameCount += 1;
const diffX = nekoPosX - mousePosX;
const diffY = nekoPosY - mousePosY;
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
let direction;
direction = diffY / distance > 0.5 ? "N" : "";
direction += diffY / distance < -0.5 ? "S" : "";
direction += diffX / distance > 0.5 ? "W" : "";
direction += diffX / distance < -0.5 ? "E" : "";
setSprite(direction, frameCount);
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
}
init();
})();

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

View file

@ -1,12 +0,0 @@
var express = require("express");
var app = express();
app.set("view engine", "ejs");
app.use(express.static("public"))
app.get("/", function(req, res) {
res.render("index");
});
app.listen(8080);
console.log("Server is listening on port 8080");

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20.0234 14.1406">
<g>
<path d="M9.80469 7.65625C10.0312 7.65625 10.2344 7.56641 10.4805 7.34766L18.3477 0.339844C17.9336 0.113281 17.4609 0.0078125 16.8555 0.0078125L2.75781 0.0078125C2.15625 0.0078125 1.68359 0.113281 1.26172 0.339844L9.13281 7.34766C9.37891 7.56641 9.58594 7.65625 9.80469 7.65625ZM0.1875 12.5703L6.01953 6.74609L0.195312 1.55469C0.0859375 1.82031 0 2.23438 0 2.75L0 11.3984C0 11.8711 0.0703125 12.2461 0.1875 12.5703ZM2.62891 14.1406L16.9883 14.1406C17.5547 14.1406 18.0273 14.0195 18.3789 13.8281L12.375 7.82422L11.4336 8.66406C10.8984 9.14062 10.3984 9.35156 9.80469 9.35156C9.21875 9.35156 8.71875 9.14062 8.18359 8.66406L7.24219 7.82422L1.23828 13.8281C1.58984 14.0195 2.05859 14.1406 2.62891 14.1406ZM19.4297 12.5703C19.543 12.2461 19.6172 11.8711 19.6172 11.3984L19.6172 2.75C19.6172 2.23438 19.5312 1.82031 19.4219 1.55469L13.5938 6.74609Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,4 @@
<svg role="img" aria-hidden="true" viewBox="0 0 24 24" stroke-width="2" fill="none">
<path d="M21 3h-6m6 0l-9 9m9-9v6" stroke-width="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 348 B

5
src/assets/svg/fm.svg Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.3.8 -->
<svg width="16" viewBox="0 0 293 179" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" stroke="none" d="M -0.282013 158.117004 C -0.282013 168.822006 8.071991 177.434998 18.773987 177.434998 C 30 177.434998 38.35199 168.822006 38.35199 158.117004 C 38.35199 147.153992 29.998993 138.804001 18.773987 138.804001 C 8.071991 138.804001 -0.282013 147.153992 -0.282013 158.117004 Z M 52.72699 76.673996 L 52.72699 175.869003 L 80.135986 175.869003 L 80.135986 76.673996 L 110.939026 76.673996 L 110.939026 55.271004 L 80.135986 55.271004 L 80.135986 44.306 C 80.135986 27.862 87.184998 22.641006 98.669983 22.641006 C 106.762024 22.641006 112.244019 24.466003 118.508972 27.862 L 122.945984 4.888 C 115.638 1.494995 107.02301 -0.332001 96.582001 -0.332001 C 73.609009 -0.332001 52.72699 10.630997 52.72699 43.261002 L 52.72699 55.271004 L 35.238007 55.271004 L 35.238007 76.673996 Z M 206.153992 79.026001 C 203.020996 59.447998 190.231018 52.397003 173.523987 52.397003 C 156.817993 52.397003 142.461975 59.968002 136.195007 78.500999 L 132.802002 55.271004 L 110.614014 55.271004 L 110.614014 175.869003 L 138.02301 175.869003 L 138.02301 107.739998 C 138.02301 84.504997 150.031006 75.629997 162.822021 75.629997 C 176.133972 75.629997 181.617004 84.504997 181.617004 98.862 L 181.617004 175.867996 L 208.763977 175.867996 L 208.763977 107.477997 C 208.763977 84.503998 221.03302 75.628998 233.825012 75.628998 C 246.877014 75.628998 252.356995 84.503998 252.356995 98.861 L 252.356995 175.867004 L 279.765991 175.867004 L 279.765991 89.207001 C 279.765991 63.363998 264.625977 52.397003 244.526001 52.397003 C 227.560974 52.397003 212.419006 59.968002 206.153992 79.026001 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,9 @@
<svg viewBox="0 0 212 212" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(6,6)">
<path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" stroke="currentColor" stroke-width="25" fill="none" />
<path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" stroke="currentColor" stroke-width="25" fill="none" />
<circle cx="142" cy="20" r="18" stroke="currentColor" stroke-width="15" fill="none" />
<circle cx="142" cy="88" r="18" stroke="currentColor" stroke-width="15" fill="none" />
<circle cx="58" cy="180" r="18" stroke="currentColor" stroke-width="15" fill="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 593 B

View file

@ -0,0 +1 @@
<svg viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 965 B

1
src/assets/svg/heart.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-147q-14 0-28.5-5T426-168l-69-63q-106-97-191.5-192.5T80-634q0-94 63-157t157-63q53 0 100 22.5t80 61.5q33-39 80-61.5T660-854q94 0 157 63t63 157q0 115-85 211T602-230l-68 62q-11 11-25.5 16t-28.5 5Z"/></svg>

After

Width:  |  Height:  |  Size: 322 B

6
src/assets/svg/plane.svg Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 18.0508 17.6562">
<g>
<path d="M10.0156 17.6562C10.6836 17.6562 11.1406 17.1562 11.4297 16.4023L16.6484 2.74609C16.7773 2.41797 16.8438 2.11719 16.8438 1.86328C16.8438 1.22266 16.4336 0.816406 15.793 0.816406C15.5352 0.816406 15.2344 0.882812 14.9102 1.00781L1.21875 6.24609C0.511719 6.51172 0 6.96875 0 7.64062C0 8.43359 0.574219 8.76953 1.37109 9.01562L5.125 10.1719C5.75 10.3672 6.13672 10.3633 6.59766 9.94922L15.5117 1.79297C15.6328 1.68359 15.7852 1.69531 15.875 1.78516C15.9609 1.875 15.9766 2.02734 15.8633 2.14844L7.72656 11.0547C7.33594 11.4805 7.30078 11.9297 7.48438 12.5312L8.62891 16.2422C8.87891 17.0586 9.21875 17.6562 10.0156 17.6562Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 851 B

6
src/assets/svg/xmark.svg Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 12.9879 12.5923">
<g>
<path d="M0.250141 12.3415C0.589669 12.6732 1.16232 12.669 1.4856 12.3457L6.29154 7.53973L11.0947 12.3443C11.4236 12.6732 11.9956 12.6782 12.3287 12.3401C12.6619 12.0005 12.6633 11.4413 12.3344 11.111L7.53122 6.30005L12.3344 1.49692C12.6633 1.16802 12.6683 0.602398 12.3287 0.269275C11.9892-0.0702532 11.4236-0.0716594 11.0947 0.263651L6.29154 5.06678L1.4856 0.262244C1.16232-0.0660346 0.583263-0.0780657 0.250141 0.267869C-0.0815755 0.607398-0.0773569 1.17224 0.245922 1.49552L5.05187 6.30005L0.245922 11.1138C-0.0773569 11.4357-0.0879818 12.0083 0.250141 12.3415Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View file

@ -0,0 +1,31 @@
---
import GroupBox from "@components/GroupBox.astro";
---
<style is:global>
.ua {
font-family: monospace;
}
</style>
<ass-browser>
<GroupBox>
<p>
<h2>YOUR BROWSER IS ASS</h2>
<span>
Your browser, <span class="ua"></span> does not support the full
set of web features which means that nin0.dev may not work as well
as it would on a non-ass browser.
<br />
<br />
Consider switching to a browser that is not ass such as <a
href="https://www.apple.com/ca/safari/">Safari</a
>, <a href="https://brave.com/">Brave</a>, or if you really have
no option, <a href="https://www.google.com/intl/en_ca/chrome/"
>Chrome</a
>. (these are personal preferences and I am not paid to say
this)
</span>
</p>
</GroupBox>
</ass-browser>

View file

@ -0,0 +1,55 @@
---
import { Image } from "astro:assets";
---
<div class="avatar-wrapper">
<Image
alt="My profile picture"
src={"/logo.png"}
class="avatar"
width={80}
height={80}
aria-hidden
/>
</div>
<style>
.avatar-wrapper {
width: 80px;
height: 80px;
clip-path: var(--clip-avatar-outer);
background-color: var(--overlay0);
}
.avatar {
width: 80px;
height: 80px;
clip-path: var(--clip-avatar-inner);
}
</style>
<script>
const colours = {
online: "var(--green)",
idle: "var(--yellow)",
dnd: "var(--red)",
offline: "var(--overlay0)"
};
const avatarWrapper: HTMLElement =
document.querySelector(".avatar-wrapper");
window.addEventListener("lanyard-update", (e: CustomEvent) => {
const presence = e.detail;
avatarWrapper.animate(
[
{
backgroundColor:
colours[presence.discord_status] ?? colours.offline
}
],
{
duration: 100,
fill: "forwards"
}
);
});
</script>

107
src/components/Button.astro Normal file
View file

@ -0,0 +1,107 @@
---
const { position, text, url, id, onClick, name } = Astro.props;
import ExternalIcon from "@assets/svg/external.svg";
---
<style>
.button-wrapper {
background: linear-gradient(#343343, #2e2e3e, #2e2e3e);
transition: 0.54s
linear(
0,
0.0021,
0.0081 1.26%,
0.0302 2.51%,
0.106,
0.2088 7.53%,
0.5809 15.7%,
0.7667 20.72%,
0.8391 23.23%,
0.9106 26.37%,
0.9538 28.88%,
0.9926 32.02%,
1.0134,
1.0269 37.04%,
1.0359,
1.0384 43.32%,
1.0353 47.72%,
1.0124 61.53%,
1.003 70.32%,
0.9988 81.62%,
0.9992 99.83%
);
clip-path: var(--clip-button-outer);
cursor: pointer;
user-select: none;
}
button,
a {
width: 100%;
text-decoration: none;
cursor: pointer;
-webkit-user-drag: none;
}
button {
border: none;
background-color: #2b2b3b;
clip-path: var(--clip-button-inner);
color: var(--text);
padding: 10px 20px;
}
.button-wrapper:has(button:hover:active) {
background: #2f2f41;
transform: scale(0.98);
}
button:hover:active {
background-color: #2f2f41;
}
.link-button {
padding: 15px;
text-align: left;
display: flex;
gap: 0.5rem;
flex-direction: column;
align-items: stretch;
}
.link-button-heading {
display: flex;
align-items: center;
justify-content: space-between;
}
.link-button-heading > span {
display: flex;
align-items: center;
gap: 0.35rem;
line-height: 0;
}
:global(.link-button-heading svg) {
width: 16px;
height: 16px;
}
</style>
{
url ? (
<a href={url}>
<div class="button-wrapper">
<button id={id} class="link-button">
{name && (
<div class="link-button-heading">
<span>
<slot name="icon" />
<b>{name}</b>
</span>
<ExternalIcon />
</div>
)}
<slot />
</button>
</div>
</a>
) : (
<div class="button-wrapper">
<button id={id}>
<slot />
</button>
</div>
)
}

21
src/components/Chip.astro Normal file
View file

@ -0,0 +1,21 @@
---
const { ...attrs } = Astro.props;
---
<a {...attrs}>
<slot />
</a>
<style>
a {
clip-path: var(--clip-chip);
background-color: rgba(var(--lavender-rgb), 0.2);
color: rgba(var(--lavender-rgb), 0.75);
font-weight: 600;
padding: 2px 7px;
margin-right: 10px;
gap: 5px;
display: flex;
align-items: center;
text-decoration: none;
}
</style>

View file

@ -0,0 +1,20 @@
<style>
.group-box-wrapper {
background: linear-gradient(#343343, #2e2e3e, #2e2e3e);
clip-path: var(--clip-group-box-outer);
}
.group-box {
padding: 15px;
clip-path: var(--clip-group-box-inner);
background-color: #2b2b3b;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>
<div class="group-box-wrapper">
<div class="group-box">
<slot />
</div>
</div>

View file

@ -0,0 +1,19 @@
---
import GroupBox from "@components/GroupBox.astro";
---
<noscript>
<GroupBox>
<p>
<h2>ENABLE JAVASCRIPT OR I WILL KILL YOU</h2>
I will not. However, you seem to be living in fear of technology, as
you have JavaScript disabled. This means that some features (eg. dragging,
submitting forms) will not work. Enable JS to fully enjoy this website!
<br />
Rest assured, there's no external tracking. You can check this site's
source code on the <a href="https://git.nin0.dev/nin0/website"
>nin0/website</a
> repository on my Forgejo.
</p>
</GroupBox>
</noscript>

View file

@ -0,0 +1,96 @@
---
const { title, showClose, maxWidth, hideByDefault, customClass, offset } =
Astro.props;
const randomID = customClass || Math.random().toString().replace(".", "");
import XmarkIcon from "@assets/svg/xmark.svg";
---
<style define:vars={{ maxWidth, offset }}>
.window {
width: var(--maxWidth);
top: var(--offset);
left: var(--offset);
background: var(--surface1);
clip-path: var(--clip-window-outer);
position: absolute;
}
.window-inner {
background-color: #20202c;
clip-path: var(--clip-window-inner);
}
.title-bar {
background-color: var(--crust);
user-select: none;
display: flex;
justify-content: space-between;
align-items: stretch;
height: 42px;
padding-left: 1rem;
}
.title-bar span {
align-self: center;
}
svg {
fill: var(--text);
margin: 0;
padding: 0;
height: 24px;
}
#close {
background: none;
color: inherit;
border: none;
padding: 0;
margin: 0;
width: 50px;
font: inherit;
cursor: pointer;
outline: inherit;
}
.window-body {
gap: 1rem;
display: flex;
padding: 1rem;
flex-direction: column;
}
@media (pointer: coarse) {
.window {
position: absolute;
top: 0px;
left: 0px;
margin: 5%;
width: 90%;
}
}
</style>
<div
class="background"
id={`window-${randomID}`}
class="window"
style={hideByDefault && "display: none;"}
>
<div class="window" style="max-width: 100%">
<div class="window-inner">
<div class="title-bar">
<span>{title}</span>
{
showClose && (
<button
id="close"
aria-label="Close"
onclick={`document.querySelector("#window-${randomID}").style.display = "none";`}
>
<XmarkIcon width="12" height="12" />
</button>
)
}
</div>
<div class="window-body">
<slot />
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,29 @@
---
import Button from "@components/Button.astro";
import Window from "@components/Window.astro";
import ForgejoIcon from "@assets/svg/forgejo.svg";
import GithubIcon from "@assets/svg/github.svg";
---
<Window
title="Code"
maxWidth="400px"
showClose={true}
hideByDefault={true}
customClass="code-window"
offset="55px"
>
As mentioned in the main page, I write code. Some used by a lot, some not as
much. You can find it below.
<Button name="GitHub" url="https://gh.nin0.dev/">
<GithubIcon slot="icon" />
Here, you'll find most of my external contributions. I rarely start personal
projects on GitHub.
</Button>
<Button name="Forgejo" url="https://git.nin0.dev/nin0">
<ForgejoIcon slot="icon" />
Some more personal/niche projects can be found on my own instance (git.nin0.dev),
which will probably be down.
</Button>
</Window>

View file

@ -0,0 +1,22 @@
---
import Chip from "@components/Chip.astro";
import Envelope from "@assets/svg/envelope.svg";
import Plane from "@assets/svg/plane.svg";
---
<div>
<Chip href="mailto:support@nin0.dev">
<Envelope />
support@nin0.dev
</Chip>
<Chip href="https://nin0dev.t.me">
<Plane />
@nin0dev
</Chip>
</div>
<style>
div {
display: flex;
flex-wrap: wrap;
}
</style>

View file

@ -0,0 +1,26 @@
---
import Avatar from "components/Avatar.astro";
import SpotifyCard from "./SpotifyCard.astro";
---
<>
<div class="avatar-stack">
<Avatar />
<div class="text-stack">
<h2>nin0</h2>
<sub>he/him</sub>
</div>
</div>
<SpotifyCard />
</>
<style>
.avatar-stack {
display: flex;
gap: 12px;
align-items: center;
}
.text-stack {
display: flex;
flex-direction: column;
}
</style>

View file

@ -0,0 +1,49 @@
---
import Contacts from "./Contacts.astro";
import InfoStack from "./InfoStack.astro";
import Window from "@components/Window.astro";
import Button from "@components/Button.astro";
import GroupBox from "@components/GroupBox.astro";
---
<script>
import { WindowManager } from "@models/WindowManager";
import { Lanyard } from "@models/Lanyard";
import { LANYARD_ID } from "utils/constants";
document
.querySelector("#show-code-window")
.addEventListener(
"click",
() =>
(WindowManager.topmost =
document.getElementById("window-code-window"))
);
new Lanyard(LANYARD_ID, presence =>
window.dispatchEvent(
new CustomEvent("lanyard-update", {
detail: presence
})
)
);
</script>
<Window title="Home" maxWidth="600px" showClose={false}>
<InfoStack />
<Button position="left" id="show-code-window">My code</Button>
<GroupBox>
<h3>About me</h3>
<p>
I'm a Canadian self-taught software developer. I also make things
that some people use with varying degrees of usefulness.
</p>
</GroupBox>
<GroupBox>
<h3>Reach out</h3>
<Contacts />
</GroupBox>
</Window>

View file

@ -0,0 +1,254 @@
---
import LastFMIcon from "@assets/svg/fm.svg";
import HeartIcon from "@assets/svg/heart.svg";
---
<style is:global>
ass-browser {
display: none;
}
.ass-browser {
ass-browser {
display: block !important;
}
.spotify-card {
background-color: #2b2b3b !important;
}
.spotify-card-wrapper {
background-color: #2b2b3b !important;
}
.spotify-card-background {
display: none !important;
}
}
</style>
<div class="spotify-card-wrapper-shadow">
<h3 style="margin-bottom: 10px;">Listening to</h3>
<div class="spotify-card-wrapper">
<div class="spotify-card">
<svg aria-hidden="true" class="blur">
<filter id="sharpBlur" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="100"></feGaussianBlur>
<feColorMatrix
type="matrix"
values="1 0 0 0 0, 0 1 0 0 0, 0 0 1 0 0, 0 0 0 9 0"
></feColorMatrix>
<feComposite in2="SourceGraphic" operator="in"
></feComposite>
</filter>
</svg>
<div class="spotify-card-background"></div>
<div class="spotify-card-art-shadow">
<div class="spotify-card-art"></div>
<HeartIcon id="loved-icon" />
</div>
<div class="spotfiy-card-track-info">
<div class="spotify-card-track-name"></div>
<div class="spotify-card-track-artist"></div>
<div class="spotify-card-track-album"></div>
</div>
<div class="links">
<a class="lastfm-link" href="https://www.last.fm/user/nin0dev"
><LastFMIcon /></a
>
</div>
</div>
</div>
</div>
<style>
h4 {
color: var(--subtext0);
margin-bottom: 10px;
}
.blur {
display: none;
}
.spotify-card-wrapper-shadow {
display: none;
filter: drop-shadow(0 0 5px #00000040);
}
.spotify-card-wrapper {
position: relative;
display: flex;
background: var(--album-color);
clip-path: var(--clip-sp-1);
}
.spotify-card {
display: flex;
flex: 1;
gap: 12px;
padding: 15px;
align-items: center;
clip-path: var(--clip-sp-2);
}
.spotify-card-background {
position: absolute;
left: 0;
top: 0;
width: 105%;
height: 105%;
background: var(--album-art);
background-size: 100% 100%;
filter: url("#sharpBlur") brightness(0.75);
z-index: -1;
}
.spotify-card-art-shadow {
filter: drop-shadow(0 0 5px #00000040);
#loved-icon {
display: var(--loved);
position: absolute;
width: 20px;
bottom: -6px;
right: -5px;
fill: var(--red);
stroke: #641c2c;
stroke-width: 50px;
filter: drop-shadow(0 0 19px #0000006d);
}
}
.spotify-card-art-wrapper {
clip-path: var(--clip-sp-art-wrapper);
background: var(--album-color);
}
.spotify-card-art {
background-image: var(--album-art);
background-size: 100%;
width: 4rem;
height: 4rem;
clip-path: var(--clip-sp-art);
}
.spotfiy-card-track-info {
display: flex;
flex-direction: column;
line-height: 1;
}
.spotify-card-track-name {
margin-bottom: 5px;
}
.spotify-card-track-name::after {
content: var(--track);
font-weight: 500;
font-size: 1.05rem;
color: #ffffffd5;
filter: drop-shadow(0 0 5px #0000006d);
}
.spotify-card-track-artist::after,
.spotify-card-track-album::after {
color: #ffffffa1;
font-weight: 500;
font-size: 0.85rem;
filter: drop-shadow(0 0 5px #00000040);
}
.spotify-card-track-artist::after {
content: var(--artist);
}
.spotify-card-track-album::after {
content: var(--album);
}
.links {
position: absolute;
bottom: 0;
right: 0;
margin: 10px;
display: flex;
gap: 7px;
}
.links a {
color: rgba(255, 255, 255, 0.516);
text-decoration: none;
}
</style>
<script>
import { mergeStyles } from "utils/mergeStyles";
import { average } from "color.js";
import { LASTFM_KEY, LASTFM_API, LASTFM_USER } from "utils/constants";
import {
type FMResponse,
type FMRawTrack,
type FMTrack,
transformRawTrack
} from "types/lastfm";
const spotifyCardWrapper: HTMLElement = document.querySelector(
".spotify-card-wrapper-shadow"
);
async function getLastFmTracks() {
const params = new URLSearchParams({
method: "user.getrecenttracks",
api_key: LASTFM_KEY,
user: LASTFM_USER,
limit: "50",
format: "json",
extended: "true"
});
const response: FMResponse = await (
await fetch(`${LASTFM_API}${params}`)
).json();
if (
response.recenttracks.track.some(t => {
try {
return t["@attr"].nowplaying === "true";
} catch (e) {
return false;
}
})
) {
const rawTrack: FMRawTrack = response.recenttracks.track[0];
const track: FMTrack = transformRawTrack(rawTrack);
mergeStyles(
{
display: "block",
"--track": `'${track.name.replaceAll("'", "\\'")}'`,
"--artist": `'${track.artist.replaceAll("'", "\\'")}'`,
"--album": `'${track.album.replaceAll("'", "\\'")}'`,
"--album-art": `url("${track.albumArt}")`,
"--album-color": await average(track.albumArt, {
format: "hex"
}),
"--loved": track.loved ? "block" : "none"
},
spotifyCardWrapper.style
);
(document.querySelector(".lastfm-link") as HTMLLinkElement).href =
track.url;
if (
new URLSearchParams(window.location.search).has("iamanidiot") &&
(track.artist.includes("Taylor Swift") ||
track.name.includes("Taylor Swift"))
) {
spotifyCardWrapper.style = "";
}
} else {
spotifyCardWrapper.style = "";
}
}
getLastFmTracks();
setInterval(getLastFmTracks, 10000);
</script>

81
src/css/catppuccin.css Normal file
View file

@ -0,0 +1,81 @@
/* https://github.com/catppuccin/palette/blob/main/docs/css.md */
:root {
--rosewater: #f5e0dc;
--rosewater-rgb: 245, 224, 220;
--rosewater-hsl: 9.6, 55.556%, 91.176%;
--flamingo: #f2cdcd;
--flamingo-rgb: 242, 205, 205;
--flamingo-hsl: 0, 58.73%, 87.647%;
--pink: #f5c2e7;
--pink-rgb: 245, 194, 231;
--pink-hsl: 316.471, 71.831%, 86.078%;
--mauve: #cba6f7;
--mauve-rgb: 203, 166, 247;
--mauve-hsl: 267.407, 83.505%, 80.98%;
--red: #f38ba8;
--red-rgb: 243, 139, 168;
--red-hsl: 343.269, 81.25%, 74.902%;
--maroon: #eba0ac;
--maroon-rgb: 235, 160, 172;
--maroon-hsl: 350.4, 65.217%, 77.451%;
--peach: #fab387;
--peach-rgb: 250, 179, 135;
--peach-hsl: 22.957, 92%, 75.49%;
--yellow: #f9e2af;
--yellow-rgb: 249, 226, 175;
--yellow-hsl: 41.351, 86.047%, 83.137%;
--green: #a6e3a1;
--green-rgb: 166, 227, 161;
--green-hsl: 115.455, 54.098%, 76.078%;
--teal: #94e2d5;
--teal-rgb: 148, 226, 213;
--teal-hsl: 170, 57.353%, 73.333%;
--sky: #89dceb;
--sky-rgb: 137, 220, 235;
--sky-hsl: 189.184, 71.014%, 72.941%;
--sapphire: #74c7ec;
--sapphire-rgb: 116, 199, 236;
--sapphire-hsl: 198.5, 75.949%, 69.02%;
--blue: #89b4fa;
--blue-rgb: 137, 180, 250;
--blue-hsl: 217.168, 91.87%, 75.882%;
--lavender: #b4befe;
--lavender-rgb: 180, 190, 254;
--lavender-hsl: 231.892, 97.368%, 85.098%;
--text: #cdd6f4;
--text-rgb: 205, 214, 244;
--text-hsl: 226.154, 63.934%, 88.039%;
--subtext1: #bac2de;
--subtext1-rgb: 186, 194, 222;
--subtext1-hsl: 226.667, 35.294%, 80%;
--subtext0: #a6adc8;
--subtext0-rgb: 166, 173, 200;
--subtext0-hsl: 227.647, 23.611%, 71.765%;
--overlay2: #9399b2;
--overlay2-rgb: 147, 153, 178;
--overlay2-hsl: 228.387, 16.757%, 63.725%;
--overlay1: #7f849c;
--overlay1-rgb: 127, 132, 156;
--overlay1-hsl: 229.655, 12.775%, 55.49%;
--overlay0: #6c7086;
--overlay0-rgb: 108, 112, 134;
--overlay0-hsl: 230.769, 10.744%, 47.451%;
--surface2: #585b70;
--surface2-rgb: 88, 91, 112;
--surface2-hsl: 232.5, 12%, 39.216%;
--surface1: #45475a;
--surface1-rgb: 69 71 90;
--surface1-hsl: 234.286, 13.208%, 31.176%;
--surface0: #313244;
--surface0-rgb: 49 50 68;
--surface0-hsl: 236.842, 16.239%, 22.941%;
--base: #1e1e2e;
--base-rgb: 30, 30, 46;
--base-hsl: 240, 21.053%, 14.902%;
--mantle: #181825;
--mantle-rgb: 24, 24, 37;
--mantle-hsl: 240, 21.311%, 11.961%;
--crust: #11111b;
--crust-rgb: 17, 17, 27;
--crust-hsl: 240, 22.727%, 8.627%;
}

2532
src/css/clip-paths.css Normal file

File diff suppressed because it is too large Load diff

39
src/css/global.css Normal file
View file

@ -0,0 +1,39 @@
@import "@css/catppuccin.css";
@import "@css/clip-paths.css";
@import "@css/inter.css";
:root {
background: url("/background.png");
background-size: cover;
height: 100vh;
-webkit-font-smoothing: antialiased;
}
* {
font-family: BlinkMacSystemFont, "Inter", sans-serif;
}
body {
font-size: 0.95rem;
font-weight: 300;
color: var(--text);
padding: 30px;
overflow: hidden;
}
a {
color: #89b4fa;
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul {
margin: 0;
}
h4,
h5,
h6,
sub {
color: var(--overlay0);
}

11
src/css/inter.css Normal file
View file

@ -0,0 +1,11 @@
@font-face {
font-family: "Inter";
font-style: normal;
src: url("/fonts/Inter/Inter.ttf");
}
@font-face {
font-family: "Inter";
font-style: italic;
src: url("/fonts/Inter/Inter Italic.ttf");
}

46
src/css/me.css Normal file
View file

@ -0,0 +1,46 @@
.me-container {
display: flex;
align-items: center;
gap: 0.75rem;
.img-container {
width: 80px;
height: 80px;
clip-path: var(--clip-avatar-outer);
img {
width: 80px;
height: 80px;
clip-path: var(--clip-avatar-inner);
}
}
}
.spotify-card-wrapper {
background: var(--album-color);
position: relative;
clip-path: var(--clip-sp-1);
.spotify-card {
.secondary-meta {
color: #a6adc8;
font-size: 0.85rem;
}
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
clip-path: var(--clip-sp-2);
}
}
.spotify-card-wrapper::before {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
clip-path: var(--clip-sp-3);
position: absolute;
background-image: var(--album-art);
background-size: cover;
background-position: center;
filter: blur(20px) brightness(0.5);
}

2
src/env.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference types="astro/astro-jsx" />

View file

@ -0,0 +1,35 @@
---
import "@css/global.css";
const { tabTitle } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<meta name="darkreader-lock">
<title>{tabTitle}</title>
</head>
<body>
<slot />
<script>
import { WindowManager } from "@models/WindowManager.ts";
document.addEventListener("DOMContentLoaded", () => {
document
.querySelectorAll(".window")
.forEach(window => WindowManager.observe(window));
const isBrowserAss = () =>
["Firefox"].some(ass => navigator.userAgent.includes(ass));
if (isBrowserAss()) document.body.classList.add("ass-browser");
document
.querySelectorAll(".ua")
// @ts-ignore
.forEach(u => (u.innerText = navigator.userAgent));
});
</script>
</body>
</html>

77
src/models/Lanyard.ts Normal file
View file

@ -0,0 +1,77 @@
import type {
AnyLanyardPayload,
LanyardInitStatePayload,
LanyardPayload,
LanyardPresence,
LanyardPresenceUpdatePayload
} from "types/lanyard";
export class Lanyard {
private socket: WebSocket;
private readonly userId: string;
private readonly updateCallback: (presence: LanyardPresence) => void;
private heartbeatInterval?: number;
constructor(
userId: string,
updateCallback: (presence: LanyardPresence) => void
) {
this.userId = userId;
this.updateCallback = updateCallback;
this.socket = new WebSocket("wss://api.lanyard.rest/socket");
this.initialize();
}
private send(data: AnyLanyardPayload) {
this.socket.send(JSON.stringify(data));
}
private initialize() {
this.socket.onmessage = event => {
const payload: LanyardPayload = JSON.parse(event.data);
switch (payload.op) {
case 1: {
// Initialize
this.send({
op: 2,
d: {
subscribe_to_ids: [this.userId]
}
});
this.send({ op: 3 });
const interval =
"heartbeat_interval" in payload.d
? payload.d.heartbeat_interval
: 30000;
this.heartbeatInterval = window.setInterval(() => {
this.send({ op: 3 });
}, interval);
break;
}
case 0: {
const typedPayload = payload as
| LanyardInitStatePayload
| LanyardPresenceUpdatePayload;
const presence =
typedPayload.t === "INIT_STATE"
? typedPayload.d[this.userId]
: typedPayload.d;
this.updateCallback(presence);
break;
}
}
};
}
public close() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.socket.close();
}
}

View file

@ -0,0 +1,70 @@
export class WindowManager {
private static _topmost: HTMLElement | null;
static get topmost() {
return this._topmost;
}
static set topmost(e) {
if (this._topmost) {
this._topmost.style.zIndex = "0";
}
this._topmost = e;
this._topmost.style.display = "block";
this._topmost.style.zIndex = "1";
}
static observe(element) {
element.addEventListener("mousedown", () => {
this.topmost = element;
});
const titleBar = element.querySelector(".title-bar");
titleBar.addEventListener("mousedown", startDrag);
titleBar.addEventListener("touchstart", startDrag, { passive: false });
function startDrag(e) {
if (e.target !== titleBar) return;
let isTouch = e.type.startsWith("touch");
let startX = isTouch ? e.touches[0].clientX : e.clientX;
let startY = isTouch ? e.touches[0].clientY : e.clientY;
let offsetX = startX - element.offsetLeft;
let offsetY = startY - element.offsetTop;
function onMove(e) {
let clientX = isTouch ? e.touches[0].clientX : e.clientX;
let clientY = isTouch ? e.touches[0].clientY : e.clientY;
let newX = clientX - offsetX;
let newY = clientY - offsetY;
element.style.top = `${newY}px`;
element.style.left = `${newX}px`;
}
function stopMove() {
let maxX = window.innerWidth - element.offsetWidth;
let maxY = window.innerHeight - element.offsetHeight;
let finalX = Math.min(Math.max(element.offsetLeft, 0), maxX);
let finalY = Math.min(Math.max(element.offsetTop, 0), maxY);
element.style.top = `${finalY}px`;
element.style.left = `${finalX}px`;
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", stopMove);
document.removeEventListener("touchmove", onMove);
document.removeEventListener("touchend", stopMove);
}
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", stopMove);
document.addEventListener("touchmove", onMove, { passive: false });
document.addEventListener("touchend", stopMove);
}
}
}

14
src/pages/index.astro Normal file
View file

@ -0,0 +1,14 @@
---
import Noscript from "@components/Noscript.astro";
import MainWindow from "@components/windows/main/MainWindow.astro";
import BaseLayout from "../layouts/BaseLayout.astro";
import CodeWindow from "@components/windows/code/CodeWindow.astro";
import AssBrowser from "@components/AssBrowser.astro";
---
<BaseLayout tabTitle="nin0.dev">
<Noscript />
<AssBrowser />
<MainWindow />
<CodeWindow />
</BaseLayout>

65
src/types/lanyard.ts Normal file
View file

@ -0,0 +1,65 @@
export interface LanyardPresence {
listening_to_spotify: boolean;
spotify: {
track_id: string;
timestamps: {
start: number;
end: number;
};
song: string;
artist: string;
album_art_url: string;
album: string;
};
discord_status: string;
activities: {
type: number;
timestamps: {
start: number;
end: number;
};
state: string;
name: string;
id: string;
details: string;
assets: {
small_text: string;
small_image: string;
large_text: string;
large_image: string;
};
application_id: string;
}[];
}
export interface AnyLanyardPayload {
op: 0 | 1 | 2 | 3;
d?: object;
}
export interface LanyardHelloPayload {
op: 1;
d: {
heartbeat_interval: number;
};
}
export interface LanyardInitStatePayload {
op: 0;
t: "INIT_STATE";
d: {
[user_id: string]: LanyardPresence;
};
}
export interface LanyardPresenceUpdatePayload {
op: 0;
t: "PRESENCE_UPDATE";
d: LanyardPresence;
}
export type LanyardPayload =
| LanyardHelloPayload
| LanyardInitStatePayload
| LanyardPresenceUpdatePayload
| AnyLanyardPayload;

44
src/types/lastfm.ts Normal file
View file

@ -0,0 +1,44 @@
export interface FMRawTrack {
name: string;
url: string;
album: {
"#text": string;
};
artist: {
name: string;
};
image: Array<{
size: string;
"#text": string;
}>;
loved: string;
"@attr"?: {
nowplaying?: string;
};
}
export interface FMTrack {
name: string;
artist: string;
album: string;
url: string;
albumArt: string;
loved: boolean;
}
export interface FMResponse {
recenttracks: {
track: FMRawTrack[];
};
}
export function transformRawTrack(raw: FMRawTrack): FMTrack {
return {
name: raw.name,
artist: raw.artist.name,
album: raw.album["#text"],
url: raw.url,
albumArt: raw.image[1]["#text"],
loved: raw.loved === "1"
};
}

5
src/utils/constants.ts Normal file
View file

@ -0,0 +1,5 @@
export const LASTFM_KEY: string = "3c5623fa1abbd11c49f53ca18e992ead";
export const LASTFM_API: string = "https://ws.audioscrobbler.com/2.0/?";
export const LASTFM_USER: string = "nin0dev";
export const LANYARD_ID: string = "886685857560539176";
export const LANYARD_API: string = "wss://api.lanyard.rest/socket";

5
src/utils/mergeStyles.ts Normal file
View file

@ -0,0 +1,5 @@
export function mergeStyles(source, target: CSSStyleDeclaration) {
for (const prop in source) {
target.setProperty(prop, source[prop]);
}
}

13
tsconfig.json Normal file
View file

@ -0,0 +1,13 @@
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"baseUrl": "./src/",
"paths": {
"@components/*": ["components/*"],
"@layouts/*": ["layouts/*"],
"@css/*": ["css/*"],
"@models/*": ["models/*"],
"@assets/*": ["assets/*"]
}
}
}

View file

@ -1,105 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
* {
font-family: sans-serif !important;
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nin0dev</title>
<link rel="stylesheet" href="https://unpkg.com/7.css">
<link rel="stylesheet" href="css/style.css">
<link rel="icon" type="image/x-icon" href="logo-but-round.png">
<meta name="theme-color" content="#00D0D0">
<meta property="og:url" content="https://nin0.dev" />
<meta property="og:title" content="nin0dev" />
<meta property="og:description" content="Hey, I'm nin0dev, a Canadian software developer." />
<meta property="og:image" content="https://nin0.dev/logo.png" />
<script src="js/drag.js"></script>
</head>
<body>
<div class="window" id="main-window" style="position: absolute;">
<div class="title-bar">
<div class="title-bar-text">Home</div>
</div>
<div class="window-body">
<div id="header">
<img src="img/logo.png" alt="the nin0dev logo" id="pfp">
<h3>nin0dev <span style="font-size: 0.4em;">(he/him)</span></h3>
</div>
<br/>
<div id="presence" style="display: none; margin-bottom: 12px;">
<img src="img/game.ico" id="presence-icon">
<p id="presence-content"></p>
</div>
<fieldset>
<legend>About me</legend>
I'm a Canadian self-taught software developer that makes useless things in Python, HTML, JavaScript, and Kotlin.
<br/>
yeah that's it
</fieldset>
<br/>
<fieldset>
<legend>My projects</legend>
<ul style="margin-top: 5px; margin-bottom: 5px; padding-left: 20px;">
<li>
VendroidEnhanced: A Discord client for Android that loads the mobile website and injects Vencord.
<ul>
<li><a href="https://github.com/VendroidEnhanced/Vendroid">GitHub repo</a></li>
<li>Actively maintained</li>
<li>Fork of <a href="https://github.com/Vencord/Vendroid">Vencord/Vendroid</a></li>
</ul>
</li>
<li>
website: The website you're currently viewing
<ul>
<li><a href="https://github.com/nin0-dev/website">GitHub repo</a></li>
</ul>
</li>
<li>
nin0-bot: An in-development kitchen-sink Discord Bot with moderation, fun and utility
<ul>
<li><a href="https://github.com/nin0-dev/Sink">GitHub repo</a></li>
<li>In development</li>
</ul>
</li>
</ul>
</fieldset>
</br>
<fieldset>
<legend>Reach out!</legend>
<ul style="margin-top: 5px; margin-bottom: 5px; padding-left: 20px;">
<li>Discord: @nin0.dev</li>
<li>
Email: <a href="mailto:support@nin0.dev">support@nin0.dev</a>
</li>
<li>Telegram: <a href="https://t.me/nin0dev">@nin0dev</a></li>
<li>GitHub: <a href="https://github.com/nin0-dev">nin0-dev</a></li>
</ul>
</fieldset>
<span id="credits" style="display: none;">
<br/>
<fieldset>
<legend>Credits</legend>
<ul style="margin-top: 5px; margin-bottom: 5px; padding-left: 20px;">
<li>UI library: <a href="https://jdan.github.io/98.css/">98.css by jdan</a></li>
<li>Presence/status API: <a href="https://discord.gg/lanyard">Lanyard</a></li>
<li>Icons: <a href="https://win98icons.alexmeub.com/">official Windows 98 icons</a></li>
</ul>
</fieldset>
</span>
<div id="bottom-actions" style="margin-top: 13px;">
<button onclick="showCredits()" id="credits-button">Credits</button>
</div>
</div>
</div>
<script src="js/oneko.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lanyard-wrapper/dist/index.browser.js"></script>
<script src="js/index.js"></script>
</body>
</html>