UI/allow logo selection (#4982)

# Description of Changes

- Allow switching between logos in-app using the same section in
settings

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
EthanHealy01
2025-11-25 15:22:14 +00:00
committed by GitHub
parent ae5b1a4b02
commit a6614e1bfb
110 changed files with 651 additions and 189 deletions

View File

@@ -0,0 +1,95 @@
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
interface UnsavedChangesContextType {
isDirty: boolean;
setIsDirty: (dirty: boolean) => void;
/**
* Call this before navigating away or closing.
* Returns a promise that resolves to true if safe to proceed, false if blocked.
*/
confirmIfDirty: () => Promise<boolean>;
/**
* Reset dirty state (call after successful save)
*/
markClean: () => void;
}
const UnsavedChangesContext = createContext<UnsavedChangesContextType | undefined>(undefined);
interface UnsavedChangesProviderProps {
children: ReactNode;
}
export function UnsavedChangesProvider({ children }: UnsavedChangesProviderProps) {
const { t } = useTranslation();
const [isDirty, setIsDirty] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null);
const confirmIfDirty = useCallback((): Promise<boolean> => {
if (!isDirty) {
return Promise.resolve(true);
}
return new Promise((resolve) => {
setResolvePromise(() => resolve);
setModalOpen(true);
});
}, [isDirty]);
const markClean = useCallback(() => {
setIsDirty(false);
}, []);
const handleDiscard = () => {
setModalOpen(false);
setIsDirty(false);
resolvePromise?.(true);
setResolvePromise(null);
};
const handleCancel = () => {
setModalOpen(false);
resolvePromise?.(false);
setResolvePromise(null);
};
return (
<UnsavedChangesContext.Provider value={{ isDirty, setIsDirty, confirmIfDirty, markClean }}>
{children}
<Modal
opened={modalOpen}
onClose={handleCancel}
title={t('admin.settings.unsavedChanges.title', 'Unsaved Changes')}
centered
size="sm"
zIndex={1500}
>
<Stack gap="md">
<Text size="sm">
{t('admin.settings.unsavedChanges.message', 'You have unsaved changes. Do you want to discard them?')}
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={handleCancel}>
{t('admin.settings.unsavedChanges.cancel', 'Keep Editing')}
</Button>
<Button color="red" onClick={handleDiscard}>
{t('admin.settings.unsavedChanges.discard', 'Discard Changes')}
</Button>
</Group>
</Stack>
</Modal>
</UnsavedChangesContext.Provider>
);
}
export function useUnsavedChanges(): UnsavedChangesContextType {
const context = useContext(UnsavedChangesContext);
if (!context) {
throw new Error('useUnsavedChanges must be used within an UnsavedChangesProvider');
}
return context;
}