From d80a4d85eb630ecbc39c68933f677111c2f03bab Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Fri, 25 Jul 2025 17:34:42 +0200 Subject: [PATCH] fix contrib form --- src/pages/api/contribute/tool.ts | 76 +++---- src/pages/contribute/index.astro | 85 ++++--- src/pages/contribute/tool.astro | 70 +++--- src/styles/global.css | 205 ++++++++++++++++- src/utils/gitContributions.ts | 379 +++++++++++++++++++++---------- 5 files changed, 582 insertions(+), 233 deletions(-) diff --git a/src/pages/api/contribute/tool.ts b/src/pages/api/contribute/tool.ts index 19c9a70..117bdce 100644 --- a/src/pages/api/contribute/tool.ts +++ b/src/pages/api/contribute/tool.ts @@ -175,17 +175,6 @@ export const POST: APIRoute = async ({ request }) => { const errorMessages = error.errors.map(err => `${err.path.join('.')}: ${err.message}` ); - // BEFORE: Manual validation error response (7 lines) - // return new Response(JSON.stringify({ - // success: false, - // error: 'Validation failed', - // details: errorMessages - // }), { - // status: 400, - // headers: { 'Content-Type': 'application/json' } - // }); - - // AFTER: Single line with consolidated helper return apiError.validation('Validation failed', errorMessages); } @@ -208,46 +197,35 @@ export const POST: APIRoute = async ({ request }) => { } }; - // Submit contribution via Git - const gitManager = new GitContributionManager(); - const result = await gitManager.submitContribution(contributionData); + // CRITICAL FIX: Enhanced error handling for Git operations + try { + const gitManager = new GitContributionManager(); + const result = await gitManager.submitContribution(contributionData); - if (result.success) { - // Log successful contribution - console.log(`[CONTRIBUTION] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail} - PR: ${result.prUrl}`); + if (result.success) { + // Log successful contribution + console.log(`[CONTRIBUTION SUCCESS] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail} - PR: ${result.prUrl}`); + + // ENSURE proper success response + return apiResponse.created({ + success: true, + message: result.message, + prUrl: result.prUrl, + branchName: result.branchName + }); + } else { + // Log failed contribution + console.error(`[CONTRIBUTION FAILED] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}: ${result.message}`); + + return apiServerError.internal(`Contribution failed: ${result.message}`); + } + } catch (gitError) { + // CRITICAL: Handle Git operation errors properly + console.error(`[GIT ERROR] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}:`, gitError); - // BEFORE: Manual success response (7 lines) - // return new Response(JSON.stringify({ - // success: true, - // message: result.message, - // prUrl: result.prUrl, - // branchName: result.branchName - // }), { - // status: 200, - // headers: { 'Content-Type': 'application/json' } - // }); - - // AFTER: Single line with consolidated helper - return apiResponse.created({ - message: result.message, - prUrl: result.prUrl, - branchName: result.branchName - }); - } else { - // Log failed contribution - console.error(`[CONTRIBUTION FAILED] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}: ${result.message}`); - - // BEFORE: Manual error response (7 lines) - // return new Response(JSON.stringify({ - // success: false, - // error: result.message - // }), { - // status: 500, - // headers: { 'Content-Type': 'application/json' } - // }); - - // AFTER: Single line with consolidated helper - return apiServerError.internal(`Contribution failed: ${result.message}`); + // Return proper error response + const errorMessage = gitError instanceof Error ? gitError.message : 'Git operation failed'; + return apiServerError.internal(`Git operation failed: ${errorMessage}`); } }, 'Contribution processing failed'); diff --git a/src/pages/contribute/index.astro b/src/pages/contribute/index.astro index 7113e01..8959344 100644 --- a/src/pages/contribute/index.astro +++ b/src/pages/contribute/index.astro @@ -50,37 +50,68 @@ const { authenticated, userEmail, userId } = authResult; " > - -
-
-
- - + + + +
+
+
+ + + +
+

Software, Methoden oder Konzepte

+
+ +

+ Ergänzt Software/Tools, forensische Methoden und relevante Konzepte zu unserer Datenbank. + Füllt einfach ein kurzes Formular aus! +

+ +
+ Software/Tools + Methoden + Konzepte +
+ +
+ + + + + + Neuer Eintrag + + + +
+
+ + + +
+

Existierenden Eintrag bearbeiten

+

+ Suchen Sie das Tool/Methode/Konzept auf der Hauptseite, + öffnen Sie die Details und klicken Sie den Edit-Button. +

+ +
-

Software, Methoden oder Konzepte

-
- -

- Ergänzt Software/Tools, forensische Methoden und relevante Konzepte zu unserer Datenbank. - Füllt einfach ein kurzes Formular aus! -

- -
- Software/Tools - Methoden - Konzepte -
- -
- +
+
-
+
- ))} - -
Strg gedrückt halten, um mehrere zu selektieren. Freilassen, wenn es zu keiner Domäne passt.
+
+
Mehrfachauswahl erlaubt
- ))} - -
Zutreffende auswählen
+
+
Mehrfachauswahl erlaubt
@@ -287,23 +289,25 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
- +
- Select concepts that users should understand when using this tool/method + Wählt Konzepte, die hilfreich sind für das Verständnis dieses Tools oder dieser Methode
+
@@ -323,9 +327,6 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
-
 # YAML preview will appear here
