commit e4253e6de24191a54898ffe67163925d048561d7
Author: worble
Date: Sun Mar 2 17:15:58 2025 +0000
init
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..80131b2
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,5 @@
+# Generate a client id through the twitch CLI
+VITE_CLIENT_ID=412c7e3c3df177bbd466a213ce4d05
+VITE_TWITCH_API_BASE=http://localhost:8080/mock
+# Then get yourself a token via curl -X POST 'http://localhost:8080/auth/authorize?client_id={client_id}&client_secret={client_secret}&grant_type=user_token&user_id={user_id}&scope={scopes}
+VITE_TOKEN_OVERRIDE=ed70cddba8db975
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..386dfbd
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,2 @@
+VITE_CLIENT_ID=vzn24kqo5sq0g9duja3vpuenknend6
+VITE_TWITCH_API_BASE=https://api.twitch.tv/helix
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..8392d15
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..30ba776
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+dist
+.direnv
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..5b68051
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "organizeImports": {
+ "enabled": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "all": true,
+ "correctness": {
+ "noUndeclaredDependencies": "off"
+ },
+ "complexity": {
+ "useLiteralKeys": "off"
+ },
+ "style": {
+ "useExplicitLengthCheck": "off",
+ "useNamingConvention": "off"
+ }
+ },
+ "ignore": [
+ "src/Services/Miniflux/Requests/*",
+ "src/Services/Miniflux/Responses/*"
+ ]
+ }
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..12ff1eb
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,64 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": [
+ "systems"
+ ]
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1736200483,
+ "narHash": "sha256-JO+lFN2HsCwSLMUWXHeOad6QUxOuwe9UOAF/iSl1J4I=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "3f0a8ac25fb674611b98089ca3a5dd6480175751",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-24.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs",
+ "systems": "systems"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1680978846,
+ "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=",
+ "owner": "nix-systems",
+ "repo": "x86_64-linux",
+ "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "x86_64-linux",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..4d38f0d
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,26 @@
+{
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
+ inputs.systems.url = "github:nix-systems/x86_64-linux";
+ inputs.flake-utils = {
+ url = "github:numtide/flake-utils";
+ inputs.systems.follows = "systems";
+ };
+
+ outputs =
+ { nixpkgs, flake-utils, ... }:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in
+ {
+ devShells.default = pkgs.mkShell {
+ packages = with pkgs;[
+ biome
+ nodejs_22
+ twitch-cli
+ ];
+ };
+ }
+ );
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6b00cb0
--- /dev/null
+++ b/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Twitch Chatter Helper
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..cf158a0
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1023 @@
+{
+ "name": "vite-template-solid",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vite-template-solid",
+ "version": "0.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@picocss/pico": "^2.0.6",
+ "es-toolkit": "^1.32.0",
+ "solid-js": "^1.9.5"
+ },
+ "devDependencies": {
+ "typescript": "^5.8.2",
+ "vite": "^6.2.0",
+ "vite-plugin-solid": "^2.11.6",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.26.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.26.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.9",
+ "@babel/helper-compilation-targets": "^7.26.5",
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helpers": "^7.26.9",
+ "@babel/parser": "^7.26.9",
+ "@babel/template": "^7.26.9",
+ "@babel/traverse": "^7.26.9",
+ "@babel/types": "^7.26.9",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.26.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.26.9",
+ "@babel/types": "^7.26.9",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.26.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.26.5",
+ "@babel/helper-validator-option": "^7.25.9",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.25.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.26.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/traverse": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.26.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.25.9",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.26.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.26.9",
+ "@babel/types": "^7.26.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.9"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.25.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.26.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/parser": "^7.26.9",
+ "@babel/types": "^7.26.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.26.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.9",
+ "@babel/parser": "^7.26.9",
+ "@babel/template": "^7.26.9",
+ "@babel/types": "^7.26.9",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.0",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@picocss/pico": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-2.0.6.tgz",
+ "integrity": "sha512-/d8qsykowelD6g8k8JYgmCagOIulCPHMEc2NC4u7OjmpQLmtSetLhEbt0j1n3fPNJVcrT84dRp0RfJBn3wJROA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.19.0"
+ }
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.34.9",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.34.9",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/babel-plugin-jsx-dom-expressions": {
+ "version": "0.39.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "7.18.6",
+ "@babel/plugin-syntax-jsx": "^7.18.6",
+ "@babel/types": "^7.20.7",
+ "html-entities": "2.3.3",
+ "parse5": "^7.1.2",
+ "validate-html-nesting": "^1.2.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.20.12"
+ }
+ },
+ "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/babel-preset-solid": {
+ "version": "1.9.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jsx-dom-expressions": "^0.39.7"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.4",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001688",
+ "electron-to-chromium": "^1.5.73",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.1"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001701",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.109",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.32.0.tgz",
+ "integrity": "sha512-ZfSfHP1l6ubgW/B/FRtqb9bYdMvI6jizbOSfbwwJNcOQ1QE6TFsC3jpQkZ900uUPSR3t3SU5Ds7UWKnYz+uP8Q==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.0",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.0",
+ "@esbuild/android-arm": "0.25.0",
+ "@esbuild/android-arm64": "0.25.0",
+ "@esbuild/android-x64": "0.25.0",
+ "@esbuild/darwin-arm64": "0.25.0",
+ "@esbuild/darwin-x64": "0.25.0",
+ "@esbuild/freebsd-arm64": "0.25.0",
+ "@esbuild/freebsd-x64": "0.25.0",
+ "@esbuild/linux-arm": "0.25.0",
+ "@esbuild/linux-arm64": "0.25.0",
+ "@esbuild/linux-ia32": "0.25.0",
+ "@esbuild/linux-loong64": "0.25.0",
+ "@esbuild/linux-mips64el": "0.25.0",
+ "@esbuild/linux-ppc64": "0.25.0",
+ "@esbuild/linux-riscv64": "0.25.0",
+ "@esbuild/linux-s390x": "0.25.0",
+ "@esbuild/linux-x64": "0.25.0",
+ "@esbuild/netbsd-arm64": "0.25.0",
+ "@esbuild/netbsd-x64": "0.25.0",
+ "@esbuild/openbsd-arm64": "0.25.0",
+ "@esbuild/openbsd-x64": "0.25.0",
+ "@esbuild/sunos-x64": "0.25.0",
+ "@esbuild/win32-arm64": "0.25.0",
+ "@esbuild/win32-ia32": "0.25.0",
+ "@esbuild/win32-x64": "0.25.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-entities": {
+ "version": "2.3.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/merge-anything": {
+ "version": "5.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.34.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.34.9",
+ "@rollup/rollup-android-arm64": "4.34.9",
+ "@rollup/rollup-darwin-arm64": "4.34.9",
+ "@rollup/rollup-darwin-x64": "4.34.9",
+ "@rollup/rollup-freebsd-arm64": "4.34.9",
+ "@rollup/rollup-freebsd-x64": "4.34.9",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.34.9",
+ "@rollup/rollup-linux-arm-musleabihf": "4.34.9",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.9",
+ "@rollup/rollup-linux-arm64-musl": "4.34.9",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.34.9",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9",
+ "@rollup/rollup-linux-riscv64-gnu": "4.34.9",
+ "@rollup/rollup-linux-s390x-gnu": "4.34.9",
+ "@rollup/rollup-linux-x64-gnu": "4.34.9",
+ "@rollup/rollup-linux-x64-musl": "4.34.9",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.9",
+ "@rollup/rollup-win32-ia32-msvc": "4.34.9",
+ "@rollup/rollup-win32-x64-msvc": "4.34.9",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/seroval": {
+ "version": "1.2.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/seroval-plugins": {
+ "version": "1.2.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "seroval": "^1.0"
+ }
+ },
+ "node_modules/solid-js": {
+ "version": "1.9.5",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.0",
+ "seroval": "^1.1.0",
+ "seroval-plugins": "^1.1.0"
+ }
+ },
+ "node_modules/solid-refresh": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/generator": "^7.23.6",
+ "@babel/helper-module-imports": "^7.22.15",
+ "@babel/types": "^7.23.6"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.3"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tsconfck": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tsconfck": "bin/tsconfck.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/validate-html-nesting": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/vite": {
+ "version": "6.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "postcss": "^8.5.3",
+ "rollup": "^4.30.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-solid": {
+ "version": "2.11.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.3",
+ "@types/babel__core": "^7.20.4",
+ "babel-preset-solid": "^1.8.4",
+ "merge-anything": "^5.1.7",
+ "solid-refresh": "^0.6.3",
+ "vitefu": "^1.0.4"
+ },
+ "peerDependencies": {
+ "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*",
+ "solid-js": "^1.7.2",
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@testing-library/jest-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-tsconfig-paths": {
+ "version": "5.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "globrex": "^0.1.2",
+ "tsconfck": "^3.0.3"
+ },
+ "peerDependencies": {
+ "vite": "*"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitefu": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0586175
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "vite-template-solid",
+ "version": "0.0.0",
+ "description": "",
+ "type": "module",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build": "vite build",
+ "serve": "vite preview"
+ },
+ "license": "MIT",
+ "devDependencies": {
+ "typescript": "^5.8.2",
+ "vite": "^6.2.0",
+ "vite-plugin-solid": "^2.11.6",
+ "vite-tsconfig-paths": "^5.1.4"
+ },
+ "dependencies": {
+ "@picocss/pico": "^2.0.6",
+ "es-toolkit": "^1.32.0",
+ "solid-js": "^1.9.5"
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..0bc3e35
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,14 @@
+import { Chatters } from "@components/Chatters.tsx";
+import { Contexts } from "@contexts/Contexts.tsx";
+import type { Component } from "solid-js";
+
+export const App: Component = () => {
+ return (
+
+ Current Chatters
+
+
+
+
+ );
+};
diff --git a/src/components/Chatters.tsx b/src/components/Chatters.tsx
new file mode 100644
index 0000000..1871a51
--- /dev/null
+++ b/src/components/Chatters.tsx
@@ -0,0 +1,214 @@
+import { TwitchServiceContext } from "@contexts/TwitchServiceContext";
+import { UserContext } from "@contexts/UserContext";
+import type { ChatterResponse } from "@services/twitch/responses/GetChattersResponse.ts";
+import type { SubscriptionResponse } from "@services/twitch/responses/GetSubscriptionsResponse.ts";
+import { useRequiredContext } from "@utils/useRequiredContext";
+import { sortBy } from "es-toolkit";
+import {
+ type Accessor,
+ type Component,
+ For,
+ type Setter,
+ Show,
+ createResource,
+ createSignal,
+} from "solid-js";
+import { ResourceComponent } from "./ResourceComponent.tsx";
+
+const ExportDialog: Component<{
+ setRef: Setter;
+ ref: Accessor;
+ getJson: () => string;
+}> = (props) => {
+ const [clipboardStatus, setClipboardStatus] = createSignal<
+ "Writing" | "Written" | "Initial"
+ >("Initial");
+
+ const copyClipboard = async () => {
+ setClipboardStatus("Writing");
+ await navigator.clipboard.writeText(props.getJson());
+ setClipboardStatus("Written");
+ setTimeout(() => setClipboardStatus("Initial"), 1000);
+ };
+
+ return (
+
+ );
+};
+
+const ChatttersView: Component<{
+ chatters: ChatterResponse[];
+ subscriptions: SubscriptionResponse[];
+}> = (props) => {
+ const [selected, setSelected] = createSignal([]);
+ const [search, setSearch] = createSignal("");
+ const subs = sortBy(props.subscriptions, ["user_name"]);
+ const scrubs = sortBy(
+ props.chatters.filter((chat) =>
+ props.subscriptions.find((sub) => sub.user_id !== chat.user_id),
+ ),
+ ["user_name"],
+ );
+
+ const onChange = (id: string) => {
+ setSelected((prev: string[]) =>
+ prev.includes(id)
+ ? prev.filter((prevId) => prevId !== id)
+ : [...prev, id],
+ );
+ };
+
+ const oneHundredClick = () => {
+ setSelected([
+ ...subs.slice(0, 100).map((sub) => sub.user_id),
+ ...scrubs.slice(0, 100 - subs.length).map((scrub) => scrub.user_id),
+ ]);
+ };
+
+ const [dialogRef, setDialogRef] = createSignal();
+
+ const getJson = () =>
+ JSON.stringify(
+ props.chatters
+ .filter((chat) => selected().includes(chat.user_id))
+ .map((chat) => chat.user_name),
+ null,
+ 4,
+ );
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export const Chatters: Component = () => {
+ const twitchService = useRequiredContext(TwitchServiceContext);
+ const user = useRequiredContext(UserContext);
+
+ const [resource] = createResource(async () => {
+ const chatters = await twitchService.getChatters(user.id);
+ const subscriptions = await twitchService.getSubscribers(
+ user.id,
+ chatters.map((e) => e.user_id),
+ );
+ return {
+ chatters,
+ subscriptions,
+ };
+ });
+
+ return (
+
+ {(response) => (
+
+ )}
+
+ );
+};
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
new file mode 100644
index 0000000..95f1920
--- /dev/null
+++ b/src/components/Loading.tsx
@@ -0,0 +1,5 @@
+import type { Component } from "solid-js";
+
+export const Loading: Component = () => {
+ return "Loading...";
+};
diff --git a/src/components/ResourceComponent.tsx b/src/components/ResourceComponent.tsx
new file mode 100644
index 0000000..a20ec21
--- /dev/null
+++ b/src/components/ResourceComponent.tsx
@@ -0,0 +1,68 @@
+import { Loading } from "@components/Loading.tsx";
+import { parseError } from "@utils/parseError.ts";
+import {
+ type Accessor,
+ type Component,
+ type JSX,
+ Match,
+ type Resource,
+ Show,
+ Switch,
+} from "solid-js";
+
+export const ResourceComponent = (props: {
+ children: (value: Accessor>) => JSX.Element;
+ resource: Resource;
+ errorComponent?: (e: Accessor) => JSX.Element;
+ loadingComponent?: () => JSX.Element;
+ fallback?: JSX.Element;
+}) => {
+ const isError = () => {
+ const error = props.resource.error;
+ if (error) {
+ return parseError(error);
+ }
+ return undefined;
+ };
+
+ return (
+
+
+ }>
+ {(loadingComponent) => loadingComponent()()}
+
+
+
+ {(error) => (
+ }
+ >
+ {(errorComponent) => errorComponent()(error)}
+
+ )}
+
+ {(value) => props.children(value)}
+
+ );
+};
+
+const FallbackError: Component<{ error: Accessor }> = (props) => {
+ return (
+
+
+ An error occurred
+
+ Click for details
+
+ {JSON.stringify(
+ props.error(),
+ Object.getOwnPropertyNames(props.error()),
+ 2,
+ )}
+
+
+
+
+ );
+};
diff --git a/src/contexts/Contexts.tsx b/src/contexts/Contexts.tsx
new file mode 100644
index 0000000..1410271
--- /dev/null
+++ b/src/contexts/Contexts.tsx
@@ -0,0 +1,11 @@
+import type { ParentComponent } from "solid-js";
+import { TwitchServiceContextProvider } from "./TwitchServiceContext.tsx";
+import { UserContextProvider } from "./UserContext.tsx";
+
+export const Contexts: ParentComponent = (props) => {
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/src/contexts/TwitchServiceContext.tsx b/src/contexts/TwitchServiceContext.tsx
new file mode 100644
index 0000000..f08b5fe
--- /dev/null
+++ b/src/contexts/TwitchServiceContext.tsx
@@ -0,0 +1,56 @@
+import { TwitchApiService } from "@services/twitch/TwitchApiService.ts";
+import {
+ type ParentComponent,
+ Show,
+ createContext,
+ createRenderEffect,
+ createSignal,
+} from "solid-js";
+
+const searchParams = new URLSearchParams({
+ client_id: import.meta.env["VITE_CLIENT_ID"],
+ redirect_uri: new URL(location.origin).toString(),
+ response_type: "token",
+ scope: "moderator:read:chatters channel:read:subscriptions",
+});
+const url = new URL(
+ `https://id.twitch.tv/oauth2/authorize?${searchParams.toString()}`,
+);
+
+export const TwitchServiceContext = createContext();
+
+export const TwitchServiceContextProvider: ParentComponent = (props) => {
+ const [twitchApiService, setTwitchApiService] =
+ createSignal();
+
+ createRenderEffect(() => {
+ const devOverride = import.meta.env["VITE_TOKEN_OVERRIDE"];
+ if (devOverride) {
+ setTwitchApiService(new TwitchApiService(devOverride));
+ return;
+ }
+
+ const urlSearchParams = new URLSearchParams(location.hash.slice(1));
+ const accessToken = urlSearchParams.get("access_token");
+ if (accessToken) {
+ setTwitchApiService(new TwitchApiService(accessToken));
+ window.location.hash = "";
+ }
+ }, []);
+
+ return (
+
+ To use this application, please{" "}
+ Connect with Twitch
+
+ }
+ >
+
+ {props.children}
+
+
+ );
+};
diff --git a/src/contexts/UserContext.tsx b/src/contexts/UserContext.tsx
new file mode 100644
index 0000000..b88d0bf
--- /dev/null
+++ b/src/contexts/UserContext.tsx
@@ -0,0 +1,23 @@
+import { ResourceComponent } from "@components/ResourceComponent.tsx";
+import type { UserResponse } from "@services/twitch/responses/GetUsersResponse.ts";
+import { type ParentComponent, createContext, createResource } from "solid-js";
+import { useRequiredContext } from "../utils/useRequiredContext.ts";
+import { TwitchServiceContext } from "./TwitchServiceContext.tsx";
+
+export const UserContext = createContext();
+
+export const UserContextProvider: ParentComponent = (props) => {
+ const twitchService = useRequiredContext(TwitchServiceContext);
+
+ const [resource] = createResource(async () => await twitchService.getUser());
+
+ return (
+
+ {(user) => (
+
+ {props.children}
+
+ )}
+
+ );
+};
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..bb945f2
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,8 @@
+/* @refresh reload */
+import "@picocss/pico";
+import { render } from "solid-js/web";
+import { App } from "./App.tsx";
+
+const root = document.createElement("div");
+document.body.appendChild(root);
+render(() => , root);
diff --git a/src/services/twitch/TwitchApiService.ts b/src/services/twitch/TwitchApiService.ts
new file mode 100644
index 0000000..b716539
--- /dev/null
+++ b/src/services/twitch/TwitchApiService.ts
@@ -0,0 +1,89 @@
+import type {
+ ChatterResponse,
+ GetChattersResponse,
+} from "./responses/GetChattersResponse.ts";
+import type {
+ GetSubscriptionsResponse,
+ SubscriptionResponse,
+} from "./responses/GetSubscriptionsResponse.ts";
+import type {
+ GetUsersResponse,
+ UserResponse,
+} from "./responses/GetUsersResponse.ts";
+
+export class TwitchApiService {
+ baseUrl: string;
+ headers: HeadersInit;
+
+ constructor(accessToken: string) {
+ this.baseUrl = import.meta.env["VITE_TWITCH_API_BASE"];
+ this.headers = {
+ Authorization: `Bearer ${accessToken}`,
+ "Client-Id": import.meta.env["VITE_CLIENT_ID"],
+ };
+ }
+
+ private async parseResponse(response: Response): Promise {
+ if (!response.ok) {
+ throw new Error(
+ `${response.status.toString()}: ${await response.text()}`,
+ );
+ }
+ return await response.json();
+ }
+
+ getUser = async (): Promise => {
+ const url = new URL([this.baseUrl, "users"].join("/"));
+ const response = await fetch(url.toString(), { headers: this.headers });
+ const users = await this.parseResponse(response);
+ return users.data[0] as UserResponse;
+ };
+
+ getChatters = async (id: string): Promise => {
+ const params = new URLSearchParams({
+ broadcaster_id: id,
+ moderator_id: id,
+ first: (1000).toString(),
+ });
+ const url = new URL([this.baseUrl, "chat", "chatters"].join("/"));
+
+ const response = await fetch(`${url.toString()}?${params.toString()}`, {
+ headers: this.headers,
+ });
+ let parsed = await this.parseResponse(response);
+ let chatters = parsed.data;
+
+ while (parsed.pagination?.cursor) {
+ params.set("after", parsed.pagination.cursor);
+ const response = await fetch(`${url.toString()}?${params.toString()}`, {
+ headers: this.headers,
+ });
+ parsed = await this.parseResponse(response);
+ chatters = chatters.concat(parsed.data);
+ }
+
+ return chatters;
+ };
+
+ getSubscribers = async (
+ id: string,
+ userIds: string[],
+ ): Promise => {
+ const url = new URL([this.baseUrl, "subscriptions"].join("/"));
+ let subscribers: SubscriptionResponse[] = [];
+
+ for (let index = 0; index < userIds.length; index += 100) {
+ const ids = userIds
+ .slice(index, index + 100)
+ .map((id) => ["user_id", id]);
+ const params = new URLSearchParams([["broadcaster_id", id]].concat(ids));
+ const response = await fetch(`${url.toString()}?${params.toString()}`, {
+ headers: this.headers,
+ });
+ const parsed =
+ await this.parseResponse(response);
+ subscribers = subscribers.concat(parsed.data);
+ }
+ return subscribers;
+ };
+}
diff --git a/src/services/twitch/responses/GetChattersResponse.ts b/src/services/twitch/responses/GetChattersResponse.ts
new file mode 100644
index 0000000..27dcc3d
--- /dev/null
+++ b/src/services/twitch/responses/GetChattersResponse.ts
@@ -0,0 +1,13 @@
+export interface ChatterResponse {
+ user_id: string;
+ user_login: string;
+ user_name: string;
+}
+
+export interface GetChattersResponse {
+ data: ChatterResponse[];
+ pagination?: {
+ cursor: string;
+ };
+ total: number;
+}
diff --git a/src/services/twitch/responses/GetSubscriptionsResponse.ts b/src/services/twitch/responses/GetSubscriptionsResponse.ts
new file mode 100644
index 0000000..7f442d1
--- /dev/null
+++ b/src/services/twitch/responses/GetSubscriptionsResponse.ts
@@ -0,0 +1,23 @@
+export interface SubscriptionResponse {
+ broadcaster_id: string;
+ broadcaster_login: string;
+ broadcaster_name: string;
+ gifter_id: string;
+ gifter_login: string;
+ gifter_name: string;
+ is_gift: boolean;
+ tier: string;
+ plan_name: string;
+ user_id: string;
+ user_name: string;
+ user_login: string;
+}
+
+export interface GetSubscriptionsResponse {
+ data: SubscriptionResponse[];
+ pagination: {
+ cursor: string;
+ };
+ total: number;
+ points: number;
+}
diff --git a/src/services/twitch/responses/GetUsersResponse.ts b/src/services/twitch/responses/GetUsersResponse.ts
new file mode 100644
index 0000000..396d32d
--- /dev/null
+++ b/src/services/twitch/responses/GetUsersResponse.ts
@@ -0,0 +1,17 @@
+export interface UserResponse {
+ id: string;
+ login: string;
+ display_name: string;
+ type: string;
+ broadcaster_type: string;
+ description: string;
+ profile_image_url: string;
+ offline_image_url: string;
+ view_count: number;
+ email: string;
+ created_at: string;
+}
+
+export interface GetUsersResponse {
+ data: UserResponse[];
+}
diff --git a/src/utils/parseError.ts b/src/utils/parseError.ts
new file mode 100644
index 0000000..7c58460
--- /dev/null
+++ b/src/utils/parseError.ts
@@ -0,0 +1,3 @@
+export function parseError(error: unknown): Error {
+ return error instanceof Error ? error : new Error(JSON.stringify(error));
+}
diff --git a/src/utils/useRequiredContext.ts b/src/utils/useRequiredContext.ts
new file mode 100644
index 0000000..d5d681f
--- /dev/null
+++ b/src/utils/useRequiredContext.ts
@@ -0,0 +1,13 @@
+import { type Context, useContext } from "solid-js";
+
+export const useRequiredContext = (context: Context): T => {
+ const getContext = useContext(context);
+
+ if (getContext === undefined) {
+ throw new Error(
+ `${context.constructor.name} must be used within a provider instance`,
+ );
+ }
+
+ return getContext;
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5742db4
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,41 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "allowUnusedLabels": false,
+ "allowUnreachableCode": false,
+ "exactOptionalPropertyTypes": false,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+
+ "isolatedModules": true,
+
+ "checkJs": true,
+
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "types": ["vite/client"],
+ "noEmit": true,
+ "allowImportingTsExtensions": true,
+
+ "paths": {
+ "@components/*": ["./src/components/*"],
+ "@services/*": ["./src/services/*"],
+ "@contexts/*": ["./src/contexts/*"],
+ "@utils/*": ["./src/utils/*"],
+ "@src/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..5322a13
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "vite";
+import solidPlugin from "vite-plugin-solid";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [tsconfigPaths(), solidPlugin()],
+ server: {
+ port: 3000,
+ },
+ build: {
+ target: "esnext",
+ watch: {
+ exclude: ["**/.devenv/**", "**/.direnv/**"],
+ },
+ },
+});