fix: button size no longer changes on loading. locking for dl buttons
This commit is contained in:
@@ -47,7 +47,7 @@ export function Spinner(props: SpinnerProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
class={`text-overlay2 font-mono ${sizeClass()} ${props.class || ""}`}
|
class={`text-crust font-mono ${sizeClass()} ${props.class || ""}`}
|
||||||
style={style()}
|
style={style()}
|
||||||
aria-label={props["aria-label"] || "Loading..."}
|
aria-label={props["aria-label"] || "Loading..."}
|
||||||
role="status"
|
role="status"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { JSX, splitProps, Show } from "solid-js";
|
import { JSX, splitProps, Show, createSignal, createEffect } from "solid-js";
|
||||||
import { Spinner } from "~/components/Spinner";
|
import { Spinner } from "~/components/Spinner";
|
||||||
|
|
||||||
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
variant?: "primary" | "secondary" | "danger" | "ghost" | "download";
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
@@ -19,6 +19,20 @@ export default function Button(props: ButtonProps) {
|
|||||||
"disabled"
|
"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 variant = () => local.variant || "primary";
|
||||||
const size = () => local.size || "md";
|
const size = () => local.size || "md";
|
||||||
|
|
||||||
@@ -37,6 +51,10 @@ export default function Button(props: ButtonProps) {
|
|||||||
return isDisabledOrLoading
|
return isDisabledOrLoading
|
||||||
? "bg-surface0 cursor-not-allowed brightness-75"
|
? "bg-surface0 cursor-not-allowed brightness-75"
|
||||||
: "bg-surface0 hover:brightness-125 active:scale-90";
|
: "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":
|
case "danger":
|
||||||
return isDisabledOrLoading
|
return isDisabledOrLoading
|
||||||
? "bg-red cursor-not-allowed brightness-75"
|
? "bg-red cursor-not-allowed brightness-75"
|
||||||
@@ -71,8 +89,25 @@ export default function Button(props: ButtonProps) {
|
|||||||
disabled={local.disabled || local.loading}
|
disabled={local.disabled || local.loading}
|
||||||
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`}
|
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`}
|
||||||
>
|
>
|
||||||
<Show when={local.loading} fallback={local.children}>
|
<Show
|
||||||
|
when={local.loading}
|
||||||
|
fallback={
|
||||||
|
<span ref={contentRef} style={{ display: "inline-flex" }}>
|
||||||
|
{local.children}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
|
"min-width": dimensions() ? `${dimensions()!.width}px` : undefined,
|
||||||
|
"min-height": dimensions() ? `${dimensions()!.height}px` : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Spinner size={24} />
|
<Spinner size={24} />
|
||||||
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,23 +3,7 @@ import { A } from "@solidjs/router";
|
|||||||
import { createSignal, onMount, onCleanup } from "solid-js";
|
import { createSignal, onMount, onCleanup } from "solid-js";
|
||||||
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
||||||
import { glitchText } from "~/lib/client-utils";
|
import { glitchText } from "~/lib/client-utils";
|
||||||
|
import Button from "~/components/ui/Button";
|
||||||
const DownloadButton = ({
|
|
||||||
onClick,
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
onClick: () => void;
|
|
||||||
children: Element | string;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
class="bg-green hover:bg-green/90 cursor-pointer rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DownloadsPage() {
|
export default function DownloadsPage() {
|
||||||
const [LaLText, setLaLText] = createSignal("Life and Lineage");
|
const [LaLText, setLaLText] = createSignal("Life and Lineage");
|
||||||
@@ -27,7 +11,23 @@ export default function DownloadsPage() {
|
|||||||
const [corkText, setCorkText] = createSignal("Cork");
|
const [corkText, setCorkText] = createSignal("Cork");
|
||||||
const [gazeText, setGazeText] = createSignal("Gaze");
|
const [gazeText, setGazeText] = createSignal("Gaze");
|
||||||
|
|
||||||
|
// Track loading states for each download button
|
||||||
|
const [loadingState, setLoadingState] = createSignal<Record<string, boolean>>(
|
||||||
|
{
|
||||||
|
lineage: false,
|
||||||
|
cork: false,
|
||||||
|
gaze: false,
|
||||||
|
"shapes-with-abigail": false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const download = (assetName: string) => {
|
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
|
// Call the tRPC endpoint directly
|
||||||
import("~/lib/api").then(({ api }) => {
|
import("~/lib/api").then(({ api }) => {
|
||||||
api.downloads.getDownloadUrl
|
api.downloads.getDownloadUrl
|
||||||
@@ -40,6 +40,10 @@ export default function DownloadsPage() {
|
|||||||
console.error("Download error:", error);
|
console.error("Download error:", error);
|
||||||
// Optionally show user a message
|
// Optionally show user a message
|
||||||
alert("Failed to initiate download. Please try again.");
|
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() {
|
|||||||
<span class="text-subtext0 font-mono text-sm">
|
<span class="text-subtext0 font-mono text-sm">
|
||||||
platform: macOS (14.6+)
|
platform: macOS (14.6+)
|
||||||
</span>
|
</span>
|
||||||
<DownloadButton onClick={() => download("gaze")}>
|
<Button
|
||||||
|
variant="download"
|
||||||
|
size="lg"
|
||||||
|
loading={loadingState()["gaze"]}
|
||||||
|
onClick={() => download("gaze")}
|
||||||
|
>
|
||||||
download.dmg
|
download.dmg
|
||||||
</DownloadButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
||||||
@@ -107,9 +116,14 @@ export default function DownloadsPage() {
|
|||||||
<span class="text-subtext0 font-mono text-sm">
|
<span class="text-subtext0 font-mono text-sm">
|
||||||
platform: android
|
platform: android
|
||||||
</span>
|
</span>
|
||||||
<DownloadButton onClick={() => download("lineage")}>
|
<Button
|
||||||
|
variant="download"
|
||||||
|
size="lg"
|
||||||
|
loading={loadingState()["lineage"]}
|
||||||
|
onClick={() => download("lineage")}
|
||||||
|
>
|
||||||
download.apk
|
download.apk
|
||||||
</DownloadButton>
|
</Button>
|
||||||
<span class="text-subtext1 max-w-xs text-center text-xs italic">
|
<span class="text-subtext1 max-w-xs text-center text-xs italic">
|
||||||
# android build not optimized
|
# android build not optimized
|
||||||
</span>
|
</span>
|
||||||
@@ -138,9 +152,14 @@ export default function DownloadsPage() {
|
|||||||
<span class="text-subtext0 font-mono text-sm">
|
<span class="text-subtext0 font-mono text-sm">
|
||||||
platform: macOS (13+)
|
platform: macOS (13+)
|
||||||
</span>
|
</span>
|
||||||
<DownloadButton onClick={() => download("cork")}>
|
<Button
|
||||||
|
variant="download"
|
||||||
|
size="lg"
|
||||||
|
loading={loadingState()["cork"]}
|
||||||
|
onClick={() => download("cork")}
|
||||||
|
>
|
||||||
download.zip
|
download.zip
|
||||||
</DownloadButton>
|
</Button>
|
||||||
<span class="text-subtext1 text-xs">
|
<span class="text-subtext1 text-xs">
|
||||||
# unzip → drag to /Applications
|
# unzip → drag to /Applications
|
||||||
</span>
|
</span>
|
||||||
@@ -158,11 +177,14 @@ export default function DownloadsPage() {
|
|||||||
<span class="text-subtext0 font-mono text-sm">
|
<span class="text-subtext0 font-mono text-sm">
|
||||||
platform: android
|
platform: android
|
||||||
</span>
|
</span>
|
||||||
<DownloadButton
|
<Button
|
||||||
|
variant="download"
|
||||||
|
size="lg"
|
||||||
|
loading={loadingState()["shapes-with-abigail"]}
|
||||||
onClick={() => download("shapes-with-abigail")}
|
onClick={() => download("shapes-with-abigail")}
|
||||||
>
|
>
|
||||||
download.apk
|
download.apk
|
||||||
</DownloadButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-3">
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { v4 as uuidV4 } from "uuid";
|
import { v4 as uuidV4 } from "uuid";
|
||||||
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
||||||
import type { H3Event } from "vinxi/http";
|
import type { H3Event } from "vinxi/http";
|
||||||
import {
|
import { useSession, clearSession, getSession, getCookie } from "vinxi/http";
|
||||||
useSession,
|
|
||||||
updateSession,
|
|
||||||
clearSession,
|
|
||||||
getSession,
|
|
||||||
getCookie
|
|
||||||
} from "vinxi/http";
|
|
||||||
import { ConnectionFactory } from "./database";
|
import { ConnectionFactory } from "./database";
|
||||||
import { env } from "~/env/server";
|
import { env } from "~/env/server";
|
||||||
import { AUTH_CONFIG, expiryToSeconds } from "~/config";
|
import { AUTH_CONFIG, expiryToSeconds } from "~/config";
|
||||||
@@ -142,16 +136,52 @@ export async function createAuthSession(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Update Vinxi session with dynamic maxAge based on rememberMe
|
// Update Vinxi session with dynamic maxAge based on rememberMe
|
||||||
await updateSession(
|
const configWithMaxAge = {
|
||||||
event,
|
|
||||||
{
|
|
||||||
...sessionConfig,
|
...sessionConfig,
|
||||||
maxAge: rememberMe
|
maxAge: rememberMe
|
||||||
? expiryToSeconds(AUTH_CONFIG.REFRESH_TOKEN_EXPIRY_LONG)
|
? expiryToSeconds(AUTH_CONFIG.REFRESH_TOKEN_EXPIRY_LONG)
|
||||||
: undefined // Session cookie (expires on browser close)
|
: undefined // Session cookie (expires on browser close)
|
||||||
},
|
};
|
||||||
sessionData
|
|
||||||
);
|
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<SessionData>(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<SessionData>(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
|
// Log audit event
|
||||||
await logAuditEvent({
|
await logAuditEvent({
|
||||||
@@ -187,6 +217,12 @@ export async function getAuthSession(
|
|||||||
const { unsealSession } = await import("vinxi/http");
|
const { unsealSession } = await import("vinxi/http");
|
||||||
const cookieName = sessionConfig.name || "session";
|
const cookieName = sessionConfig.name || "session";
|
||||||
const cookieValue = getCookie(event, cookieName);
|
const cookieValue = getCookie(event, cookieName);
|
||||||
|
console.log(
|
||||||
|
"[Session Get] skipUpdate mode, cookieName:",
|
||||||
|
cookieName,
|
||||||
|
"has cookie:",
|
||||||
|
!!cookieValue
|
||||||
|
);
|
||||||
if (!cookieValue) {
|
if (!cookieValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -194,13 +230,26 @@ export async function getAuthSession(
|
|||||||
try {
|
try {
|
||||||
// unsealSession returns Partial<Session<T>>, not T directly
|
// unsealSession returns Partial<Session<T>>, not T directly
|
||||||
const session = await unsealSession(event, sessionConfig, cookieValue);
|
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") {
|
if (!session?.data || typeof session.data !== "object") {
|
||||||
|
console.log("[Session Get] Invalid session structure");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = session.data as SessionData;
|
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) {
|
if (!data.userId || !data.sessionId) {
|
||||||
|
console.log("[Session Get] Missing userId or sessionId");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,16 +260,27 @@ export async function getAuthSession(
|
|||||||
data.refreshToken
|
data.refreshToken
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("[Session Get] DB validation result:", isValid);
|
||||||
return isValid ? data : null;
|
return isValid ? data : null;
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error("[Session Get] Error in skipUpdate path:", err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal path - allow session updates
|
// Normal path - allow session updates
|
||||||
|
console.log("[Session Get] Normal path, getting session");
|
||||||
const session = await getSession<SessionData>(event, sessionConfig);
|
const session = await getSession<SessionData>(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) {
|
if (!session.data || !session.data.userId || !session.data.sessionId) {
|
||||||
|
console.log(
|
||||||
|
"[Session Get] Missing data or userId/sessionId in normal path"
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user