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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 710 additions and 60 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,178 @@
/* Light theme variables */
:root {
--cc-bg: #ffffff;
--cc-primary-color: #1c1c1c;
--cc-secondary-color: #666666;
--cc-btn-primary-bg: #007BFF;
--cc-btn-primary-color: #ffffff;
--cc-btn-primary-border-color: #007BFF;
--cc-btn-primary-hover-bg: #0056b3;
--cc-btn-primary-hover-color: #ffffff;
--cc-btn-primary-hover-border-color: #0056b3;
--cc-btn-secondary-bg: #f1f3f4;
--cc-btn-secondary-color: #1c1c1c;
--cc-btn-secondary-border-color: #f1f3f4;
--cc-btn-secondary-hover-bg: #007BFF;
--cc-btn-secondary-hover-color: #ffffff;
--cc-btn-secondary-hover-border-color: #007BFF;
--cc-separator-border-color: #e0e0e0;
--cc-toggle-on-bg: #007BFF;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #ffffff;
--cc-toggle-off-knob-bg: #ffffff;
--cc-toggle-enabled-icon-color: #ffffff;
--cc-toggle-disabled-icon-color: #ffffff;
--cc-toggle-readonly-bg: #f1f3f4;
--cc-toggle-readonly-knob-bg: #79747E;
--cc-toggle-readonly-knob-icon-color: #f1f3f4;
--cc-section-category-border: #e0e0e0;
--cc-cookie-category-block-bg: #f1f3f4;
--cc-cookie-category-block-border: #f1f3f4;
--cc-cookie-category-block-hover-bg: #e9eff4;
--cc-cookie-category-block-hover-border: #e9eff4;
--cc-cookie-category-expanded-block-bg: #f1f3f4;
--cc-cookie-category-expanded-block-hover-bg: #e9eff4;
--cc-footer-bg: #ffffff;
--cc-footer-color: #1c1c1c;
--cc-footer-border-color: #ffffff;
}
/* Dark theme variables */
.cc--darkmode{
--cc-bg: #2d2d2d;
--cc-primary-color: #e5e5e5;
--cc-secondary-color: #b0b0b0;
--cc-btn-primary-bg: #4dabf7;
--cc-btn-primary-color: #2d2d2d;
--cc-btn-primary-border-color: #4dabf7;
--cc-btn-primary-hover-bg: #3d3d3d;
--cc-btn-primary-hover-color: #e5e5e5;
--cc-btn-primary-hover-border-color: #3d3d3d;
--cc-btn-secondary-bg: #3d3d3d;
--cc-btn-secondary-color: #e5e5e5;
--cc-btn-secondary-border-color: #3d3d3d;
--cc-btn-secondary-hover-bg: #4dabf7;
--cc-btn-secondary-hover-color: #2d2d2d;
--cc-btn-secondary-hover-border-color: #4dabf7;
--cc-separator-border-color: #555555;
--cc-toggle-on-bg: #4dabf7;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #2d2d2d;
--cc-toggle-off-knob-bg: #2d2d2d;
--cc-toggle-enabled-icon-color: #2d2d2d;
--cc-toggle-disabled-icon-color: #2d2d2d;
--cc-toggle-readonly-bg: #555555;
--cc-toggle-readonly-knob-bg: #8e8e8e;
--cc-toggle-readonly-knob-icon-color: #555555;
--cc-section-category-border: #555555;
--cc-cookie-category-block-bg: #3d3d3d;
--cc-cookie-category-block-border: #3d3d3d;
--cc-cookie-category-block-hover-bg: #4d4d4d;
--cc-cookie-category-block-hover-border: #4d4d4d;
--cc-cookie-category-expanded-block-bg: #3d3d3d;
--cc-cookie-category-expanded-block-hover-bg: #4d4d4d;
--cc-footer-bg: #2d2d2d;
--cc-footer-color: #e5e5e5;
--cc-footer-border-color: #2d2d2d;
}
.cm__body{
max-width: 90% !important;
flex-direction: row !important;
align-items: center !important;
}
.cm__desc{
max-width: 70rem !important;
}
.cm__btns{
flex-direction: row-reverse !important;
gap:10px !important;
padding-top: 3.4rem !important;
}
@media only screen and (max-width: 1400px) {
.cm__body{
max-width: 90% !important;
flex-direction: column !important;
align-items: normal !important;
}
.cm__btns{
padding-top: 1rem !important;
}
}
/* Toggle visibility fixes */
#cc-main .section__toggle {
opacity: 0 !important; /* Keep invisible but functional */
}
#cc-main .toggle__icon {
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
}
#cc-main .toggle__icon-circle {
display: block !important;
position: absolute !important;
transition: transform 0.25s ease !important;
}
#cc-main .toggle__icon-on,
#cc-main .toggle__icon-off {
display: flex !important;
align-items: center !important;
justify-content: center !important;
position: absolute !important;
width: 100% !important;
height: 100% !important;
}
/* Ensure toggles are visible in both themes */
#cc-main .toggle__icon {
background: var(--cc-toggle-off-bg) !important;
border: 1px solid var(--cc-toggle-off-bg) !important;
}
#cc-main .section__toggle:checked ~ .toggle__icon {
background: var(--cc-toggle-on-bg) !important;
border: 1px solid var(--cc-toggle-on-bg) !important;
}
/* Ensure toggle text is visible */
#cc-main .pm__section-title {
color: var(--cc-primary-color) !important;
}
#cc-main .pm__section-desc {
color: var(--cc-secondary-color) !important;
}
/* Make sure the modal has proper contrast */
#cc-main .pm {
background: var(--cc-bg) !important;
color: var(--cc-primary-color) !important;
}

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,7 @@ import HomePage from "./pages/HomePage";
// Import global styles
import "./styles/tailwind.css";
import "./styles/cookieconsent.css";
import "./index.css";
import { RightRailProvider } from "./contexts/RightRailContext";
@ -40,7 +41,7 @@ export default function App() {
<ToolWorkflowProvider>
<SidebarProvider>
<RightRailProvider>
<HomePage />
<HomePage />
</RightRailProvider>
</SidebarProvider>
</ToolWorkflowProvider>

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",

View File

@ -0,0 +1,230 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
declare global {
interface Window {
CookieConsent: {
run: (config: any) => void;
show: (show?: boolean) => void;
};
}
}
interface CookieConsentConfig {
analyticsEnabled?: boolean;
}
export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConfig = {}) => {
const { t } = useTranslation();
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
if (!analyticsEnabled) {
console.log('Cookie consent not enabled - analyticsEnabled is false');
return;
}
// Prevent double initialization
if (window.CookieConsent) {
setIsInitialized(true);
// Force show the modal if it exists but isn't visible
setTimeout(() => {
window.CookieConsent.show();
}, 100);
return;
}
// Load the cookie consent CSS files first
const mainCSS = document.createElement('link');
mainCSS.rel = 'stylesheet';
mainCSS.href = '/css/cookieconsent.css';
document.head.appendChild(mainCSS);
const customCSS = document.createElement('link');
customCSS.rel = 'stylesheet';
customCSS.href = '/css/cookieconsentCustomisation.css';
document.head.appendChild(customCSS);
// Load the cookie consent library
const script = document.createElement('script');
script.src = '/js/thirdParty/cookieconsent.umd.js';
script.onload = () => {
// Small delay to ensure DOM is ready
setTimeout(() => {
// Detect current theme and set appropriate mode
const detectTheme = () => {
const mantineScheme = document.documentElement.getAttribute('data-mantine-color-scheme');
const hasLightClass = document.documentElement.classList.contains('light');
const hasDarkClass = document.documentElement.classList.contains('dark');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Priority: Mantine attribute > CSS classes > system preference
let isDarkMode = false;
if (mantineScheme) {
isDarkMode = mantineScheme === 'dark';
} else if (hasLightClass) {
isDarkMode = false;
} else if (hasDarkClass) {
isDarkMode = true;
} else {
isDarkMode = systemPrefersDark;
}
// Always explicitly set or remove the class
document.documentElement.classList.toggle('cc--darkmode', isDarkMode);
return isDarkMode;
};
// Initial theme detection with slight delay to ensure DOM is ready
setTimeout(() => {
detectTheme();
}, 50);
// Check if CookieConsent is available
if (!window.CookieConsent) {
console.error('CookieConsent is not available on window object');
return;
}
// Listen for theme changes
const themeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'data-mantine-color-scheme' ||
mutation.attributeName === 'class')) {
detectTheme();
}
});
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-mantine-color-scheme', 'class']
});
// Initialize cookie consent with full configuration
try {
window.CookieConsent.run({
autoShow: true,
hideFromBots: false,
guiOptions: {
consentModal: {
layout: "bar",
position: "bottom",
equalWeightButtons: true,
flipButtons: true
},
preferencesModal: {
layout: "box",
position: "right",
equalWeightButtons: true,
flipButtons: true
}
},
categories: {
necessary: {
readOnly: true
},
analytics: {}
},
language: {
default: "en",
translations: {
en: {
consentModal: {
title: t('cookieBanner.popUp.title', 'How we use Cookies'),
description: t('cookieBanner.popUp.description.1', 'We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you\'ll love.') +
"<br>" +
t('cookieBanner.popUp.description.2', 'If you\'d rather not, clicking \'No Thanks\' will only enable the essential cookies needed to keep things running smoothly.'),
acceptAllBtn: t('cookieBanner.popUp.acceptAllBtn', 'Okay'),
acceptNecessaryBtn: t('cookieBanner.popUp.acceptNecessaryBtn', 'No Thanks'),
showPreferencesBtn: t('cookieBanner.popUp.showPreferencesBtn', 'Manage preferences'),
},
preferencesModal: {
title: t('cookieBanner.preferencesModal.title', 'Consent Preferences Center'),
acceptAllBtn: t('cookieBanner.preferencesModal.acceptAllBtn', 'Accept all'),
acceptNecessaryBtn: t('cookieBanner.preferencesModal.acceptNecessaryBtn', 'Reject all'),
savePreferencesBtn: t('cookieBanner.preferencesModal.savePreferencesBtn', 'Save preferences'),
closeIconLabel: t('cookieBanner.preferencesModal.closeIconLabel', 'Close modal'),
serviceCounterLabel: t('cookieBanner.preferencesModal.serviceCounterLabel', 'Service|Services'),
sections: [
{
title: t('cookieBanner.preferencesModal.subtitle', 'Cookie Usage'),
description: t('cookieBanner.preferencesModal.description.1', 'Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users.') +
"<br><br>" +
t('cookieBanner.preferencesModal.description.2', 'Stirling PDF cannot—and will never—track or access the content of the documents you use.') +
"<b> " +
t('cookieBanner.preferencesModal.description.3', 'Your privacy and trust are at the core of what we do.') +
"</b>"
},
{
title: t('cookieBanner.preferencesModal.necessary.title.1', 'Strictly Necessary Cookies') +
"<span class=\"pm__badge\">" +
t('cookieBanner.preferencesModal.necessary.title.2', 'Always Enabled') +
"</span>",
description: t('cookieBanner.preferencesModal.necessary.description', 'These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can\'t be turned off.'),
linkedCategory: "necessary"
},
{
title: t('cookieBanner.preferencesModal.analytics.title', 'Analytics'),
description: t('cookieBanner.preferencesModal.analytics.description', 'These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.'),
linkedCategory: "analytics"
}
]
}
}
}
}
});
// Force show after initialization
setTimeout(() => {
window.CookieConsent.show();
// Debug: Check if modal elements exist
const ccMain = document.getElementById('cc-main');
const consentModal = document.querySelector('.cm-wrapper');
}, 200);
} catch (error) {
console.error('Error initializing CookieConsent:', error);
}
setIsInitialized(true);
}, 100); // Small delay to ensure DOM is ready
};
script.onerror = () => {
console.error('Failed to load cookie consent library');
};
document.head.appendChild(script);
return () => {
// Cleanup script and CSS when component unmounts
if (document.head.contains(script)) {
document.head.removeChild(script);
}
if (document.head.contains(mainCSS)) {
document.head.removeChild(mainCSS);
}
if (document.head.contains(customCSS)) {
document.head.removeChild(customCSS);
}
};
}, [analyticsEnabled, t]);
const showCookiePreferences = () => {
if (isInitialized && window.CookieConsent) {
window.CookieConsent.show(true);
}
};
return {
showCookiePreferences
};
};

