From e4253e6de24191a54898ffe67163925d048561d7 Mon Sep 17 00:00:00 2001
From: worble
Date: Sun, 2 Mar 2025 17:15:58 +0000
Subject: [PATCH] init
---
.env.development | 5 +
.env.production | 2 +
.envrc | 1 +
.gitignore | 3 +
biome.json | 26 +
flake.lock | 64 ++
flake.nix | 26 +
index.html | 18 +
package-lock.json | 1023 +++++++++++++++++
package.json | 24 +
src/App.tsx | 14 +
src/components/Chatters.tsx | 214 ++++
src/components/Loading.tsx | 5 +
src/components/ResourceComponent.tsx | 68 ++
src/contexts/Contexts.tsx | 11 +
src/contexts/TwitchServiceContext.tsx | 56 +
src/contexts/UserContext.tsx | 23 +
src/index.tsx | 8 +
src/services/twitch/TwitchApiService.ts | 89 ++
.../twitch/responses/GetChattersResponse.ts | 13 +
.../responses/GetSubscriptionsResponse.ts | 23 +
.../twitch/responses/GetUsersResponse.ts | 17 +
src/utils/parseError.ts | 3 +
src/utils/useRequiredContext.ts | 13 +
tsconfig.json | 41 +
vite.config.ts | 16 +
26 files changed, 1806 insertions(+)
create mode 100644 .env.development
create mode 100644 .env.production
create mode 100644 .envrc
create mode 100644 .gitignore
create mode 100644 biome.json
create mode 100644 flake.lock
create mode 100644 flake.nix
create mode 100644 index.html
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 src/App.tsx
create mode 100644 src/components/Chatters.tsx
create mode 100644 src/components/Loading.tsx
create mode 100644 src/components/ResourceComponent.tsx
create mode 100644 src/contexts/Contexts.tsx
create mode 100644 src/contexts/TwitchServiceContext.tsx
create mode 100644 src/contexts/UserContext.tsx
create mode 100644 src/index.tsx
create mode 100644 src/services/twitch/TwitchApiService.ts
create mode 100644 src/services/twitch/responses/GetChattersResponse.ts
create mode 100644 src/services/twitch/responses/GetSubscriptionsResponse.ts
create mode 100644 src/services/twitch/responses/GetUsersResponse.ts
create mode 100644 src/utils/parseError.ts
create mode 100644 src/utils/useRequiredContext.ts
create mode 100644 tsconfig.json
create mode 100644 vite.config.ts
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/**"],
+ },
+ },
+});