refactor: Extract utilities and fix Ant Design message context warnings
- Extract ad utilities to separate files for better code organization - Create adUtils.js with generateAdId, copy/refresh handlers - Create zoomUtils.js with useZoomState custom hook - Create tabUtils.js with buildTabsFromCategories function - Replace static message API with App.useApp() hook to fix context warnings - Implement factory pattern for utility functions with callback support - Improve code separation, testability and maintainability - Remove direct antd context dependencies from utility functions Components now follow single responsibility principle with clean separation between business logic and UI concerns.
This commit is contained in:
@@ -1,26 +1,16 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Tabs, Button, Spin, Skeleton, Result, Grid, Select, Layout, Tooltip, message } from 'antd';
|
import { Tabs, Button, Spin, Skeleton, Result, Grid, Select, Layout, App } from 'antd';
|
||||||
import { ArrowLeftOutlined, LoadingOutlined, LockOutlined, MinusOutlined, PlusOutlined, EllipsisOutlined, ShareAltOutlined, ReloadOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined, LoadingOutlined, LockOutlined, MinusOutlined, PlusOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||||
import UserMenu from '../components/UserMenu';
|
import UserMenu from '../components/UserMenu';
|
||||||
import FilePreview, { formatFileSize, formatDuration } from '../components/FilePreview';
|
import { useZoomState } from '../utils/zoomUtils';
|
||||||
|
import { scrollToAnchor, createCopyLinkHandler, createRefreshAdHandler } from '../utils/adUtils';
|
||||||
|
import { buildTabsFromCategories } from '../utils/tabUtils';
|
||||||
import debugLogger from '../utils/debugLogger';
|
import debugLogger from '../utils/debugLogger';
|
||||||
|
|
||||||
const backendUrl = process.env.REACT_APP_BACKEND || '';
|
const backendUrl = process.env.REACT_APP_BACKEND || '';
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
|
|
||||||
// Hash-Funktion für Ad-IDs
|
|
||||||
function generateAdId(adName, subcategoryName, categoryName) {
|
|
||||||
const combined = `${categoryName}-${subcategoryName}-${adName}`;
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < combined.length; i++) {
|
|
||||||
const char = combined.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + char;
|
|
||||||
hash = hash & hash; // 32-bit integer
|
|
||||||
}
|
|
||||||
return Math.abs(hash).toString(36); // Base36 für kürzere IDs
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overrideParams, ...props }) {
|
function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overrideParams, ...props }) {
|
||||||
// URL-Parameter holen, mit Override-Support für Smart Resolution
|
// URL-Parameter holen, mit Override-Support für Smart Resolution
|
||||||
const routeParams = useParams();
|
const routeParams = useParams();
|
||||||
@@ -46,70 +36,20 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
// Hole Projekt-Logo (und ggf. weitere Metadaten)
|
// Hole Projekt-Logo (und ggf. weitere Metadaten)
|
||||||
const [projectLogo, setProjectLogo] = useState(null);
|
const [projectLogo, setProjectLogo] = useState(null);
|
||||||
|
|
||||||
// Zoom-Logik (muss im Component-Scope stehen, nicht in useEffect!)
|
// Zoom-Logik mit Custom Hook
|
||||||
const ZOOM_LEVELS = [0.15, 0.25, 0.5, 0.75, 1];
|
const { zoom, handleZoom, ZOOM_LEVELS } = useZoomState();
|
||||||
const [zoom, setZoom] = useState(() => {
|
|
||||||
const z = parseFloat(localStorage.getItem('adsZoom') || '1');
|
|
||||||
return ZOOM_LEVELS.includes(z) ? z : 1;
|
|
||||||
});
|
|
||||||
function handleZoom(dir) {
|
|
||||||
setZoom(z => {
|
|
||||||
const idx = ZOOM_LEVELS.indexOf(z);
|
|
||||||
let next = z;
|
|
||||||
if (dir === 'in') next = ZOOM_LEVELS[Math.min(ZOOM_LEVELS.length - 1, idx + 1)];
|
|
||||||
if (dir === 'out') next = ZOOM_LEVELS[Math.max(0, idx - 1)];
|
|
||||||
localStorage.setItem('adsZoom', next);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Link kopieren Funktionalität
|
// Message-Hook aus App-Kontext
|
||||||
const handleCopyLink = async (adId, adName) => {
|
const { message } = App.useApp();
|
||||||
try {
|
|
||||||
// Entferne bestehende Sprungmarke aus der URL
|
|
||||||
const currentUrl = window.location.href;
|
|
||||||
const baseUrl = currentUrl.split('#')[0]; // Alles vor dem ersten #
|
|
||||||
const linkWithAnchor = `${baseUrl}#${adId}`;
|
|
||||||
await navigator.clipboard.writeText(linkWithAnchor);
|
|
||||||
message.success(`Link zu "${adName}" wurde kopiert!`);
|
|
||||||
} catch (err) {
|
|
||||||
// Fallback für ältere Browser
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
const baseUrl = window.location.href.split('#')[0];
|
|
||||||
textArea.value = `${baseUrl}#${adId}`;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
message.success(`Link zu "${adName}" wurde kopiert!`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// iFrame Refresh Funktionalität
|
// Handler für Ad-Aktionen mit Message-Callbacks
|
||||||
const handleRefreshAd = (adId, adName) => {
|
const handleCopyLink = createCopyLinkHandler((msg) => message.success(msg));
|
||||||
// Finde alle iFrames innerhalb des Ad-Containers
|
const handleRefreshAd = createRefreshAdHandler(
|
||||||
const adContainer = document.getElementById(adId);
|
(msg) => message.success(msg),
|
||||||
if (adContainer) {
|
(msg) => message.info(msg)
|
||||||
const iframes = adContainer.querySelectorAll('iframe');
|
);
|
||||||
let refreshedCount = 0;
|
|
||||||
|
|
||||||
iframes.forEach((iframe) => {
|
|
||||||
if (iframe.src) {
|
|
||||||
// Füge einen Timestamp als URL-Parameter hinzu, um den Cache zu umgehen
|
|
||||||
const url = new URL(iframe.src);
|
|
||||||
url.searchParams.set('refresh', Date.now().toString());
|
|
||||||
iframe.src = url.toString();
|
|
||||||
refreshedCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (refreshedCount > 0) {
|
|
||||||
message.success(`Animation${refreshedCount > 1 ? 'en' : ''} "${adName}" ${refreshedCount > 1 ? 'wurden' : 'wurde'} neu geladen!`);
|
|
||||||
} else {
|
|
||||||
message.info(`Keine Animationen in "${adName}" gefunden.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchLogo() {
|
async function fetchLogo() {
|
||||||
@@ -144,78 +84,10 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
// Tabs mit useMemo erstellen, damit sie sich bei Zoom-Änderung aktualisieren ohne API-Call
|
// Tabs mit useMemo erstellen, damit sie sich bei Zoom-Änderung aktualisieren ohne API-Call
|
||||||
const tabs = React.useMemo(() => {
|
const tabs = React.useMemo(() => {
|
||||||
if (!tabsData) return [];
|
if (!tabsData) return [];
|
||||||
return buildTabsFromCategories(tabsData);
|
return buildTabsFromCategories(tabsData, zoom, darkMode, handleCopyLink, handleRefreshAd);
|
||||||
}, [tabsData, zoom]);
|
}, [tabsData, zoom, darkMode, handleCopyLink, handleRefreshAd]);
|
||||||
|
|
||||||
|
|
||||||
function buildTabsFromCategories(data) {
|
|
||||||
let categories = [];
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
categories = data.filter(child => child.type === 'category');
|
|
||||||
} else if (data && Array.isArray(data.children)) {
|
|
||||||
categories = data.children.filter(child => child.type === 'category');
|
|
||||||
}
|
|
||||||
return categories.map(cat => ({
|
|
||||||
key: cat.title,
|
|
||||||
label: cat.title,
|
|
||||||
children: (
|
|
||||||
<div>
|
|
||||||
{Array.isArray(cat.children) && cat.children.filter(sub => sub.type === 'subcategory').length > 0 ? (
|
|
||||||
cat.children.filter(sub => sub.type === 'subcategory').map(sub => (
|
|
||||||
<div key={sub.name} style={{ marginTop: 24 }}>
|
|
||||||
<h2 style={{ marginBottom: 4 }}>{sub.name}</h2>
|
|
||||||
{/* Ads unterhalb der Subcategory anzeigen */}
|
|
||||||
{Array.isArray(sub.children) && sub.children.filter(ad => ad.type === 'ad').length > 0 ? (
|
|
||||||
sub.children.filter(ad => ad.type === 'ad').map(ad => {
|
|
||||||
const adId = generateAdId(ad.name, sub.name, cat.title);
|
|
||||||
return (
|
|
||||||
<div key={ad.name} id={adId} style={{ marginLeft: 0, marginBottom: 16 }}>
|
|
||||||
<div style={{ color: '#555', fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
{ad.name}
|
|
||||||
<Tooltip title="Link kopieren">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
icon={<ShareAltOutlined />}
|
|
||||||
onClick={() => handleCopyLink(adId, ad.name)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
{/* Refresh-Button nur bei HTML-Dateien (iFrames) anzeigen */}
|
|
||||||
{Array.isArray(ad.files) && ad.files.some(file => file.type === 'html') && (
|
|
||||||
<Tooltip title="Animationen neu laden">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={() => handleRefreshAd(adId, ad.name)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Dateien unterhalb des Ads anzeigen */}
|
|
||||||
{Array.isArray(ad.files) && ad.files.length > 0 ? (
|
|
||||||
<div style={{ marginLeft: 0, display: 'flex', flexWrap: 'wrap', gap: 32 }}>
|
|
||||||
{ad.files.map(file => (
|
|
||||||
<FilePreview key={file.name + '__' + zoom} file={file} zoom={zoom} darkMode={darkMode} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ color: '#bbb', marginLeft: 16, fontSize: 13 }}>Keine Dateien vorhanden.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div style={{ color: '#aaa', marginLeft: 16, fontSize: 14 }}>Keine Ads vorhanden.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div style={{ color: '#888', marginTop: 16 }}>Keine Subkategorien vorhanden.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Wenn Routing-Parameter fehlen, Tabs zurücksetzen
|
// Wenn Routing-Parameter fehlen, Tabs zurücksetzen
|
||||||
@@ -320,25 +192,7 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && tabs.length > 0) {
|
if (!loading && tabs.length > 0) {
|
||||||
const hash = window.location.hash.substring(1);
|
const hash = window.location.hash.substring(1);
|
||||||
if (hash) {
|
scrollToAnchor(hash, 20);
|
||||||
// Kurz warten bis die Inhalte gerendert sind
|
|
||||||
setTimeout(() => {
|
|
||||||
const element = document.getElementById(hash);
|
|
||||||
if (element) {
|
|
||||||
// Scroll mit Offset, um Navigation zu berücksichtigen
|
|
||||||
const elementTop = element.offsetTop - 20;
|
|
||||||
window.scrollTo({
|
|
||||||
top: elementTop,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
// Kurz hervorheben
|
|
||||||
element.style.backgroundColor = '#edff5f';
|
|
||||||
setTimeout(() => {
|
|
||||||
element.style.backgroundColor = '';
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [loading, tabs, activeKey]);
|
}, [loading, tabs, activeKey]);
|
||||||
|
|
||||||
|
|||||||
82
frontend/src/utils/adUtils.js
Normal file
82
frontend/src/utils/adUtils.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Hash-Funktion für Ad-IDs
|
||||||
|
export function generateAdId(adName, subcategoryName, categoryName) {
|
||||||
|
const combined = `${categoryName}-${subcategoryName}-${adName}`;
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < combined.length; i++) {
|
||||||
|
const char = combined.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // 32-bit integer
|
||||||
|
}
|
||||||
|
return Math.abs(hash).toString(36); // Base36 für kürzere IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link kopieren Funktionalität
|
||||||
|
export const createCopyLinkHandler = (onSuccess) => async (adId, adName) => {
|
||||||
|
try {
|
||||||
|
// Entferne bestehende Sprungmarke aus der URL
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const baseUrl = currentUrl.split('#')[0]; // Alles vor dem ersten #
|
||||||
|
const linkWithAnchor = `${baseUrl}#${adId}`;
|
||||||
|
await navigator.clipboard.writeText(linkWithAnchor);
|
||||||
|
onSuccess(`Link zu "${adName}" wurde kopiert!`);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback für ältere Browser
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
const baseUrl = window.location.href.split('#')[0];
|
||||||
|
textArea.value = `${baseUrl}#${adId}`;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
onSuccess(`Link zu "${adName}" wurde kopiert!`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// iFrame Refresh Funktionalität
|
||||||
|
export const createRefreshAdHandler = (onSuccess, onInfo) => (adId, adName) => {
|
||||||
|
// Finde alle iFrames innerhalb des Ad-Containers
|
||||||
|
const adContainer = document.getElementById(adId);
|
||||||
|
if (adContainer) {
|
||||||
|
const iframes = adContainer.querySelectorAll('iframe');
|
||||||
|
let refreshedCount = 0;
|
||||||
|
|
||||||
|
iframes.forEach((iframe) => {
|
||||||
|
if (iframe.src) {
|
||||||
|
// Füge einen Timestamp als URL-Parameter hinzu, um den Cache zu umgehen
|
||||||
|
const url = new URL(iframe.src);
|
||||||
|
url.searchParams.set('refresh', Date.now().toString());
|
||||||
|
iframe.src = url.toString();
|
||||||
|
refreshedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (refreshedCount > 0) {
|
||||||
|
onSuccess(`Animation${refreshedCount > 1 ? 'en' : ''} "${adName}" ${refreshedCount > 1 ? 'wurden' : 'wurde'} neu geladen!`);
|
||||||
|
} else {
|
||||||
|
onInfo(`Keine Animationen in "${adName}" gefunden.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scroll zu Sprungmarke mit Offset
|
||||||
|
export const scrollToAnchor = (hash, offset = 20) => {
|
||||||
|
if (hash) {
|
||||||
|
// Kurz warten bis die Inhalte gerendert sind
|
||||||
|
setTimeout(() => {
|
||||||
|
const element = document.getElementById(hash);
|
||||||
|
if (element) {
|
||||||
|
// Scroll mit Offset, um Navigation zu berücksichtigen
|
||||||
|
const elementTop = element.offsetTop - offset;
|
||||||
|
window.scrollTo({
|
||||||
|
top: elementTop,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
// Kurz hervorheben
|
||||||
|
element.style.backgroundColor = '#edff5f';
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.backgroundColor = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
77
frontend/src/utils/tabUtils.js
Normal file
77
frontend/src/utils/tabUtils.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Tooltip } from 'antd';
|
||||||
|
import { ShareAltOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
|
import FilePreview from '../components/FilePreview';
|
||||||
|
import { generateAdId } from './adUtils';
|
||||||
|
|
||||||
|
// Tabs aus Kategorien-Daten erstellen
|
||||||
|
export const buildTabsFromCategories = (data, zoom, darkMode, handleCopyLink, handleRefreshAd) => {
|
||||||
|
let categories = [];
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
categories = data.filter(child => child.type === 'category');
|
||||||
|
} else if (data && Array.isArray(data.children)) {
|
||||||
|
categories = data.children.filter(child => child.type === 'category');
|
||||||
|
}
|
||||||
|
|
||||||
|
return categories.map(cat => ({
|
||||||
|
key: cat.title,
|
||||||
|
label: cat.title,
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
{Array.isArray(cat.children) && cat.children.filter(sub => sub.type === 'subcategory').length > 0 ? (
|
||||||
|
cat.children.filter(sub => sub.type === 'subcategory').map(sub => (
|
||||||
|
<div key={sub.name} style={{ marginTop: 24 }}>
|
||||||
|
<h2 style={{ marginBottom: 4 }}>{sub.name}</h2>
|
||||||
|
{/* Ads unterhalb der Subcategory anzeigen */}
|
||||||
|
{Array.isArray(sub.children) && sub.children.filter(ad => ad.type === 'ad').length > 0 ? (
|
||||||
|
sub.children.filter(ad => ad.type === 'ad').map(ad => {
|
||||||
|
const adId = generateAdId(ad.name, sub.name, cat.title);
|
||||||
|
return (
|
||||||
|
<div key={ad.name} id={adId} style={{ marginLeft: 0, marginBottom: 16 }}>
|
||||||
|
<div style={{ color: '#555', fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
{ad.name}
|
||||||
|
<Tooltip title="Link kopieren">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
shape="circle"
|
||||||
|
icon={<ShareAltOutlined />}
|
||||||
|
onClick={() => handleCopyLink(adId, ad.name)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{/* Refresh-Button nur bei HTML-Dateien (iFrames) anzeigen */}
|
||||||
|
{Array.isArray(ad.files) && ad.files.some(file => file.type === 'html') && (
|
||||||
|
<Tooltip title="Animationen neu laden">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
shape="circle"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={() => handleRefreshAd(adId, ad.name)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Dateien unterhalb des Ads anzeigen */}
|
||||||
|
{Array.isArray(ad.files) && ad.files.length > 0 ? (
|
||||||
|
<div style={{ marginLeft: 0, display: 'flex', flexWrap: 'wrap', gap: 32 }}>
|
||||||
|
{ad.files.map(file => (
|
||||||
|
<FilePreview key={file.name + '__' + zoom} file={file} zoom={zoom} darkMode={darkMode} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ color: '#bbb', marginLeft: 16, fontSize: 13 }}>Keine Dateien vorhanden.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div style={{ color: '#aaa', marginLeft: 16, fontSize: 14 }}>Keine Ads vorhanden.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div style={{ color: '#888', marginTop: 16 }}>Keine Subkategorien vorhanden.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
};
|
||||||
29
frontend/src/utils/zoomUtils.js
Normal file
29
frontend/src/utils/zoomUtils.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// Zoom-Levels Konstante
|
||||||
|
export const ZOOM_LEVELS = [0.15, 0.25, 0.5, 0.75, 1];
|
||||||
|
|
||||||
|
// Zoom-State Hook
|
||||||
|
export const useZoomState = () => {
|
||||||
|
const [zoom, setZoom] = useState(() => {
|
||||||
|
const z = parseFloat(localStorage.getItem('adsZoom') || '1');
|
||||||
|
return ZOOM_LEVELS.includes(z) ? z : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleZoom = (direction) => {
|
||||||
|
setZoom(currentZoom => {
|
||||||
|
const idx = ZOOM_LEVELS.indexOf(currentZoom);
|
||||||
|
let nextZoom = currentZoom;
|
||||||
|
if (direction === 'in') {
|
||||||
|
nextZoom = ZOOM_LEVELS[Math.min(ZOOM_LEVELS.length - 1, idx + 1)];
|
||||||
|
}
|
||||||
|
if (direction === 'out') {
|
||||||
|
nextZoom = ZOOM_LEVELS[Math.max(0, idx - 1)];
|
||||||
|
}
|
||||||
|
localStorage.setItem('adsZoom', nextZoom);
|
||||||
|
return nextZoom;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { zoom, handleZoom, ZOOM_LEVELS };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user