mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-03 17:52:30 +02:00
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:
parent
62c929b89b
commit
eecc410b77
1
frontend/public/css/cookieconsent.css
Normal file
1
frontend/public/css/cookieconsent.css
Normal file
File diff suppressed because one or more lines are too long
178
frontend/public/css/cookieconsentCustomisation.css
Normal file
178
frontend/public/css/cookieconsentCustomisation.css
Normal 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;
|
||||
}
|
7
frontend/public/js/thirdParty/cookieconsent.umd.js
vendored
Normal file
7
frontend/public/js/thirdParty/cookieconsent.umd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
@ -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>
|
||||
|
21
frontend/src/components/layout/Workbench.css
Normal file
21
frontend/src/components/layout/Workbench.css
Normal 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);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
}}
|
||||
>
|
||||
|
||||
|
90
frontend/src/components/shared/Footer.tsx
Normal file
90
frontend/src/components/shared/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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%',
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -91,7 +91,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
||||
|
||||
return (
|
||||
<Box
|
||||
h="100vh"
|
||||
h="100%"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
230
frontend/src/hooks/useCookieConsent.ts
Normal file
230
frontend/src/hooks/useCookieConsent.ts
Normal 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
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
59
frontend/src/styles/cookieconsent.css
Normal file
59
frontend/src/styles/cookieconsent.css
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user