Archived
Redesign marketing pages and isolate public routes from app auth.
Give the landing, legal, and sign-in flows a consistent product shell while keeping marketing pages free of tRPC/session calls, fixing dev auth URL handling, and refreshing env and deploy docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
@@ -35,9 +34,8 @@ import {
|
||||
type RadiusPreference,
|
||||
type SidebarStyle,
|
||||
} from "~/lib/branding";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
type AppearancePreferences = {
|
||||
export type AppearancePreferences = {
|
||||
interfaceTheme: InterfaceTheme;
|
||||
bodyFontPreference: FontPreference;
|
||||
headingFontPreference: FontPreference;
|
||||
@@ -57,37 +55,17 @@ type AppearancePreferences = {
|
||||
pdfShowPageNumbers: boolean;
|
||||
};
|
||||
|
||||
type AppearancePatch = Partial<AppearancePreferences>;
|
||||
export type AppearancePatch = Partial<AppearancePreferences>;
|
||||
|
||||
type ServerAppearance = {
|
||||
interfaceTheme: InterfaceTheme;
|
||||
bodyFontPreference: FontPreference;
|
||||
headingFontPreference: FontPreference;
|
||||
radiusPreference: RadiusPreference;
|
||||
sidebarStyle: SidebarStyle;
|
||||
theme: ColorMode;
|
||||
colorTheme: ColorTheme;
|
||||
customColor?: string;
|
||||
brandName: string;
|
||||
brandTagline: string;
|
||||
brandLogoText: string;
|
||||
brandIcon: string;
|
||||
pdfTemplate: PdfTemplate;
|
||||
pdfAccentColor: string;
|
||||
pdfFooterText: string;
|
||||
pdfShowLogo: boolean;
|
||||
pdfShowPageNumbers: boolean;
|
||||
};
|
||||
|
||||
type AppearanceContextValue = AppearancePreferences & {
|
||||
export type AppearanceContextValue = AppearancePreferences & {
|
||||
updateAppearance: (patch: AppearancePatch) => void;
|
||||
updateAppearanceDebounced: (patch: AppearancePatch) => void;
|
||||
isUpdating: boolean;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "bv.appearance";
|
||||
export const STORAGE_KEY = "bv.appearance";
|
||||
|
||||
const defaultAppearance: AppearancePreferences = {
|
||||
export const defaultAppearance: AppearancePreferences = {
|
||||
interfaceTheme: defaultInterfaceTheme,
|
||||
bodyFontPreference: defaultBodyFontPreference,
|
||||
headingFontPreference: defaultHeadingFontPreference,
|
||||
@@ -106,33 +84,10 @@ const defaultAppearance: AppearancePreferences = {
|
||||
pdfShowPageNumbers: fallbackAppearance.pdfShowPageNumbers,
|
||||
};
|
||||
|
||||
const AppearanceContext = createContext<AppearanceContextValue | null>(null);
|
||||
export const AppearanceContext =
|
||||
createContext<AppearanceContextValue | null>(null);
|
||||
|
||||
function getServerAppearancePatch(
|
||||
serverAppearance: ServerAppearance,
|
||||
): AppearancePatch {
|
||||
return {
|
||||
interfaceTheme: serverAppearance.interfaceTheme,
|
||||
bodyFontPreference: serverAppearance.bodyFontPreference,
|
||||
headingFontPreference: serverAppearance.headingFontPreference,
|
||||
radiusPreference: serverAppearance.radiusPreference,
|
||||
sidebarStyle: serverAppearance.sidebarStyle,
|
||||
colorMode: serverAppearance.theme,
|
||||
colorTheme: serverAppearance.colorTheme,
|
||||
customColor: serverAppearance.customColor,
|
||||
brandName: serverAppearance.brandName,
|
||||
brandTagline: serverAppearance.brandTagline,
|
||||
brandLogoText: serverAppearance.brandLogoText,
|
||||
brandIcon: serverAppearance.brandIcon,
|
||||
pdfTemplate: serverAppearance.pdfTemplate,
|
||||
pdfAccentColor: serverAppearance.pdfAccentColor,
|
||||
pdfFooterText: serverAppearance.pdfFooterText,
|
||||
pdfShowLogo: serverAppearance.pdfShowLogo,
|
||||
pdfShowPageNumbers: serverAppearance.pdfShowPageNumbers,
|
||||
};
|
||||
}
|
||||
|
||||
function readStoredAppearance(): Partial<AppearancePreferences> | null {
|
||||
export function readStoredAppearance(): Partial<AppearancePreferences> | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
@@ -201,7 +156,7 @@ function readStoredAppearance(): Partial<AppearancePreferences> | null {
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredAppearance(prefs: AppearancePreferences) {
|
||||
export function writeStoredAppearance(prefs: AppearancePreferences) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
|
||||
} catch {
|
||||
@@ -209,7 +164,7 @@ function writeStoredAppearance(prefs: AppearancePreferences) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyAppearance(prefs: AppearancePreferences) {
|
||||
export function applyAppearance(prefs: AppearancePreferences) {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const root = document.documentElement;
|
||||
@@ -230,6 +185,7 @@ function applyAppearance(prefs: AppearancePreferences) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Local-only appearance provider for marketing and auth pages (no tRPC). */
|
||||
export function AppearanceProvider({
|
||||
children,
|
||||
}: {
|
||||
@@ -237,65 +193,6 @@ export function AppearanceProvider({
|
||||
}) {
|
||||
const [appearance, setAppearance] =
|
||||
useState<AppearancePreferences>(defaultAppearance);
|
||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingDebouncedPatchRef = useRef<AppearancePatch>({});
|
||||
const utils = api.useUtils();
|
||||
const updateMutation = api.settings.updateTheme.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.settings.getTheme.invalidate();
|
||||
},
|
||||
onError: () => {
|
||||
const cachedAppearance = utils.settings.getTheme.getData();
|
||||
const fallback = cachedAppearance
|
||||
? {
|
||||
...defaultAppearance,
|
||||
...getServerAppearancePatch(cachedAppearance),
|
||||
}
|
||||
: defaultAppearance;
|
||||
|
||||
setAppearance(fallback);
|
||||
applyAppearance(fallback);
|
||||
writeStoredAppearance(fallback);
|
||||
},
|
||||
});
|
||||
|
||||
const persistAppearance = useCallback(
|
||||
(patch: AppearancePatch) => {
|
||||
if (
|
||||
patch.customColor !== undefined &&
|
||||
!isHslChannels(patch.customColor)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutate({
|
||||
interfaceTheme: patch.interfaceTheme,
|
||||
bodyFontPreference: patch.bodyFontPreference,
|
||||
headingFontPreference: patch.headingFontPreference,
|
||||
radiusPreference: patch.radiusPreference,
|
||||
sidebarStyle: patch.sidebarStyle,
|
||||
theme: patch.colorMode,
|
||||
colorTheme: patch.colorTheme,
|
||||
customColor: patch.customColor,
|
||||
brandName: patch.brandName,
|
||||
brandTagline: patch.brandTagline,
|
||||
brandLogoText: patch.brandLogoText,
|
||||
brandIcon: patch.brandIcon,
|
||||
pdfTemplate: patch.pdfTemplate,
|
||||
pdfAccentColor: patch.pdfAccentColor,
|
||||
pdfFooterText: patch.pdfFooterText,
|
||||
pdfShowLogo: patch.pdfShowLogo,
|
||||
pdfShowPageNumbers: patch.pdfShowPageNumbers,
|
||||
});
|
||||
},
|
||||
[updateMutation],
|
||||
);
|
||||
|
||||
const { data: serverAppearance } = api.settings.getTheme.useQuery(undefined, {
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const storedAppearance = readStoredAppearance();
|
||||
@@ -305,92 +202,37 @@ export function AppearanceProvider({
|
||||
setAppearance((prev) => ({ ...prev, ...storedAppearance }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverAppearance) return;
|
||||
const next = getServerAppearancePatch(serverAppearance);
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setAppearance((prev) => ({ ...prev, ...next }));
|
||||
}, [serverAppearance]);
|
||||
|
||||
useEffect(() => {
|
||||
applyAppearance(appearance);
|
||||
writeStoredAppearance(appearance);
|
||||
}, [appearance]);
|
||||
|
||||
const updateAppearance = useCallback(
|
||||
(patch: AppearancePatch) => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
if (Object.keys(pendingDebouncedPatchRef.current).length > 0) {
|
||||
persistAppearance(pendingDebouncedPatchRef.current);
|
||||
pendingDebouncedPatchRef.current = {};
|
||||
}
|
||||
const updateAppearance = useCallback((patch: AppearancePatch) => {
|
||||
setAppearance((prev) => {
|
||||
const next = { ...prev, ...patch };
|
||||
applyAppearance(next);
|
||||
writeStoredAppearance(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
setAppearance((prev) => {
|
||||
const next = { ...prev, ...patch };
|
||||
applyAppearance(next);
|
||||
writeStoredAppearance(next);
|
||||
return next;
|
||||
});
|
||||
|
||||
persistAppearance(patch);
|
||||
},
|
||||
[persistAppearance],
|
||||
);
|
||||
|
||||
const updateAppearanceDebounced = useCallback(
|
||||
(patch: AppearancePatch) => {
|
||||
pendingDebouncedPatchRef.current = {
|
||||
...pendingDebouncedPatchRef.current,
|
||||
...patch,
|
||||
};
|
||||
|
||||
setAppearance((prev) => {
|
||||
const next = { ...prev, ...patch };
|
||||
applyAppearance(next);
|
||||
writeStoredAppearance(next);
|
||||
return next;
|
||||
});
|
||||
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
persistAppearance(pendingDebouncedPatchRef.current);
|
||||
pendingDebouncedPatchRef.current = {};
|
||||
debounceTimerRef.current = null;
|
||||
}, 500);
|
||||
},
|
||||
[persistAppearance],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
pendingDebouncedPatchRef.current = {};
|
||||
},
|
||||
[],
|
||||
);
|
||||
const updateAppearanceDebounced = useCallback((patch: AppearancePatch) => {
|
||||
setAppearance((prev) => {
|
||||
const next = { ...prev, ...patch };
|
||||
applyAppearance(next);
|
||||
writeStoredAppearance(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AppearanceContextValue>(
|
||||
() => ({
|
||||
...appearance,
|
||||
updateAppearance,
|
||||
updateAppearanceDebounced,
|
||||
isUpdating: updateMutation.isPending,
|
||||
isUpdating: false,
|
||||
}),
|
||||
[
|
||||
appearance,
|
||||
updateAppearance,
|
||||
updateAppearanceDebounced,
|
||||
updateMutation.isPending,
|
||||
],
|
||||
[appearance, updateAppearance, updateAppearanceDebounced],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user