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 { AppSplitterProvider } from './AppSplitterProvider';
|
||||||
import { sizeToPixels, easeInOutCubic, createAutoSaveId } from './helpers';
|
import { sizeToPixels, easeInOutCubic, createAutoSaveId } from './helpers';
|
||||||
import { useMemoizedFn } from '@/hooks';
|
import { useMemoizedFn } from '@/hooks';
|
||||||
|
import { useMount } from '@/hooks/useMount';
|
||||||
|
|
||||||
interface IAppSplitterProps {
|
interface IAppSplitterProps {
|
||||||
leftChildren: React.ReactNode;
|
leftChildren: React.ReactNode;
|
||||||
|
@ -59,7 +60,6 @@ interface SplitterState {
|
||||||
containerSize: number;
|
containerSize: number;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
isInitialized: boolean;
|
|
||||||
sizeSetByAnimation: boolean;
|
sizeSetByAnimation: boolean;
|
||||||
hasUserInteracted: boolean;
|
hasUserInteracted: boolean;
|
||||||
}
|
}
|
||||||
|
@ -68,20 +68,28 @@ const AppSplitterWrapper = forwardRef<AppSplitterRef, IAppSplitterProps>(
|
||||||
({ autoSaveId, style, className, split = 'vertical', ...props }, componentRef) => {
|
({ autoSaveId, style, className, split = 'vertical', ...props }, componentRef) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isVertical = split === 'vertical';
|
const isVertical = split === 'vertical';
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
const splitterAutoSaveId = createAutoSaveId(autoSaveId);
|
const splitterAutoSaveId = createAutoSaveId(autoSaveId);
|
||||||
|
|
||||||
|
useMount(() => {
|
||||||
|
setMounted(true);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
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}>
|
style={style}>
|
||||||
<AppSplitterBase
|
{mounted && (
|
||||||
{...props}
|
<AppSplitterBase
|
||||||
ref={componentRef}
|
{...props}
|
||||||
isVertical={isVertical}
|
ref={componentRef}
|
||||||
containerRef={containerRef}
|
isVertical={isVertical}
|
||||||
splitterAutoSaveId={splitterAutoSaveId}
|
containerRef={containerRef}
|
||||||
/>
|
splitterAutoSaveId={splitterAutoSaveId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -132,7 +140,6 @@ const AppSplitterBase = forwardRef<
|
||||||
|
|
||||||
const bustStorageOnInitSplitter = (preservedSideValue: number | null) => {
|
const bustStorageOnInitSplitter = (preservedSideValue: number | null) => {
|
||||||
const refWidth = containerRef.current?.offsetWidth;
|
const refWidth = containerRef.current?.offsetWidth;
|
||||||
console.log('bustStorageOnInitSplitter', splitterAutoSaveId, refWidth);
|
|
||||||
// Don't bust storage if container hasn't been sized yet
|
// Don't bust storage if container hasn't been sized yet
|
||||||
if (!refWidth || refWidth === 0) return false;
|
if (!refWidth || refWidth === 0) return false;
|
||||||
return typeof bustStorageOnInit === 'function'
|
return typeof bustStorageOnInit === 'function'
|
||||||
|
@ -151,7 +158,6 @@ const AppSplitterBase = forwardRef<
|
||||||
containerSize: containerRef.current?.offsetWidth ?? 0,
|
containerSize: containerRef.current?.offsetWidth ?? 0,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
isAnimating: false,
|
isAnimating: false,
|
||||||
isInitialized: false,
|
|
||||||
sizeSetByAnimation: false,
|
sizeSetByAnimation: false,
|
||||||
hasUserInteracted: false
|
hasUserInteracted: false
|
||||||
});
|
});
|
||||||
|
@ -232,16 +238,10 @@ const AppSplitterBase = forwardRef<
|
||||||
|
|
||||||
// Calculate panel sizes with simplified logic
|
// Calculate panel sizes with simplified logic
|
||||||
const { leftSize, rightSize } = useMemo(() => {
|
const { leftSize, rightSize } = useMemo(() => {
|
||||||
const {
|
const { containerSize, isAnimating, sizeSetByAnimation, isDragging, hasUserInteracted } =
|
||||||
containerSize,
|
state;
|
||||||
isInitialized,
|
|
||||||
isAnimating,
|
|
||||||
sizeSetByAnimation,
|
|
||||||
isDragging,
|
|
||||||
hasUserInteracted
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
if (!containerSize || !isInitialized) {
|
if (!containerSize) {
|
||||||
return { leftSize: 0, rightSize: 0 };
|
return { leftSize: 0, rightSize: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,9 +302,7 @@ const AppSplitterBase = forwardRef<
|
||||||
const newState = { ...prev, containerSize: size };
|
const newState = { ...prev, containerSize: size };
|
||||||
|
|
||||||
// Initialize if needed - only when container has actual size
|
// Initialize if needed - only when container has actual size
|
||||||
if (!prev.isInitialized && !prev.isAnimating && size > 0) {
|
if (!prev.isAnimating && size > 0) {
|
||||||
newState.isInitialized = true;
|
|
||||||
|
|
||||||
// Set initial size if no saved layout exists
|
// Set initial size if no saved layout exists
|
||||||
if (savedLayout === null || savedLayout === undefined) {
|
if (savedLayout === null || savedLayout === undefined) {
|
||||||
const initialSize = calculateInitialSize(size);
|
const initialSize = calculateInitialSize(size);
|
||||||
|
@ -314,13 +312,7 @@ const AppSplitterBase = forwardRef<
|
||||||
|
|
||||||
// Handle container resize when one panel is at 0px
|
// Handle container resize when one panel is at 0px
|
||||||
// Only adjust layout during resize if we're not currently animating
|
// Only adjust layout during resize if we're not currently animating
|
||||||
if (
|
if (prev.containerSize > 0 && size > 0 && savedLayout !== null && !prev.isAnimating) {
|
||||||
prev.isInitialized &&
|
|
||||||
prev.containerSize > 0 &&
|
|
||||||
size > 0 &&
|
|
||||||
savedLayout !== null &&
|
|
||||||
!prev.isAnimating
|
|
||||||
) {
|
|
||||||
const currentSavedSize = savedLayout;
|
const currentSavedSize = savedLayout;
|
||||||
|
|
||||||
// If a panel is at 0px, preserve the other panel's size during resize
|
// If a panel is at 0px, preserve the other panel's size during resize
|
||||||
|
@ -570,7 +562,6 @@ const AppSplitterBase = forwardRef<
|
||||||
hidden={leftHidden}>
|
hidden={leftHidden}>
|
||||||
{renderLeftPanel && leftChildren}
|
{renderLeftPanel && leftChildren}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{showSplitter && (
|
{showSplitter && (
|
||||||
<Splitter
|
<Splitter
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
@ -581,7 +572,6 @@ const AppSplitterBase = forwardRef<
|
||||||
hidden={shouldHideSplitter}
|
hidden={shouldHideSplitter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Panel
|
<Panel
|
||||||
className={rightPanelClassName}
|
className={rightPanelClassName}
|
||||||
width={isVertical ? rightSize : 'auto'}
|
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
|
// Helper function to convert size values to pixels
|
||||||
export const sizeToPixels = (size: string | number, containerSize: number): number => {
|
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 || {};
|
} = options || {};
|
||||||
|
|
||||||
const executeBustStorage = useMemoizedFn(() => {
|
const executeBustStorage = useMemoizedFn(() => {
|
||||||
|
console.log(
|
||||||
|
'***executeBustStorage',
|
||||||
|
key,
|
||||||
|
typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue
|
||||||
|
);
|
||||||
if (!isServer) window.localStorage.removeItem(key);
|
if (!isServer) window.localStorage.removeItem(key);
|
||||||
return typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue;
|
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
|
// Initialize state from localStorage on mount
|
||||||
useMount(() => {
|
useMount(() => {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ChatContent } from './ChatContent';
|
||||||
import { ChatHeader } from './ChatHeader';
|
import { ChatHeader } from './ChatHeader';
|
||||||
|
|
||||||
export const ChatContainer = React.memo(({ mounted }: { mounted?: boolean }) => {
|
export const ChatContainer = React.memo(({ mounted }: { mounted?: boolean }) => {
|
||||||
|
console.log('ChatContainer', mounted);
|
||||||
return (
|
return (
|
||||||
<AppPageLayout
|
<AppPageLayout
|
||||||
headerSizeVariant="default"
|
headerSizeVariant="default"
|
||||||
|
|
|
@ -20,7 +20,6 @@ interface ChatSplitterProps {
|
||||||
|
|
||||||
export const ChatLayout: React.FC<ChatSplitterProps> = ({ children }) => {
|
export const ChatLayout: React.FC<ChatSplitterProps> = ({ children }) => {
|
||||||
const appSplitterRef = useRef<AppSplitterRef>(null);
|
const appSplitterRef = useRef<AppSplitterRef>(null);
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
|
|
||||||
const chatLayoutProps = useChatLayoutContext({ appSplitterRef });
|
const chatLayoutProps = useChatLayoutContext({ appSplitterRef });
|
||||||
const { selectedLayout, selectedFile } = chatLayoutProps;
|
const { selectedLayout, selectedFile } = chatLayoutProps;
|
||||||
|
@ -39,25 +38,29 @@ export const ChatLayout: React.FC<ChatSplitterProps> = ({ children }) => {
|
||||||
const renderLeftPanel = selectedLayout !== 'file-only';
|
const renderLeftPanel = selectedLayout !== 'file-only';
|
||||||
const renderRightPanel = selectedLayout !== 'chat-only';
|
const renderRightPanel = selectedLayout !== 'chat-only';
|
||||||
const secondaryFileView = chatLayoutProps.secondaryView;
|
const secondaryFileView = chatLayoutProps.secondaryView;
|
||||||
|
const mounted = true;
|
||||||
|
|
||||||
const bustStorageOnInit = (preservedSideValue: number | null) => {
|
const bustStorageOnInit = (preservedSideValue: number | null, containerSize: number) => {
|
||||||
console.log('bustStorageOnInit', autoSaveId, preservedSideValue, {
|
console.log(
|
||||||
selectedLayout,
|
selectedLayout === 'chat-only' || selectedLayout === 'file-only' || !!secondaryFileView,
|
||||||
secondaryFileView
|
'bustStorageOnInit',
|
||||||
});
|
autoSaveId,
|
||||||
|
preservedSideValue,
|
||||||
|
{
|
||||||
|
selectedLayout,
|
||||||
|
secondaryFileView,
|
||||||
|
containerSize
|
||||||
|
}
|
||||||
|
);
|
||||||
return selectedLayout === 'chat-only' || selectedLayout === 'file-only' || !!secondaryFileView;
|
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 (
|
return (
|
||||||
<ChatLayoutContextProvider chatLayoutProps={chatLayoutProps}>
|
<ChatLayoutContextProvider chatLayoutProps={chatLayoutProps}>
|
||||||
<ChatContextProvider>
|
<ChatContextProvider>
|
||||||
<AppSplitter
|
<AppSplitter
|
||||||
ref={appSplitterRef}
|
ref={appSplitterRef}
|
||||||
leftChildren={useMemo(() => mounted && <ChatContainer mounted={mounted} />, [mounted])}
|
leftChildren={<div>HUH?</div>}
|
||||||
rightChildren={useMemo(
|
rightChildren={useMemo(
|
||||||
() => mounted && <FileContainer>{children}</FileContainer>,
|
() => mounted && <FileContainer>{children}</FileContainer>,
|
||||||
[children, mounted]
|
[children, mounted]
|
||||||
|
|
Loading…
Reference in New Issue