mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Make banner reusable
This commit is contained in:
parent
ec89b77e95
commit
ae66e74687
@ -1,5 +1,6 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { AppProviders } from "@app/components/AppProviders";
|
import { AppProviders } from "@app/components/AppProviders";
|
||||||
|
import { AppLayout } from "@app/components/AppLayout";
|
||||||
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
||||||
import HomePage from "@app/pages/HomePage";
|
import HomePage from "@app/pages/HomePage";
|
||||||
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
||||||
@ -16,8 +17,10 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<AppProviders>
|
<AppProviders>
|
||||||
<HomePage />
|
<AppLayout>
|
||||||
<OnboardingTour />
|
<HomePage />
|
||||||
|
<OnboardingTour />
|
||||||
|
</AppLayout>
|
||||||
</AppProviders>
|
</AppProviders>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
31
frontend/src/core/components/AppLayout.tsx
Normal file
31
frontend/src/core/components/AppLayout.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { useBanner } from '@app/contexts/BannerContext';
|
||||||
|
|
||||||
|
interface AppLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App layout wrapper that handles banner rendering and viewport sizing
|
||||||
|
* Automatically adjusts child components to fit remaining space after banner
|
||||||
|
*/
|
||||||
|
export function AppLayout({ children }: AppLayoutProps) {
|
||||||
|
const { banner } = useBanner();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
.h-screen,
|
||||||
|
.right-rail {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{banner}
|
||||||
|
<div style={{ flex: 1, minHeight: 0, height: 0 }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ import { OnboardingProvider } from "@app/contexts/OnboardingContext";
|
|||||||
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
|
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
|
||||||
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
|
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
|
||||||
import { PageEditorProvider } from "@app/contexts/PageEditorContext";
|
import { PageEditorProvider } from "@app/contexts/PageEditorContext";
|
||||||
|
import { BannerProvider } from "@app/contexts/BannerContext";
|
||||||
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
|
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
|
||||||
import { useScarfTracking } from "@app/hooks/useScarfTracking";
|
import { useScarfTracking } from "@app/hooks/useScarfTracking";
|
||||||
import { useAppInitialization } from "@app/hooks/useAppInitialization";
|
import { useAppInitialization } from "@app/hooks/useAppInitialization";
|
||||||
@ -50,22 +51,23 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
|
|||||||
<PreferencesProvider>
|
<PreferencesProvider>
|
||||||
<RainbowThemeProvider>
|
<RainbowThemeProvider>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<OnboardingProvider>
|
<BannerProvider>
|
||||||
<AppConfigProvider
|
<OnboardingProvider>
|
||||||
retryOptions={appConfigRetryOptions}
|
<AppConfigProvider
|
||||||
{...appConfigProviderProps}
|
retryOptions={appConfigRetryOptions}
|
||||||
>
|
{...appConfigProviderProps}
|
||||||
<ScarfTrackingInitializer />
|
>
|
||||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
<ScarfTrackingInitializer />
|
||||||
<AppInitializer />
|
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||||
<ToolRegistryProvider>
|
<AppInitializer />
|
||||||
<NavigationProvider>
|
<ToolRegistryProvider>
|
||||||
<FilesModalProvider>
|
<NavigationProvider>
|
||||||
<ToolWorkflowProvider>
|
<FilesModalProvider>
|
||||||
<HotkeyProvider>
|
<ToolWorkflowProvider>
|
||||||
<SidebarProvider>
|
<HotkeyProvider>
|
||||||
<ViewerProvider>
|
<SidebarProvider>
|
||||||
<PageEditorProvider>
|
<ViewerProvider>
|
||||||
|
<PageEditorProvider>
|
||||||
<SignatureProvider>
|
<SignatureProvider>
|
||||||
<RightRailProvider>
|
<RightRailProvider>
|
||||||
<TourOrchestrationProvider>
|
<TourOrchestrationProvider>
|
||||||
@ -76,16 +78,17 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
|
|||||||
</RightRailProvider>
|
</RightRailProvider>
|
||||||
</SignatureProvider>
|
</SignatureProvider>
|
||||||
</PageEditorProvider>
|
</PageEditorProvider>
|
||||||
</ViewerProvider>
|
</ViewerProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</HotkeyProvider>
|
</HotkeyProvider>
|
||||||
</ToolWorkflowProvider>
|
</ToolWorkflowProvider>
|
||||||
</FilesModalProvider>
|
</FilesModalProvider>
|
||||||
</NavigationProvider>
|
</NavigationProvider>
|
||||||
</ToolRegistryProvider>
|
</ToolRegistryProvider>
|
||||||
</FileContextProvider>
|
</FileContextProvider>
|
||||||
</AppConfigProvider>
|
</AppConfigProvider>
|
||||||
</OnboardingProvider>
|
</OnboardingProvider>
|
||||||
|
</BannerProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</RainbowThemeProvider>
|
</RainbowThemeProvider>
|
||||||
</PreferencesProvider>
|
</PreferencesProvider>
|
||||||
|
|||||||
72
frontend/src/core/components/shared/InfoBanner.tsx
Normal file
72
frontend/src/core/components/shared/InfoBanner.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Paper, Group, Text, Button, ActionIcon } from '@mantine/core';
|
||||||
|
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||||
|
|
||||||
|
interface InfoBannerProps {
|
||||||
|
icon: string;
|
||||||
|
message: string;
|
||||||
|
buttonText: string;
|
||||||
|
buttonIcon?: string;
|
||||||
|
onButtonClick: () => void;
|
||||||
|
onDismiss: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
show?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic info banner component for displaying dismissible messages at the top of the app
|
||||||
|
*/
|
||||||
|
export const InfoBanner: React.FC<InfoBannerProps> = ({
|
||||||
|
icon,
|
||||||
|
message,
|
||||||
|
buttonText,
|
||||||
|
buttonIcon = 'check-circle-rounded',
|
||||||
|
onButtonClick,
|
||||||
|
onDismiss,
|
||||||
|
loading = false,
|
||||||
|
show = true,
|
||||||
|
}) => {
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
p="sm"
|
||||||
|
radius={0}
|
||||||
|
style={{
|
||||||
|
background: 'var(--mantine-color-blue-0)',
|
||||||
|
borderBottom: '1px solid var(--mantine-color-blue-2)',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap="sm" align="center" wrap="nowrap">
|
||||||
|
<LocalIcon icon={icon} width="1.2rem" height="1.2rem" style={{ color: 'var(--mantine-color-blue-6)', flexShrink: 0 }} />
|
||||||
|
<Text fw={500} size="sm" style={{ color: 'var(--mantine-color-blue-9)' }}>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
size="xs"
|
||||||
|
onClick={onButtonClick}
|
||||||
|
loading={loading}
|
||||||
|
leftSection={<LocalIcon icon={buttonIcon} width="0.9rem" height="0.9rem" />}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="sm"
|
||||||
|
onClick={onDismiss}
|
||||||
|
aria-label="Dismiss"
|
||||||
|
style={{ position: 'absolute', top: '50%', right: '0.5rem', transform: 'translateY(-50%)' }}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
26
frontend/src/core/contexts/BannerContext.tsx
Normal file
26
frontend/src/core/contexts/BannerContext.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface BannerContextType {
|
||||||
|
banner: ReactNode;
|
||||||
|
setBanner: (banner: ReactNode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BannerContext = createContext<BannerContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function BannerProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [banner, setBanner] = useState<ReactNode>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BannerContext.Provider value={{ banner, setBanner }}>
|
||||||
|
{children}
|
||||||
|
</BannerContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBanner() {
|
||||||
|
const context = useContext(BannerContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useBanner must be used within BannerProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { Routes, Route } from "react-router-dom";
|
|
||||||
import { AppProviders } from "@app/components/AppProviders";
|
|
||||||
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
|
||||||
import Landing from "@app/routes/Landing";
|
|
||||||
import Login from "@app/routes/Login";
|
|
||||||
import Signup from "@app/routes/Signup";
|
|
||||||
import AuthCallback from "@app/routes/AuthCallback";
|
|
||||||
import InviteAccept from "@app/routes/InviteAccept";
|
|
||||||
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
|
||||||
import { DefaultAppBanner } from "@app/components/shared/DefaultAppBanner";
|
|
||||||
|
|
||||||
// Import global styles
|
|
||||||
import "@app/styles/tailwind.css";
|
|
||||||
import "@app/styles/cookieconsent.css";
|
|
||||||
import "@app/styles/index.css";
|
|
||||||
import "@app/styles/auth-theme.css";
|
|
||||||
|
|
||||||
// Import file ID debugging helpers (development only)
|
|
||||||
import "@app/utils/fileIdSafety";
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
|
||||||
<AppProviders>
|
|
||||||
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<DefaultAppBanner />
|
|
||||||
<div
|
|
||||||
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
|
||||||
className="desktop-app-content"
|
|
||||||
>
|
|
||||||
<style>{`
|
|
||||||
.desktop-app-content .h-screen {
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
<Routes>
|
|
||||||
{/* Auth routes - no nested providers needed */}
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
<Route path="/signup" element={<Signup />} />
|
|
||||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
|
||||||
<Route path="/invite/:token" element={<InviteAccept />} />
|
|
||||||
|
|
||||||
{/* Main app routes - Landing handles auth logic */}
|
|
||||||
<Route path="/*" element={<Landing />} />
|
|
||||||
</Routes>
|
|
||||||
<OnboardingTour />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AppProviders>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
|
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
|
||||||
import { DesktopConfigSync } from '@app/components/DesktopConfigSync';
|
import { DesktopConfigSync } from '@app/components/DesktopConfigSync';
|
||||||
|
import { DesktopBannerInitializer } from '@app/components/DesktopBannerInitializer';
|
||||||
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
|
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,6 +23,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DesktopConfigSync />
|
<DesktopConfigSync />
|
||||||
|
<DesktopBannerInitializer />
|
||||||
{children}
|
{children}
|
||||||
</ProprietaryAppProviders>
|
</ProprietaryAppProviders>
|
||||||
);
|
);
|
||||||
|
|||||||
13
frontend/src/desktop/components/DesktopBannerInitializer.tsx
Normal file
13
frontend/src/desktop/components/DesktopBannerInitializer.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useBanner } from '@app/contexts/BannerContext';
|
||||||
|
import { DefaultAppBanner } from '@app/components/shared/DefaultAppBanner';
|
||||||
|
|
||||||
|
export function DesktopBannerInitializer() {
|
||||||
|
const { setBanner } = useBanner();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBanner(<DefaultAppBanner />);
|
||||||
|
}, [setBanner]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Paper, Group, Text, Button, ActionIcon } from '@mantine/core';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
import { InfoBanner } from '@app/components/shared/InfoBanner';
|
||||||
import { defaultAppService } from '@app/services/defaultAppService';
|
import { defaultAppService } from '@app/services/defaultAppService';
|
||||||
import { alert } from '@app/components/toast';
|
import { alert } from '@app/components/toast';
|
||||||
|
|
||||||
@ -64,47 +63,16 @@ export const DefaultAppBanner: React.FC = () => {
|
|||||||
localStorage.setItem(PROMPT_DISMISSED_KEY, 'true');
|
localStorage.setItem(PROMPT_DISMISSED_KEY, 'true');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (promptDismissed || isDefault !== false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<InfoBanner
|
||||||
p="sm"
|
icon="picture-as-pdf-rounded"
|
||||||
radius={0}
|
message={t('defaultApp.prompt.message', 'Make Stirling PDF your default application for opening PDF files.')}
|
||||||
style={{
|
buttonText={t('defaultApp.setDefault', 'Set Default')}
|
||||||
background: 'var(--mantine-color-blue-0)',
|
buttonIcon="check-circle-rounded"
|
||||||
borderBottom: '1px solid var(--mantine-color-blue-2)',
|
onButtonClick={handleSetDefault}
|
||||||
position: 'relative',
|
onDismiss={handleDismissPrompt}
|
||||||
}}
|
loading={isLoading}
|
||||||
>
|
show={!promptDismissed && isDefault === false}
|
||||||
<Group gap="sm" align="center" wrap="nowrap">
|
/>
|
||||||
<LocalIcon icon="picture-as-pdf-rounded" width="1.2rem" height="1.2rem" style={{ color: 'var(--mantine-color-blue-6)', flexShrink: 0 }} />
|
|
||||||
<Text fw={500} size="sm" style={{ color: 'var(--mantine-color-blue-9)' }}>
|
|
||||||
{t('defaultApp.prompt.message', 'Make Stirling PDF your default application for opening PDF files.')}
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
color="blue"
|
|
||||||
size="xs"
|
|
||||||
onClick={handleSetDefault}
|
|
||||||
loading={isLoading}
|
|
||||||
leftSection={<LocalIcon icon="check-circle-rounded" width="0.9rem" height="0.9rem" />}
|
|
||||||
style={{ flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
{t('defaultApp.setDefault', 'Set Default')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleDismissPrompt}
|
|
||||||
aria-label={t('defaultApp.dismiss', 'Dismiss')}
|
|
||||||
style={{ position: 'absolute', top: '0.5rem', right: '0.5rem' }}
|
|
||||||
>
|
|
||||||
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Paper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
import { AppProviders } from "@app/components/AppProviders";
|
import { AppProviders } from "@app/components/AppProviders";
|
||||||
|
import { AppLayout } from "@app/components/AppLayout";
|
||||||
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
||||||
import Landing from "@app/routes/Landing";
|
import Landing from "@app/routes/Landing";
|
||||||
import Login from "@app/routes/Login";
|
import Login from "@app/routes/Login";
|
||||||
@ -22,17 +23,19 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<AppProviders>
|
<AppProviders>
|
||||||
<Routes>
|
<AppLayout>
|
||||||
{/* Auth routes - no nested providers needed */}
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
{/* Auth routes - no nested providers needed */}
|
||||||
<Route path="/signup" element={<Signup />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
<Route path="/signup" element={<Signup />} />
|
||||||
<Route path="/invite/:token" element={<InviteAccept />} />
|
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||||
|
<Route path="/invite/:token" element={<InviteAccept />} />
|
||||||
|
|
||||||
{/* Main app routes - Landing handles auth logic */}
|
{/* Main app routes - Landing handles auth logic */}
|
||||||
<Route path="/*" element={<Landing />} />
|
<Route path="/*" element={<Landing />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<OnboardingTour />
|
<OnboardingTour />
|
||||||
|
</AppLayout>
|
||||||
</AppProviders>
|
</AppProviders>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user