View File

@ -1,5 +1,11 @@
body {
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow-x: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
@ -11,3 +17,38 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* CSS Variables */
:root {
--footer-height: 2rem;
}
/* Footer link styling - make buttons and links look identical */
.footer-link {
color: var(--mantine-color-gray-6);
text-decoration: none;
transition: color 0.2s ease;
border: none;
background: none;
cursor: pointer;
font-family: inherit;
font-size: inherit;
padding: 0;
}
.footer-link:hover {
color: var(--mantine-color-blue-8);
text-decoration: underline;
}
.stirling-link {
color: var(--mantine-color-blue-6);
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.stirling-link:hover {
color: var(--mantine-color-blue-8);
text-decoration: underline;
}

View File

@ -7,6 +7,7 @@ import { ColorSchemeScript } from '@mantine/core';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './i18n'; // Initialize i18next
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
// Compute initial color scheme
@ -21,22 +22,39 @@ function getInitialScheme(): 'light' | 'dark' {
}
}
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2025-05-24',
capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
debug: false,
opt_out_capturing_by_default: false, // We handle opt-out via cookie consent
});
function updatePosthogConsent(){
if(typeof(posthog) == "undefined") {
return;
}
const optIn = (window.CookieConsent as any).acceptedCategory('analytics');
optIn?
posthog.opt_in_capturing() : posthog.opt_out_capturing();
console.log("Updated analytics consent: ", optIn? "opted in" : "opted out");
}
window.addEventListener("cc:onConsent", updatePosthogConsent);
window.addEventListener("cc:onChange", updatePosthogConsent);
const container = document.getElementById('root');
if (!container) {
throw new Error("Root container missing in index.html");
}
const root = ReactDOM.createRoot(container); // Finds the root DOM element
root.render(
<React.StrictMode>
<ColorSchemeScript defaultColorScheme={getInitialScheme()} />
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2025-05-24',
capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
debug: import.meta.env.MODE === 'development',
}}
client={posthog}
>
<BrowserRouter>
<App />

