{error.message}{" "}
@@ -449,81 +502,197 @@ export default function PageClient() {
value: allItems.length === 0 ? 100 : (completed / allItems.length) * 100,
};
+ // Track which section is expanded (only one at a time, excluding "Checks complete")
+ const [expandedTaskId, setExpandedTaskId] = useState(null);
+ const prevNextTaskIdRef = useRef(null);
+
+ // Auto-expand the section containing the next task on mount and when next task changes
+ useEffect(() => {
+ const nextTaskId = next?.task.id ?? null;
+ const prevNextTaskId = prevNextTaskIdRef.current;
+
+ // Only auto-expand if:
+ // 1. This is the initial load (prevNextTaskId is null), OR
+ // 2. The next task actually changed to a different section
+ if (prevNextTaskId === null || (nextTaskId !== null && nextTaskId !== prevNextTaskId)) {
+ if (nextTaskId !== null) {
+ setExpandedTaskId(nextTaskId);
+ } else {
+ // If all tasks are done, collapse all sections
+ setExpandedTaskId(null);
+ }
+ }
+
+ // Update the ref to track the current next task
+ prevNextTaskIdRef.current = nextTaskId;
+ }, [next]);
+
+ const handleTaskToggle = (taskId: string) => {
+ setExpandedTaskId((current) => (current === taskId ? null : taskId));
+ };
+
+ // Animate progress bar on mount and when progress changes
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setAnimatedProgress(checklistProgress.value);
+ }, 100);
+ return () => clearTimeout(timer);
+ }, [checklistProgress.value]);
+
+ // Trigger confetti when production mode is turned on
+ useEffect(() => {
+ const currentProductionMode = project.isProductionMode;
+ const prevProductionMode = prevProductionModeRef.current;
+
+ // Only trigger confetti when production mode changes from false to true
+ if (prevProductionMode !== undefined && !prevProductionMode && currentProductionMode) {
+ // Create a confetti effect dropping from the top
+ const duration = 3000;
+ const animationEnd = Date.now() + duration;
+ const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 9999 };
+
+ function randomInRange(min: number, max: number) {
+ return Math.random() * (max - min) + min;
+ }
+
+ const interval = setInterval(() => {
+ const timeLeft = animationEnd - Date.now();
+
+ if (timeLeft <= 0) {
+ clearInterval(interval);
+ return;
+ }
+
+ const particleCount = 50 * (timeLeft / duration);
+ const result = confetti.default({
+ ...defaults,
+ particleCount,
+ origin: { x: randomInRange(0.1, 0.9), y: 0 },
+ });
+ if (result) {
+ runAsynchronously(result, { noErrorLogging: true });
+ }
+ }, 250);
+
+ // Cleanup interval on unmount or when production mode changes
+ return () => {
+ clearInterval(interval);
+ };
+ }
+
+ // Update the ref to track the current production mode state
+ prevProductionModeRef.current = currentProductionMode;
+ }, [project.isProductionMode]);
+
const providerEntries = Array.from(PROVIDER_GUIDES.entries());
const defaultProviderTab = providerEntries[0]?.[0] ?? "google";
const oauthChildren =
sharedOAuthProviders.length > 0 ? (
-
-
- Need new credentials?
-
-
- Create an OAuth app with the provider, set Stack as the callback URL,
- then paste the client ID and secret into the provider settings.
-
-