Something went wrong. Try again.
Tools for the Atmosphere tools.slices.network
quickslice atproto html
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; connect-src 'self' https://xyzstatusphere.slices.network https://*.webcontainer.io; img-src 'self' https: data:;" /> <title>Statusphere</title> <style> /* CSS Reset */ *, *::before, *::after { box-sizing: border-box; } * { margin: 0; } body { line-height: 1.5; -webkit-font-smoothing: antialiased; } input, button { font: inherit; }
/* CSS Variables */ :root { --primary-500: #0078ff; --primary-400: #339dff; --primary-600: #0060cc; --gray-100: #f5f5f5; --gray-200: #e5e5e5; --gray-500: #737373; --gray-700: #404040; --gray-900: #171717; --border-color: #e5e5e5; --error-bg: #fef2f2; --error-border: #fecaca; --error-text: #dc2626; }
/* Layout */ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--gray-100); color: var(--gray-900); min-height: 100vh; padding: 2rem 1rem; }
#app { max-width: 600px; margin: 0 auto; }
/* Header */ header { text-align: center; margin-bottom: 2rem; }
header h1 { font-size: 2.5rem; color: var(--primary-500); margin-bottom: 0.25rem; }
.tagline { color: var(--gray-500); font-size: 1rem; }
/* Cards */ .card { background: white; border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
/* Auth Section */ .login-form { display: flex; flex-direction: column; gap: 1rem; }
.form-group { display: flex; flex-direction: column; gap: 0.25rem; }
.form-group label { font-size: 0.875rem; font-weight: 500; color: var(--gray-700); }
.form-group input { padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 0.375rem; font-size: 1rem; }
.form-group input:focus { outline: none; border-color: var(--primary-500); box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.1); }
.btn { padding: 0.75rem 1.5rem; border: none; border-radius: 0.375rem; font-size: 1rem; font-weight: 500; cursor: pointer; transition: background-color 0.15s; }
.btn-primary { background: var(--primary-500); color: white; }
.btn-primary:hover { background: var(--primary-600); }
.btn-primary:disabled { background: var(--gray-200); color: var(--gray-500); cursor: not-allowed; }
.btn-secondary { background: var(--gray-200); color: var(--gray-700); }
.btn-secondary:hover { background: var(--border-color); }
/* User Card */ .user-card { display: flex; align-items: center; justify-content: space-between; }
.user-info { display: flex; align-items: center; gap: 0.75rem; }
.user-avatar { width: 48px; height: 48px; border-radius: 50%; background: var(--gray-200); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; }
.user-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.user-name { font-weight: 600; }
.user-handle { font-size: 0.875rem; color: var(--gray-500); }
/* Emoji Picker */ .emoji-grid { display: grid; grid-template-columns: repeat(9, 1fr); gap: 0.5rem; }
.emoji-btn { width: 100%; aspect-ratio: 1; font-size: 1.5rem; border: 2px solid var(--border-color); border-radius: 50%; background: white; cursor: pointer; transition: all 0.15s; display: flex; align-items: center; justify-content: center; }
.emoji-btn:hover { background: rgba(0, 120, 255, 0.1); border-color: var(--primary-400); }
.emoji-btn.selected { border-color: var(--primary-500); box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.2); }
.emoji-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.emoji-btn:disabled:hover { background: white; border-color: var(--border-color); }
/* Status Feed */ .feed-title { font-size: 1.125rem; font-weight: 600; margin-bottom: 1rem; color: var(--gray-700); }
.status-list { list-style: none; padding: 0; }
.status-item { position: relative; padding-left: 2rem; padding-bottom: 1.5rem; }
.status-item::before { content: ""; position: absolute; left: 0.75rem; top: 1.5rem; bottom: 0; width: 2px; background: var(--border-color); }
.status-item:last-child::before { display: none; }
.status-item:last-child { padding-bottom: 0; }
.status-emoji { position: absolute; left: 0; top: 0; font-size: 1.5rem; }
.status-content { padding-top: 0.25rem; }
.status-author { color: var(--primary-500); text-decoration: none; font-weight: 500; }
.status-author:hover { text-decoration: underline; }
.status-text { color: var(--gray-700); }
.status-date { font-size: 0.875rem; color: var(--gray-500); }
/* Error Banner */ #error-banner { position: fixed; top: 1rem; left: 50%; transform: translateX(-50%); background: var(--error-bg); border: 1px solid var(--error-border); color: var(--error-text); padding: 0.75rem 1rem; border-radius: 0.375rem; display: flex; align-items: center; gap: 0.75rem; max-width: 90%; z-index: 100; }
#error-banner.hidden { display: none; }
#error-banner button { background: none; border: none; color: var(--error-text); cursor: pointer; font-size: 1.25rem; line-height: 1; }
/* Loading State */ .loading { text-align: center; color: var(--gray-500); padding: 2rem; }
/* Responsive */ @media (max-width: 480px) { .emoji-grid { grid-template-columns: repeat(6, 1fr); }
.emoji-btn { font-size: 1.25rem; } }
/* Hidden utility */ .hidden { display: none !important; } </style> </head> <body> <div id="app"> <header> <h1>Statusphere</h1> <p class="tagline">Set your status on the Atmosphere</p> </header> <main> <div id="auth-section"></div> <div id="emoji-picker"></div> <div id="status-feed"></div> </main> <div id="error-banner" class="hidden"></div> </div>
<!-- Quickslice Client SDK --> <script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@main/quickslice-client-js/dist/quickslice-client.min.js"></script>
<script> // ============================================================================= // CONFIGURATION // =============================================================================
const SERVER_URL = "https://xyzstatusphere.slices.network"; const CLIENT_ID = "client_vPEnCW98y5BNr5PrYOHPXg"; // Set your OAuth client ID here
const EMOJIS = [ "👍", "👎", "💙", "😧", "😤", "🙃", "😉", "😎", "🤩", "🥳", "😭", "😱", "🥺", "😡", "💀", "🤖", "👻", "👽", "🎃", "🤡", "💩", "🔥", "⭐", "🌈", "🍕", "🎉", "💯", ];
// Client instance let client;
// ============================================================================= // INITIALIZATION // =============================================================================
async function main() { // Check if this is an OAuth callback if (window.location.search.includes("code=")) { if (!CLIENT_ID) { showError("OAuth callback received but CLIENT_ID is not configured."); renderLoginForm(); return; }
try { client = await QuicksliceClient.createQuicksliceClient({ server: SERVER_URL, clientId: CLIENT_ID, }); await client.handleRedirectCallback(); console.log("OAuth callback handled successfully"); } catch (error) { console.error("OAuth callback error:", error); showError(`Authentication failed: ${error.message}`); renderLoginForm(); renderEmojiPicker(null, false); await loadAndRenderStatuses(); return; } } else if (CLIENT_ID) { // Initialize client with configured ID try { client = await QuicksliceClient.createQuicksliceClient({ server: SERVER_URL, clientId: CLIENT_ID, }); } catch (error) { console.error("Failed to initialize client:", error); } }
// Render based on auth state await renderApp(); }
async function renderApp() { const isLoggedIn = client && (await client.isAuthenticated());
if (isLoggedIn) { try { const viewer = await fetchViewer(); renderUserCard(viewer); } catch (error) { console.error("Failed to fetch viewer:", error); renderUserCard(null); } } else { renderLoginForm(); }
// Render emoji picker (enabled only if logged in) renderEmojiPicker(null, isLoggedIn);
// Load statuses await loadAndRenderStatuses(); }
// ============================================================================= // DATA FETCHING // =============================================================================
async function fetchStatuses() { const query = ` query GetStatuses { xyzStatusphereStatus( first: 20 sortBy: [{ field: "createdAt", direction: DESC }] ) { edges { node { uri did status createdAt appBskyActorProfileByDid { actorHandle displayName } } } } } `;
// Use client if available, otherwise create a temporary one for public query if (client) { const data = await client.publicQuery(query); return data.xyzStatusphereStatus?.edges?.map((e) => e.node) || []; } else { // For unauthenticated users, make a direct fetch const response = await fetch(`${SERVER_URL}/graphql`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }), }); const result = await response.json(); return result.data?.xyzStatusphereStatus?.edges?.map((e) => e.node) || []; } }
async function fetchViewer() { const query = ` query { viewer { did handle appBskyActorProfileByDid { displayName avatar { url } } } } `;
const data = await client.query(query); return data?.viewer; }
async function postStatus(emoji) { const mutation = ` mutation CreateStatus($status: String!, $createdAt: DateTime!) { createXyzStatusphereStatus( input: { status: $status, createdAt: $createdAt } ) { uri status createdAt } } `;
const variables = { status: emoji, createdAt: new Date().toISOString(), };
return await client.mutate(mutation, variables); }
async function loadAndRenderStatuses() { renderLoading("status-feed"); try { const statuses = await fetchStatuses(); renderStatusFeed(statuses); } catch (error) { console.error("Failed to fetch statuses:", error); document.getElementById("status-feed").innerHTML = ` <div class="card"> <p class="loading" style="color: var(--error-text);"> Failed to load statuses. Is the quickslice server running at ${SERVER_URL}? </p> </div> `; } }
// ============================================================================= // EVENT HANDLERS // =============================================================================
async function handleLogin(event) { event.preventDefault();
const handle = document.getElementById("handle").value.trim();
if (!handle) { showError("Please enter your Bluesky handle"); return; }
try { client = await QuicksliceClient.createQuicksliceClient({ server: SERVER_URL, clientId: CLIENT_ID, });
await client.loginWithRedirect({ handle }); } catch (error) { showError(`Login failed: ${error.message}`); } }
async function selectStatus(emoji) { if (!client || !(await client.isAuthenticated())) { showError("Please login to set your status"); return; }
try { // Disable buttons while posting document.querySelectorAll(".emoji-btn").forEach((btn) => (btn.disabled = true));
await postStatus(emoji);
// Refresh the page to show new status window.location.reload(); } catch (error) { showError(`Failed to post status: ${error.message}`); // Re-enable buttons document.querySelectorAll(".emoji-btn").forEach((btn) => (btn.disabled = false)); } }
function logout() { if (client) { client.logout(); } else { window.location.reload(); } }
// ============================================================================= // UI RENDERING // =============================================================================
function showError(message) { const banner = document.getElementById("error-banner"); banner.innerHTML = ` <span>${escapeHtml(message)}</span> <button onclick="hideError()">×</button> `; banner.classList.remove("hidden"); }
function hideError() { document.getElementById("error-banner").classList.add("hidden"); }
function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; }
function formatDate(dateString) { const date = new Date(dateString); const now = new Date(); const isToday = date.toDateString() === now.toDateString();
if (isToday) { return "today"; }
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, }); }
function renderLoginForm() { const container = document.getElementById("auth-section");
// Show configuration message if CLIENT_ID is not set if (!CLIENT_ID) { container.innerHTML = ` <div class="card"> <p style="color: var(--error-text); text-align: center; margin-bottom: 1rem;"> <strong>Configuration Required</strong> </p> <p style="color: var(--gray-700); text-align: center;"> Please set the <code style="background: var(--gray-100); padding: 0.125rem 0.375rem; border-radius: 0.25rem;">CLIENT_ID</code> constant in this file to your OAuth client ID. </p> </div> `; return; }
container.innerHTML = ` <div class="card"> <form class="login-form" onsubmit="handleLogin(event)"> <div class="form-group"> <label for="handle">Bluesky Handle</label> <input type="text" id="handle" placeholder="you.bsky.social" required > </div> <button type="submit" class="btn btn-primary">Login with Bluesky</button> </form> <p style="margin-top: 1rem; font-size: 0.875rem; color: var(--gray-500); text-align: center;"> Don't have a Bluesky account? <a href="https://bsky.app" target="_blank">Sign up</a> </p> </div> `; }
function renderUserCard(viewer) { const container = document.getElementById("auth-section"); const displayName = viewer?.appBskyActorProfileByDid?.displayName || "User"; const handle = viewer?.handle || "unknown"; const avatarUrl = viewer?.appBskyActorProfileByDid?.avatar?.url;
container.innerHTML = ` <div class="card user-card"> <div class="user-info"> <div class="user-avatar"> ${avatarUrl ? `<img src="${escapeHtml(avatarUrl)}" alt="Avatar">` : "👤"} </div> <div> <div class="user-name">Hi, ${escapeHtml(displayName)}!</div> <div class="user-handle">@${escapeHtml(handle)}</div> </div> </div> <button class="btn btn-secondary" onclick="logout()">Logout</button> </div> `; }
function renderEmojiPicker(currentStatus, enabled = true) { const container = document.getElementById("emoji-picker");
container.innerHTML = ` <div class="card"> <div class="emoji-grid"> ${EMOJIS.map( (emoji) => ` <button class="emoji-btn ${emoji === currentStatus ? "selected" : ""}" onclick="selectStatus('${emoji}')" ${!enabled ? "disabled" : ""} title="${enabled ? "Set status" : "Login to set status"}" > ${emoji} </button> `, ).join("")} </div> </div> `; }
function renderStatusFeed(statuses) { const container = document.getElementById("status-feed");
if (statuses.length === 0) { container.innerHTML = ` <div class="card"> <p class="loading">No statuses yet. Be the first to post!</p> </div> `; return; }
container.innerHTML = ` <div class="card"> <h2 class="feed-title">Recent Statuses</h2> <ul class="status-list"> ${statuses .map((status) => { const handle = status.appBskyActorProfileByDid?.actorHandle || status.did; const displayHandle = handle.startsWith("did:") ? handle.substring(0, 20) + "..." : handle; const profileUrl = handle.startsWith("did:") ? `https://bsky.app/profile/${status.did}` : `https://bsky.app/profile/${handle}`;
return ` <li class="status-item"> <span class="status-emoji">${status.status}</span> <div class="status-content"> <span class="status-text"> <a href="${profileUrl}" target="_blank" class="status-author">@${escapeHtml( displayHandle, )}</a> is feeling ${status.status} </span> <div class="status-date">${formatDate(status.createdAt)}</div> </div> </li> `; }) .join("")} </ul> </div> `; }
function renderLoading(container) { document.getElementById(container).innerHTML = ` <div class="card"> <p class="loading">Loading...</p> </div> `; }
// Run on page load main(); </script> </body></html>