This commit is contained in:
overcuriousity
2025-07-22 22:36:08 +02:00
parent 043a2d32ac
commit f4acf39ca7
9 changed files with 496 additions and 85 deletions

View File

@@ -0,0 +1,101 @@
---
// src/components/ContributionButton.astro
export interface Props {
type: 'edit' | 'new' | 'write';
toolName?: string;
variant?: 'primary' | 'secondary' | 'small';
text?: string;
className?: string;
style?: string;
}
const {
type,
toolName,
variant = 'secondary',
text,
className = '',
style = ''
} = Astro.props;
// Generate appropriate URLs and text based on type
let href: string;
let defaultText: string;
let icon: string;
switch (type) {
case 'edit':
href = `/contribute/tool?edit=${encodeURIComponent(toolName || '')}`;
defaultText = 'Edit';
icon = `<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>`;
break;
case 'new':
href = '/contribute/tool';
defaultText = 'Add Tool';
icon = `<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>`;
break;
case 'write':
href = '/contribute/knowledgebase';
defaultText = 'Write Article';
icon = `<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>`;
break;
default:
href = '/contribute';
defaultText = 'Contribute';
icon = `<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>`;
}
const displayText = text || defaultText;
const buttonClass = `btn btn-${variant} ${className}`.trim();
const iconSize = variant === 'small' ? '14' : '16';
---
<a
href={href}
class={buttonClass}
style={style}
data-contribute-button={type}
data-tool-name={toolName}
title={`${displayText}${toolName ? `: ${toolName}` : ''}`}
>
<svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<Fragment set:html={icon} />
</svg>
{displayText}
</a>
<script>
// Check authentication status and redirect if needed
document.addEventListener('DOMContentLoaded', () => {
const contributeButtons = document.querySelectorAll('[data-contribute-button]');
contributeButtons.forEach(button => {
button.addEventListener('click', async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
if (data.authRequired && !data.authenticated) {
const returnUrl = (button as HTMLAnchorElement).href;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
} else {
window.location.href = (button as HTMLAnchorElement).href;
}
} catch (error) {
console.error('Auth check failed:', error);
// Fallback - proceed anyway
window.location.href = (button as HTMLAnchorElement).href;
}
});
});
});
</script>

View File

@@ -1,4 +1,5 @@
---
// src/components/Navigation.astro
import ThemeToggle from './ThemeToggle.astro';
const currentPath = Astro.url.pathname;
@@ -24,6 +25,11 @@ const currentPath = Astro.url.pathname;
~/knowledgebase
</a>
</li>
<li>
<a href="/contribute" class={`nav-link ${currentPath.startsWith('/contribute') ? 'active' : ''}`}>
~/contribute
</a>
</li>
<li>
<a href="/status" class={`nav-link ${currentPath === '/status' ? 'active' : ''}`}>
~/status
@@ -40,35 +46,4 @@ const currentPath = Astro.url.pathname;
</ul>
</div>
</div>
</nav>
<style>
/* Logo theme switching */
.nav-logo-light {
display: block;
}
.nav-logo-dark {
display: none;
}
[data-theme="dark"] .nav-logo-light {
display: none;
}
[data-theme="dark"] .nav-logo-dark {
display: block;
}
/* Make brand clickable */
.nav-brand {
text-decoration: none;
color: inherit;
transition: var(--transition-fast);
}
.nav-brand:hover {
text-decoration: none;
opacity: 0.8;
}
</style>
</nav>

View File

@@ -1,4 +1,8 @@
---
// src/components/ToolCard.astro (Updated)
import ContributionButton from './ContributionButton.astro';
import ShareButton from './ShareButton.astro';
export interface Props {
tool: {
name: string;
@@ -52,6 +56,7 @@ const cardClass = isConcept ? 'card card-concept tool-card' :
<!-- Only show CC24-Server and Knowledgebase badges -->
{!isMethod && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
{hasKnowledgebase && <span class="badge badge-error">📖</span>}
<ShareButton toolName={tool.name} context="card" size="small" />
</div>
</div>
@@ -101,33 +106,45 @@ const cardClass = isConcept ? 'card card-concept tool-card' :
))}
</div>
<!-- Buttons - Fixed at Bottom -->
<!-- Buttons - Fixed at Bottom with Contribution Button -->
<div class="tool-card-buttons" onclick="event.stopPropagation();">
{isConcept ? (
<!-- Concept button -->
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-concept); border-color: var(--color-concept);">
Mehr erfahren
</a>
) : isMethod ? (
<!-- Method button -->
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-method); border-color: var(--color-method);">
Zur Methode
</a>
) : hasValidProjectUrl ? (
<!-- Two buttons for hosted tools -->
<!-- Concept buttons with edit -->
<div class="button-row">
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
Homepage
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept); flex: 2;">
Mehr erfahren
</a>
<a href={tool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
Zugreifen
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit" style="flex: 1; font-size: 0.75rem;" />
</div>
) : isMethod ? (
<!-- Method buttons with edit -->
<div class="button-row">
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method); flex: 2;">
Zur Methode
</a>
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit" style="flex: 1; font-size: 0.75rem;" />
</div>
) : hasValidProjectUrl ? (
<!-- Three buttons for hosted tools -->
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<div class="button-row">
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
Homepage
</a>
<a href={tool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
Zugreifen
</a>
</div>
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit Entry" className="single-button" />
</div>
) : (
<!-- Single button for external tools -->
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button">
Software-Homepage
</a>
<!-- Two buttons for external tools -->
<div class="button-row">
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="flex: 2;">
Software-Homepage
</a>
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit" style="flex: 1; font-size: 0.75rem;" />
</div>
)}
</div>
</div>

