312 lines
17 KiB
HTML
312 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
{% from "components/callouts.html" import warning, info %}
|
|
|
|
{% block title %}{{ tool.owner }}/{{ tool.name }} - SmartTools Registry{% endblock %}
|
|
|
|
{% block meta_description %}{{ tool.description or 'A SmartTools command-line tool' }}{% endblock %}
|
|
|
|
{% block og_title %}{{ tool.owner }}/{{ tool.name }}{% endblock %}
|
|
{% block og_description %}{{ tool.description or 'A SmartTools command-line tool' }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="bg-gray-50 min-h-screen">
|
|
<!-- Breadcrumb -->
|
|
<div class="bg-white border-b border-gray-200">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<nav class="flex items-center text-sm text-gray-500" aria-label="Breadcrumb">
|
|
<a href="{{ url_for('web.tools') }}" class="hover:text-gray-700">Tools</a>
|
|
<svg class="w-4 h-4 mx-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
|
</svg>
|
|
<a href="{{ url_for('web.publisher', slug=tool.owner) }}" class="hover:text-gray-700">{{ tool.owner }}</a>
|
|
<svg class="w-4 h-4 mx-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
|
</svg>
|
|
<span class="text-gray-900 font-medium">{{ tool.name }}</span>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div class="lg:grid lg:grid-cols-3 lg:gap-8">
|
|
<!-- Main Content -->
|
|
<main class="lg:col-span-2">
|
|
<!-- Tool Header -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex items-center">
|
|
<div class="w-16 h-16 bg-indigo-100 rounded-lg flex items-center justify-center">
|
|
<svg class="w-8 h-8 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<h1 class="text-2xl font-bold text-gray-900">{{ tool.name }}</h1>
|
|
<p class="text-gray-500">
|
|
by <a href="{{ url_for('web.publisher', slug=tool.owner) }}" class="text-indigo-600 hover:text-indigo-800">{{ tool.owner }}</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{% if tool.category %}
|
|
<span class="px-3 py-1 text-xs font-medium text-cyan-700 bg-cyan-100 rounded-full">
|
|
{{ tool.category }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if tool.description %}
|
|
<p class="mt-4 text-gray-600">{{ tool.description }}</p>
|
|
{% endif %}
|
|
|
|
{% if tool.tags %}
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
{% for tag in tool.tags.split(',') %}
|
|
<span class="px-2 py-1 text-xs text-gray-600 bg-gray-100 rounded">{{ tag.strip() }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Deprecation Warning -->
|
|
{% if tool.deprecated %}
|
|
{{ warning(
|
|
title='This tool is deprecated',
|
|
content=tool.deprecated_message or 'This tool is no longer maintained.'
|
|
) }}
|
|
{% if tool.replacement %}
|
|
{{ info(
|
|
title='Recommended replacement',
|
|
content='Consider using <a href="' ~ url_for('web.tool_detail', owner=tool.replacement.split('/')[0], name=tool.replacement.split('/')[1]) ~ '" class="underline">' ~ tool.replacement ~ '</a> instead.'
|
|
) }}
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<!-- README Content -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<div class="prose prose-indigo max-w-none">
|
|
{% if tool.readme %}
|
|
{{ tool.readme_html|safe }}
|
|
{% else %}
|
|
<h2>About {{ tool.name }}</h2>
|
|
<p>{{ tool.description or 'No additional documentation available.' }}</p>
|
|
|
|
<h2>Installation</h2>
|
|
<pre><code>smarttools install {{ tool.owner }}/{{ tool.name }}</code></pre>
|
|
|
|
<h2>Usage</h2>
|
|
<pre><code>{{ tool.name }} --help</code></pre>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Sidebar -->
|
|
<aside class="mt-8 lg:mt-0">
|
|
<div class="sticky top-4 space-y-6">
|
|
<!-- Install Card -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Install</h3>
|
|
<div class="flex items-center bg-gray-100 rounded-lg p-3 group">
|
|
<span class="text-gray-400 mr-2 select-none">$</span>
|
|
<code id="install-cmd" class="flex-1 text-sm text-gray-800 font-mono overflow-x-auto">smarttools install {{ tool.owner }}/{{ tool.name }}</code>
|
|
<button type="button"
|
|
onclick="copyInstall()"
|
|
class="ml-2 p-1.5 text-gray-400 hover:text-gray-600"
|
|
aria-label="Copy install command">
|
|
<svg id="copy-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
</svg>
|
|
<svg id="check-icon" class="w-4 h-4 hidden text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Versions -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Versions</h3>
|
|
<ul class="space-y-2">
|
|
{% for version in versions %}
|
|
<li class="flex items-center justify-between">
|
|
<a href="{{ url_for('web.tool_version', owner=tool.owner, name=tool.name, version=version.version) }}"
|
|
class="text-sm {{ 'font-medium text-indigo-600' if version.version == tool.version else 'text-gray-600 hover:text-indigo-600' }}">
|
|
v{{ version.version }}
|
|
{% if version.version == tool.version %}
|
|
<span class="ml-1 text-xs text-green-600">(current)</span>
|
|
{% endif %}
|
|
</a>
|
|
<span class="text-xs text-gray-400">{{ version.published_at|timeago }}</span>
|
|
</li>
|
|
{% else %}
|
|
<li class="text-sm text-gray-500">v{{ tool.version }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Stats</h3>
|
|
<dl class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<dt class="text-sm text-gray-500">Downloads</dt>
|
|
<dd class="text-sm font-medium text-gray-900">{{ tool.downloads|format_number }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<dt class="text-sm text-gray-500">Published</dt>
|
|
<dd class="text-sm font-medium text-gray-900">{{ tool.published_at|timeago }}</dd>
|
|
</div>
|
|
{% if tool.category %}
|
|
<div class="flex items-center justify-between">
|
|
<dt class="text-sm text-gray-500">Category</dt>
|
|
<dd>
|
|
<a href="{{ url_for('web.category', name=tool.category) }}"
|
|
class="text-sm font-medium text-indigo-600 hover:text-indigo-800">
|
|
{{ tool.category }}
|
|
</a>
|
|
</dd>
|
|
</div>
|
|
{% endif %}
|
|
</dl>
|
|
</div>
|
|
|
|
<!-- Publisher -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Publisher</h3>
|
|
<a href="{{ url_for('web.publisher', slug=tool.owner) }}"
|
|
class="flex items-center group">
|
|
<div class="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center">
|
|
<span class="text-gray-600 font-medium">{{ tool.owner[0]|upper }}</span>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-gray-900 group-hover:text-indigo-600">
|
|
{{ publisher.display_name if publisher else tool.owner }}
|
|
</p>
|
|
<p class="text-xs text-gray-500">@{{ tool.owner }}</p>
|
|
</div>
|
|
{% if publisher and publisher.verified %}
|
|
<svg class="ml-auto w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
</svg>
|
|
{% endif %}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Report Tool -->
|
|
<div class="text-center">
|
|
<button type="button"
|
|
onclick="openReportModal()"
|
|
class="text-sm text-gray-500 hover:text-red-600 transition-colors">
|
|
Report this tool
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Report Modal -->
|
|
<div id="report-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
|
|
<div class="min-h-screen px-4 text-center">
|
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeReportModal()"></div>
|
|
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
|
|
<form action="/api/v1/reports" method="POST" id="report-form">
|
|
<div class="p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900">Report Tool</h3>
|
|
<p class="mt-2 text-sm text-gray-600">
|
|
Help us maintain a safe registry by reporting tools that violate our guidelines.
|
|
</p>
|
|
|
|
<input type="hidden" name="tool_id" value="{{ tool.id }}">
|
|
|
|
<div class="mt-6 space-y-4">
|
|
<div>
|
|
<label for="reason" class="block text-sm font-medium text-gray-700 mb-1">Reason</label>
|
|
<select name="reason" id="reason" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
|
|
<option value="">Select a reason...</option>
|
|
<option value="malicious">Malicious code</option>
|
|
<option value="spam">Spam or advertising</option>
|
|
<option value="copyright">Copyright violation</option>
|
|
<option value="inappropriate">Inappropriate content</option>
|
|
<option value="broken">Broken or non-functional</option>
|
|
<option value="other">Other</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="details" class="block text-sm font-medium text-gray-700 mb-1">Details (optional)</label>
|
|
<textarea name="details" id="details" rows="3"
|
|
placeholder="Provide additional context..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 resize-none"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
|
|
<button type="button"
|
|
onclick="closeReportModal()"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
|
|
Cancel
|
|
</button>
|
|
<button type="submit"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
|
|
Submit Report
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function copyInstall() {
|
|
const cmd = 'smarttools install {{ tool.owner }}/{{ tool.name }}';
|
|
navigator.clipboard.writeText(cmd).then(() => {
|
|
const copyIcon = document.getElementById('copy-icon');
|
|
const checkIcon = document.getElementById('check-icon');
|
|
copyIcon.classList.add('hidden');
|
|
checkIcon.classList.remove('hidden');
|
|
setTimeout(() => {
|
|
copyIcon.classList.remove('hidden');
|
|
checkIcon.classList.add('hidden');
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function openReportModal() {
|
|
document.getElementById('report-modal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeReportModal() {
|
|
document.getElementById('report-modal').classList.add('hidden');
|
|
}
|
|
|
|
// Handle report form submission
|
|
document.getElementById('report-form').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(this);
|
|
const data = Object.fromEntries(formData);
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/reports', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (response.ok) {
|
|
closeReportModal();
|
|
alert('Thank you for your report. We will review it shortly.');
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.error || 'Failed to submit report. Please try again.');
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to submit report. Please try again.');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|