Skip to content

Instantly share code, notes, and snippets.

@benc-uk
Last active March 22, 2024 22:36
Show Gist options
  • Save benc-uk/3c4b00ce33432ade88914aee28737c4c to your computer and use it in GitHub Desktop.
Save benc-uk/3c4b00ce33432ade88914aee28737c4c to your computer and use it in GitHub Desktop.
Web stuff
// ----------------------------------------------------------------------------
// Copyright (c) Ben Coleman, 2024. Licensed under the MIT License.
// Generic API client for calling an REST API
// ----------------------------------------------------------------------------
export class APIClientBase {
endpoint = "/api";
config = {
verbose: false, // Extra logging
headers: {}, // Pass in extra headers on all requests
delay: 0, // Fake network delay in ms
authProvider: null, // Should be an object with getAccessToken() method
success: (resp) => resp.ok, // Success checker, you can plug in your own
};
constructor(endpoint, config = {}) {
// Trim any trailing slash from the endpoint
this.endpoint = endpoint.replace(/\/$/, "");
this.config = { ...this.config, ...config };
this.debug(`### API client created for endpoint ${this.endpoint}`);
if (this.config.authProvider) {
this.debug(
`### API client: auth enabled with ${this.config.authProvider.constructor.name}`
);
}
}
// All requests go through this, it handles serialization, auth etc
async _request(path, method = "GET", payload, auth = false, reqHeaders = {}) {
this.debug(`### API request: ${method} ${this.endpoint}/${path}`);
let headers = {};
let body = null;
if (payload) {
try {
body = JSON.stringify(payload);
headers["Content-Type"] = "application/json";
} catch (e) {
// If we can't JSON stringify, just send the raw payload and hope for the best
body = payload;
}
}
// This handles authentication if enabled and the request requires it
if (auth && this.config.authProvider) {
let token = null;
try {
this.debug(`### API client: Getting access token...`);
// Call the auth provider to get a token
token = await this.config.authProvider.getAccessToken();
} catch (e) {
throw new Error("Failed to get access token");
}
// Append the access token to the request if we have one
if (token) {
headers.Authorization = `Bearer ${token}`;
}
}
// Make the actual HTTP request
const response = await fetch(`${this.endpoint}/${path}`, {
method,
body,
headers: { ...headers, ...reqHeaders, ...this.config.headers },
});
this.debug(`### API response: ${response.status} ${response.statusText}`);
// Add a fake delay to simulate network latency
if (this.config.delay > 0) {
await new Promise((resolve) => setTimeout(resolve, this.config.delay));
}
// All responses are checked via the success function
if (!this.config.success(response)) {
// Check if there is a JSON error object in the response
let errorData = null;
try {
errorData = await response.json();
} catch (e) {
throw new Error(
`API error /${path} ${response.status} ${response.statusText}`
);
}
// Support for RFC 7807 / 9457 error messages
if (errorData.title !== undefined) {
throw new Error(
`${errorData.title} (${errorData.instance}): ${errorData.detail}`
);
}
throw new Error(
`API error /${path} ${response.status} ${response.statusText}`
);
}
// Return unmarshalled object if response is JSON
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return await response.json();
}
// Otherwise return plain text
return await response.text();
}
// Debug logging
debug(...args) {
if (this.config.verbose) {
console.log(...args);
}
}
}
:root {
--slate-50: #f8fafc;
--slate-100: #f1f5f9;
--slate-200: #e2e8f0;
--slate-300: #cbd5e1;
--slate-400: #94a3b8;
--slate-500: #64748b;
--slate-600: #475569;
--slate-700: #334155;
--slate-800: #1e293b;
--slate-900: #0f172a;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
--zinc-50: #fafafa;
--zinc-100: #f4f4f5;
--zinc-200: #e4e4e7;
--zinc-300: #d4d4d8;
--zinc-400: #a1a1aa;
--zinc-500: #71717a;
--zinc-600: #52525b;
--zinc-700: #3f3f46;
--zinc-800: #27272a;
--zinc-900: #18181b;
--neutral-50: #fafafa;
--neutral-100: #f5f5f5;
--neutral-200: #e5e5e5;
--neutral-300: #d4d4d4;
--neutral-400: #a3a3a3;
--neutral-500: #737373;
--neutral-600: #525252;
--neutral-700: #404040;
--neutral-800: #262626;
--neutral-900: #171717;
--stone-50: #fafaf9;
--stone-100: #f5f5f4;
--stone-200: #e7e5e4;
--stone-300: #d6d3d1;
--stone-400: #a8a29e;
--stone-500: #78716c;
--stone-600: #57534e;
--stone-700: #44403c;
--stone-800: #292524;
--stone-900: #1c1917;
--red-50: #fef2f2;
--red-100: #fee2e2;
--red-200: #fecaca;
--red-300: #fca5a5;
--red-400: #f87171;
--red-500: #ef4444;
--red-600: #dc2626;
--red-700: #b91c1c;
--red-800: #991b1b;
--red-900: #7f1d1d;
--orange-50: #fff7ed;
--orange-100: #ffedd5;
--orange-200: #fed7aa;
--orange-300: #fdba74;
--orange-400: #fb923c;
--orange-500: #f97316;
--orange-600: #ea580c;
--orange-700: #c2410c;
--orange-800: #9a3412;
--orange-900: #7c2d12;
--amber-50: #fffbeb;
--amber-100: #fef3c7;
--amber-200: #fde68a;
--amber-300: #fcd34d;
--amber-400: #fbbf24;
--amber-500: #f59e0b;
--amber-600: #d97706;
--amber-700: #b45309;
--amber-800: #92400e;
--amber-900: #78350f;
--yellow-50: #fefce8;
--yellow-100: #fef9c3;
--yellow-200: #fef08a;
--yellow-300: #fde047;
--yellow-400: #facc15;
--yellow-500: #eab308;
--yellow-600: #ca8a04;
--yellow-700: #a16207;
--yellow-800: #854d0e;
--yellow-900: #713f12;
--lime-50: #f7fee7;
--lime-100: #ecfccb;
--lime-200: #d9f99d;
--lime-300: #bef264;
--lime-400: #a3e635;
--lime-500: #84cc16;
--lime-600: #65a30d;
--lime-700: #4d7c0f;
--lime-800: #3f6212;
--lime-900: #365314;
--green-50: #f0fdf4;
--green-100: #dcfce7;
--green-200: #bbf7d0;
--green-300: #86efac;
--green-400: #4ade80;
--green-500: #22c55e;
--green-600: #16a34a;
--green-700: #15803d;
--green-800: #166534;
--green-900: #14532d;
--emerald-50: #ecfdf5;
--emerald-100: #d1fae5;
--emerald-200: #a7f3d0;
--emerald-300: #6ee7b7;
--emerald-400: #34d399;
--emerald-500: #10b981;
--emerald-600: #059669;
--emerald-700: #047857;
--emerald-800: #065f46;
--emerald-900: #064e3b;
--teal-50: #f0fdf4;
--teal-100: #ccfbf1;
--teal-200: #99f6e4;
--teal-300: #5eead4;
--teal-400: #2dd4bf;
--teal-500: #14b8a6;
--teal-600: #0d9488;
--teal-700: #0f766e;
--teal-800: #115e59;
--teal-900: #134e4a;
--cyan-50: #ecfeff;
--cyan-100: #cffafe;
--cyan-200: #a5f3fc;
--cyan-300: #67e8f9;
--cyan-400: #22d3ee;
--cyan-500: #06b6d4;
--cyan-600: #0891b2;
--cyan-700: #0e7490;
--cyan-800: #155e75;
--cyan-900: #164e63;
--sky-50: #f0f9ff;
--sky-100: #e0f2fe;
--sky-200: #bae6fd;
--sky-300: #7dd3fc;
--sky-400: #38bdf8;
--sky-500: #0ea5e9;
--sky-600: #0284c7;
--sky-700: #0369a1;
--sky-800: #075985;
--sky-900: #0c4a6e;
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-200: #bfdbfe;
--blue-300: #93c5fd;
--blue-400: #60a5fa;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-700: #1d4ed8;
--blue-800: #1e40af;
--blue-900: #1e3a8a;
--indigo-50: #eef2ff;
--indigo-100: #e0e7ff;
--indigo-200: #c7d2fe;
--indigo-300: #a5b4fc;
--indigo-400: #818cf8;
--indigo-500: #6366f1;
--indigo-600: #4f46e5;
--indigo-700: #4338ca;
--indigo-800: #3730a3;
--indigo-900: #312e81;
--violet-50: #f5f3ff;
--violet-100: #ede9fe;
--violet-200: #ddd6fe;
--violet-300: #c4b5fd;
--violet-400: #a78bfa;
--violet-500: #8b5cf6;
--violet-600: #7c3aed;
--violet-700: #6d28d9;
--violet-800: #5b21b6;
--violet-900: #4c1d95;
--purple-50: #faf5ff;
--purple-100: #f3e8ff;
--purple-200: #e9d5ff;
--purple-300: #d8b4fe;
--purple-400: #c084fc;
--purple-500: #a855f7;
--purple-600: #9333ea;
--purple-700: #7e22ce;
--purple-800: #6b21a8;
--purple-900: #581c87;
--fuchsia-50: #fdf4ff;
--fuchsia-100: #fae8ff;
--fuchsia-200: #f5d0fe;
--fuchsia-300: #f0abfc;
--fuchsia-400: #e879f9;
--fuchsia-500: #d946ef;
--fuchsia-600: #c026d3;
--fuchsia-700: #a21caf;
--fuchsia-800: #86198f;
--fuchsia-900: #701a75;
--pink-50: #fdf4ff;
--pink-100: #fae8ff;
--pink-200: #f5d0fe;
--pink-300: #f0abfc;
--pink-400: #e879f9;
--pink-500: #d946ef;
--pink-600: #c026d3;
--pink-700: #a21caf;
--pink-800: #86198f;
--pink-900: #701a75;
--rose-50: #fff1f2;
--rose-100: #ffe4e6;
--rose-200: #fecdd3;
--rose-300: #fda4af;
--rose-400: #fb7185;
--rose-500: #f43f5e;
--rose-600: #e11d48;
--rose-700: #be123c;
--rose-800: #9f1239;
--rose-900: #881337;
}
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='1em' x='-10' font-size='88'>💩</text></svg>"
/>

