mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 18:44:05 +02:00
Feature/onboarding slides (#4955)
# Description of Changes - Added onboarding slides/upgrade banner conditions for all the following cases - 'licensed' - 'no-login-user-under-limit-no-license' - 'no-login-admin-under-limit-no-license' - 'no-login-user-over-limit-no-license' - 'no-login-admin-over-limit-no-license' - 'login-user-under-limit-no-license' - 'login-admin-under-limit-no-license' - 'login-user-over-limit-no-license' - 'login-admin-over-limit-no-license'; --- ## 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. --------- Co-authored-by: Anthony Stirling <[email protected]> Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
Connor Yoh
parent
80f2980755
commit
a8db2fda18
@@ -1,4 +1,13 @@
|
||||
import { Modal, Stack, Button, Text, Title, Anchor } from '@mantine/core';
|
||||
import {
|
||||
Modal,
|
||||
Stack,
|
||||
Button,
|
||||
Text,
|
||||
Title,
|
||||
Anchor,
|
||||
useMantineTheme,
|
||||
useComputedColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import { Z_ANALYTICS_MODAL } from '@app/styles/zIndex';
|
||||
@@ -15,6 +24,21 @@ export default function AdminAnalyticsChoiceModal({ opened, onClose }: AdminAnal
|
||||
const { refetch } = useAppConfig();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const theme = useMantineTheme();
|
||||
const computedColorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true });
|
||||
const isDark = computedColorScheme === 'dark';
|
||||
const privacyHighlightStyles = {
|
||||
color: isDark ? '#FFFFFF' : theme.colors.blue[7],
|
||||
padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
|
||||
borderRadius: theme.radius.md,
|
||||
fontWeight: 700,
|
||||
textAlign: 'center' as const,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: theme.spacing.xs,
|
||||
letterSpacing: 0.3,
|
||||
};
|
||||
|
||||
const handleChoice = async (enableAnalytics: boolean) => {
|
||||
setLoading(true);
|
||||
@@ -60,7 +84,10 @@ export default function AdminAnalyticsChoiceModal({ opened, onClose }: AdminAnal
|
||||
<Title order={2}>{t('analytics.title', 'Do you want make Stirling PDF better?')}</Title>
|
||||
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('analytics.paragraph1', 'Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.')}
|
||||
{t('analytics.paragraph1', 'Stirling PDF has opt in analytics to help us improve the product.')}
|
||||
</Text>
|
||||
<Text size="sm" style={privacyHighlightStyles}>
|
||||
• {t('analytics.privacyAssurance', 'We do not track any personal information or the contents of your files.')} •
|
||||
</Text>
|
||||
|
||||
<Text size="sm" c="dimmed">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { Modal, Text, ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { Modal, Text, ActionIcon, Tooltip, Group } from '@mantine/core';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { createConfigNavSections } from '@app/components/shared/config/configNavSections';
|
||||
@@ -8,6 +8,7 @@ import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import '@app/components/shared/AppConfigModal.css';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE, Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { useLicenseAlert } from '@app/hooks/useLicenseAlert';
|
||||
|
||||
interface AppConfigModalProps {
|
||||
opened: boolean;
|
||||
@@ -20,6 +21,7 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { config } = useAppConfig();
|
||||
const licenseAlert = useLicenseAlert();
|
||||
|
||||
// Extract section from URL path (e.g., /settings/people -> people)
|
||||
const getSectionFromPath = (pathname: string): NavKey | null => {
|
||||
@@ -64,8 +66,8 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
headerBorder: 'var(--modal-header-border)',
|
||||
}), []);
|
||||
|
||||
// Get isAdmin, runningEE, and loginEnabled from app config
|
||||
const isAdmin = config?.isAdmin ?? false;
|
||||
// Get isAdmin and runningEE from app config
|
||||
const isAdmin = true // config?.isAdmin ?? false;
|
||||
const runningEE = config?.runningEE ?? false;
|
||||
const loginEnabled = config?.enableLogin ?? false;
|
||||
|
||||
@@ -138,6 +140,10 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
const isDisabled = item.disabled ?? false;
|
||||
const color = isActive ? colors.navItemActive : colors.navItem;
|
||||
const iconSize = isMobile ? 28 : 18;
|
||||
const showPlanWarning =
|
||||
item.key === 'adminPlan' &&
|
||||
licenseAlert.active &&
|
||||
licenseAlert.audience === 'admin';
|
||||
|
||||
const navItemContent = (
|
||||
<div
|
||||
@@ -157,9 +163,19 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
>
|
||||
<LocalIcon icon={item.icon} width={iconSize} height={iconSize} style={{ color }} />
|
||||
{!isMobile && (
|
||||
<Text size="sm" fw={500} style={{ color }}>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Group gap={4} align="center" wrap="nowrap">
|
||||
<Text size="sm" fw={500} style={{ color }}>
|
||||
{item.label}
|
||||
</Text>
|
||||
{showPlanWarning && (
|
||||
<LocalIcon
|
||||
icon="warning-rounded"
|
||||
width={14}
|
||||
height={14}
|
||||
style={{ color: 'var(--mantine-color-orange-7)' }}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flex } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCookieConsent } from '@app/hooks/useCookieConsent';
|
||||
import { useCookieConsentContext } from '@app/contexts/CookieConsentContext';
|
||||
|
||||
interface FooterProps {
|
||||
privacyPolicy?: string;
|
||||
@@ -20,7 +20,7 @@ export default function Footer({
|
||||
analyticsEnabled = false
|
||||
}: FooterProps) {
|
||||
const { t } = useTranslation();
|
||||
const { showCookiePreferences } = useCookieConsent({ analyticsEnabled });
|
||||
const { showCookiePreferences } = useCookieConsentContext();
|
||||
|
||||
// Helper to check if a value is valid (not null/undefined/empty string)
|
||||
const isValidLink = (link?: string) => link && link.trim().length > 0;
|
||||
|
||||
@@ -1,16 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Paper, Group, Text, Button, ActionIcon } from '@mantine/core';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Paper, Group, Text, Button, ActionIcon, Stack } from '@mantine/core';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
|
||||
type InfoBannerTone = 'info' | 'warning';
|
||||
|
||||
const toneStyles: Record<
|
||||
InfoBannerTone,
|
||||
{
|
||||
background: string;
|
||||
border: string;
|
||||
text: string;
|
||||
icon: string;
|
||||
buttonColor: string;
|
||||
}
|
||||
> = {
|
||||
info: {
|
||||
background: 'var(--mantine-color-blue-0)',
|
||||
border: 'var(--mantine-color-blue-2)',
|
||||
text: 'var(--mantine-color-blue-9)',
|
||||
icon: 'var(--mantine-color-blue-6)',
|
||||
buttonColor: 'blue',
|
||||
},
|
||||
warning: {
|
||||
background: 'var(--mantine-color-orange-0)',
|
||||
border: 'var(--mantine-color-orange-3)',
|
||||
text: 'var(--mantine-color-orange-9)',
|
||||
icon: 'var(--mantine-color-orange-7)',
|
||||
buttonColor: 'orange',
|
||||
},
|
||||
};
|
||||
|
||||
interface InfoBannerProps {
|
||||
icon: string;
|
||||
message: string;
|
||||
buttonText: string;
|
||||
title?: ReactNode;
|
||||
message: ReactNode;
|
||||
buttonText?: string;
|
||||
buttonIcon?: string;
|
||||
onButtonClick: () => void;
|
||||
onDismiss: () => void;
|
||||
onButtonClick?: () => void;
|
||||
onDismiss?: () => void;
|
||||
dismissible?: boolean;
|
||||
loading?: boolean;
|
||||
show?: boolean;
|
||||
tone?: InfoBannerTone;
|
||||
background?: string;
|
||||
borderColor?: string;
|
||||
textColor?: string;
|
||||
iconColor?: string;
|
||||
buttonColor?: string;
|
||||
buttonVariant?: 'light' | 'filled' | 'white' | 'outline' | 'subtle';
|
||||
minHeight?: number | string;
|
||||
closeIconColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,55 +57,97 @@ interface InfoBannerProps {
|
||||
*/
|
||||
export const InfoBanner: React.FC<InfoBannerProps> = ({
|
||||
icon,
|
||||
title,
|
||||
message,
|
||||
buttonText,
|
||||
buttonIcon = 'check-circle-rounded',
|
||||
onButtonClick,
|
||||
onDismiss,
|
||||
dismissible = true,
|
||||
loading = false,
|
||||
show = true,
|
||||
tone = 'info',
|
||||
background,
|
||||
borderColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
buttonColor,
|
||||
buttonVariant = 'light',
|
||||
minHeight = 56,
|
||||
closeIconColor,
|
||||
}) => {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toneStyle = toneStyles[tone] ?? toneStyles.info;
|
||||
const handleDismiss = () => {
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
p="sm"
|
||||
radius={0}
|
||||
style={{
|
||||
background: 'var(--mantine-color-blue-0)',
|
||||
borderBottom: '1px solid var(--mantine-color-blue-2)',
|
||||
position: 'relative',
|
||||
background: background ?? toneStyle.background,
|
||||
borderBottom: `1px solid ${borderColor ?? toneStyle.border}`,
|
||||
minHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<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 gap="sm" align="center" wrap="nowrap" justify="space-between" style={{ width: '100%' }}>
|
||||
<Group gap="sm" align="center" wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
|
||||
<LocalIcon
|
||||
icon={icon}
|
||||
width="1.2rem"
|
||||
height="1.2rem"
|
||||
style={{ color: iconColor ?? toneStyle.icon, flexShrink: 0 }}
|
||||
/>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
{title && (
|
||||
<Text fw={600} size="sm" style={{ color: textColor ?? toneStyle.text }}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
fw={title ? 400 : 500}
|
||||
size="sm"
|
||||
style={{ color: textColor ?? toneStyle.text }}
|
||||
lineClamp={2}
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group gap="xs" align="center" wrap="nowrap">
|
||||
{buttonText && onButtonClick && (
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
color={buttonColor ?? toneStyle.buttonColor}
|
||||
size="xs"
|
||||
onClick={onButtonClick}
|
||||
loading={loading}
|
||||
leftSection={<LocalIcon icon={buttonIcon} width="0.9rem" height="0.9rem" />}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
{dismissible && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color={closeIconColor ? undefined : 'gray'}
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss"
|
||||
style={closeIconColor ? { color: closeIconColor } : undefined}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, forwardRef, useEffect } from "react";
|
||||
import { ActionIcon, Stack, Divider, Menu } from "@mantine/core";
|
||||
import { ActionIcon, Stack, Divider, Menu, Indicator } from "@mantine/core";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
@@ -16,6 +16,7 @@ import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolBu
|
||||
import AppConfigModal from '@app/components/shared/AppConfigModal';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { useOnboarding } from '@app/contexts/OnboardingContext';
|
||||
import { useLicenseAlert } from "@app/hooks/useLicenseAlert";
|
||||
|
||||
import {
|
||||
isNavButtonActive,
|
||||
@@ -34,6 +35,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const { getToolNavigation } = useSidebarNavigation();
|
||||
const { config } = useAppConfig();
|
||||
const { startTour } = useOnboarding();
|
||||
const licenseAlert = useLicenseAlert();
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const [activeButton, setActiveButton] = useState<string>('tools');
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
@@ -314,9 +316,27 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
);
|
||||
}
|
||||
|
||||
const buttonNode = renderNavButton(buttonConfig, index);
|
||||
const shouldShowSettingsBadge =
|
||||
buttonConfig.id === 'config' &&
|
||||
licenseAlert.active &&
|
||||
licenseAlert.audience === 'admin';
|
||||
|
||||
return (
|
||||
<React.Fragment key={buttonConfig.id}>
|
||||
{renderNavButton(buttonConfig, index)}
|
||||
{shouldShowSettingsBadge ? (
|
||||
<Indicator
|
||||
inline
|
||||
size={12}
|
||||
color="orange"
|
||||
position="top-end"
|
||||
offset={4}
|
||||
>
|
||||
{buttonNode}
|
||||
</Indicator>
|
||||
) : (
|
||||
buttonNode
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user