simplify knowledgebase articles
This commit is contained in:
parent
a9c15eb9c6
commit
4cc28bceb7
6
.astro/content.d.ts
vendored
6
.astro/content.d.ts
vendored
@ -164,11 +164,9 @@ declare module 'astro:content' {
|
|||||||
type DataEntryMap = {
|
type DataEntryMap = {
|
||||||
"knowledgebase": Record<string, {
|
"knowledgebase": Record<string, {
|
||||||
id: string;
|
id: string;
|
||||||
render(): Render[".md"];
|
body?: string;
|
||||||
slug: string;
|
|
||||||
body: string;
|
|
||||||
collection: "knowledgebase";
|
collection: "knowledgebase";
|
||||||
data: InferEntrySchema<"knowledgebase">;
|
data: any;
|
||||||
rendered?: RenderedContent;
|
rendered?: RenderedContent;
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
}>;
|
}>;
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_variables": {
|
"_variables": {
|
||||||
"lastUpdateCheck": 1752478949435
|
"lastUpdateCheck": 1753528124767
|
||||||
}
|
}
|
||||||
}
|
}
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -81,3 +81,6 @@ src/_data/config.local.yaml
|
|||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
.astro/data-store.json
|
.astro/data-store.json
|
||||||
|
.astro/settings.json
|
||||||
|
.astro/data-store.json
|
||||||
|
.astro/content.d.ts
|
||||||
|
31
README.md
31
README.md
@ -37,7 +37,7 @@ Ein kuratiertes Verzeichnis für Digital Forensics und Incident Response (DFIR)
|
|||||||
|
|
||||||
## 🛠 Technische Grundlage
|
## 🛠 Technische Grundlage
|
||||||
|
|
||||||
- **Framework:** Astro 5.x mit TypeScript
|
- **Framework:** Astro 4.x mit TypeScript
|
||||||
- **Styling:** CSS Custom Properties mit Dark/Light Mode
|
- **Styling:** CSS Custom Properties mit Dark/Light Mode
|
||||||
- **API:** Node.js Backend mit Astro API Routes
|
- **API:** Node.js Backend mit Astro API Routes
|
||||||
- **Datenbank:** YAML-basierte Konfiguration (tools.yaml)
|
- **Datenbank:** YAML-basierte Konfiguration (tools.yaml)
|
||||||
@ -117,8 +117,8 @@ sudo systemctl enable nginx
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Klonen des Repositorys
|
# Klonen des Repositorys
|
||||||
sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /var/www/cc24-hub
|
sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /opt/cc24-hub
|
||||||
cd /var/www/cc24-hub
|
cd /opt/cc24-hub
|
||||||
|
|
||||||
# Abhängigkeiten installieren
|
# Abhängigkeiten installieren
|
||||||
sudo npm install
|
sudo npm install
|
||||||
@ -127,12 +127,12 @@ sudo npm install
|
|||||||
sudo npm run build
|
sudo npm run build
|
||||||
|
|
||||||
# Berechtigungen setzen
|
# Berechtigungen setzen
|
||||||
sudo chown -R www-data:www-data /var/www/cc24-hub
|
sudo chown -R www-data:www-data /opt/cc24-hub
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Umgebungsvariablen konfigurieren
|
#### 3. Umgebungsvariablen konfigurieren
|
||||||
|
|
||||||
Erstelle `/var/www/cc24-hub/.env`:
|
Erstelle `/opt/cc24-hub/.env`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# === GRUNDKONFIGURATION ===
|
# === GRUNDKONFIGURATION ===
|
||||||
@ -166,13 +166,13 @@ GIT_API_TOKEN=your_git_api_token
|
|||||||
GIT_REPO_URL=https://ihr-git-server.de/user/cc24-hub
|
GIT_REPO_URL=https://ihr-git-server.de/user/cc24-hub
|
||||||
|
|
||||||
# === UPLOAD-KONFIGURATION ===
|
# === UPLOAD-KONFIGURATION ===
|
||||||
LOCAL_UPLOAD_PATH=/var/www/cc24-hub/public/uploads
|
LOCAL_UPLOAD_PATH=/opt/cc24-hub/public/uploads
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Berechtigungen sichern
|
# Berechtigungen sichern
|
||||||
sudo chmod 600 /var/www/cc24-hub/.env
|
sudo chmod 600 /opt/cc24-hub/.env
|
||||||
sudo chown www-data:www-data /var/www/cc24-hub/.env
|
sudo chown www-data:www-data /opt/cc24-hub/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. Nginx konfigurieren
|
#### 4. Nginx konfigurieren
|
||||||
@ -205,7 +205,7 @@ server {
|
|||||||
# Static Files
|
# Static Files
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ @nodejs;
|
try_files $uri $uri/ @nodejs;
|
||||||
root /var/www/cc24-hub/dist;
|
root /opt/cc24-hub/dist;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# Cache static assets
|
# Cache static assets
|
||||||
@ -256,7 +256,7 @@ Wants=nginx.service
|
|||||||
Type=exec
|
Type=exec
|
||||||
User=www-data
|
User=www-data
|
||||||
Group=www-data
|
Group=www-data
|
||||||
WorkingDirectory=/var/www/cc24-hub
|
WorkingDirectory=/opt/cc24-hub
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
ExecStart=/usr/bin/node ./dist/server/entry.mjs
|
ExecStart=/usr/bin/node ./dist/server/entry.mjs
|
||||||
Restart=always
|
Restart=always
|
||||||
@ -269,7 +269,7 @@ NoNewPrivileges=yes
|
|||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=yes
|
ProtectHome=yes
|
||||||
ReadWritePaths=/var/www/cc24-hub
|
ReadWritePaths=/opt/cc24-hub
|
||||||
CapabilityBoundingSet=
|
CapabilityBoundingSet=
|
||||||
|
|
||||||
# Resource Limits
|
# Resource Limits
|
||||||
@ -431,7 +431,7 @@ domain-agnostic-software:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Repository aktualisieren
|
# Repository aktualisieren
|
||||||
cd /var/www/cc24-hub
|
cd /opt/cc24-hub
|
||||||
sudo git pull
|
sudo git pull
|
||||||
|
|
||||||
# Dependencies aktualisieren
|
# Dependencies aktualisieren
|
||||||
@ -449,8 +449,8 @@ sudo systemctl restart cc24-hub
|
|||||||
Wichtige Dateien für Backup:
|
Wichtige Dateien für Backup:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/var/www/cc24-hub/src/data/tools.yaml
|
/opt/cc24-hub/src/data/tools.yaml
|
||||||
/var/www/cc24-hub/.env
|
/opt/cc24-hub/.env
|
||||||
/etc/nginx/sites-available/cc24-hub
|
/etc/nginx/sites-available/cc24-hub
|
||||||
/etc/systemd/system/cc24-hub.service
|
/etc/systemd/system/cc24-hub.service
|
||||||
```
|
```
|
||||||
@ -472,4 +472,5 @@ Bei Problemen oder Fragen:
|
|||||||
- **Dokumentation:** Siehe `/knowledgebase` auf der Website
|
- **Dokumentation:** Siehe `/knowledgebase` auf der Website
|
||||||
|
|
||||||
## 📄 Lizenz
|
## 📄 Lizenz
|
||||||
Dieses Projekt steht unter der BSD-3-Clause Lizenz.
|
|
||||||
|
Dieses Projekt steht unter der **BSD-3-Clause** Lizenz.
|
File diff suppressed because it is too large
Load Diff
@ -4,26 +4,18 @@ const knowledgebaseCollection = defineCollection({
|
|||||||
type: 'content',
|
type: 'content',
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
tool_name: z.string(),
|
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
last_updated: z.date(),
|
last_updated: z.date(),
|
||||||
author: z.string().default('CC24-Team'),
|
|
||||||
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']),
|
tool_name: z.string().optional(),
|
||||||
|
related_tools: z.array(z.string()).default([]),
|
||||||
|
|
||||||
|
author: z.string().default('Anon'),
|
||||||
|
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']).optional(),
|
||||||
categories: z.array(z.string()).default([]),
|
categories: z.array(z.string()).default([]),
|
||||||
tags: z.array(z.string()).default([]),
|
tags: z.array(z.string()).default([]),
|
||||||
sections: z.object({
|
|
||||||
overview: z.boolean().default(true),
|
published: z.boolean().default(true),
|
||||||
installation: z.boolean().default(false),
|
|
||||||
configuration: z.boolean().default(false),
|
|
||||||
usage_examples: z.boolean().default(true),
|
|
||||||
best_practices: z.boolean().default(true),
|
|
||||||
troubleshooting: z.boolean().default(false),
|
|
||||||
advanced_topics: z.boolean().default(false),
|
|
||||||
}).default({}),
|
|
||||||
review_status: z.enum(['draft', 'review', 'published']).default('published'),
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = {
|
|
||||||
knowledgebase: knowledgebaseCollection,
|
|
||||||
};
|
|
@ -3,7 +3,7 @@ title: "Kali Linux - Die Hacker-Distribution für Forensik & Penetration Testing
|
|||||||
tool_name: "Kali Linux"
|
tool_name: "Kali Linux"
|
||||||
description: "Leitfaden zur Installation, Nutzung und Best Practices für Kali Linux – die All-in-One-Plattform für Security-Profis."
|
description: "Leitfaden zur Installation, Nutzung und Best Practices für Kali Linux – die All-in-One-Plattform für Security-Profis."
|
||||||
last_updated: 2025-07-20
|
last_updated: 2025-07-20
|
||||||
author: "CC24-Team"
|
author: "Claude 4 Sonnet"
|
||||||
difficulty: "intermediate"
|
difficulty: "intermediate"
|
||||||
categories: ["incident-response", "forensics", "penetration-testing"]
|
categories: ["incident-response", "forensics", "penetration-testing"]
|
||||||
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"]
|
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"]
|
||||||
|
@ -3,7 +3,7 @@ title: "MISP - Plattform für Threat Intelligence Sharing"
|
|||||||
tool_name: "MISP"
|
tool_name: "MISP"
|
||||||
description: "Das Rückgrat des modernen Threat-Intelligence-Sharings mit über 40.000 aktiven Instanzen weltweit."
|
description: "Das Rückgrat des modernen Threat-Intelligence-Sharings mit über 40.000 aktiven Instanzen weltweit."
|
||||||
last_updated: 2025-07-20
|
last_updated: 2025-07-20
|
||||||
author: "CC24-Team"
|
author: "Claude 4 Sonnet"
|
||||||
difficulty: "intermediate"
|
difficulty: "intermediate"
|
||||||
categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"]
|
categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"]
|
||||||
tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"]
|
tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"]
|
||||||
|
@ -3,7 +3,7 @@ title: "Nextcloud - Sichere Kollaborationsplattform"
|
|||||||
tool_name: "Nextcloud"
|
tool_name: "Nextcloud"
|
||||||
description: "Detaillierte Anleitung und Best Practices für Nextcloud in forensischen Einsatzszenarien"
|
description: "Detaillierte Anleitung und Best Practices für Nextcloud in forensischen Einsatzszenarien"
|
||||||
last_updated: 2025-07-20
|
last_updated: 2025-07-20
|
||||||
author: "CC24-Team"
|
author: "Claude 4 Sonnet"
|
||||||
difficulty: "novice"
|
difficulty: "novice"
|
||||||
categories: ["collaboration-general"]
|
categories: ["collaboration-general"]
|
||||||
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"]
|
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"]
|
||||||
|
@ -3,7 +3,7 @@ title: "Regular Expressions (Regex) – Musterbasierte Textanalyse"
|
|||||||
tool_name: "Regular Expressions (Regex)"
|
tool_name: "Regular Expressions (Regex)"
|
||||||
description: "Pattern matching language für Suche, Extraktion und Manipulation von Text in forensischen Analysen."
|
description: "Pattern matching language für Suche, Extraktion und Manipulation von Text in forensischen Analysen."
|
||||||
last_updated: 2025-07-20
|
last_updated: 2025-07-20
|
||||||
author: "CC24-Team"
|
author: "Claude 4 Sonnet"
|
||||||
difficulty: "intermediate"
|
difficulty: "intermediate"
|
||||||
categories: ["incident-response", "malware-analysis", "network-forensics", "fraud-investigation"]
|
categories: ["incident-response", "malware-analysis", "network-forensics", "fraud-investigation"]
|
||||||
tags: ["pattern-matching", "text-processing", "log-analysis", "string-manipulation", "search-algorithms"]
|
tags: ["pattern-matching", "text-processing", "log-analysis", "string-manipulation", "search-algorithms"]
|
||||||
|
@ -3,7 +3,7 @@ title: "Velociraptor – Skalierbare Endpoint-Forensik mit VQL"
|
|||||||
tool_name: "Velociraptor"
|
tool_name: "Velociraptor"
|
||||||
description: "Detaillierte Anleitung und Best Practices für Velociraptor – Remote-Forensik der nächsten Generation"
|
description: "Detaillierte Anleitung und Best Practices für Velociraptor – Remote-Forensik der nächsten Generation"
|
||||||
last_updated: 2025-07-20
|
last_updated: 2025-07-20
|
||||||
author: "CC24-Team"
|
author: "Claude 4 Sonnet"
|
||||||
difficulty: "advanced"
|
difficulty: "advanced"
|
||||||
categories: ["incident-response", "malware-analysis", "network-forensics"]
|
categories: ["incident-response", "malware-analysis", "network-forensics"]
|
||||||
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
|
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
|
||||||
|
@ -1,16 +1,51 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
import { getToolsData } from '../utils/dataService.js';
|
import { getToolsData } from '../utils/dataService.js';
|
||||||
import ContributionButton from '../components/ContributionButton.astro';
|
import ContributionButton from '../components/ContributionButton.astro';
|
||||||
|
|
||||||
// Load tools data
|
// Load tools data and knowledgebase articles
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
|
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
|
||||||
|
// Only include published articles
|
||||||
|
return entry.data.published !== false;
|
||||||
|
});
|
||||||
|
|
||||||
// Filter tools that have knowledgebase flag set to true
|
// Create unified knowledgebase entries with optional tool association
|
||||||
const knowledgebaseTools = data.tools.filter((tool: any) => tool.knowledgebase === true);
|
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
||||||
|
const associatedTool = entry.data.tool_name
|
||||||
|
? data.tools.find((tool: any) => tool.name === entry.data.tool_name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Article metadata
|
||||||
|
slug: entry.slug,
|
||||||
|
title: entry.data.title,
|
||||||
|
description: entry.data.description,
|
||||||
|
author: entry.data.author,
|
||||||
|
last_updated: entry.data.last_updated,
|
||||||
|
difficulty: entry.data.difficulty,
|
||||||
|
categories: entry.data.categories || [],
|
||||||
|
tags: entry.data.tags || [],
|
||||||
|
|
||||||
|
// Tool association (optional)
|
||||||
|
tool_name: entry.data.tool_name,
|
||||||
|
related_tools: entry.data.related_tools || [],
|
||||||
|
associatedTool,
|
||||||
|
|
||||||
|
// Derived properties for consistency with existing UI
|
||||||
|
name: entry.data.title, // For search compatibility
|
||||||
|
type: associatedTool?.type || 'article',
|
||||||
|
icon: associatedTool?.icon || '📖',
|
||||||
|
platforms: associatedTool?.platforms || [],
|
||||||
|
skillLevel: entry.data.difficulty || associatedTool?.skillLevel || 'intermediate',
|
||||||
|
phases: associatedTool?.phases || [],
|
||||||
|
license: associatedTool?.license
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Sort alphabetically by name
|
// Sort alphabetically by title
|
||||||
knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
|
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
|
||||||
@ -48,16 +83,16 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tools Count -->
|
<!-- Articles Count -->
|
||||||
<div style="text-align: center; margin-bottom: 2rem;">
|
<div style="text-align: center; margin-bottom: 2rem;">
|
||||||
<p class="text-muted" style="font-size: 0.875rem;">
|
<p class="text-muted" style="font-size: 0.875rem;">
|
||||||
<span id="visible-count">{knowledgebaseTools.length}</span> von {knowledgebaseTools.length} Einträgen
|
<span id="visible-count">{knowledgebaseEntries.length}</span> von {knowledgebaseEntries.length} Einträgen
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Knowledgebase Entries -->
|
<!-- Knowledgebase Entries -->
|
||||||
<div style="max-width: 1000px; margin: 0 auto;">
|
<div style="max-width: 1000px; margin: 0 auto;">
|
||||||
{knowledgebaseTools.length === 0 ? (
|
{knowledgebaseEntries.length === 0 ? (
|
||||||
<div class="card" style="text-align: center; padding: 3rem;">
|
<div class="card" style="text-align: center; padding: 3rem;">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" style="margin: 0 auto 1rem;">
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" style="margin: 0 auto 1rem;">
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
@ -68,54 +103,61 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|||||||
</svg>
|
</svg>
|
||||||
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Noch keine Knowledgebase-Einträge</h3>
|
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Noch keine Knowledgebase-Einträge</h3>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Knowledgebase-Einträge werden automatisch angezeigt, sobald Datenbankeinträge das Attribut "knowledgebase: true" haben.
|
Knowledgebase-Einträge werden automatisch angezeigt, sobald Artikel veröffentlicht werden.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div id="kb-entries">
|
<div id="kb-entries">
|
||||||
{knowledgebaseTools.map((tool: any, index: number) => {
|
{knowledgebaseEntries.map((entry: any, index: number) => {
|
||||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
const hasAssociatedTool = !!entry.associatedTool;
|
||||||
tool.projectUrl !== null &&
|
const hasValidProjectUrl = hasAssociatedTool &&
|
||||||
tool.projectUrl !== "" &&
|
entry.associatedTool.projectUrl !== undefined &&
|
||||||
tool.projectUrl.trim() !== "";
|
entry.associatedTool.projectUrl !== null &&
|
||||||
|
entry.associatedTool.projectUrl !== "" &&
|
||||||
|
entry.associatedTool.projectUrl.trim() !== "";
|
||||||
|
|
||||||
const toolSlug = tool.name.toLowerCase()
|
const isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
|
||||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
|
||||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
const isStandalone = !hasAssociatedTool;
|
||||||
.replace(/-+/g, '-') // Remove duplicate hyphens
|
|
||||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
class="kb-entry card"
|
class="kb-entry card"
|
||||||
id={`kb-${toolSlug}`}
|
id={`kb-${entry.slug}`}
|
||||||
data-tool-name={tool.name.toLowerCase()}
|
data-tool-name={entry.title.toLowerCase()}
|
||||||
|
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
|
||||||
>
|
>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||||
<h3 style="margin: 0; color: var(--color-primary);">
|
<h3 style="margin: 0; color: var(--color-primary);">
|
||||||
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
|
<span style="margin-right: 0.5rem;">{entry.icon}</span>
|
||||||
{tool.name}
|
{entry.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
<!-- Type indicator badges -->
|
<!-- Type indicator badges -->
|
||||||
{tool.type === 'concept' && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
{isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
|
||||||
{tool.type === 'method' && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
||||||
{tool.type === 'software' && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
||||||
|
{hasAssociatedTool && !isMethod && !isConcept && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
||||||
|
|
||||||
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
||||||
{tool.license !== 'Proprietary' && tool.type !== 'concept' && tool.type !== 'method' && <span class="badge badge-success">Open Source</span>}
|
{hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
|
||||||
|
|
||||||
<!-- Difficulty indicator -->
|
<!-- Difficulty indicator -->
|
||||||
<span class="badge" style="background-color: var(--color-text-secondary); color: white; font-size: 0.75rem;">
|
{entry.difficulty && (
|
||||||
{tool.skillLevel || 'intermediate'}
|
<span class="badge" style="background-color: var(--color-text-secondary); color: white; font-size: 0.75rem;">
|
||||||
</span>
|
{entry.difficulty}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Knowledge Base indicator -->
|
||||||
|
<span class="badge badge-error">📖</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<div style="display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0;">
|
<div style="display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0;">
|
||||||
<a href={`/knowledgebase/${toolSlug}`} class="btn btn-primary" style="font-size: 0.8125rem;">
|
<a href={`/knowledgebase/${entry.slug}`} class="btn btn-primary" style="font-size: 0.8125rem;">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
@ -124,38 +166,58 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|||||||
Artikel öffnen
|
Artikel öffnen
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- NEW: Edit button for existing knowledgebase articles -->
|
<!-- Edit button for knowledgebase articles -->
|
||||||
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
|
<ContributionButton type="edit" toolName={entry.tool_name || entry.title} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rest of existing article content remains unchanged -->
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p style="margin: 1rem 0; color: var(--color-text-secondary); line-height: 1.5;">
|
<p style="margin: 1rem 0; color: var(--color-text-secondary); line-height: 1.5;">
|
||||||
{tool.description}
|
{entry.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Tags and Metadata -->
|
<!-- Metadata and Tags -->
|
||||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-top: 1rem;">
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-top: 1rem;">
|
||||||
{tool.tags && tool.tags.length > 0 && (
|
<!-- Tags -->
|
||||||
|
{entry.tags && entry.tags.length > 0 && (
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
||||||
{tool.tags.map((tag: string) => (
|
{entry.tags.map((tag: string) => (
|
||||||
<span class="tag" style="font-size: 0.75rem;">{tag}</span>
|
<span class="tag" style="font-size: 0.75rem;">{tag}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tool.phases && tool.phases.length > 0 && (
|
<!-- Categories -->
|
||||||
|
{entry.categories && entry.categories.length > 0 && (
|
||||||
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||||
<strong>Phasen:</strong> {tool.phases.join(', ')}
|
<strong>Kategorien:</strong> {entry.categories.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tool.platforms && tool.platforms.length > 0 && tool.type !== 'concept' && tool.type !== 'method' && (
|
<!-- Tool-specific metadata (only if associated with tool) -->
|
||||||
|
{hasAssociatedTool && entry.phases && entry.phases.length > 0 && (
|
||||||
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||||
<strong>Plattformen:</strong> {tool.platforms.join(', ')}
|
<strong>Phasen:</strong> {entry.phases.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{hasAssociatedTool && entry.platforms && entry.platforms.length > 0 && !isMethod && !isConcept && (
|
||||||
|
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||||
|
<strong>Plattformen:</strong> {entry.platforms.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Related tools for standalone articles -->
|
||||||
|
{isStandalone && entry.related_tools && entry.related_tools.length > 0 && (
|
||||||
|
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||||
|
<strong>Verwandte Tools:</strong> {entry.related_tools.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Author and date -->
|
||||||
|
<div style="font-size: 0.8125rem; color: var(--color-text-secondary); margin-left: auto;">
|
||||||
|
<strong>Autor:</strong> {entry.author} • <strong>Aktualisiert:</strong> {entry.last_updated.toLocaleDateString('de-DE')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
@ -170,18 +232,19 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|||||||
<p class="text-muted">Versuchen Sie es mit anderen Suchbegriffen.</p>
|
<p class="text-muted">Versuchen Sie es mit anderen Suchbegriffen.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Floating Action Button -->
|
||||||
<div id="fab-container" style="position: fixed; bottom: 2rem; right: 2rem; z-index: 100; display: none;">
|
<div id="fab-container" style="position: fixed; bottom: 2rem; right: 2rem; z-index: 100; display: none;">
|
||||||
<ContributionButton
|
<ContributionButton
|
||||||
type="write"
|
type="write"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
text="✍️"
|
text="✍️"
|
||||||
style="border-radius: 50%; width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-lg); font-size: 1.5rem; padding: 0;"
|
style="border-radius: 50%; width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-lg); font-size: 1.5rem; padding: 0;"
|
||||||
className="fab-button"
|
className="fab-button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Enhanced knowledgebase functionality with search
|
// Enhanced knowledgebase functionality with search
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@ -228,5 +291,22 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|||||||
filterEntries(target.value);
|
filterEntries(target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show floating action button on scroll (optional enhancement)
|
||||||
|
let lastScrollY = window.scrollY;
|
||||||
|
const fabContainer = document.getElementById('fab-container');
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
if (fabContainer) {
|
||||||
|
if (window.scrollY > 200 && window.scrollY < lastScrollY) {
|
||||||
|
// Scrolling up and past threshold
|
||||||
|
fabContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
// Scrolling down or at top
|
||||||
|
fabContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
lastScrollY = window.scrollY;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
@ -26,20 +26,35 @@ const { Content } = await entry.render();
|
|||||||
|
|
||||||
// Load tools data to get the tool details
|
// Load tools data to get the tool details
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
const tool = data.tools.find((t: any) => t.name === entry.data.tool_name);
|
|
||||||
|
|
||||||
if (!tool) {
|
// UPGRADED: Handle optional tool association
|
||||||
console.warn(`Tool not found for knowledgebase entry: ${entry.data.tool_name}`);
|
const primaryTool = entry.data.tool_name
|
||||||
return Astro.redirect('/knowledgebase');
|
? data.tools.find((t: any) => t.name === entry.data.tool_name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// UPGRADED: Handle multiple related tools
|
||||||
|
const relatedTools = entry.data.related_tools
|
||||||
|
? entry.data.related_tools.map((toolName: string) =>
|
||||||
|
data.tools.find((t: any) => t.name === toolName)
|
||||||
|
).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// UPGRADED: Use primary tool or first related tool for styling, fallback to generic
|
||||||
|
const displayTool = primaryTool || relatedTools[0];
|
||||||
|
|
||||||
|
// UPGRADED: Don't redirect - show article even without tool association
|
||||||
|
if (!displayTool && !entry.data.tool_name && relatedTools.length === 0) {
|
||||||
|
console.log(`Standalone knowledgebase article: ${entry.slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine tool type for styling
|
// Determine styling based on tool type or fallback to generic
|
||||||
const isMethod = tool.type === 'method';
|
const isMethod = displayTool?.type === 'method';
|
||||||
const isConcept = tool.type === 'concept';
|
const isConcept = displayTool?.type === 'concept';
|
||||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
const isStandalone = !displayTool;
|
||||||
tool.projectUrl !== null &&
|
const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &&
|
||||||
tool.projectUrl !== "" &&
|
displayTool.projectUrl !== null &&
|
||||||
tool.projectUrl.trim() !== "";
|
displayTool.projectUrl !== "" &&
|
||||||
|
displayTool.projectUrl.trim() !== "";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
||||||
@ -49,7 +64,7 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
|||||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
||||||
<div style="flex: 1;">
|
<div style="flex: 1;">
|
||||||
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);">
|
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);">
|
||||||
{tool.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{tool.icon}</span>}
|
{displayTool?.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{displayTool.icon}</span>}
|
||||||
{entry.data.title}
|
{entry.data.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
|
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
|
||||||
@ -58,32 +73,59 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
|
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
|
||||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
<!-- UPGRADED: Conditional badges based on tool type or standalone -->
|
||||||
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
{isStandalone ? (
|
||||||
{!isMethod && !isConcept && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
<span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>
|
||||||
{!isMethod && !isConcept && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
) : (
|
||||||
{!isMethod && !isConcept && tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
<>
|
||||||
|
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
||||||
|
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
||||||
|
{!isMethod && !isConcept && !isStandalone && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
||||||
|
{!isMethod && !isConcept && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
||||||
|
{!isMethod && !isConcept && !isStandalone && displayTool?.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<span class="badge badge-error">📖</span>
|
<span class="badge badge-error">📖</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Metadata -->
|
<!-- UPGRADED: Flexible metadata section -->
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
|
||||||
<div>
|
<!-- Difficulty (always shown if present) -->
|
||||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
|
{entry.data.difficulty && (
|
||||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p>
|
<div>
|
||||||
</div>
|
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
|
||||||
|
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Last Updated (always shown) -->
|
||||||
<div>
|
<div>
|
||||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Letztes Update</strong>
|
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Letztes Update</strong>
|
||||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.last_updated.toLocaleDateString('de-DE')}</p>
|
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.last_updated.toLocaleDateString('de-DE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Author (always shown) -->
|
||||||
<div>
|
<div>
|
||||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Autor</strong>
|
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Autor</strong>
|
||||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.author}</p>
|
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.author}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- UPGRADED: Show article type -->
|
||||||
|
<div>
|
||||||
|
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Typ</strong>
|
||||||
|
<p style="margin: 0; font-size: 0.9375rem;">
|
||||||
|
{isStandalone ? 'Allgemeiner Artikel' :
|
||||||
|
isConcept ? 'Konzept-Artikel' :
|
||||||
|
isMethod ? 'Methoden-Artikel' :
|
||||||
|
'Software-Artikel'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UPGRADED: Categories (if present) -->
|
||||||
{entry.data.categories && entry.data.categories.length > 0 && (
|
{entry.data.categories && entry.data.categories.length > 0 && (
|
||||||
<div>
|
<div style="grid-column: 1 / -1;">
|
||||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Kategorien</strong>
|
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Kategorien</strong>
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
|
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
|
||||||
{entry.data.categories.map((cat: string) => (
|
{entry.data.categories.map((cat: string) => (
|
||||||
@ -105,57 +147,107 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="card" style="padding: 2rem;">
|
<div class="card" style="padding: 2rem;">
|
||||||
<div class="kb-content markdown-content" style="line-height: 1.7;">
|
<div class="kb-content markdown-content" style="line-height: 1.7;">
|
||||||
<Content />
|
<Content />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tool Actions -->
|
<!-- UPGRADED: Flexible Tool Actions Section -->
|
||||||
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary);">
|
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary);">
|
||||||
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">Tool-Aktionen</h3>
|
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">
|
||||||
|
{isStandalone ? 'Verwandte Aktionen' : 'Tool-Aktionen'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||||
{isConcept ? (
|
{isStandalone ? (
|
||||||
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
|
<!-- UPGRADED: Standalone article actions -->
|
||||||
|
<a href="/knowledgebase" class="btn btn-primary">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
<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"/>
|
||||||
</svg>
|
</svg>
|
||||||
Mehr erfahren
|
Weitere Artikel
|
||||||
</a>
|
|
||||||
) : isMethod ? (
|
|
||||||
<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);">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
Zur Methode
|
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
<!-- UPGRADED: Tool-specific actions (existing logic) -->
|
||||||
<>
|
<>
|
||||||
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
{isConcept ? (
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
Software-Homepage
|
|
||||||
</a>
|
|
||||||
{hasValidProjectUrl && (
|
|
||||||
<a href={tool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
<circle cx="12" cy="12" r="10"/>
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
<path d="M12 16l4-4-4-4"/>
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
<path d="M8 12h8"/>
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
Zugreifen
|
Mehr erfahren
|
||||||
</a>
|
</a>
|
||||||
|
) : isMethod ? (
|
||||||
|
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method);">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
Zur Methode
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
Software-Homepage
|
||||||
|
</a>
|
||||||
|
{hasValidProjectUrl && (
|
||||||
|
<a href={displayTool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<path d="M12 16l4-4-4-4"/>
|
||||||
|
<path d="M8 12h8"/>
|
||||||
|
</svg>
|
||||||
|
Zugreifen
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<!-- UPGRADED: Show related tools if present -->
|
||||||
|
{relatedTools.length > 0 && relatedTools.length > (primaryTool ? 1 : 0) && (
|
||||||
|
<div style="margin-left: auto;">
|
||||||
|
<details style="position: relative;">
|
||||||
|
<summary class="btn btn-secondary" style="cursor: pointer; list-style: none;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
|
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="8.5" cy="7" r="4"/>
|
||||||
|
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||||
|
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||||
|
</svg>
|
||||||
|
Verwandte Tools ({relatedTools.length})
|
||||||
|
</summary>
|
||||||
|
<div style="position: absolute; top: 100%; left: 0; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 200px; z-index: 100; box-shadow: var(--shadow-lg);">
|
||||||
|
{relatedTools.map((tool: any) => (
|
||||||
|
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer"
|
||||||
|
style="display: block; padding: 0.5rem; border-radius: 0.25rem; text-decoration: none; color: var(--color-text); margin-bottom: 0.25rem;"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'"
|
||||||
|
onmouseout="this.style.backgroundColor='transparent'">
|
||||||
|
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
|
||||||
|
{tool.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Always show return to main page -->
|
||||||
<a href="/" class="btn btn-secondary">
|
<a href="/" class="btn btn-secondary">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||||
@ -166,4 +258,4 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
Loading…
x
Reference in New Issue
Block a user