Run A Local HTTP Server

So you need to a run a HTTP server locally, you have a lot of options

Node.JS

Good old http-server is your friend

npx http-server -c-1

Browsersync will do fancy hot reloading on files changing, and is designed for SPAs but has so many options it's super confusing

npx browser-sync start --watch --server --directory --no-ui

Python

python3 -m http.server 8080

Deno

deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts -p 8080 --host localhost'

Go

Try using ran https://github.com/m3ng9i/ran

go install github.com/m3ng9i/ran@latest
ran -listdir
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 30px;
padding-top: 5px;
}
h1, h2, h3, h4 {
color: rgb(0,120,215);
border-bottom: 3px solid grey;
padding-bottom: 3px;
font-weight: 100;
}
article, main {
border: 1px solid grey;
border-radius: 10px;
padding: 20px;
box-shadow: 0px 5px 11px 0px rgba(0,0,0,0.27);
}
button, .btn {
margin: 10px;
font-size: 20px;
background-color: rgb(0,120,215);
color: white;
border: none;
padding: 8px;
box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.27);
}
// ----------------------------------------------------------------------------
// Copyright (c) Ben Coleman, 2024. Licensed under the MIT License.
// AuthProvider for APIClientBase that uses MSAL for authentication
// ----------------------------------------------------------------------------
export class MSALAuthProvider {
msalApp = null;
scopes = [];
constructor(clientId, scopes = ["User.Read"], tenant = "common") {
const config = {
auth: {
clientId,
redirectUri: window.location.origin,
authority: `https://login.microsoftonline.com/${tenant}`,
},
cache: {
cacheLocation: "localStorage",
},
};
this.msalApp = new msal.PublicClientApplication(config);
this.scopes = scopes;
}
// Get an access token, either from cache or by prompting the user
// This implements our contract with the API client
async getAccessToken() {
let tokenRes = null;
try {
tokenRes = await this.msalApp.acquireTokenSilent({
scopes: this.scopes,
});
} catch (e) {
tokenRes = await this.msalApp.acquireTokenPopup({
scopes: this.scopes,
});
}
if (!tokenRes || !tokenRes.accessToken) {
throw new Error("Failed to get token from MSAL");
}
return tokenRes.accessToken;
}
}
// ==========================================================================
// 🍞 toast.js - A simple & standalone, pure JS toast/popup library for JS
// Ben Coleman, 2021
// =========================================================================
const toastStyles = document.createElement('style')
toastStyles.innerHTML = `
.toast {
background-color: #444;
position: fixed;
z-index: 50;
padding: 1rem;
box-shadow: 0.2rem 0.5rem 0.8rem rgba(0, 0, 0, 0.5);
border-radius: 0.5rem;
cursor: default;
}
.toastShown {
visibility: visible;
opacity: 1;
transition: opacity 0.3s linear;
}
.toastHidden {
visibility: hidden;
opacity: 0;
transition: visibility 0s 0.5s, opacity 0.3s linear;
}`
document.body.appendChild(toastStyles)
// Show a toast message
export function showToast(message, duration = 2000, pos = 'top-center') {
const toast = document.createElement(`div`)
toast.classList.add(`toast`)
toast.classList.add(`toastHidden`)
toast.innerHTML = message
toast.addEventListener('click', () => {
toast.classList.add(`toastHidden`)
})
document.body.appendChild(toast)
switch (pos) {
case 'top-center':
toast.style.top = '2rem'
toast.style.left = '50%'
toast.style.transform = 'translateX(-50%)'
break
case 'top-right':
toast.style.top = '2rem'
toast.style.right = '2rem'
break
case 'top-left':
toast.style.top = '2rem'
toast.style.left = '2rem'
break
case 'bottom-center':
toast.style.bottom = '2rem'
toast.style.left = '50%'
toast.style.transform = 'translateX(-50%)'
break
case 'bottom-right':
toast.style.bottom = '2rem'
toast.style.right = '2rem'
break
case 'bottom-left':
toast.style.bottom = '2rem'
toast.style.left = '2rem'
break
default:
toast.style.top = '2rem'
toast.style.left = '50%'
toast.style.transform = 'translateX(-50%)'
}
// Show the toast
toast.classList.replace('toastHidden', 'toastShown')
// Set a timeout to hide the toast
setTimeout(function () {
toast.classList.replace('toastShown', 'toastHidden')
// Remove from the DOM *after* fading out
setTimeout(function () {
document.body.removeChild(toast)
}, 1000)
}, duration)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment