Basic footer structure and Cookie Consent (#4320)

* Added footer with blank links to be filled 
* Cookie consent to match V1 
* Made scrolling work on tool search results
* Made scrolling the same on tool search, tool picker and workbench 
* Cleaned up height variables, view height only used at workbench level 
<img width="1525" height="1270"
alt="{F3C1B15F-A4BE-4DF0-A5A8-92D2A3B14443}"
src="https://github.com/user-attachments/assets/0c23fe35-9973-45c0-85af-0002c5ff58d2"
/>
<img width="1511" height="1262"
alt="{4DDD51C0-4BC5-4E9F-A4F2-E5F49AF5F5FD}"
src="https://github.com/user-attachments/assets/2596d980-0312-4cd7-ad34-9fd3a8d1869e"
/>

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
ConnorYoh
2025-08-29 14:01:46 +01:00
committed by GitHub
parent 62c929b89b
commit eecc410b77
21 changed files with 710 additions and 60 deletions

View File

@@ -454,7 +454,6 @@ const FileEditor = ({
multiple={true}
maxSize={2 * 1024 * 1024 * 1024}
style={{
height: '100vh',
border: 'none',
borderRadius: 0,
backgroundColor: 'transparent'
@@ -462,7 +461,7 @@ const FileEditor = ({
activateOnClick={false}
activateOnDrag={true}
>
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
<Box pos="relative" style={{ overflow: 'auto' }}>
<LoadingOverlay visible={false} />
<Box p="md" pt="xl">
@@ -563,7 +562,7 @@ const FileEditor = ({
color="blue"
mt="md"
onClose={() => setStatus(null)}
style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 10001 }}
style={{ position: 'fixed', bottom: 40, right: 80, zIndex: 10001 }}
>
{status}
</Notification>

View File

@@ -0,0 +1,21 @@
.workbench-scrollable {
overflow-y: auto !important;
overflow-x: hidden !important;
}
.workbench-scrollable::-webkit-scrollbar {
width: 0.375rem;
}
.workbench-scrollable::-webkit-scrollbar-track {
background: transparent;
}
.workbench-scrollable::-webkit-scrollbar-thumb {
background-color: var(--mantine-color-gray-4);
border-radius: 0.1875rem;
}
.workbench-scrollable::-webkit-scrollbar-thumb:hover {
background-color: var(--mantine-color-gray-5);
}

View File

@@ -4,9 +4,10 @@ import { useTranslation } from 'react-i18next';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useFileHandler } from '../../hooks/useFileHandler';
import { useFileState, useFileActions } from '../../contexts/FileContext';
import { useFileState } from '../../contexts/FileContext';
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
import { useToolManagement } from '../../hooks/useToolManagement';
import './Workbench.css';
import TopControls from '../shared/TopControls';
import FileEditor from '../fileEditor/FileEditor';
@@ -14,6 +15,7 @@ import PageEditor from '../pageEditor/PageEditor';
import PageEditorControls from '../pageEditor/PageEditorControls';
import Viewer from '../viewer/Viewer';
import LandingPage from '../shared/LandingPage';
import Footer from '../shared/Footer';
// No props needed - component uses contexts directly
export default function Workbench() {
@@ -22,7 +24,6 @@ export default function Workbench() {
// Use context-based hooks to eliminate all prop drilling
const { state } = useFileState();
const { actions } = useFileActions();
const { workbench: currentView } = useNavigationState();
const { actions: navActions } = useNavigationActions();
const setCurrentView = navActions.setWorkbench;
@@ -37,10 +38,10 @@ export default function Workbench() {
} = useToolWorkflow();
const { handleToolSelect } = useToolWorkflow();
// Get navigation state - this is the source of truth
const { selectedTool: selectedToolId } = useNavigationState();
// Get tool registry to look up selected tool
const { toolRegistry } = useToolManagement();
const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null;
@@ -142,7 +143,7 @@ export default function Workbench() {
return (
<Box
className="flex-1 h-screen min-w-80 relative flex flex-col"
className="flex-1 h-full min-w-80 relative flex flex-col"
style={
isRainbowMode
? {} // No background color in rainbow mode
@@ -158,7 +159,7 @@ export default function Workbench() {
{/* Main content area */}
<Box
className="flex-1 min-h-0 relative z-10"
className="flex-1 min-h-0 relative z-10 workbench-scrollable "
style={{
transition: 'opacity 0.15s ease-in-out',
marginTop: '1rem',
@@ -166,6 +167,8 @@ export default function Workbench() {
>
{renderMainContent()}
</Box>
<Footer analyticsEnabled />
</Box>
);
}

View File

@@ -683,11 +683,11 @@ const PageEditor = ({
const displayedPages = displayDocument?.pages || [];
return (
<Box pos="relative" h="100vh" pt={40} style={{ overflow: 'auto' }} data-scrolling-container="true">
<Box pos="relative" h='100%' pt={40} style={{ overflow: 'auto' }} data-scrolling-container="true">
<LoadingOverlay visible={globalProcessing && !mergedPdfDocument} />
{!mergedPdfDocument && !globalProcessing && activeFileIds.length === 0 && (
<Center h="100vh">
<Center h='100%'>
<Stack align="center" gap="md">
<Text size="lg" c="dimmed">📄</Text>
<Text c="dimmed">No PDF files loaded</Text>

View File

@@ -39,7 +39,7 @@ interface PageEditorControlsProps {
selectionMode: boolean;
selectedPageIds: string[];
displayDocument?: { pages: { id: string; pageNumber: number }[] };
// Split state (for tooltip logic)
splitPositions?: Set<number>;
totalPages?: number;
@@ -70,40 +70,40 @@ const PageEditorControls = ({
if (!splitPositions || !totalPages || selectedPageIds.length === 0) {
return "Split Selected";
}
// Convert selected pages to split positions (same logic as handleSplit)
const selectedPageNumbers = displayDocument ? selectedPageIds.map(id => {
const page = displayDocument.pages.find(p => p.id === id);
return page?.pageNumber || 0;
}).filter(num => num > 0) : [];
const selectedSplitPositions = selectedPageNumbers.map(pageNum => pageNum - 1).filter(pos => pos < totalPages - 1);
if (selectedSplitPositions.length === 0) {
return "Split Selected";
}
// Smart toggle logic: follow the majority, default to adding splits if equal
const existingSplitsCount = selectedSplitPositions.filter(pos => splitPositions.has(pos)).length;
const noSplitsCount = selectedSplitPositions.length - existingSplitsCount;
// Remove splits only if majority already have splits
// If equal (50/50), default to adding splits
// If equal (50/50), default to adding splits
const willRemoveSplits = existingSplitsCount > noSplitsCount;
if (willRemoveSplits) {
return existingSplitsCount === selectedSplitPositions.length
? "Remove All Selected Splits"
return existingSplitsCount === selectedSplitPositions.length
? "Remove All Selected Splits"
: "Remove Selected Splits";
} else {
return existingSplitsCount === 0
? "Split Selected"
return existingSplitsCount === 0
? "Split Selected"
: "Complete Selected Splits";
}
};
// Calculate page break tooltip text
const getPageBreakTooltip = () => {
return selectedPageIds.length > 0
return selectedPageIds.length > 0
? `Insert ${selectedPageIds.length} Page Break${selectedPageIds.length > 1 ? 's' : ''}`
: "Insert Page Breaks";
};
@@ -141,7 +141,7 @@ const PageEditorControls = ({
flexWrap: 'wrap',
justifyContent: 'center',
padding: "1rem",
paddingBottom: "2rem"
paddingBottom: "1rem"
}}
>

View File

@@ -0,0 +1,90 @@
import { Flex } from '@mantine/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCookieConsent } from '../../hooks/useCookieConsent';
interface FooterProps {
privacyPolicy?: string;
termsAndConditions?: string;
accessibilityStatement?: string;
cookiePolicy?: string;
impressum?: string;
analyticsEnabled?: boolean;
}
export default function Footer({
privacyPolicy = '/privacy',
termsAndConditions = '/terms',
accessibilityStatement = 'accessibility',
analyticsEnabled = false
}: FooterProps) {
const { t } = useTranslation();
const { showCookiePreferences } = useCookieConsent({ analyticsEnabled });
return (
<div style={{
height: 'var(--footer-height)',
zIndex: 999999,
backgroundColor: 'var(--mantine-color-gray-1)',
borderTop: '1px solid var(--mantine-color-gray-2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}>
<Flex gap="md"
justify="center"
align="center"
direction="row"
style={{ fontSize: '0.75rem' }}>
<a
className="footer-link px-3"
id="survey"
target="_blank"
rel="noopener noreferrer"
href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu"
>
{t('survey.nav', 'Survey')}
</a>
{privacyPolicy && (
<a
className="footer-link px-3"
target="_blank"
rel="noopener noreferrer"
href={privacyPolicy}
>
{t('legal.privacy', 'Privacy Policy')}
</a>
)}
{termsAndConditions && (
<a
className="footer-link px-3"
target="_blank"
rel="noopener noreferrer"
href={termsAndConditions}
>
{t('legal.terms', 'Terms and Conditions')}
</a>
)}
{accessibilityStatement && (
<a
className="footer-link px-3"
target="_blank"
rel="noopener noreferrer"
href={accessibilityStatement}
>
{t('legal.accessibility', 'Accessibility')}
</a>
)}
{analyticsEnabled && (
<button
className="footer-link px-3"
id="cookieBanner"
onClick={showCookiePreferences}
>
{t('legal.showCookieBanner', 'Cookie Preferences')}
</button>
)}
</Flex>
</div>
);
}

View File

@@ -36,13 +36,13 @@ const LandingPage = () => {
};
return (
<Container size="70rem" p={0} h="102%" className="flex items-center justify-center" style={{ position: 'relative' }}>
<Container size="70rem" p={0} h="100%" className="flex items-center justify-center" style={{ position: 'relative' }}>
{/* White PDF Page Background */}
<Dropzone
onDrop={handleFileDrop}
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
multiple={true}
className="w-4/5 flex items-center justify-center h-[95vh]"
className="w-4/5 flex items-center justify-center h-[95%]"
style={{
position: 'absolute',
left: '50%',

View File

@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useToolSections } from '../../hooks/useToolSections';
import SubcategoryHeader from './shared/SubcategoryHeader';
import NoToolsFound from './shared/NoToolsFound';
import "./toolPicker/ToolPicker.css";
interface SearchResultsProps {
filteredTools: [string, ToolRegistryEntry][];
@@ -21,11 +22,12 @@ const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }
}
return (
<Stack p="sm" gap="xs">
<Stack p="sm" gap="xs"
className="tool-picker-scrollable">
{searchGroups.map(group => (
<Box key={group.subcategoryId} w="100%">
<Box key={group.subcategoryId} w="100%">
<SubcategoryHeader label={getSubcategoryLabel(t, group.subcategoryId)} />
<Stack gap="xs">
<Stack gap="xs">
{group.tools.map(({ id, tool }) => (
<ToolButton
key={id}

View File

@@ -72,17 +72,15 @@ export default function ToolPanel() {
{searchQuery.trim().length > 0 ? (
// Searching view (replaces both picker and content)
<div className="flex-1 flex flex-col">
<div className="flex-1 min-h-0">
<div className="flex-1 flex flex-col overflow-y-auto">
<SearchResults
filteredTools={filteredTools}
onSelect={handleToolSelect}
/>
</div>
</div>
) : leftPanelView === 'toolPicker' ? (
// Tool Picker View
<div className="flex-1 flex flex-col">
<div className="flex-1 flex flex-col overflow-auto">
<ToolPicker
selectedToolKey={selectedToolKey}
onSelect={handleToolSelect}

View File

@@ -91,7 +91,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
return (
<Box
h="100vh"
h="100%"
style={{
display: "flex",
flexDirection: "column",

View File

@@ -1,8 +1,6 @@
.tool-picker-scrollable {
overflow-y: auto !important;
overflow-x: hidden !important;
scrollbar-width: thin;
scrollbar-color: var(--mantine-color-gray-4) transparent;
}
.tool-picker-scrollable::-webkit-scrollbar {

View File

@@ -439,7 +439,7 @@ const Viewer = ({
}, [pageImages]);
return (
<Box style={{ position: 'relative', height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Box style={{ position: 'relative', height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* Close Button - Only show in preview mode */}
{onClose && previewFile && (
<ActionIcon
@@ -558,7 +558,7 @@ const Viewer = ({
radius="xl xl 0 0"
shadow="sm"
p={12}
pb={24}
pb={12}
style={{
display: "flex",
alignItems: "center",