init
This commit is contained in:
15
src/hooks/use-appshell-height.ts
Normal file
15
src/hooks/use-appshell-height.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useMemo } from "react";
|
||||
import { useIsMobile } from "./use-is-mobile";
|
||||
import useHeaderConfig from "@/features/core/hooks/use-header-config";
|
||||
const useAppShellHeight = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const headerConfig = useHeaderConfig();
|
||||
|
||||
const height = useMemo(() =>
|
||||
`calc(100dvh - var(--app-shell-header-height, 0px) - ${isMobile && !headerConfig.collapsed ? '4rem' : '0px'} - 1.285rem)`,
|
||||
[isMobile, headerConfig.collapsed]);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
export default useAppShellHeight;
|
||||
61
src/hooks/use-haptic.ts
Normal file
61
src/hooks/use-haptic.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Sneaky way of triggering haptic feedback, without using the vibration API (not available on iOS)
|
||||
// Source: https://github.com/posaune0423/use-haptic
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
const detectiOS = (): boolean => {
|
||||
if (typeof navigator === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const toMatch = [/iPhone/i, /iPad/i, /iPod/i];
|
||||
|
||||
return toMatch.some((toMatchItem) => {
|
||||
return RegExp(toMatchItem).exec(navigator.userAgent);
|
||||
});
|
||||
};
|
||||
|
||||
const useHaptic = (
|
||||
duration = 200,
|
||||
): { triggerHaptic: () => void } => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const labelRef = useRef<HTMLLabelElement | null>(null);
|
||||
const isIOS = useMemo(() => detectiOS(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "checkbox";
|
||||
input.id = "haptic-switch";
|
||||
input.setAttribute("switch", "");
|
||||
input.style.display = "none";
|
||||
document.body.appendChild(input);
|
||||
inputRef.current = input;
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.htmlFor = "haptic-switch";
|
||||
label.style.display = "none";
|
||||
document.body.appendChild(label);
|
||||
labelRef.current = label;
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(input);
|
||||
document.body.removeChild(label);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const triggerHaptic = useCallback(() => {
|
||||
if (isIOS) {
|
||||
labelRef.current?.click();
|
||||
} else {
|
||||
if (navigator?.vibrate) {
|
||||
navigator.vibrate(duration);
|
||||
} else {
|
||||
labelRef.current?.click();
|
||||
}
|
||||
}
|
||||
}, [isIOS]);
|
||||
|
||||
return { triggerHaptic };
|
||||
};
|
||||
|
||||
export default useHaptic;
|
||||
6
src/hooks/use-is-mobile.ts
Normal file
6
src/hooks/use-is-mobile.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
export const useIsMobile = () => {
|
||||
const isMobile = useMediaQuery('(max-width: 48em)');
|
||||
return isMobile;
|
||||
}
|
||||
153
src/hooks/use-server-events.ts
Normal file
153
src/hooks/use-server-events.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Logger } from "@/lib/logger";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
/*
|
||||
import { authClient } from "~/lib/auth-client";
|
||||
import { getIdeaFn } from "~/routes/-fn/getIdeaFn";
|
||||
import { useSession } from "~/lib/sessionContext";
|
||||
*/
|
||||
|
||||
// Global state for new ideas notification
|
||||
let newIdeasAvailable = false;
|
||||
let newIdeasCallbacks: (() => void)[] = [];
|
||||
|
||||
const logger = new Logger('ServerEvents');
|
||||
|
||||
// Event handler types for better type safety
|
||||
type SSEEvent = {
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type EventHandler = (event: SSEEvent, queryClient: ReturnType<typeof useQueryClient>, currentSessionId?: string) => void;
|
||||
|
||||
// Event handlers map - add new handlers here for easy extension
|
||||
const eventHandlers: Record<string, EventHandler> = {
|
||||
"connected": () => {
|
||||
logger.info("ServerEvents | New Connection");
|
||||
},
|
||||
|
||||
"ping": () => {
|
||||
// Keep-alive ping, no action needed
|
||||
},
|
||||
|
||||
"test": (event, queryClient) => {
|
||||
},
|
||||
|
||||
// Add new event handlers here:
|
||||
// "idea-updated": (event, queryClient) => {
|
||||
// queryClient.invalidateQueries({ queryKey: ["ideas"] });
|
||||
// },
|
||||
// "vote-changed": (event, queryClient) => {
|
||||
// queryClient.invalidateQueries({ queryKey: ["idea", event.ideaId] });
|
||||
// },
|
||||
};
|
||||
|
||||
// Functions to manage new ideas notification state
|
||||
export function getNewIdeasAvailable(): boolean {
|
||||
return newIdeasAvailable;
|
||||
}
|
||||
|
||||
export function clearNewIdeasAvailable(): void {
|
||||
newIdeasAvailable = false;
|
||||
}
|
||||
|
||||
export function subscribeToNewIdeas(callback: () => void): () => void {
|
||||
newIdeasCallbacks.push(callback);
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = newIdeasCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
newIdeasCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useServerEvents() {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
const retryCountRef = useRef(0);
|
||||
const shouldConnectRef = useRef(true);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.id) return;
|
||||
|
||||
shouldConnectRef.current = true;
|
||||
retryCountRef.current = 0;
|
||||
|
||||
const connectEventSource = () => {
|
||||
if (!shouldConnectRef.current) return;
|
||||
|
||||
|
||||
const eventSource = new EventSource(`/api/events/$`);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
// Reset retry count on successful connection
|
||||
retryCountRef.current = 0;
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data: SSEEvent = JSON.parse(event.data);
|
||||
logger.info("Event received", data);
|
||||
|
||||
// Use the event handler pattern for extensible event processing
|
||||
const handler = eventHandlers[data.type];
|
||||
if (handler) {
|
||||
handler(data, queryClient, user?.id);
|
||||
} else {
|
||||
logger.warn(`Unhandled SSE event type: ${data.type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error parsing SSE message", error);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
logger.error("SSE connection error", error);
|
||||
eventSource.close();
|
||||
|
||||
// Only retry if we should still be connecting and haven't exceeded max retries
|
||||
if (shouldConnectRef.current && retryCountRef.current < 5) {
|
||||
retryCountRef.current += 1;
|
||||
const delay = Math.min(
|
||||
1000 * Math.pow(2, retryCountRef.current - 1),
|
||||
30000
|
||||
); // Cap at 30 seconds
|
||||
|
||||
logger.info(
|
||||
`SSE reconnection attempt ${retryCountRef.current}/5 in ${delay}ms`
|
||||
);
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (shouldConnectRef.current) {
|
||||
connectEventSource();
|
||||
}
|
||||
}, delay);
|
||||
} else if (retryCountRef.current >= 5) {
|
||||
logger.error("SSE max reconnection attempts reached");
|
||||
}
|
||||
};
|
||||
|
||||
return eventSource;
|
||||
};
|
||||
|
||||
const eventSource = connectEventSource();
|
||||
|
||||
return () => {
|
||||
logger.info("Closing SSE connection");
|
||||
shouldConnectRef.current = false;
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
}, [user?.id, queryClient]);
|
||||
}
|
||||
24
src/hooks/use-sheet.ts
Normal file
24
src/hooks/use-sheet.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
interface UseSheetOptions {
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export function useSheet(options: UseSheetOptions = {}) {
|
||||
const [isOpen, setIsOpen] = useState(options.defaultOpen ?? false);
|
||||
|
||||
const open = useCallback(() => setIsOpen(true), []);
|
||||
const close = useCallback(() => setIsOpen(false), []);
|
||||
const toggle = useCallback(() => setIsOpen(prev => !prev), []);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
props: {
|
||||
opened: isOpen,
|
||||
onChange: setIsOpen,
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user