coundown fixed
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,7 +20,6 @@ app.config.timestamp_*.js
|
||||
*.launch
|
||||
.settings/
|
||||
tasks
|
||||
main.go
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, createSignal, onMount, onCleanup } from "solid-js";
|
||||
import { Component, createSignal, createEffect, onCleanup } from "solid-js";
|
||||
|
||||
interface CountdownCircleTimerProps {
|
||||
duration: number;
|
||||
@@ -19,12 +19,25 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
props.initialRemainingTime ?? props.duration
|
||||
);
|
||||
|
||||
const progress = () => remainingTime() / props.duration;
|
||||
const strokeDashoffset = () => circumference * (1 - progress());
|
||||
const progress = () => {
|
||||
const time = remainingTime();
|
||||
if (isNaN(time) || !props.duration) return 0;
|
||||
return Math.max(0, Math.min(1, time / props.duration));
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const strokeDashoffset = () => {
|
||||
const prog = progress();
|
||||
if (isNaN(prog)) return 0;
|
||||
return circumference * (1 - prog);
|
||||
};
|
||||
|
||||
// If isPlaying is set, manage countdown internally
|
||||
createEffect(() => {
|
||||
if (props.isPlaying !== undefined && props.isPlaying) {
|
||||
const startTime = Date.now();
|
||||
const initialTime = remainingTime();
|
||||
const initialTime = props.initialRemainingTime ?? props.duration;
|
||||
setRemainingTime(initialTime);
|
||||
|
||||
let animationFrameId: number;
|
||||
|
||||
const animate = () => {
|
||||
@@ -44,8 +57,27 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
onCleanup(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, just sync with the prop value - parent controls the countdown
|
||||
const newTime = props.initialRemainingTime ?? props.duration;
|
||||
|
||||
if (isNaN(newTime)) {
|
||||
setRemainingTime(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setRemainingTime(newTime);
|
||||
|
||||
if (newTime <= 0) {
|
||||
props.onComplete?.();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
78
src/lib/useCountdown.ts
Normal file
78
src/lib/useCountdown.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { createSignal, onCleanup } from "solid-js";
|
||||
|
||||
export interface UseCountdownOptions {
|
||||
/**
|
||||
* Initial remaining time in seconds
|
||||
*/
|
||||
initialTime?: number;
|
||||
/**
|
||||
* Cookie name to read expiration time from
|
||||
*/
|
||||
cookieName?: string;
|
||||
/**
|
||||
* Callback when countdown reaches zero
|
||||
*/
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing countdown timers from expiration timestamps
|
||||
* @param options Configuration options
|
||||
* @returns [remainingTime, startCountdown]
|
||||
*/
|
||||
export function useCountdown(options: UseCountdownOptions = {}) {
|
||||
const [remainingTime, setRemainingTime] = createSignal(
|
||||
options.initialTime ?? 0
|
||||
);
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const calculateRemaining = (expiresAt: string | Date) => {
|
||||
const expires =
|
||||
typeof expiresAt === "string" ? new Date(expiresAt) : expiresAt;
|
||||
const remaining = expires.getTime() - Date.now();
|
||||
const remainingInSeconds = remaining / 1000;
|
||||
|
||||
if (remainingInSeconds <= 0) {
|
||||
setRemainingTime(0);
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
options.onComplete?.();
|
||||
} else {
|
||||
setRemainingTime(remainingInSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
const startCountdown = (expiresAt: string | Date) => {
|
||||
// Clear any existing interval
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
// Calculate immediately
|
||||
calculateRemaining(expiresAt);
|
||||
|
||||
// Then update every second
|
||||
intervalId = setInterval(() => calculateRemaining(expiresAt), 1000);
|
||||
};
|
||||
|
||||
const stopCountdown = () => {
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
onCleanup(() => {
|
||||
stopCountdown();
|
||||
});
|
||||
|
||||
return {
|
||||
remainingTime,
|
||||
startCountdown,
|
||||
stopCountdown,
|
||||
setRemainingTime
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, onMount, onCleanup, Show } from "solid-js";
|
||||
import { createSignal, onMount, createEffect, Show } from "solid-js";
|
||||
import {
|
||||
useSearchParams,
|
||||
useNavigate,
|
||||
@@ -15,6 +15,7 @@ import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
||||
import RevealDropDown from "~/components/RevealDropDown";
|
||||
import Input from "~/components/ui/Input";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { useCountdown } from "~/lib/useCountdown";
|
||||
import type { UserProfile } from "~/types/user";
|
||||
import { getCookie, setCookie } from "vinxi/http";
|
||||
import { z } from "zod";
|
||||
@@ -154,7 +155,6 @@ export default function ContactPage() {
|
||||
deferStream: true
|
||||
});
|
||||
|
||||
const [countDown, setCountDown] = createSignal<number>(0);
|
||||
const [emailSent, setEmailSent] = createSignal<boolean>(
|
||||
searchParams.success === "true"
|
||||
);
|
||||
@@ -165,36 +165,11 @@ export default function ContactPage() {
|
||||
const [user, setUser] = createSignal<UserProfile | null>(null);
|
||||
const [jsEnabled, setJsEnabled] = createSignal<boolean>(false);
|
||||
|
||||
let timerIdRef: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const calcRemainder = (timer: string) => {
|
||||
const expires = new Date(timer);
|
||||
const remaining = expires.getTime() - Date.now();
|
||||
const remainingInSeconds = remaining / 1000;
|
||||
|
||||
if (remainingInSeconds <= 0) {
|
||||
setCountDown(0);
|
||||
if (timerIdRef !== null) {
|
||||
clearInterval(timerIdRef);
|
||||
}
|
||||
} else {
|
||||
setCountDown(remainingInSeconds);
|
||||
}
|
||||
};
|
||||
const { remainingTime, startCountdown, setRemainingTime } = useCountdown();
|
||||
|
||||
onMount(() => {
|
||||
setJsEnabled(true);
|
||||
|
||||
const serverData = contactData();
|
||||
if (serverData?.remainingTime) {
|
||||
setCountDown(serverData.remainingTime);
|
||||
}
|
||||
|
||||
const timer = getClientCookie("contactRequestSent");
|
||||
if (timer) {
|
||||
timerIdRef = setInterval(() => calcRemainder(timer), 1000);
|
||||
}
|
||||
|
||||
api.user.getProfile
|
||||
.query()
|
||||
.then((userData) => {
|
||||
@@ -203,25 +178,28 @@ export default function ContactPage() {
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
if (searchParams.success || searchParams.error) {
|
||||
const timer = setTimeout(() => {
|
||||
const newUrl =
|
||||
location.pathname +
|
||||
(viewer() !== "default" ? `?viewer=${viewer()}` : "");
|
||||
navigate(newUrl, { replace: true });
|
||||
setEmailSent(false);
|
||||
setError("");
|
||||
}, 5000);
|
||||
|
||||
onCleanup(() => clearTimeout(timer));
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (timerIdRef !== null) {
|
||||
clearInterval(timerIdRef);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
// Try server data first (more accurate)
|
||||
const serverData = contactData();
|
||||
if (serverData?.remainingTime && serverData.remainingTime > 0) {
|
||||
const expirationTime = new Date(
|
||||
Date.now() + serverData.remainingTime * 1000
|
||||
);
|
||||
startCountdown(expirationTime);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to client cookie if server data not available yet
|
||||
const timer = getClientCookie("contactRequestSent");
|
||||
if (timer) {
|
||||
try {
|
||||
startCountdown(timer);
|
||||
} catch (e) {
|
||||
console.error("Failed to start countdown from cookie:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sendEmailTrigger = async (e: Event) => {
|
||||
@@ -251,13 +229,11 @@ export default function ContactPage() {
|
||||
setError("");
|
||||
(e.target as HTMLFormElement).reset();
|
||||
|
||||
const timer = getClientCookie("contactRequestSent");
|
||||
if (timer) {
|
||||
if (timerIdRef !== null) {
|
||||
clearInterval(timerIdRef);
|
||||
}
|
||||
timerIdRef = setInterval(() => calcRemainder(timer), 1000);
|
||||
}
|
||||
// Set countdown directly - cookie might not be readable immediately
|
||||
const expirationTime = new Date(
|
||||
Date.now() + COOLDOWN_TIMERS.CONTACT_REQUEST_MS
|
||||
);
|
||||
startCountdown(expirationTime);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "An error occurred");
|
||||
@@ -351,9 +327,10 @@ export default function ContactPage() {
|
||||
};
|
||||
|
||||
const renderTime = ({ remainingTime }: { remainingTime: number }) => {
|
||||
const time = isNaN(remainingTime) ? 0 : Math.max(0, remainingTime);
|
||||
return (
|
||||
<div class="timer">
|
||||
<div class="value">{remainingTime.toFixed(0)}</div>
|
||||
<div class="value">{time.toFixed(0)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -418,7 +395,8 @@ export default function ContactPage() {
|
||||
<div class="mx-auto flex w-full justify-end pt-4">
|
||||
<Show
|
||||
when={
|
||||
countDown() > 0 || (contactData()?.remainingTime ?? 0) > 0
|
||||
remainingTime() > 0 ||
|
||||
(contactData()?.remainingTime ?? 0) > 0
|
||||
}
|
||||
fallback={
|
||||
<Button type="submit" loading={loading()} class="w-36">
|
||||
@@ -438,11 +416,11 @@ export default function ContactPage() {
|
||||
>
|
||||
<CountdownCircleTimer
|
||||
duration={COUNTDOWN_CONFIG.CONTACT_FORM_DURATION_S}
|
||||
initialRemainingTime={countDown()}
|
||||
initialRemainingTime={remainingTime()}
|
||||
size={48}
|
||||
strokeWidth={6}
|
||||
colors={"#60a5fa"}
|
||||
onComplete={() => setCountDown(0)}
|
||||
onComplete={() => setRemainingTime(0)}
|
||||
>
|
||||
{renderTime}
|
||||
</CountdownCircleTimer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
|
||||
import { createSignal, createEffect, Show } from "solid-js";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
||||
@@ -8,47 +8,24 @@ import { COUNTDOWN_CONFIG } from "~/config";
|
||||
import Input from "~/components/ui/Input";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import FormFeedback from "~/components/ui/FormFeedback";
|
||||
import { useCountdown } from "~/lib/useCountdown";
|
||||
|
||||
export default function RequestPasswordResetPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [countDown, setCountDown] = createSignal(0);
|
||||
const [showSuccessMessage, setShowSuccessMessage] = createSignal(false);
|
||||
const [error, setError] = createSignal("");
|
||||
|
||||
const { remainingTime, startCountdown } = useCountdown();
|
||||
|
||||
let emailRef: HTMLInputElement | undefined;
|
||||
let timerInterval: number | undefined;
|
||||
|
||||
const calcRemainder = (timer: string) => {
|
||||
const expires = new Date(timer);
|
||||
const remaining = expires.getTime() - Date.now();
|
||||
const remainingInSeconds = remaining / 1000;
|
||||
|
||||
if (remainingInSeconds <= 0) {
|
||||
setCountDown(0);
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
}
|
||||
} else {
|
||||
setCountDown(remainingInSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const timer = getClientCookie("passwordResetRequested");
|
||||
if (timer) {
|
||||
timerInterval = setInterval(
|
||||
() => calcRemainder(timer),
|
||||
1000
|
||||
) as unknown as number;
|
||||
startCountdown(timer);
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const requestPasswordResetTrigger = async (e: Event) => {
|
||||
@@ -85,12 +62,7 @@ export default function RequestPasswordResetPage() {
|
||||
|
||||
const timer = getClientCookie("passwordResetRequested");
|
||||
if (timer) {
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
}
|
||||
timerInterval = setInterval(() => {
|
||||
calcRemainder(timer);
|
||||
}, 1000) as unknown as number;
|
||||
startCountdown(timer);
|
||||
}
|
||||
} else {
|
||||
const errorMsg = result.error?.message || "Failed to send reset email";
|
||||
@@ -151,7 +123,7 @@ export default function RequestPasswordResetPage() {
|
||||
/>
|
||||
|
||||
<Show
|
||||
when={countDown() > 0}
|
||||
when={remainingTime() > 0}
|
||||
fallback={
|
||||
<Button type="submit" loading={loading()} class="my-6">
|
||||
Request Password Reset
|
||||
@@ -162,7 +134,7 @@ export default function RequestPasswordResetPage() {
|
||||
<CountdownCircleTimer
|
||||
isPlaying={true}
|
||||
duration={COUNTDOWN_CONFIG.PASSWORD_RESET_DURATION_S}
|
||||
initialRemainingTime={countDown()}
|
||||
initialRemainingTime={remainingTime()}
|
||||
size={48}
|
||||
strokeWidth={6}
|
||||
colors="#60a5fa"
|
||||
|
||||
Reference in New Issue
Block a user