Last active
March 22, 2024 22:36
-
-
Save benc-uk/3c4b00ce33432ade88914aee28737c4c to your computer and use it in GitHub Desktop.
Web stuff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ---------------------------------------------------------------------------- | |
// 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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
: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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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>" | |
/> |
So you need to a run a HTTP server locally, you have a lot of options
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
python3 -m http.server 8080
deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts -p 8080 --host localhost'
Try using ran
https://github.com/m3ng9i/ran
go install github.com/m3ng9i/ran@latest
ran -listdir
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ---------------------------------------------------------------------------- | |
// 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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ========================================================================== | |
// 🍞 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