feat: Add anchor link sharing for ads with scroll offset
- Add generateAdId() function to create unique hashes from ad names and categories - Implement handleCopyLink() to copy shareable links with anchor fragments - Add scroll-to-anchor functionality with 20px offset to avoid navigation overlap - Add visual highlight animation when navigating to shared links - Import message component for user feedback on successful copy operations Each ad now gets a unique ID and share button that copies a direct link. Shared links automatically scroll to the target ad with proper offset.
This commit is contained in:
@@ -155,7 +155,7 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', marginBottom: 16, marginTop: 16 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', marginBottom: 16, marginTop: 8 }}>
|
||||||
{renderFileContent()}
|
{renderFileContent()}
|
||||||
{/* Badge für jede einzelne Datei */}
|
{/* Badge für jede einzelne Datei */}
|
||||||
{(file.width || file.height || file.size || file.duration) && (
|
{(file.width || file.height || file.size || file.duration) && (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 } from 'antd';
|
import { Tabs, Button, Spin, Skeleton, Result, Grid, Select, Layout, Tooltip, message } from 'antd';
|
||||||
import { ArrowLeftOutlined, LoadingOutlined, LockOutlined, MinusOutlined, PlusOutlined, EllipsisOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined, LoadingOutlined, LockOutlined, MinusOutlined, PlusOutlined, EllipsisOutlined, ShareAltOutlined } from '@ant-design/icons';
|
||||||
import UserMenu from '../components/UserMenu';
|
import UserMenu from '../components/UserMenu';
|
||||||
import FilePreview, { formatFileSize, formatDuration } from '../components/FilePreview';
|
import FilePreview, { formatFileSize, formatDuration } from '../components/FilePreview';
|
||||||
import debugLogger from '../utils/debugLogger';
|
import debugLogger from '../utils/debugLogger';
|
||||||
@@ -9,6 +9,18 @@ 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();
|
||||||
@@ -51,6 +63,28 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Link kopieren Funktionalität
|
||||||
|
const handleCopyLink = 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);
|
||||||
|
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!`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchLogo() {
|
async function fetchLogo() {
|
||||||
if (!client || !project) {
|
if (!client || !project) {
|
||||||
@@ -105,9 +139,21 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
<h2 style={{ marginBottom: 4 }}>{sub.name}</h2>
|
<h2 style={{ marginBottom: 4 }}>{sub.name}</h2>
|
||||||
{/* Ads unterhalb der Subcategory anzeigen */}
|
{/* Ads unterhalb der Subcategory anzeigen */}
|
||||||
{Array.isArray(sub.children) && sub.children.filter(ad => ad.type === 'ad').length > 0 ? (
|
{Array.isArray(sub.children) && sub.children.filter(ad => ad.type === 'ad').length > 0 ? (
|
||||||
sub.children.filter(ad => ad.type === 'ad').map(ad => (
|
sub.children.filter(ad => ad.type === 'ad').map(ad => {
|
||||||
<div key={ad.name} style={{ marginLeft: 0, marginBottom: 16 }}>
|
const adId = generateAdId(ad.name, sub.name, cat.title);
|
||||||
<div style={{ color: '#555', fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 }}>{ad.name}</div>
|
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>
|
||||||
|
</div>
|
||||||
{/* Dateien unterhalb des Ads anzeigen */}
|
{/* Dateien unterhalb des Ads anzeigen */}
|
||||||
{Array.isArray(ad.files) && ad.files.length > 0 ? (
|
{Array.isArray(ad.files) && ad.files.length > 0 ? (
|
||||||
<div style={{ marginLeft: 0, display: 'flex', flexWrap: 'wrap', gap: 32 }}>
|
<div style={{ marginLeft: 0, display: 'flex', flexWrap: 'wrap', gap: 32 }}>
|
||||||
@@ -119,7 +165,8 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
<div style={{ color: '#bbb', marginLeft: 16, fontSize: 13 }}>Keine Dateien vorhanden.</div>
|
<div style={{ color: '#bbb', marginLeft: 16, fontSize: 13 }}>Keine Dateien vorhanden.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<div style={{ color: '#aaa', marginLeft: 16, fontSize: 14 }}>Keine Ads vorhanden.</div>
|
<div style={{ color: '#aaa', marginLeft: 16, fontSize: 14 }}>Keine Ads vorhanden.</div>
|
||||||
)}
|
)}
|
||||||
@@ -232,6 +279,32 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
fetchTabs();
|
fetchTabs();
|
||||||
}, [user, client, project, tab]);
|
}, [user, client, project, tab]);
|
||||||
|
|
||||||
|
// Scroll zu Sprungmarke nach dem Laden
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && tabs.length > 0) {
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
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 - 20;
|
||||||
|
window.scrollTo({
|
||||||
|
top: elementTop,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
// Kurz hervorheben
|
||||||
|
element.style.backgroundColor = '#edff5f';
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.backgroundColor = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loading, tabs, activeKey]);
|
||||||
|
|
||||||
// Tab-Wechsel: Schreibe Tab in die URL
|
// Tab-Wechsel: Schreibe Tab in die URL
|
||||||
const handleTabChange = (key) => {
|
const handleTabChange = (key) => {
|
||||||
setActiveKey(key);
|
setActiveKey(key);
|
||||||
|
|||||||
Reference in New Issue
Block a user