mirror of https://github.com/buster-so/buster.git
cookie state update
This commit is contained in:
parent
90a11d5fd8
commit
f7a7ab7e7a
|
@ -17,6 +17,7 @@ import { Splitter } from './Splitter';
|
|||
import { AppSplitterProvider } from './AppSplitterProvider';
|
||||
import { sizeToPixels, easeInOutCubic, createAutoSaveId } from './helpers';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { useMount } from '@/hooks/useMount';
|
||||
|
||||
interface IAppSplitterProps {
|
||||
leftChildren: React.ReactNode;
|
||||
|
@ -59,7 +60,6 @@ interface SplitterState {
|
|||
containerSize: number;
|
||||
isDragging: boolean;
|
||||
isAnimating: boolean;
|
||||
isInitialized: boolean;
|
||||
sizeSetByAnimation: boolean;
|
||||
hasUserInteracted: boolean;
|
||||
}
|
||||
|
@ -68,20 +68,28 @@ const AppSplitterWrapper = forwardRef<AppSplitterRef, IAppSplitterProps>(
|
|||
({ autoSaveId, style, className, split = 'vertical', ...props }, componentRef) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isVertical = split === 'vertical';
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const splitterAutoSaveId = createAutoSaveId(autoSaveId);
|
||||
|
||||
useMount(() => {
|
||||
setMounted(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('swag1 flex h-full w-full', isVertical ? 'flex-row' : 'flex-col', className)}
|
||||
id={splitterAutoSaveId}
|
||||
className={cn('flex h-full w-full', isVertical ? 'flex-row' : 'flex-col', className)}
|
||||
style={style}>
|
||||
<AppSplitterBase
|
||||
{...props}
|
||||
ref={componentRef}
|
||||
isVertical={isVertical}
|
||||
containerRef={containerRef}
|
||||
splitterAutoSaveId={splitterAutoSaveId}
|
||||
/>
|
||||
{mounted && (
|
||||
<AppSplitterBase
|
||||
{...props}
|
||||
ref={componentRef}
|
||||
isVertical={isVertical}
|
||||
containerRef={containerRef}
|
||||
splitterAutoSaveId={splitterAutoSaveId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -132,7 +140,6 @@ const AppSplitterBase = forwardRef<
|
|||
|
||||
const bustStorageOnInitSplitter = (preservedSideValue: number | null) => {
|
||||
const refWidth = containerRef.current?.offsetWidth;
|
||||
console.log('bustStorageOnInitSplitter', splitterAutoSaveId, refWidth);
|
||||
// Don't bust storage if container hasn't been sized yet
|
||||
if (!refWidth || refWidth === 0) return false;
|
||||
return typeof bustStorageOnInit === 'function'
|
||||
|
@ -151,7 +158,6 @@ const AppSplitterBase = forwardRef<
|
|||
containerSize: containerRef.current?.offsetWidth ?? 0,
|
||||
isDragging: false,
|
||||
isAnimating: false,
|
||||
isInitialized: false,
|
||||
sizeSetByAnimation: false,
|
||||
hasUserInteracted: false
|
||||
});
|
||||
|
@ -232,16 +238,10 @@ const AppSplitterBase = forwardRef<
|
|||
|
||||
// Calculate panel sizes with simplified logic
|
||||
const { leftSize, rightSize } = useMemo(() => {
|
||||
const {
|
||||
containerSize,
|
||||
isInitialized,
|
||||
isAnimating,
|
||||
sizeSetByAnimation,
|
||||
isDragging,
|
||||
hasUserInteracted
|
||||
} = state;
|
||||
const { containerSize, isAnimating, sizeSetByAnimation, isDragging, hasUserInteracted } =
|
||||
state;
|
||||
|
||||
if (!containerSize || !isInitialized) {
|
||||
if (!containerSize) {
|
||||
return { leftSize: 0, rightSize: 0 };
|
||||
}
|
||||
|
||||
|
@ -302,9 +302,7 @@ const AppSplitterBase = forwardRef<
|
|||
const newState = { ...prev, containerSize: size };
|
||||
|
||||
// Initialize if needed - only when container has actual size
|
||||
if (!prev.isInitialized && !prev.isAnimating && size > 0) {
|
||||
newState.isInitialized = true;
|
||||
|
||||
if (!prev.isAnimating && size > 0) {
|
||||
// Set initial size if no saved layout exists
|
||||
if (savedLayout === null || savedLayout === undefined) {
|
||||
const initialSize = calculateInitialSize(size);
|
||||
|
@ -314,13 +312,7 @@ const AppSplitterBase = forwardRef<
|
|||
|
||||
// Handle container resize when one panel is at 0px
|
||||
// Only adjust layout during resize if we're not currently animating
|
||||
if (
|
||||
prev.isInitialized &&
|
||||
prev.containerSize > 0 &&
|
||||
size > 0 &&
|
||||
savedLayout !== null &&
|
||||
!prev.isAnimating
|
||||
) {
|
||||
if (prev.containerSize > 0 && size > 0 && savedLayout !== null && !prev.isAnimating) {
|
||||
const currentSavedSize = savedLayout;
|
||||
|
||||
// If a panel is at 0px, preserve the other panel's size during resize
|
||||
|
@ -570,7 +562,6 @@ const AppSplitterBase = forwardRef<
|
|||
hidden={leftHidden}>
|
||||
{renderLeftPanel && leftChildren}
|
||||
</Panel>
|
||||
|
||||
{showSplitter && (
|
||||
<Splitter
|
||||
onMouseDown={handleMouseDown}
|
||||
|
@ -581,7 +572,6 @@ const AppSplitterBase = forwardRef<
|
|||
hidden={shouldHideSplitter}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Panel
|
||||
className={rightPanelClassName}
|
||||
width={isVertical ? rightSize : 'auto'}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const createAutoSaveId = (id: string) => `splitter-${id}`;
|
||||
export const createAutoSaveId = (id: string) => `app-splitter-${id}`;
|
||||
|
||||
// Helper function to convert size values to pixels
|
||||
export const sizeToPixels = (size: string | number, containerSize: number): number => {
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMemoizedFn } from './useMemoizedFn';
|
||||
import { useMount } from './useMount';
|
||||
import { isServer } from '@tanstack/react-query';
|
||||
|
||||
type SetState<S> = S | ((prevState?: S) => S);
|
||||
|
||||
// Default expiration time: 7 days in milliseconds
|
||||
const DEFAULT_EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
interface StorageData<T> {
|
||||
value: T;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface CookieOptions {
|
||||
domain?: string;
|
||||
path?: string;
|
||||
secure?: boolean;
|
||||
sameSite?: 'strict' | 'lax' | 'none';
|
||||
}
|
||||
|
||||
interface Options<T> {
|
||||
defaultValue?: T | (() => T);
|
||||
serializer?: (value: T) => string;
|
||||
deserializer?: (value: string) => T;
|
||||
onError?: (error: unknown) => void;
|
||||
bustStorageOnInit?: boolean | ((layout: T) => boolean);
|
||||
expirationTime?: number;
|
||||
cookieOptions?: CookieOptions;
|
||||
}
|
||||
|
||||
// Helper function to parse cookies
|
||||
const parseCookies = (): Record<string, string> => {
|
||||
if (isServer) return {};
|
||||
|
||||
return document.cookie.split(';').reduce(
|
||||
(cookies, cookie) => {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name && value) {
|
||||
cookies[name] = decodeURIComponent(value);
|
||||
}
|
||||
return cookies;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to set a cookie
|
||||
const setCookie = (
|
||||
name: string,
|
||||
value: string,
|
||||
expirationTime: number,
|
||||
options: CookieOptions = {}
|
||||
): void => {
|
||||
if (isServer) return;
|
||||
|
||||
const expires = new Date(Date.now() + expirationTime).toUTCString();
|
||||
const { domain, path = '/', secure = true, sameSite = 'lax' } = options;
|
||||
|
||||
let cookieString = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=${path}; SameSite=${sameSite}`;
|
||||
|
||||
if (secure) {
|
||||
cookieString += '; Secure';
|
||||
}
|
||||
|
||||
if (domain) {
|
||||
cookieString += `; Domain=${domain}`;
|
||||
}
|
||||
|
||||
document.cookie = cookieString;
|
||||
};
|
||||
|
||||
// Helper function to remove a cookie
|
||||
const removeCookie = (name: string, options: CookieOptions = {}): void => {
|
||||
if (isServer) return;
|
||||
|
||||
const { domain, path = '/' } = options;
|
||||
let cookieString = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}`;
|
||||
|
||||
if (domain) {
|
||||
cookieString += `; Domain=${domain}`;
|
||||
}
|
||||
|
||||
document.cookie = cookieString;
|
||||
};
|
||||
|
||||
export function useCookieState<T>(
|
||||
key: string,
|
||||
options?: Options<T>
|
||||
): [T | undefined, (value?: SetState<T>) => void, () => T | undefined] {
|
||||
const {
|
||||
defaultValue,
|
||||
serializer = JSON.stringify,
|
||||
deserializer = JSON.parse,
|
||||
onError,
|
||||
bustStorageOnInit = false,
|
||||
expirationTime = DEFAULT_EXPIRATION_TIME,
|
||||
cookieOptions = {}
|
||||
} = options || {};
|
||||
|
||||
const executeBustStorage = useMemoizedFn(() => {
|
||||
if (!isServer) removeCookie(key, cookieOptions);
|
||||
return typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue;
|
||||
});
|
||||
|
||||
// Get initial value from cookies or use default
|
||||
const getInitialValue = useMemoizedFn((): T | undefined => {
|
||||
// If bustStorageOnInit is true, ignore cookies and use default value
|
||||
if (bustStorageOnInit === true) {
|
||||
return executeBustStorage();
|
||||
}
|
||||
|
||||
try {
|
||||
const cookies = parseCookies();
|
||||
const cookieValue = cookies[key];
|
||||
|
||||
if (!cookieValue) {
|
||||
return executeBustStorage();
|
||||
}
|
||||
|
||||
// Parse the stored data which includes value and timestamp
|
||||
const storageData: StorageData<T> = JSON.parse(cookieValue);
|
||||
|
||||
// Check if the stored data has the expected structure
|
||||
if (
|
||||
typeof storageData !== 'object' ||
|
||||
storageData === null ||
|
||||
!('value' in storageData) ||
|
||||
!('timestamp' in storageData)
|
||||
) {
|
||||
// If the data doesn't have the expected structure (legacy data), treat as expired
|
||||
return executeBustStorage();
|
||||
}
|
||||
|
||||
// Check if the data has expired
|
||||
const currentTime = Date.now();
|
||||
const timeDifference = currentTime - storageData.timestamp;
|
||||
|
||||
if (timeDifference > expirationTime) {
|
||||
// Data has expired, remove it and return default value
|
||||
return executeBustStorage();
|
||||
}
|
||||
|
||||
// Data is still valid, deserialize and return the value
|
||||
const deserializedValue = deserializer(JSON.stringify(storageData.value));
|
||||
|
||||
if (typeof bustStorageOnInit === 'function' && bustStorageOnInit(deserializedValue)) {
|
||||
return executeBustStorage();
|
||||
}
|
||||
|
||||
return deserializedValue;
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
return executeBustStorage();
|
||||
}
|
||||
});
|
||||
|
||||
const [state, setState] = useState<T | undefined>(() => getInitialValue());
|
||||
|
||||
// Initialize state from cookies on mount
|
||||
useMount(() => {
|
||||
setState(getInitialValue());
|
||||
});
|
||||
|
||||
// Update cookies when state changes
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (state === undefined && !isServer) {
|
||||
removeCookie(key, cookieOptions);
|
||||
} else {
|
||||
// Create storage data with current timestamp
|
||||
const storageData: StorageData<T> = {
|
||||
value: JSON.parse(serializer(state)),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
setCookie(key, JSON.stringify(storageData), expirationTime, cookieOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
}
|
||||
}, [key, state, serializer, onError, expirationTime, cookieOptions]);
|
||||
|
||||
// Setter function that handles both direct values and function updates
|
||||
const setStoredState = useMemoizedFn((value?: SetState<T>) => {
|
||||
try {
|
||||
if (typeof value === 'function') {
|
||||
setState((prevState) => {
|
||||
const newState = (value as (prevState?: T) => T)(prevState);
|
||||
return newState;
|
||||
});
|
||||
} else {
|
||||
setState(value);
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
}
|
||||
});
|
||||
|
||||
return [state, setStoredState, executeBustStorage];
|
||||
}
|
|
@ -38,6 +38,11 @@ export function useLocalStorageState<T>(
|
|||
} = options || {};
|
||||
|
||||
const executeBustStorage = useMemoizedFn(() => {
|
||||
console.log(
|
||||
'***executeBustStorage',
|
||||
key,
|
||||
typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue
|
||||
);
|
||||
if (!isServer) window.localStorage.removeItem(key);
|
||||
return typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue;
|
||||
});
|
||||
|
@ -92,7 +97,7 @@ export function useLocalStorageState<T>(
|
|||
}
|
||||
});
|
||||
|
||||
const [state, setState] = useState<T | undefined>(() => getInitialValue());
|
||||
const [state, setState] = useState<T | undefined>(getInitialValue);
|
||||
|
||||
// Initialize state from localStorage on mount
|
||||
useMount(() => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ChatContent } from './ChatContent';
|
|||
import { ChatHeader } from './ChatHeader';
|
||||
|
||||
export const ChatContainer = React.memo(({ mounted }: { mounted?: boolean }) => {
|
||||
console.log('ChatContainer', mounted);
|
||||
return (
|
||||
<AppPageLayout
|
||||
headerSizeVariant="default"
|
||||
|
|
|
@ -20,7 +20,6 @@ interface ChatSplitterProps {
|
|||
|
||||
export const ChatLayout: React.FC<ChatSplitterProps> = ({ children }) => {
|
||||
const appSplitterRef = useRef<AppSplitterRef>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const chatLayoutProps = useChatLayoutContext({ appSplitterRef });
|
||||
const { selectedLayout, selectedFile } = chatLayoutProps;
|
||||
|
@ -39,25 +38,29 @@ export const ChatLayout: React.FC<ChatSplitterProps> = ({ children }) => {
|
|||
const renderLeftPanel = selectedLayout !== 'file-only';
|
||||
const renderRightPanel = selectedLayout !== 'chat-only';
|
||||
const secondaryFileView = chatLayoutProps.secondaryView;
|
||||
const mounted = true;
|
||||
|
||||
const bustStorageOnInit = (preservedSideValue: number | null) => {
|
||||
console.log('bustStorageOnInit', autoSaveId, preservedSideValue, {
|
||||
selectedLayout,
|
||||
secondaryFileView
|
||||
});
|
||||
const bustStorageOnInit = (preservedSideValue: number | null, containerSize: number) => {
|
||||
console.log(
|
||||
selectedLayout === 'chat-only' || selectedLayout === 'file-only' || !!secondaryFileView,
|
||||
'bustStorageOnInit',
|
||||
autoSaveId,
|
||||
preservedSideValue,
|
||||
{
|
||||
selectedLayout,
|
||||
secondaryFileView,
|
||||
containerSize
|
||||
}
|
||||
);
|
||||
return selectedLayout === 'chat-only' || selectedLayout === 'file-only' || !!secondaryFileView;
|
||||
};
|
||||
|
||||
useMount(() => {
|
||||
setMounted(true); //we need to wait for the app splitter to be mounted because this is nested in the app splitter
|
||||
});
|
||||
|
||||
return (
|
||||
<ChatLayoutContextProvider chatLayoutProps={chatLayoutProps}>
|
||||
<ChatContextProvider>
|
||||
<AppSplitter
|
||||
ref={appSplitterRef}
|
||||
leftChildren={useMemo(() => mounted && <ChatContainer mounted={mounted} />, [mounted])}
|
||||
leftChildren={<div>HUH?</div>}
|
||||
rightChildren={useMemo(
|
||||
() => mounted && <FileContainer>{children}</FileContainer>,
|
||||
[children, mounted]
|
||||
|
|
Loading…
Reference in New Issue