View File

@@ -152,6 +152,9 @@ domains.forEach((domain: any) => {
<div id="share-button-primary" style="display: none;">
<!-- Share button will be populated by JavaScript -->
</div>
<div id="contribute-button-primary" style="display: none;">
<!-- Contribution button will be populated by JavaScript -->
</div>
<button class="btn-icon" onclick="window.hideToolDetails('primary')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
@@ -180,6 +183,9 @@ domains.forEach((domain: any) => {
<div id="share-button-secondary" style="display: none;">
<!-- Share button will be populated by JavaScript -->
</div>
<div id="contribute-button-secondary" style="display: none;">
<!-- Contribution button will be populated by JavaScript -->
</div>
<button class="btn-icon" onclick="window.hideToolDetails('secondary')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
@@ -679,6 +685,26 @@ domains.forEach((domain: any) => {
`;
shareButtonContainer.style.display = 'block';
}
// ===== POPULATE CONTRIBUTION BUTTON =====
const contributeButtonContainer = document.getElementById(`contribute-button-${modalType}`);
if (contributeButtonContainer) {
contributeButtonContainer.innerHTML = `
<a href="/contribute/tool?edit=${encodeURIComponent(tool.name)}"
class="btn-icon"
data-contribute-button="edit"
data-tool-name="${tool.name}"
title="Edit ${tool.name}"
aria-label="Edit ${tool.name}"
onclick="event.stopPropagation();">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</a>
`;
contributeButtonContainer.style.display = 'block';
}
// Show modals and update layout
const overlay = document.getElementById('modal-overlay');
@@ -703,29 +729,35 @@ domains.forEach((domain: any) => {
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
if (modalType === 'both' || modalType === 'all') {
if (primaryModal) {
primaryModal.classList.remove('active');
// Hide share button
const shareButtonPrimary = document.getElementById('share-button-primary');
const contributeButtonPrimary = document.getElementById('contribute-button-primary');
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
if (contributeButtonPrimary) contributeButtonPrimary.style.display = 'none';
}
if (secondaryModal) {
secondaryModal.classList.remove('active');
// Hide share button
const shareButtonSecondary = document.getElementById('share-button-secondary');
const contributeButtonSecondary = document.getElementById('contribute-button-secondary');
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
if (contributeButtonSecondary) contributeButtonSecondary.style.display = 'none';
}
if (overlay) overlay.classList.remove('active');
document.body.classList.remove('modals-side-by-side');
// ... rest of existing code
} else if (modalType === 'primary' && primaryModal) {
primaryModal.classList.remove('active');
const shareButtonPrimary = document.getElementById('share-button-primary');
const contributeButtonPrimary = document.getElementById('contribute-button-primary');
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
if (contributeButtonPrimary) contributeButtonPrimary.style.display = 'none';
} else if (modalType === 'secondary' && secondaryModal) {
secondaryModal.classList.remove('active');
const shareButtonSecondary = document.getElementById('share-button-secondary');
const contributeButtonSecondary = document.getElementById('contribute-button-secondary');
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
if (contributeButtonSecondary) contributeButtonSecondary.style.display = 'none';
}
// Check if any modal is still active
@@ -820,4 +852,32 @@ domains.forEach((domain: any) => {
}
}
});
document.addEventListener('DOMContentLoaded', () => {
// Auth check for contribution buttons (similar to existing auth checking pattern)
function setupContributionButtonAuth() {
document.addEventListener('click', async (e) => {
const contributeButton = e.target.closest('[data-contribute-button]');
if (!contributeButton) return;
e.preventDefault();
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
if (data.authRequired && !data.authenticated) {
const returnUrl = contributeButton.href;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
} else {
window.location.href = contributeButton.href;
}
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = contributeButton.href;
}
});
}
setupContributionButtonAuth();
});
</script>