View File

@ -11,6 +11,7 @@ import Workbench from "../components/layout/Workbench";
import QuickAccessBar from "../components/shared/QuickAccessBar";
import RightRail from "../components/shared/RightRail";
import FileManager from "../components/FileManager";
import Footer from "../components/shared/Footer";
export default function HomePage() {
@ -38,17 +39,20 @@ export default function HomePage() {
// Note: File selection limits are now handled directly by individual tools
return (
<Group
align="flex-start"
gap={0}
className="min-h-screen w-screen overflow-hidden flex-nowrap flex"
>
<QuickAccessBar
ref={quickAccessRef} />
<ToolPanel />
<Workbench />
<RightRail />
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</Group>
<div className="h-screen overflow-hidden">
<Group
align="flex-start"
gap={0}
h="100%"
className="flex-nowrap flex"
>
<QuickAccessBar
ref={quickAccessRef} />
<ToolPanel />
<Workbench />
<RightRail />
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</Group>
</div>
);
}
}

View File

@ -0,0 +1,59 @@
/* Cookie Consent Modal Styling - Ensure proper z-index */
/* Ensure cookie consent appears above everything */
#cc-main {
z-index: 999999 !important;
}
/* Additional styling if needed */
.cm-wrapper,
.pm-wrapper {
z-index: 999999 !important;
}
/* Dark mode styling */
.cc--darkmode .cm {
background: #2d2d2d !important;
color: #ffffff !important;
border-top: 1px solid #444 !important;
}
.cc--darkmode .pm {
background: #2d2d2d !important;
color: #ffffff !important;
}
.cc--darkmode .pm-overlay {
background: rgba(0, 0, 0, 0.7) !important;
}
/* Button styling */
.cc--darkmode .cm__btn {
background: #444 !important;
color: #ffffff !important;
border: 1px solid #666 !important;
}
.cc--darkmode .cm__btn:hover {
background: #555 !important;
}
.cc--darkmode .pm__btn {
background: #444 !important;
color: #ffffff !important;
border: 1px solid #666 !important;
}
.cc--darkmode .pm__btn:hover {
background: #555 !important;
}
/* Ensure ScrollArea doesn't interfere */
.mantine-ScrollArea-root {
position: relative !important;
}
/* Override any potential conflicts */
[data-mantine-color-scheme="dark"] #cc-main {
color: #ffffff !important;
}