@@ -364,7 +365,6 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
         
- @@ -418,6 +418,7 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen'; document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('contribution-form'); + if (!form) console.error('[INIT ERROR] Form not found'); const typeSelect = document.getElementById('tool-type'); const submitBtn = document.getElementById('submit-btn'); const submitText = document.getElementById('submit-text'); @@ -619,10 +620,6 @@ related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]` typeSelect.addEventListener('change', updateFieldVisibility); } - if (refreshPreviewBtn) { - refreshPreviewBtn.addEventListener('click', updateYAMLPreview); - } - // Update preview on form changes if (form) { form.addEventListener('input', debounce(updateYAMLPreview, 500)); @@ -631,8 +628,9 @@ related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]` // Form submission if (form) { - form.addEventListener('submit', async (e) => { + form?.addEventListener('submit', async (e) => { e.preventDefault(); + console.log('[DEBUG] Submit button clicked'); const errors = validateForm(); if (errors.length > 0) { diff --git a/src/styles/global.css b/src/styles/global.css index 226c9d8..32275d6 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -279,13 +279,21 @@ input, select, textarea { background-color: var(--color-bg); color: var(--color-text); font-size: 0.875rem; - transition: var(--transition-fast); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } -input:focus, select:focus, textarea:focus { - outline: none; +input:focus, textarea:focus, select:focus { border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgb(37 99 235 / 10%); + box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1); +} + +/* Form validation states */ +input:invalid:not(:focus), textarea:invalid:not(:focus), select:invalid:not(:focus) { + border-color: var(--color-error); +} + +input:valid:not(:focus), textarea:valid:not(:focus), select:valid:not(:focus) { + border-color: var(--color-accent); } select { @@ -301,14 +309,67 @@ select { display: flex; align-items: center; gap: 0.5rem; + cursor: pointer; + transition: var(--transition-fast); + user-select: none; +} + +.checkbox-wrapper:hover { + background-color: var(--color-bg-secondary); + border-radius: 0.25rem; +} + +.checkbox-wrapper input[type="checkbox"] { + margin-right: 0.5rem; + cursor: pointer; +} + +/* Scrollable checkbox containers */ +.checkbox-container { + max-height: 200px; + overflow-y: auto; + border: 1px solid var(--color-border); + border-radius: 0.375rem; + padding: 0.75rem; + background-color: var(--color-bg); +} + +.checkbox-container::-webkit-scrollbar { + width: 8px; +} + +.checkbox-container::-webkit-scrollbar-track { + background: var(--color-bg-secondary); + border-radius: 4px; +} + +.checkbox-container::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; +} + +.checkbox-container::-webkit-scrollbar-thumb:hover { + background: var(--color-text-secondary); } input[type="checkbox"] { - width: auto; + width: 16px; + height: 16px; + accent-color: var(--color-primary); margin: 0; cursor: pointer; } +/* Better focus states for accessibility */ +input[type="checkbox"]:focus, +input[type="text"]:focus, +input[type="url"]:focus, +textarea:focus, +select:focus { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + /* Consolidated Card System */ .card { background-color: var(--color-bg); @@ -689,6 +750,7 @@ input[type="checkbox"] { width: 100%; height: 100%; background-color: rgb(0 0 0 / 50%); + backdrop-filter: blur(2px); z-index: 999; } @@ -847,6 +909,27 @@ input[type="checkbox"] { transform: translateY(-1px); } +/* Loading state improvements */ +.btn.loading { + opacity: 0.7; + pointer-events: none; + position: relative; +} + +.btn.loading::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + border: 2px solid transparent; + border-top: 2px solid currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + /* Collaboration Tools */ .collaboration-tools-compact { display: flex; @@ -1440,6 +1523,11 @@ This will literally assault the user's retinas. They'll need sunglasses to look } } +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + /* Consolidated Responsive Design */ @media (width <= 1200px) { .modals-side-by-side #tool-details-primary.active, @@ -1520,6 +1608,10 @@ This will literally assault the user's retinas. They'll need sunglasses to look width: 95%; max-width: none; } + + .form-grid.two-columns { + grid-template-columns: 1fr; + } } @media (width <= 640px) { @@ -1572,6 +1664,15 @@ This will literally assault the user's retinas. They'll need sunglasses to look grid-template-columns: 1fr; gap: 0.5rem; } + .card { + padding: 1rem; + } + .form-grid { + gap: 0.75rem; + } + .checkbox-container { + max-height: 150px; + } } @media (width <= 480px) { @@ -1844,4 +1945,98 @@ This will literally assault the user's retinas. They'll need sunglasses to look .flex-start { display: flex; align-items: center; +} + +.field-help { + font-size: 0.8125rem; + color: var(--color-text-secondary); + line-height: 1.4; +} + +/* Improved field error styling */ +.field-error { + color: var(--color-error); + font-size: 0.8125rem; + margin-top: 0.25rem; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.field-error::before { + content: "⚠"; + font-size: 0.75rem; +} + +/* Form section improvements */ +.form-section { + border: 1px solid var(--color-border); + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1.5rem; + background-color: var(--color-bg); +} + +.form-section h3 { + margin-top: 0; + margin-bottom: 1rem; + color: var(--color-primary); + font-size: 1.125rem; +} + +/* Success/warning notices in forms */ +.form-notice { + padding: 1rem; + border-radius: 0.5rem; + margin-bottom: 1rem; + border-left: 3px solid; +} + +.form-notice.success { + background-color: var(--color-oss-bg); + border-left-color: var(--color-accent); + color: var(--color-text); +} + +.form-notice.warning { + background-color: var(--color-hosted-bg); + border-left-color: var(--color-warning); + color: var(--color-text); +} + +.form-notice.info { + background-color: var(--color-bg-secondary); + border-left-color: var(--color-primary); + color: var(--color-text); +} + +/* Better form grid layout */ +.form-grid { + display: grid; + gap: 1rem; +} + +.form-grid.two-columns { + grid-template-columns: 1fr 1fr; +} + +/* Better spacing for form elements */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group:last-child { + margin-bottom: 0; +} + +.form-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: var(--color-text); +} + +.form-label.required::after { + content: " *"; + color: var(--color-error); } \ No newline at end of file diff --git a/src/utils/gitContributions.ts b/src/utils/gitContributions.ts index 378bc12..163d592 100644 --- a/src/utils/gitContributions.ts +++ b/src/utils/gitContributions.ts @@ -1,4 +1,4 @@ -// src/utils/gitContributions.ts - Enhanced for Phase 3 +// src/utils/gitContributions.ts - Enhanced for Phase 3 with YAML preservation import { execSync, spawn } from 'child_process'; import { promises as fs } from 'fs'; import { load, dump } from 'js-yaml'; @@ -285,144 +285,291 @@ export class GitContributionManager { } } - -private generateToolYAML(tool: any): string { - // Clean up the tool object - remove null/undefined values - const cleanTool: any = { - name: tool.name, - type: tool.type, - description: tool.description, - domains: tool.domains || [], - phases: tool.phases || [], - skillLevel: tool.skillLevel, - url: tool.url - }; - - // Add optional fields only if they have values - if (tool.icon) cleanTool.icon = tool.icon; - if (tool.platforms && tool.platforms.length > 0) cleanTool.platforms = tool.platforms; - if (tool.license) cleanTool.license = tool.license; - if (tool.accessType) cleanTool.accessType = tool.accessType; - if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl; - if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase; - if (tool.related_concepts && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts; - if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags; - - // Generate clean YAML - return dump(cleanTool, { - lineWidth: -1, - noRefs: true, - quotingType: '"', - forceQuotes: false, - indent: 2 - }); -} - -async submitContribution(data: ContributionData): Promise { - const branchName = `tool-${data.type}-${Date.now()}`; - - try { - await this.createBranch(branchName); - - const toolsPath = 'src/data/tools.yaml'; - const originalYaml = await this.readFile(toolsPath); - const yamlData: any = load(originalYaml); + /** + * CRITICAL FIX: Preserve YAML formatting while updating tools + * This prevents the complete rewrite that destroys multiline descriptions + */ + private async preserveYamlFormat(toolsPath: string, newTool: any, isEdit: boolean): Promise { + const originalContent = await this.readFile(toolsPath); + const yamlData: any = load(originalContent); if (!yamlData.tools || !Array.isArray(yamlData.tools)) { throw new Error('Invalid tools.yaml format'); } - if (data.type === 'edit') { - yamlData.tools = yamlData.tools.filter((t: any) => (t.name || '').toLowerCase() !== data.tool.name.toLowerCase()); + if (isEdit) { + // Find and replace existing tool + const toolIndex = yamlData.tools.findIndex((t: any) => + (t.name || '').toLowerCase() === newTool.name.toLowerCase() + ); + + if (toolIndex >= 0) { + yamlData.tools[toolIndex] = newTool; + } else { + throw new Error('Tool to edit not found'); + } + } else { + // Add new tool - insert alphabetically + const insertIndex = yamlData.tools.findIndex((t: any) => + (t.name || '').toLowerCase() > newTool.name.toLowerCase() + ); + + if (insertIndex >= 0) { + yamlData.tools.splice(insertIndex, 0, newTool); + } else { + yamlData.tools.push(newTool); + } } - yamlData.tools.push(data.tool); - - const newYaml = dump(yamlData, { lineWidth: -1, noRefs: true, quotingType: '"', forceQuotes: false, indent: 2 }); - - await this.writeFile(toolsPath, newYaml); - - const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`; - - await this.commitChanges(commitMessage); - - await this.pushBranch(branchName); - - const prUrl = await this.createPullRequest( - branchName, - `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`, - `Automated contribution for ${data.tool.name}` - ); - - return { - success: true, - message: `Tool contribution submitted successfully`, - prUrl, - branchName - }; + // Split original content into sections to preserve formatting + const lines = originalContent.split('\n'); + const toolsStartIndex = lines.findIndex(line => line.trim() === 'tools:'); - } catch (error) { - // Cleanup on failure - try { - await this.deleteBranch(branchName); - } catch (cleanupError) { - console.error('Failed to cleanup branch:', cleanupError); + if (toolsStartIndex === -1) { + // Fallback to full rewrite if structure is unexpected + console.warn('Could not find tools section, falling back to full YAML rewrite'); + return dump(yamlData, { + lineWidth: -1, + noRefs: true, + quotingType: '"', + forceQuotes: false, + indent: 2 + }); } + + // Preserve header (everything before tools:) + const header = lines.slice(0, toolsStartIndex + 1).join('\n'); - throw error; + // Find footer (domains, phases, etc.) + const domainsStartIndex = lines.findIndex(line => line.trim() === 'domains:'); + const footer = domainsStartIndex >= 0 ? '\n' + lines.slice(domainsStartIndex).join('\n') : ''; + + // Generate only the tools section with proper formatting + const toolsYaml = yamlData.tools.map((tool: any) => { + return this.formatToolYaml(tool); + }).join(''); + + return header + '\n' + toolsYaml + footer; } -} + /** + * Format a single tool entry preserving multiline descriptions + */ + private formatToolYaml(tool: any): string { + let toolEntry = ` - name: "${tool.name}"\n`; + + if (tool.icon) toolEntry += ` icon: "${tool.icon}"\n`; + toolEntry += ` type: ${tool.type}\n`; + + // PRESERVE multiline description format for longer descriptions + if (tool.description && tool.description.length > 80) { + toolEntry += ` description: >-\n`; + const words: string[] = tool.description.split(' '); + const lines: string[] = []; + let currentLine: string = ''; + + words.forEach((word: string) => { + if ((currentLine + ' ' + word).length > 80) { + if (currentLine) lines.push(currentLine); + currentLine = word; + } else { + currentLine = currentLine ? currentLine + ' ' + word : word; + } + }); + if (currentLine) lines.push(currentLine); + + lines.forEach((line: string) => { + toolEntry += ` ${line}\n`; + }); + } else { + toolEntry += ` description: "${tool.description}"\n`; + } + + // Add array fields + if (tool.domains && tool.domains.length > 0) { + toolEntry += ` domains:\n`; + tool.domains.forEach((domain: string) => { + toolEntry += ` - ${domain}\n`; + }); + } + + if (tool.phases && tool.phases.length > 0) { + toolEntry += ` phases:\n`; + tool.phases.forEach((phase: string) => { + toolEntry += ` - ${phase}\n`; + }); + } + + if (tool.platforms && tool.platforms.length > 0) { + toolEntry += ` platforms:\n`; + tool.platforms.forEach((platform: string) => { + toolEntry += ` - ${platform}\n`; + }); + } + + if (tool.related_concepts && tool.related_concepts.length > 0) { + toolEntry += ` related_concepts:\n`; + tool.related_concepts.forEach((concept: string) => { + toolEntry += ` - ${concept}\n`; + }); + } + + if (tool['domain-agnostic-software'] && tool['domain-agnostic-software'].length > 0) { + toolEntry += ` domain-agnostic-software:\n`; + tool['domain-agnostic-software'].forEach((item: string) => { + toolEntry += ` - ${item}\n`; + }); + } + + // Add scalar fields + toolEntry += ` skillLevel: ${tool.skillLevel}\n`; + if (tool.accessType) toolEntry += ` accessType: ${tool.accessType}\n`; + toolEntry += ` url: ${tool.url}\n`; + if (tool.projectUrl) toolEntry += ` projectUrl: ${tool.projectUrl}\n`; + if (tool.license) toolEntry += ` license: ${tool.license}\n`; + if (tool.knowledgebase) toolEntry += ` knowledgebase: ${tool.knowledgebase}\n`; + + if (tool.tags && tool.tags.length > 0) { + toolEntry += ` tags:\n`; + tool.tags.forEach((tag: string) => { + toolEntry += ` - ${tag}\n`; + }); + } + + return toolEntry; + } + + private generateToolYAML(tool: any): string { + // Clean up the tool object - remove null/undefined values + const cleanTool: any = { + name: tool.name, + type: tool.type, + description: tool.description, + domains: tool.domains || [], + phases: tool.phases || [], + skillLevel: tool.skillLevel, + url: tool.url + }; + + // Add optional fields only if they have values + if (tool.icon) cleanTool.icon = tool.icon; + if (tool.platforms && tool.platforms.length > 0) cleanTool.platforms = tool.platforms; + if (tool.license) cleanTool.license = tool.license; + if (tool.accessType) cleanTool.accessType = tool.accessType; + if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl; + if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase; + if (tool.related_concepts && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts; + if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags; + + // Generate clean YAML + return dump(cleanTool, { + lineWidth: -1, + noRefs: true, + quotingType: '"', + forceQuotes: false, + indent: 2 + }); + } + + async submitContribution(data: ContributionData): Promise { + const branchName = `tool-${data.type}-${Date.now()}`; + + try { + await this.createBranch(branchName); + + const toolsPath = 'src/data/tools.yaml'; + + // CRITICAL FIX: Use format-preserving method instead of dump() + const newYaml = await this.preserveYamlFormat(toolsPath, data.tool, data.type === 'edit'); + + await this.writeFile(toolsPath, newYaml); + + const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name} + +Submitted by: ${data.metadata.submitter} +${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`; + + await this.commitChanges(commitMessage); + + await this.pushBranch(branchName); + + // Generate tool YAML for PR description + const toolYaml = this.generateToolYAML(data.tool); + + const prUrl = await this.createPullRequest( + branchName, + `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`, + this.generateEnhancedPRDescription(data, toolYaml) + ); + + return { + success: true, + message: `Tool contribution submitted successfully`, + prUrl, + branchName + }; + + } catch (error) { + // Cleanup on failure + try { + await this.deleteBranch(branchName); + } catch (cleanupError) { + console.error('Failed to cleanup branch:', cleanupError); + } + + throw error; + } + } private generateEnhancedPRDescription(data: ContributionData, toolYaml: string): string { return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name} - **Type:** ${data.tool.type} - **Submitted by:** ${data.metadata.submitter} - **Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'} +**Type:** ${data.tool.type} +**Submitted by:** ${data.metadata.submitter} +**Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'} - ### Tool Details - - **Name:** ${data.tool.name} - - **Description:** ${data.tool.description} - - **URL:** ${data.tool.url} - - **Skill Level:** ${data.tool.skillLevel} - ${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''} - ${data.tool.license ? `- **License:** ${data.tool.license}` : ''} - ${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''} - ${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''} - - **Domains:** ${data.tool.domains.join(', ')} - - **Phases:** ${data.tool.phases.join(', ')} - ${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.join(', ')}` : ''} - ${data.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''} +### Tool Details +- **Name:** ${data.tool.name} +- **Description:** ${data.tool.description} +- **URL:** ${data.tool.url} +- **Skill Level:** ${data.tool.skillLevel} +${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''} +${data.tool.license ? `- **License:** ${data.tool.license}` : ''} +${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''} +${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''} +- **Domains:** ${data.tool.domains.join(', ')} +- **Phases:** ${data.tool.phases.join(', ')} +${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.join(', ')}` : ''} +${data.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''} - ${data.metadata.reason ? `### Reason for Contribution - ${data.metadata.reason} +${data.metadata.reason ? `### Reason for Contribution +${data.metadata.reason} - ` : ''}### Raw Tool Data (Copy & Paste Ready) +` : ''}### Raw Tool Data (Copy & Paste Ready) - \`\`\`yaml - ${toolYaml}\`\`\` +\`\`\`yaml +${toolYaml}\`\`\` - ### For Maintainers +### For Maintainers - **To add this tool to tools.yaml:** - 1. Copy the YAML data above - 2. ${data.type === 'add' ? 'Add it to the tools array in the appropriate alphabetical position' : 'Replace the existing tool entry with this updated data'} - 3. Verify all fields are correct - 4. Test that the tool displays properly - 5. Close this PR +**To add this tool to tools.yaml:** +1. Copy the YAML data above +2. ${data.type === 'add' ? 'Add it to the tools array in the appropriate alphabetical position' : 'Replace the existing tool entry with this updated data'} +3. Verify all fields are correct +4. Test that the tool displays properly +5. Close this PR - ### Review Checklist - - [ ] Tool information is accurate and complete - - [ ] Description is clear and informative - - [ ] Domains and phases are correctly assigned - - [ ] Tags are relevant and consistent with existing tools - - [ ] License information is correct (for software) - - [ ] URLs are valid and accessible - - [ ] No duplicate tool entries - - [ ] YAML syntax is valid +### Review Checklist +- [ ] Tool information is accurate and complete +- [ ] Description is clear and informative +- [ ] Domains and phases are correctly assigned +- [ ] Tags are relevant and consistent with existing tools +- [ ] License information is correct (for software) +- [ ] URLs are valid and accessible +- [ ] No duplicate tool entries +- [ ] YAML syntax is valid - --- - *This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`; +--- +*This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`; } } \ No newline at end of file