diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index aff1ba0..ef8dde2 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -47,7 +47,7 @@ export function Spinner(props: SpinnerProps) { return ( { - variant?: "primary" | "secondary" | "danger" | "ghost"; + variant?: "primary" | "secondary" | "danger" | "ghost" | "download"; size?: "sm" | "md" | "lg"; loading?: boolean; fullWidth?: boolean; @@ -19,6 +19,20 @@ export default function Button(props: ButtonProps) { "disabled" ]); + let contentRef: HTMLSpanElement | undefined; + const [dimensions, setDimensions] = createSignal<{ + width: number; + height: number; + } | null>(null); + + // Measure content dimensions when not loading + createEffect(() => { + if (!local.loading && contentRef) { + const rect = contentRef.getBoundingClientRect(); + setDimensions({ width: rect.width, height: rect.height }); + } + }); + const variant = () => local.variant || "primary"; const size = () => local.size || "md"; @@ -37,6 +51,10 @@ export default function Button(props: ButtonProps) { return isDisabledOrLoading ? "bg-surface0 cursor-not-allowed brightness-75" : "bg-surface0 hover:brightness-125 active:scale-90"; + case "download": + return isDisabledOrLoading + ? "bg-green text-base cursor-not-allowed brightness-75" + : "bg-green text-base hover:brightness-125 active:scale-90"; case "danger": return isDisabledOrLoading ? "bg-red cursor-not-allowed brightness-75" @@ -71,8 +89,25 @@ export default function Button(props: ButtonProps) { disabled={local.disabled || local.loading} class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`} > - - + + {local.children} + + } + > + + + ); diff --git a/src/routes/downloads.tsx b/src/routes/downloads.tsx index da70456..e838d28 100644 --- a/src/routes/downloads.tsx +++ b/src/routes/downloads.tsx @@ -3,23 +3,7 @@ import { A } from "@solidjs/router"; import { createSignal, onMount, onCleanup } from "solid-js"; import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; import { glitchText } from "~/lib/client-utils"; - -const DownloadButton = ({ - onClick, - children -}: { - onClick: () => void; - children: Element | string; -}) => { - return ( - - ); -}; +import Button from "~/components/ui/Button"; export default function DownloadsPage() { const [LaLText, setLaLText] = createSignal("Life and Lineage"); @@ -27,7 +11,23 @@ export default function DownloadsPage() { const [corkText, setCorkText] = createSignal("Cork"); const [gazeText, setGazeText] = createSignal("Gaze"); + // Track loading states for each download button + const [loadingState, setLoadingState] = createSignal>( + { + lineage: false, + cork: false, + gaze: false, + "shapes-with-abigail": false + } + ); + const download = (assetName: string) => { + // Prevent multiple rapid clicks + if (loadingState()[assetName]) return; + + // Set loading state + setLoadingState((prev) => ({ ...prev, [assetName]: true })); + // Call the tRPC endpoint directly import("~/lib/api").then(({ api }) => { api.downloads.getDownloadUrl @@ -40,6 +40,10 @@ export default function DownloadsPage() { console.error("Download error:", error); // Optionally show user a message alert("Failed to initiate download. Please try again."); + }) + .finally(() => { + // Reset loading state regardless of success/failure + setLoadingState((prev) => ({ ...prev, [assetName]: false })); }); }); }; @@ -92,9 +96,14 @@ export default function DownloadsPage() { platform: macOS (14.6+) - download("gaze")}> +
@@ -107,9 +116,14 @@ export default function DownloadsPage() { platform: android - download("lineage")}> + # android build not optimized @@ -138,9 +152,14 @@ export default function DownloadsPage() { platform: macOS (13+) - download("cork")}> + # unzip → drag to /Applications @@ -158,11 +177,14 @@ export default function DownloadsPage() { platform: android - download("shapes-with-abigail")} > download.apk - +
diff --git a/src/server/session-helpers.ts b/src/server/session-helpers.ts index c9dacca..69a046c 100644 --- a/src/server/session-helpers.ts +++ b/src/server/session-helpers.ts @@ -1,13 +1,7 @@ import { v4 as uuidV4 } from "uuid"; import { createHash, randomBytes, timingSafeEqual } from "crypto"; import type { H3Event } from "vinxi/http"; -import { - useSession, - updateSession, - clearSession, - getSession, - getCookie -} from "vinxi/http"; +import { useSession, clearSession, getSession, getCookie } from "vinxi/http"; import { ConnectionFactory } from "./database"; import { env } from "~/env/server"; import { AUTH_CONFIG, expiryToSeconds } from "~/config"; @@ -142,16 +136,52 @@ export async function createAuthSession( }; // Update Vinxi session with dynamic maxAge based on rememberMe - await updateSession( - event, - { - ...sessionConfig, - maxAge: rememberMe - ? expiryToSeconds(AUTH_CONFIG.REFRESH_TOKEN_EXPIRY_LONG) - : undefined // Session cookie (expires on browser close) - }, - sessionData - ); + const configWithMaxAge = { + ...sessionConfig, + maxAge: rememberMe + ? expiryToSeconds(AUTH_CONFIG.REFRESH_TOKEN_EXPIRY_LONG) + : undefined // Session cookie (expires on browser close) + }; + + console.log("[Session Create] Creating session with data:", { + userId: sessionData.userId, + sessionId: sessionData.sessionId, + tokenFamily: sessionData.tokenFamily, + rememberMe: sessionData.rememberMe, + maxAge: configWithMaxAge.maxAge + }); + + // Use useSession API to update session + const session = await useSession(event, configWithMaxAge); + await session.update(sessionData); + + console.log("[Session Create] Session created via useSession API:", { + id: session.id, + hasData: !!session.data, + dataKeys: session.data ? Object.keys(session.data) : [] + }); + + // Verify session was actually set by reading it back + try { + const cookieName = sessionConfig.name || "session"; + const cookieValue = getCookie(event, cookieName); + console.log("[Session Create] Immediate verification:", { + cookieName, + hasCookie: !!cookieValue, + cookieLength: cookieValue?.length || 0 + }); + + // Try reading back the session immediately + const verifySession = await getSession(event, sessionConfig); + console.log("[Session Create] Read-back verification:", { + hasData: !!verifySession.data, + dataMatches: verifySession.data?.userId === sessionData.userId, + userId: verifySession.data?.userId, + sessionId: verifySession.data?.sessionId + }); + } catch (verifyError) { + console.error("[Session Create] Failed to verify session:", verifyError); + } // Log audit event await logAuditEvent({ @@ -187,6 +217,12 @@ export async function getAuthSession( const { unsealSession } = await import("vinxi/http"); const cookieName = sessionConfig.name || "session"; const cookieValue = getCookie(event, cookieName); + console.log( + "[Session Get] skipUpdate mode, cookieName:", + cookieName, + "has cookie:", + !!cookieValue + ); if (!cookieValue) { return null; } @@ -194,13 +230,26 @@ export async function getAuthSession( try { // unsealSession returns Partial>, not T directly const session = await unsealSession(event, sessionConfig, cookieValue); + console.log("[Session Get] Unsealed session:", { + hasData: !!session?.data, + dataType: typeof session?.data, + dataKeys: session?.data ? Object.keys(session.data) : [] + }); if (!session?.data || typeof session.data !== "object") { + console.log("[Session Get] Invalid session structure"); return null; } const data = session.data as SessionData; + console.log("[Session Get] Session data:", { + hasUserId: !!data.userId, + hasSessionId: !!data.sessionId, + hasRefreshToken: !!data.refreshToken + }); + if (!data.userId || !data.sessionId) { + console.log("[Session Get] Missing userId or sessionId"); return null; } @@ -211,16 +260,27 @@ export async function getAuthSession( data.refreshToken ); + console.log("[Session Get] DB validation result:", isValid); return isValid ? data : null; - } catch { + } catch (err) { + console.error("[Session Get] Error in skipUpdate path:", err); return null; } } // Normal path - allow session updates + console.log("[Session Get] Normal path, getting session"); const session = await getSession(event, sessionConfig); + console.log("[Session Get] Got session:", { + hasData: !!session.data, + dataType: typeof session.data, + dataKeys: session.data ? Object.keys(session.data) : [] + }); if (!session.data || !session.data.userId || !session.data.sessionId) { + console.log( + "[Session Get] Missing data or userId/sessionId in normal path" + ); return null; }