Rename project from SmartTools to CmdForge
Major rename operation: - Rename Python package: smarttools -> cmdforge - Update CLI entry point: smarttools -> cmdforge - Update all imports and module references - Update pyproject.toml with new name, URLs, entry point - Update all HTML templates with new branding - Update documentation (CLAUDE.md, README.md, docs/*, wiki/*) - Update environment variables: - SMARTTOOLS_ENV -> CMDFORGE_ENV - SMARTTOOLS_REGISTRY_DB -> CMDFORGE_REGISTRY_DB - SMARTTOOLS_TOKEN -> CMDFORGE_TOKEN - SMARTTOOLS_REGISTRY_* -> CMDFORGE_REGISTRY_* - Update Dockerfile and docker-compose.yml - Update tests to use new package name - Update scripts and examples - Update package.json and tailwind.config.js All 158 unit tests pass. The CLI is working correctly with the new cmdforge command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}CmdForge{% endblock %} - Build Custom AI Commands</title>
|
||||
|
||||
<!-- Meta tags -->
|
||||
<meta name="description" content="{% block meta_description %}Create Unix-style pipeable tools that work with any AI provider. Provider-agnostic, composable, and community-driven.{% endblock %}">
|
||||
{% block meta_extra %}{% endblock %}
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="{% block og_title %}CmdForge{% endblock %}">
|
||||
<meta property="og:description" content="{% block og_description %}Build custom AI commands in YAML{% endblock %}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.url }}">
|
||||
{% block og_extra %}{% endblock %}
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{% block twitter_title %}CmdForge{% endblock %}">
|
||||
<meta name="twitter:description" content="{% block twitter_description %}Build custom AI commands in YAML{% endblock %}">
|
||||
{% block twitter_extra %}{% endblock %}
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href="{% block canonical %}{{ request.url }}{% endblock %}">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='css/main.css') }}">
|
||||
{% block styles %}{% endblock %}
|
||||
|
||||
<!-- Schema.org -->
|
||||
{% block schema %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "CmdForge",
|
||||
"url": "{{ request.host_url }}",
|
||||
"description": "Build custom AI commands in YAML"
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="min-h-screen bg-gray-50 text-gray-900 font-sans antialiased" data-analytics-consent="{{ 'true' if session.get('consent_analytics') else 'false' }}">
|
||||
<!-- Skip to content link for accessibility -->
|
||||
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-indigo-600 text-white px-4 py-2 rounded-md z-50">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
<!-- Header -->
|
||||
{% include "components/header.html" %}
|
||||
|
||||
<!-- Main content -->
|
||||
<main id="main-content" class="{% block main_class %}{% endblock %}">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
{% include "components/footer.html" %}
|
||||
|
||||
<!-- Cookie consent banner (if not consented) -->
|
||||
{% if not session.get('consent_given') %}
|
||||
{% include "components/consent_banner.html" %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="{{ url_for('web.static', filename='js/main.js') }}" defer></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
{# Callout/alert macros for documentation and content pages #}
|
||||
|
||||
{# Info callout #}
|
||||
{% macro info(title='Note', content='') %}
|
||||
<div class="my-4 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-r-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-blue-800">{{ title }}</h4>
|
||||
<p class="mt-1 text-sm text-blue-700">{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Warning callout #}
|
||||
{% macro warning(title='Warning', content='') %}
|
||||
<div class="my-4 p-4 bg-amber-50 border-l-4 border-amber-500 rounded-r-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-amber-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-amber-800">{{ title }}</h4>
|
||||
<p class="mt-1 text-sm text-amber-700">{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Error callout #}
|
||||
{% macro error(title='Important', content='') %}
|
||||
<div class="my-4 p-4 bg-red-50 border-l-4 border-red-500 rounded-r-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-red-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-red-800">{{ title }}</h4>
|
||||
<p class="mt-1 text-sm text-red-700">{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Tip callout #}
|
||||
{% macro tip(title='Tip', content='') %}
|
||||
<div class="my-4 p-4 bg-green-50 border-l-4 border-green-500 rounded-r-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-green-800">{{ title }}</h4>
|
||||
<p class="mt-1 text-sm text-green-700">{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{# Code block macro with syntax highlighting and copy button #}
|
||||
|
||||
{% macro code(content, language='', filename='', show_line_numbers=false) %}
|
||||
<div class="my-4 relative group">
|
||||
{% if filename %}
|
||||
<div class="bg-gray-800 text-gray-400 text-xs px-4 py-2 rounded-t-lg border-b border-gray-700">
|
||||
{{ filename }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="relative">
|
||||
<pre class="{% if filename %}rounded-t-none{% endif %} bg-gray-900 text-gray-100 text-sm p-4 rounded-lg overflow-x-auto"><code class="language-{{ language }}">{{ content }}</code></pre>
|
||||
<button type="button"
|
||||
onclick="copyCode(this)"
|
||||
data-code="{{ content|e }}"
|
||||
class="absolute top-2 right-2 p-2 text-gray-400 hover:text-white bg-gray-800 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label="Copy code">
|
||||
<svg class="w-4 h-4 copy-icon" 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 class="w-4 h-4 check-icon hidden" 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>
|
||||
{% endmacro %}
|
||||
|
||||
{# Inline code #}
|
||||
{% macro inline(content) %}
|
||||
<code class="px-1.5 py-0.5 bg-gray-100 text-gray-800 text-sm font-mono rounded">{{ content }}</code>
|
||||
{% endmacro %}
|
||||
|
||||
{# Install command (special styling) #}
|
||||
{% macro install(command) %}
|
||||
<div class="my-4 flex items-center bg-gray-100 rounded-lg p-4 group">
|
||||
<span class="text-gray-400 mr-2 select-none">$</span>
|
||||
<code class="flex-1 text-gray-800 font-mono text-sm">{{ command }}</code>
|
||||
<button type="button"
|
||||
onclick="copyToClipboard('{{ command }}')"
|
||||
class="ml-4 p-2 text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label="Copy command">
|
||||
<svg class="w-5 h-5" 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>
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,148 @@
|
||||
{# Cookie consent banner #}
|
||||
<div id="consent-banner"
|
||||
class="fixed bottom-0 inset-x-0 z-50 bg-white border-t border-gray-200 shadow-lg transform transition-transform duration-300"
|
||||
role="dialog"
|
||||
aria-labelledby="consent-title"
|
||||
aria-describedby="consent-description">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<h3 id="consent-title" class="text-sm font-semibold text-gray-900">
|
||||
We value your privacy
|
||||
</h3>
|
||||
<p id="consent-description" class="mt-1 text-sm text-gray-600">
|
||||
We use cookies to improve your experience and for analytics.
|
||||
You can choose to accept all cookies or manage your preferences.
|
||||
<a href="{{ url_for('web.privacy') }}" class="text-indigo-600 hover:text-indigo-800 underline">Learn more</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<button type="button"
|
||||
onclick="openConsentPreferences()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Manage preferences
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="rejectConsent()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||
Reject all
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="acceptAllConsent()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors">
|
||||
Accept all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consent preferences modal -->
|
||||
<div id="consent-modal"
|
||||
class="hidden fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-labelledby="consent-modal-title">
|
||||
<div class="min-h-screen px-4 text-center">
|
||||
<!-- Backdrop -->
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeConsentPreferences()"></div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="inline-block w-full max-w-lg my-8 text-left align-middle transition-all transform bg-white shadow-xl rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 id="consent-modal-title" class="text-lg font-semibold text-gray-900">
|
||||
Cookie Preferences
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Choose which cookies you want to accept. Essential cookies cannot be disabled as they are required for the site to function.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<!-- Essential cookies (always on) -->
|
||||
<div class="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<label for="consent-essential" class="text-sm font-medium text-gray-900">Essential cookies</label>
|
||||
<p class="mt-1 text-sm text-gray-500">Required for the website to function properly.</p>
|
||||
</div>
|
||||
<input type="checkbox" id="consent-essential" checked disabled aria-label="Essential cookies (always enabled)" class="mt-1 h-4 w-4 text-indigo-600 border-gray-300 rounded">
|
||||
</div>
|
||||
|
||||
<!-- Analytics cookies -->
|
||||
<div class="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<label for="consent-analytics" class="text-sm font-medium text-gray-900 cursor-pointer">Analytics cookies</label>
|
||||
<p class="mt-1 text-sm text-gray-500">Help us understand how visitors use our site.</p>
|
||||
</div>
|
||||
<input type="checkbox"
|
||||
id="consent-analytics"
|
||||
name="analytics"
|
||||
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
</div>
|
||||
|
||||
<!-- Advertising cookies -->
|
||||
<div class="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<label for="consent-ads" class="text-sm font-medium text-gray-900 cursor-pointer">Advertising cookies</label>
|
||||
<p class="mt-1 text-sm text-gray-500">Used to show relevant ads and support the project.</p>
|
||||
</div>
|
||||
<input type="checkbox"
|
||||
id="consent-ads"
|
||||
name="ads"
|
||||
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
|
||||
<button type="button"
|
||||
onclick="closeConsentPreferences()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="saveConsentPreferences()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors">
|
||||
Save preferences
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function acceptAllConsent() {
|
||||
saveConsent(true, true);
|
||||
}
|
||||
|
||||
function rejectConsent() {
|
||||
saveConsent(false, false);
|
||||
}
|
||||
|
||||
function openConsentPreferences() {
|
||||
document.getElementById('consent-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeConsentPreferences() {
|
||||
document.getElementById('consent-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function saveConsentPreferences() {
|
||||
const analytics = document.getElementById('consent-analytics').checked;
|
||||
const ads = document.getElementById('consent-ads').checked;
|
||||
saveConsent(analytics, ads);
|
||||
closeConsentPreferences();
|
||||
}
|
||||
|
||||
function saveConsent(analytics, ads) {
|
||||
fetch('/api/v1/consent', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({analytics: analytics, ads: ads})
|
||||
}).then(() => {
|
||||
document.getElementById('consent-banner').style.transform = 'translateY(100%)';
|
||||
setTimeout(() => {
|
||||
document.getElementById('consent-banner').remove();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
{# Contributor card macro #}
|
||||
{% macro contributor_card(contributor=none, display_name=none, slug=none, bio=none, website=none, verified=none, featured=false) %}
|
||||
{% if contributor is none %}
|
||||
{% set contributor = {
|
||||
"display_name": display_name,
|
||||
"slug": slug,
|
||||
"bio": bio,
|
||||
"website": website,
|
||||
"verified": verified
|
||||
} %}
|
||||
{% endif %}
|
||||
{% if featured %}
|
||||
<!-- Featured contributor (horizontal layout) -->
|
||||
<article class="bg-white rounded-lg border border-gray-200 shadow-sm p-6 flex items-center space-x-6">
|
||||
<!-- Avatar -->
|
||||
<div class="flex-shrink-0 w-16 h-16 bg-gradient-to-br from-indigo-500 to-cyan-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-2xl font-bold">{{ contributor.display_name[0]|upper }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-xl font-semibold text-gray-900">{{ contributor.display_name }}</h3>
|
||||
<p class="text-sm text-gray-500">@{{ contributor.slug }}</p>
|
||||
<p class="mt-2 text-gray-600 line-clamp-2">{{ contributor.bio or 'Active community member and tool creator.' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex-shrink-0">
|
||||
<a href="{{ url_for('web.publisher_profile', slug=contributor.slug) }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-indigo-600 text-sm font-medium rounded-md text-indigo-600 hover:bg-indigo-50 transition-colors">
|
||||
View Profile
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<!-- Regular contributor card (vertical layout) -->
|
||||
<article class="bg-white rounded-lg border border-gray-200 shadow-sm p-4 text-center">
|
||||
<!-- Avatar -->
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-cyan-500 rounded-full flex items-center justify-center mx-auto">
|
||||
<span class="text-white text-lg font-bold">{{ contributor.display_name[0]|upper }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<h3 class="mt-3 font-semibold text-gray-900">{{ contributor.display_name }}</h3>
|
||||
<p class="text-sm text-gray-500">@{{ contributor.slug }}</p>
|
||||
|
||||
{% if contributor.bio %}
|
||||
<p class="mt-2 text-sm text-gray-600 line-clamp-2">{{ contributor.bio }}</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Link -->
|
||||
<a href="{{ url_for('web.publisher_profile', slug=contributor.slug) }}"
|
||||
class="mt-3 inline-block text-sm font-medium text-indigo-600 hover:text-indigo-800 transition-colors">
|
||||
View Profile
|
||||
</a>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,60 @@
|
||||
<footer class="bg-slate-800 text-white mt-auto">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<!-- Brand -->
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<a href="{{ url_for('web.index') }}" class="text-xl font-bold">CmdForge</a>
|
||||
<p class="mt-2 text-gray-400 text-sm max-w-md">
|
||||
Build custom AI commands in YAML. Create Unix-style pipeable tools that work with any AI provider.
|
||||
Provider-agnostic, composable, and community-driven.
|
||||
</p>
|
||||
<div class="mt-4 flex space-x-4">
|
||||
<a href="https://github.com/rob/cmdforge" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-white transition-colors" aria-label="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://twitter.com/cmdforge" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-white transition-colors" aria-label="Twitter">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wider">Resources</h3>
|
||||
<ul class="mt-4 space-y-2">
|
||||
<li><a href="{{ url_for('web.docs') }}" class="text-gray-400 hover:text-white text-sm">Documentation</a></li>
|
||||
<li><a href="{{ url_for('web.tutorials') }}" class="text-gray-400 hover:text-white text-sm">Tutorials</a></li>
|
||||
<li><a href="{{ url_for('web.tools_browse') }}" class="text-gray-400 hover:text-white text-sm">Registry</a></li>
|
||||
<li><a href="{{ url_for('forum.index') }}" class="text-gray-400 hover:text-white text-sm">Forum</a></li>
|
||||
<li><a href="{{ url_for('web.community') }}" class="text-gray-400 hover:text-white text-sm">Community</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Legal -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wider">Legal</h3>
|
||||
<ul class="mt-4 space-y-2">
|
||||
<li><a href="{{ url_for('web.about') }}" class="text-gray-400 hover:text-white text-sm">About</a></li>
|
||||
<li><a href="{{ url_for('web.privacy') }}" class="text-gray-400 hover:text-white text-sm">Privacy Policy</a></li>
|
||||
<li><a href="{{ url_for('web.terms') }}" class="text-gray-400 hover:text-white text-sm">Terms of Service</a></li>
|
||||
<li><a href="{{ url_for('web.donate') }}" class="text-gray-400 hover:text-white text-sm">Donate</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-8 border-t border-slate-700 flex flex-col sm:flex-row justify-between items-center">
|
||||
<p class="text-gray-400 text-sm">
|
||||
© {{ now().year }} CmdForge. Open source under MIT License.
|
||||
</p>
|
||||
<p class="text-gray-300 text-xs mt-2 sm:mt-0">
|
||||
Made with care for the developer community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,169 @@
|
||||
{# Reusable form macros #}
|
||||
|
||||
{# Text input field #}
|
||||
{% macro text_input(name, label, type='text', placeholder='', value='', required=false, error=none, help=none) %}
|
||||
<div class="mb-4">
|
||||
<label for="{{ name }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ label }}
|
||||
{% if required %}<span class="text-red-500">*</span>{% endif %}
|
||||
</label>
|
||||
<input type="{{ type }}"
|
||||
name="{{ name }}"
|
||||
id="{{ name }}"
|
||||
value="{{ value }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
{% if required %}required{% endif %}
|
||||
class="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors
|
||||
{% if error %}border-red-500 bg-red-50{% else %}border-gray-300{% endif %}"
|
||||
aria-describedby="{{ name }}-help {{ name }}-error">
|
||||
{% if help %}
|
||||
<p id="{{ name }}-help" class="mt-1 text-sm text-gray-500">{{ help }}</p>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<p id="{{ name }}-error" class="mt-1 text-sm text-red-600" role="alert">{{ error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Textarea field #}
|
||||
{% macro textarea(name, label, placeholder='', value='', required=false, rows=4, error=none, help=none) %}
|
||||
<div class="mb-4">
|
||||
<label for="{{ name }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ label }}
|
||||
{% if required %}<span class="text-red-500">*</span>{% endif %}
|
||||
</label>
|
||||
<textarea name="{{ name }}"
|
||||
id="{{ name }}"
|
||||
rows="{{ rows }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
{% if required %}required{% endif %}
|
||||
class="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors resize-y
|
||||
{% if error %}border-red-500 bg-red-50{% else %}border-gray-300{% endif %}"
|
||||
aria-describedby="{{ name }}-help {{ name }}-error">{{ value }}</textarea>
|
||||
{% if help %}
|
||||
<p id="{{ name }}-help" class="mt-1 text-sm text-gray-500">{{ help }}</p>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<p id="{{ name }}-error" class="mt-1 text-sm text-red-600" role="alert">{{ error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Select dropdown #}
|
||||
{% macro select(name, label, options, selected='', required=false, error=none, help=none) %}
|
||||
<div class="mb-4">
|
||||
<label for="{{ name }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ label }}
|
||||
{% if required %}<span class="text-red-500">*</span>{% endif %}
|
||||
</label>
|
||||
<select name="{{ name }}"
|
||||
id="{{ name }}"
|
||||
{% if required %}required{% endif %}
|
||||
class="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors bg-white
|
||||
{% if error %}border-red-500 bg-red-50{% else %}border-gray-300{% endif %}"
|
||||
aria-describedby="{{ name }}-help {{ name }}-error">
|
||||
{% for value, label in options %}
|
||||
<option value="{{ value }}" {% if value == selected %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if help %}
|
||||
<p id="{{ name }}-help" class="mt-1 text-sm text-gray-500">{{ help }}</p>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<p id="{{ name }}-error" class="mt-1 text-sm text-red-600" role="alert">{{ error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Checkbox #}
|
||||
{% macro checkbox(name, label, checked=false, help=none) %}
|
||||
<div class="mb-4 flex items-start">
|
||||
<input type="checkbox"
|
||||
name="{{ name }}"
|
||||
id="{{ name }}"
|
||||
{% if checked %}checked{% endif %}
|
||||
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
<div class="ml-3">
|
||||
<label for="{{ name }}" class="text-sm font-medium text-gray-700">{{ label }}</label>
|
||||
{% if help %}
|
||||
<p class="text-sm text-gray-500">{{ help }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Primary button #}
|
||||
{% macro button_primary(text, type='submit', name=none, disabled=false, full_width=false) %}
|
||||
<button type="{{ type }}"
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
class="{% if full_width %}w-full{% endif %} inline-flex justify-center items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{{ text }}
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{# Secondary button #}
|
||||
{% macro button_secondary(text, type='button', name=none, disabled=false, full_width=false) %}
|
||||
<button type="{{ type }}"
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
class="{% if full_width %}w-full{% endif %} inline-flex justify-center items-center px-6 py-3 border-2 border-cyan-500 text-base font-medium rounded-md text-cyan-600 bg-transparent hover:bg-cyan-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{{ text }}
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{# Danger button #}
|
||||
{% macro button_danger(text, type='button', name=none, disabled=false) %}
|
||||
<button type="{{ type }}"
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
class="inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{{ text }}
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{# Ghost/link button #}
|
||||
{% macro button_ghost(text, href='#') %}
|
||||
<a href="{{ href }}"
|
||||
class="inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800 transition-colors">
|
||||
{{ text }}
|
||||
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{# Form error alert #}
|
||||
{% macro form_errors(errors) %}
|
||||
{% if errors %}
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg" role="alert">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-red-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">There were errors with your submission</h3>
|
||||
<ul class="mt-2 text-sm text-red-700 list-disc list-inside">
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Success alert #}
|
||||
{% macro success_alert(message) %}
|
||||
{% if message %}
|
||||
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg" role="alert">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<p class="ml-3 text-sm font-medium text-green-800">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,192 @@
|
||||
<header class="bg-slate-800 text-white sticky top-0 z-40">
|
||||
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" aria-label="Main navigation">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo -->
|
||||
<div class="flex-shrink-0">
|
||||
<a href="{{ url_for('web.index') }}" class="text-xl font-bold hover:text-indigo-400 transition-colors">
|
||||
CmdForge
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<div class="hidden md:flex items-center space-x-1">
|
||||
<a href="{{ url_for('web.docs') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/docs') %}bg-slate-700{% endif %}">
|
||||
Docs
|
||||
</a>
|
||||
<a href="{{ url_for('web.tutorials') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/tutorials') %}bg-slate-700{% endif %}">
|
||||
Tutorials
|
||||
</a>
|
||||
<a href="{{ url_for('web.tools_browse') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/tools') %}bg-slate-700{% endif %}">
|
||||
Registry
|
||||
</a>
|
||||
<a href="{{ url_for('forum.index') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/forum') %}bg-slate-700{% endif %}">
|
||||
Forum
|
||||
</a>
|
||||
<a href="{{ url_for('web.community') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/community') %}bg-slate-700{% endif %}">
|
||||
Community
|
||||
</a>
|
||||
<a href="{{ url_for('web.about') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path == '/about' %}bg-slate-700{% endif %}">
|
||||
About
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Right side: Search + Auth -->
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
<!-- Search button -->
|
||||
<button type="button"
|
||||
onclick="openSearch()"
|
||||
class="p-2 rounded-md hover:bg-slate-700 transition-colors"
|
||||
aria-label="Search">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Donate button -->
|
||||
<a href="{{ url_for('web.donate') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium bg-indigo-600 hover:bg-indigo-700 transition-colors">
|
||||
Donate
|
||||
</a>
|
||||
|
||||
<!-- Auth links -->
|
||||
{% if session.get('user') %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open"
|
||||
class="flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-slate-700 transition-colors">
|
||||
<span class="text-sm font-medium">{{ session.user.display_name }}</span>
|
||||
<svg 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="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="open"
|
||||
@click.away="open = false"
|
||||
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
|
||||
<a href="{{ url_for('web.dashboard') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Dashboard</a>
|
||||
<a href="{{ url_for('web.dashboard_settings') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a>
|
||||
<hr class="my-1">
|
||||
<form action="{{ url_for('web.logout') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ url_for('web.login') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors">
|
||||
Sign in
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<div class="md:hidden">
|
||||
<button type="button"
|
||||
onclick="toggleMobileMenu()"
|
||||
class="p-2 rounded-md hover:bg-slate-700 transition-colors"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu"
|
||||
aria-label="Toggle menu">
|
||||
<svg id="menu-icon-open" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
<svg id="menu-icon-close" class="w-6 h-6 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div id="mobile-menu" class="hidden md:hidden pb-4">
|
||||
<div class="space-y-1">
|
||||
<a href="{{ url_for('web.docs') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/docs') %}bg-slate-700{% endif %}">
|
||||
Docs
|
||||
</a>
|
||||
<a href="{{ url_for('web.tutorials') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/tutorials') %}bg-slate-700{% endif %}">
|
||||
Tutorials
|
||||
</a>
|
||||
<a href="{{ url_for('web.tools_browse') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/tools') %}bg-slate-700{% endif %}">
|
||||
Registry
|
||||
</a>
|
||||
<a href="{{ url_for('forum.index') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/forum') %}bg-slate-700{% endif %}">
|
||||
Forum
|
||||
</a>
|
||||
<a href="{{ url_for('web.community') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/community') %}bg-slate-700{% endif %}">
|
||||
Community
|
||||
</a>
|
||||
<a href="{{ url_for('web.about') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path == '/about' %}bg-slate-700{% endif %}">
|
||||
About
|
||||
</a>
|
||||
<hr class="border-slate-600 my-2">
|
||||
<a href="{{ url_for('web.donate') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium bg-indigo-600 hover:bg-indigo-700 mx-3">
|
||||
Donate
|
||||
</a>
|
||||
{% if session.get('user') %}
|
||||
<a href="{{ url_for('web.dashboard') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700">
|
||||
Dashboard
|
||||
</a>
|
||||
<form action="{{ url_for('web.logout') }}" method="POST" class="px-3 py-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="text-base font-medium hover:text-indigo-400">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{{ url_for('web.login') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700">
|
||||
Sign in
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Search modal (hidden by default) -->
|
||||
<div id="search-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true">
|
||||
<div class="min-h-screen px-4 text-center">
|
||||
<!-- Backdrop -->
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeSearch()"></div>
|
||||
|
||||
<!-- Modal content -->
|
||||
<div class="inline-block w-full max-w-2xl my-8 text-left align-middle transition-all transform bg-white shadow-xl rounded-lg">
|
||||
<div class="p-4">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<input type="text"
|
||||
id="search-input"
|
||||
placeholder="Search tools, docs, tutorials..."
|
||||
aria-label="Search tools, documentation, and tutorials"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
<div id="search-results" class="mt-4 max-h-96 overflow-y-auto">
|
||||
<!-- Search results will be inserted here -->
|
||||
<p class="text-sm text-gray-500 text-center py-8">Start typing to search...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 rounded-b-lg flex justify-between items-center text-sm text-gray-500">
|
||||
<span>Press <kbd class="px-2 py-1 bg-gray-200 rounded">ESC</kbd> to close</span>
|
||||
<button onclick="closeSearch()" class="text-gray-500 hover:text-gray-700">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
{# Tool card macro #}
|
||||
{% macro tool_card(tool=none, owner=none, name=none, description=none, category=none, downloads=none, version=none) %}
|
||||
{% if tool is none %}
|
||||
{% set tool = {
|
||||
"owner": owner,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"category": category,
|
||||
"downloads": downloads,
|
||||
"version": version
|
||||
} %}
|
||||
{% endif %}
|
||||
<article class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow p-4 relative">
|
||||
<!-- Category badge -->
|
||||
{% if tool.category %}
|
||||
<span class="absolute top-3 right-3 px-2 py-1 bg-cyan-500 text-white text-xs font-medium rounded">
|
||||
{{ tool.category }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tool icon and name -->
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-indigo-600 font-bold text-sm">{{ tool.name[0]|upper }}</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-gray-900 truncate">
|
||||
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
|
||||
class="hover:text-indigo-600 transition-colors">
|
||||
{{ tool.name }}
|
||||
</a>
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">by {{ tool.owner }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="mt-3 text-sm text-gray-600 line-clamp-2">
|
||||
{{ tool.description or 'No description available.' }}
|
||||
</p>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-gray-500">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
</svg>
|
||||
{{ tool.downloads|default(0) }} downloads
|
||||
</span>
|
||||
<span class="text-xs text-gray-600">v{{ tool.version }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Install command -->
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center bg-gray-100 rounded px-3 py-2 group">
|
||||
<code class="text-xs text-gray-700 flex-1 truncate font-mono">
|
||||
cmdforge install {{ tool.owner }}/{{ tool.name }}
|
||||
</code>
|
||||
<button type="button"
|
||||
onclick="copyToClipboard('cmdforge install {{ tool.owner }}/{{ tool.name }}')"
|
||||
class="ml-2 p-1 text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label="Copy install command">
|
||||
<svg 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{# Tutorial card macro #}
|
||||
{% macro tutorial_card(tutorial=none, title=none, description=none, href=none, slug=none, thumbnail=none, step_number=none) %}
|
||||
{% if tutorial is none %}
|
||||
{% set tutorial = {
|
||||
"slug": slug,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"thumbnail": thumbnail
|
||||
} %}
|
||||
{% endif %}
|
||||
{% if href is none and tutorial.slug %}
|
||||
{% set href = url_for('web.tutorial', slug=tutorial.slug) %}
|
||||
{% endif %}
|
||||
<article class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow overflow-hidden">
|
||||
<!-- Thumbnail (optional) -->
|
||||
{% if tutorial.thumbnail %}
|
||||
<div class="aspect-video bg-gray-100 relative">
|
||||
<img src="{{ tutorial.thumbnail }}"
|
||||
alt="{{ tutorial.title }}"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="aspect-video bg-gradient-to-br from-indigo-500 to-cyan-500 flex items-center justify-center">
|
||||
<svg class="w-12 h-12 text-white opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="p-4">
|
||||
{% if step_number %}
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-indigo-700 mb-2">Step {{ step_number }}</p>
|
||||
{% endif %}
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<a href="{{ href or '#' }}"
|
||||
class="hover:text-indigo-600 transition-colors">
|
||||
{{ tutorial.title }}
|
||||
</a>
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-600 line-clamp-2">
|
||||
{{ tutorial.description or 'Learn how to use CmdForge effectively.' }}
|
||||
</p>
|
||||
<a href="{{ href or '#' }}"
|
||||
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800 transition-colors">
|
||||
Read More
|
||||
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Dashboard Header -->
|
||||
<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-8">
|
||||
{% block dashboard_header %}
|
||||
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p class="mt-1 text-gray-600">Welcome back, {{ user.display_name }}</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside>
|
||||
<nav class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<a href="{{ url_for('web.dashboard') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'overview' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
Overview
|
||||
</a>
|
||||
<a href="{{ url_for('web.dashboard_tools') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'tools' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
My Tools
|
||||
</a>
|
||||
<a href="{{ url_for('web.dashboard_tokens') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'tokens' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
API Tokens
|
||||
</a>
|
||||
<a href="{{ url_for('web.dashboard_settings') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'settings' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
Settings
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- Quick Stats (mobile hidden) -->
|
||||
<div class="hidden lg:block mt-6 bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Quick Stats</h3>
|
||||
<dl class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm text-gray-600">Tools</dt>
|
||||
<dd class="text-sm font-medium text-gray-900">{{ stats.tools_count if stats else 0 }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm text-gray-600">Downloads</dt>
|
||||
<dd class="text-sm font-medium text-gray-900">{{ (stats.total_downloads if stats else 0)|format_number }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="lg:col-span-3">
|
||||
{% block dashboard_content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,135 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% set active_page = 'overview' %}
|
||||
|
||||
{% block title %}Dashboard - CmdForge{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-indigo-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500">Published Tools</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.tools_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-green-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500">Total Downloads</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.total_downloads|format_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-cyan-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500">API Tokens</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.tokens_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Tools -->
|
||||
<div class="bg-white rounded-lg border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Your Tools</h2>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
+ Publish New Tool
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if tools %}
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for tool in tools[:5] %}
|
||||
<li class="px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
|
||||
class="font-medium text-gray-900 hover:text-indigo-600">
|
||||
{{ tool.name }}
|
||||
</a>
|
||||
<p class="text-sm text-gray-500">v{{ tool.version }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-900">{{ tool.downloads|format_number }} downloads</p>
|
||||
<p class="text-xs text-gray-500">{{ tool.published_at|timeago }}</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if tools|length > 5 %}
|
||||
<div class="px-6 py-4 border-t border-gray-200">
|
||||
<a href="{{ url_for('web.dashboard_tools') }}"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
View all {{ tools|length }} tools
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="px-6 py-12 text-center">
|
||||
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<p class="mt-4 text-gray-600">You haven't published any tools yet.</p>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="mt-4 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Publish Your First Tool
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
{% if recent_activity %}
|
||||
<div class="mt-6 bg-white rounded-lg border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Recent Activity</h2>
|
||||
</div>
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for activity in recent_activity[:5] %}
|
||||
<li class="px-6 py-4 flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
{% if activity.type == 'download' %}
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% elif activity.type == 'publish' %}
|
||||
<div class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-900">{{ activity.description }}</p>
|
||||
<p class="text-xs text-gray-500">{{ activity.timestamp|timeago }}</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,254 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% from "components/forms.html" import text_input, textarea, button_primary, form_errors, success_alert %}
|
||||
{% set active_page = 'settings' %}
|
||||
|
||||
{% block title %}Settings - CmdForge Dashboard{% endblock %}
|
||||
|
||||
{% block dashboard_header %}
|
||||
<h1 class="text-2xl font-bold text-gray-900">Settings</h1>
|
||||
<p class="mt-1 text-gray-600">Manage your profile and account settings</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
{{ success_alert(success_message) }}
|
||||
{{ form_errors(errors) }}
|
||||
|
||||
<!-- Profile Settings -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Profile</h2>
|
||||
<p class="text-sm text-gray-600">This information will be displayed on your public profile.</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('web.dashboard_settings') }}" method="POST" class="p-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="form" value="profile">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="display_name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Display name
|
||||
</label>
|
||||
<input type="text"
|
||||
name="display_name"
|
||||
id="display_name"
|
||||
value="{{ user.display_name }}"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="slug" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Username
|
||||
</label>
|
||||
<div class="flex">
|
||||
<span class="inline-flex items-center px-3 text-gray-500 bg-gray-100 border border-r-0 border-gray-300 rounded-l-lg">
|
||||
@
|
||||
</span>
|
||||
<input type="text"
|
||||
name="slug"
|
||||
id="slug"
|
||||
value="{{ user.slug }}"
|
||||
disabled
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-r-lg bg-gray-50 text-gray-500">
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">Username cannot be changed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label for="bio" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bio
|
||||
</label>
|
||||
<textarea name="bio"
|
||||
id="bio"
|
||||
rows="3"
|
||||
placeholder="Tell others a bit about yourself..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-y">{{ user.bio or '' }}</textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">Brief description for your profile. Max 500 characters.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label for="website" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Website
|
||||
</label>
|
||||
<input type="url"
|
||||
name="website"
|
||||
id="website"
|
||||
value="{{ user.website or '' }}"
|
||||
placeholder="https://example.com"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Save Profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Email Settings -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Email</h2>
|
||||
<p class="text-sm text-gray-600">Your email address for account notifications.</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">{{ user.email }}</p>
|
||||
{% if user.verified %}
|
||||
<p class="text-xs text-green-600 flex items-center mt-1">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 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>
|
||||
Verified
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-xs text-amber-600 flex items-center mt-1">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Not verified
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not user.verified %}
|
||||
<button type="button"
|
||||
onclick="resendVerification()"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
Resend verification
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Change -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Change Password</h2>
|
||||
<p class="text-sm text-gray-600">Update your password to keep your account secure.</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('web.dashboard_settings') }}" method="POST" class="p-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="form" value="password">
|
||||
|
||||
<div class="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label for="current_password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Current password
|
||||
</label>
|
||||
<input type="password"
|
||||
name="current_password"
|
||||
id="current_password"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="new_password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
New password
|
||||
</label>
|
||||
<input type="password"
|
||||
name="new_password"
|
||||
id="new_password"
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<p class="mt-1 text-xs text-gray-500">Minimum 8 characters</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm new password
|
||||
</label>
|
||||
<input type="password"
|
||||
name="confirm_password"
|
||||
id="confirm_password"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div class="bg-white rounded-lg border border-red-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-red-200 bg-red-50">
|
||||
<h2 class="text-lg font-semibold text-red-800">Danger Zone</h2>
|
||||
<p class="text-sm text-red-600">Irreversible actions that affect your account.</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Delete Account</p>
|
||||
<p class="text-sm text-gray-500">Permanently delete your account and all associated data.</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
onclick="confirmDeleteAccount()"
|
||||
class="px-4 py-2 text-sm font-medium text-red-600 border border-red-300 rounded-md hover:bg-red-50">
|
||||
Delete Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function resendVerification() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/me/resend-verification', {method: 'POST'});
|
||||
if (response.ok) {
|
||||
alert('Verification email sent! Please check your inbox.');
|
||||
} else {
|
||||
alert('Failed to send verification email. Please try again later.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to send verification email. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteAccount() {
|
||||
const confirmed = confirm(
|
||||
'Are you absolutely sure you want to delete your account?\n\n' +
|
||||
'This will permanently delete:\n' +
|
||||
'- Your profile\n' +
|
||||
'- All your published tools\n' +
|
||||
'- All your API tokens\n\n' +
|
||||
'This action cannot be undone.'
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
const doubleConfirm = prompt('Type "DELETE" to confirm account deletion:');
|
||||
if (doubleConfirm === 'DELETE') {
|
||||
deleteAccount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/me', {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
window.location.href = '/?deleted=1';
|
||||
} else {
|
||||
alert('Failed to delete account. Please try again or contact support.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to delete account. Please try again or contact support.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,312 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% set active_page = 'tokens' %}
|
||||
|
||||
{% block title %}API Tokens - CmdForge Dashboard{% endblock %}
|
||||
|
||||
{% block dashboard_header %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">API Tokens</h1>
|
||||
<p class="mt-1 text-gray-600">Manage tokens for CLI and API access</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
onclick="openCreateTokenModal()"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<!-- Info Banner -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-blue-800">About API Tokens</h4>
|
||||
<p class="mt-1 text-sm text-blue-700">
|
||||
API tokens are used to authenticate with the CmdForge registry from the CLI.
|
||||
Use <code class="px-1 bg-blue-100 rounded">cmdforge auth login</code> to authenticate,
|
||||
or set the <code class="px-1 bg-blue-100 rounded">CMDFORGE_TOKEN</code> environment variable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if tokens %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Used
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for token in tokens %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-cyan-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-900">{{ token.name }}</p>
|
||||
<p class="text-xs text-gray-500 font-mono">st_...{{ token.token_suffix }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ token.created_at|date_format }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ token.last_used_at|timeago if token.last_used_at else 'Never' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if token.revoked_at %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-red-800 bg-red-100 rounded-full">
|
||||
Revoked
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
{% if not token.revoked_at %}
|
||||
<button type="button"
|
||||
onclick="revokeToken({{ token.id }}, '{{ token.name }}')"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Revoke
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-gray-400">Revoked {{ token.revoked_at|timeago }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No API tokens</h3>
|
||||
<p class="mt-2 text-gray-600 max-w-sm mx-auto">
|
||||
Create an API token to authenticate with the registry from the command line.
|
||||
</p>
|
||||
<button type="button"
|
||||
onclick="openCreateTokenModal()"
|
||||
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Create Your First Token
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Create Token Modal -->
|
||||
<div id="create-token-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" onclick="closeCreateTokenModal()"></div>
|
||||
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
|
||||
<form id="create-token-form" onsubmit="createToken(event)">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Create API Token</h3>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Give your token a descriptive name so you can identify it later.
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="token-name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Token name
|
||||
</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
id="token-name"
|
||||
required
|
||||
placeholder="e.g., Laptop CLI, CI/CD Pipeline"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
|
||||
<button type="button"
|
||||
onclick="closeCreateTokenModal()"
|
||||
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-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Created Modal (shows the token once) -->
|
||||
<div id="token-created-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"></div>
|
||||
<div class="inline-block w-full max-w-lg my-8 text-left align-middle bg-white shadow-xl rounded-lg">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-green-600" 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>
|
||||
</div>
|
||||
<h3 class="ml-3 text-lg font-semibold text-gray-900">Token Created</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-amber-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<p class="ml-3 text-sm text-amber-800">
|
||||
<strong>Copy your token now.</strong> You won't be able to see it again!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-100 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<code id="new-token-value" class="text-sm font-mono text-gray-800 break-all"></code>
|
||||
<button type="button"
|
||||
onclick="copyNewToken()"
|
||||
class="ml-4 p-2 text-gray-400 hover:text-gray-600 flex-shrink-0">
|
||||
<svg id="copy-token-icon" class="w-5 h-5" 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-token-icon" class="w-5 h-5 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>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-600">
|
||||
To use this token, run:
|
||||
</p>
|
||||
<pre class="mt-2 bg-gray-900 text-gray-100 text-sm p-3 rounded-lg overflow-x-auto"><code>export CMDFORGE_TOKEN="<span id="token-in-export"></span>"</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end">
|
||||
<button type="button"
|
||||
onclick="closeTokenCreatedModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openCreateTokenModal() {
|
||||
document.getElementById('create-token-modal').classList.remove('hidden');
|
||||
document.getElementById('token-name').focus();
|
||||
}
|
||||
|
||||
function closeCreateTokenModal() {
|
||||
document.getElementById('create-token-modal').classList.add('hidden');
|
||||
document.getElementById('token-name').value = '';
|
||||
}
|
||||
|
||||
async function createToken(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const name = form.querySelector('[name="name"]').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/dashboard/api/tokens', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: name})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
closeCreateTokenModal();
|
||||
showNewToken(result.data.token);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to create token');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to create token. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
function showNewToken(token) {
|
||||
document.getElementById('new-token-value').textContent = token;
|
||||
document.getElementById('token-in-export').textContent = token;
|
||||
document.getElementById('token-created-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeTokenCreatedModal() {
|
||||
document.getElementById('token-created-modal').classList.add('hidden');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function copyNewToken() {
|
||||
const token = document.getElementById('new-token-value').textContent;
|
||||
navigator.clipboard.writeText(token).then(() => {
|
||||
document.getElementById('copy-token-icon').classList.add('hidden');
|
||||
document.getElementById('check-token-icon').classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
document.getElementById('copy-token-icon').classList.remove('hidden');
|
||||
document.getElementById('check-token-icon').classList.add('hidden');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
async function revokeToken(tokenId, tokenName) {
|
||||
if (!confirm(`Are you sure you want to revoke "${tokenName}"? This cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/dashboard/api/tokens/${tokenId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to revoke token');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to revoke token. Please try again.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,259 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% set active_page = 'tools' %}
|
||||
|
||||
{% block title %}My Tools - CmdForge Dashboard{% endblock %}
|
||||
|
||||
{% block dashboard_header %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">My Tools</h1>
|
||||
<p class="mt-1 text-gray-600">Manage your published tools</p>
|
||||
</div>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Publish New Tool
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
{% if tools %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Tool
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Version
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Downloads
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for tool in tools %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-5 h-5 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">
|
||||
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
|
||||
class="text-sm font-medium text-gray-900 hover:text-indigo-600">
|
||||
{{ tool.name }}
|
||||
</a>
|
||||
<p class="text-sm text-gray-500 truncate max-w-xs">{{ tool.description or 'No description' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-gray-900">v{{ tool.version }}</span>
|
||||
<p class="text-xs text-gray-500">{{ tool.published_at|timeago }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ tool.downloads|format_number }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if tool.deprecated %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-amber-800 bg-amber-100 rounded-full">
|
||||
Deprecated
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900">View</a>
|
||||
<button type="button"
|
||||
onclick="openDeprecateModal('{{ tool.owner }}', '{{ tool.name }}', {{ 'true' if tool.deprecated else 'false' }})"
|
||||
class="text-gray-400 hover:text-amber-600">
|
||||
{% if tool.deprecated %}Restore{% else %}Deprecate{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pending PRs Section -->
|
||||
{% if pending_prs %}
|
||||
<div class="mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Pending Publications</h2>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for pr in pending_prs %}
|
||||
<li class="px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">{{ pr.owner }}/{{ pr.name }} v{{ pr.version }}</p>
|
||||
<p class="text-xs text-gray-500">Submitted {{ pr.created_at|timeago }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full">
|
||||
{{ pr.status }}
|
||||
</span>
|
||||
<a href="{{ pr.pr_url }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
View PR
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No tools yet</h3>
|
||||
<p class="mt-2 text-gray-600 max-w-sm mx-auto">
|
||||
You haven't published any tools. Create your first tool and share it with the community.
|
||||
</p>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Learn How to Publish
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Deprecate Modal -->
|
||||
<div id="deprecate-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" onclick="closeDeprecateModal()"></div>
|
||||
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
|
||||
<form id="deprecate-form" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="owner" id="deprecate-owner">
|
||||
<input type="hidden" name="name" id="deprecate-name">
|
||||
|
||||
<div class="p-6">
|
||||
<h3 id="deprecate-title" class="text-lg font-semibold text-gray-900">Deprecate Tool</h3>
|
||||
<p id="deprecate-desc" class="mt-2 text-sm text-gray-600">
|
||||
Mark this tool as deprecated. Users will see a warning when viewing or installing it.
|
||||
</p>
|
||||
|
||||
<div id="deprecate-fields" class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label for="deprecated_message" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Deprecation message (optional)
|
||||
</label>
|
||||
<textarea name="deprecated_message"
|
||||
id="deprecated_message"
|
||||
rows="2"
|
||||
placeholder="e.g., This tool has been superseded by..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="replacement" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Replacement tool (optional)
|
||||
</label>
|
||||
<input type="text"
|
||||
name="replacement"
|
||||
id="replacement"
|
||||
placeholder="owner/tool-name"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
|
||||
<button type="button"
|
||||
onclick="closeDeprecateModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit"
|
||||
id="deprecate-submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-md hover:bg-amber-700">
|
||||
Deprecate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let deprecateAction = '';
|
||||
|
||||
function openDeprecateModal(owner, name, isDeprecated) {
|
||||
document.getElementById('deprecate-owner').value = owner;
|
||||
document.getElementById('deprecate-name').value = name;
|
||||
|
||||
if (isDeprecated) {
|
||||
document.getElementById('deprecate-title').textContent = 'Restore Tool';
|
||||
document.getElementById('deprecate-desc').textContent = 'Remove the deprecation notice from this tool.';
|
||||
document.getElementById('deprecate-fields').classList.add('hidden');
|
||||
document.getElementById('deprecate-submit').textContent = 'Restore';
|
||||
document.getElementById('deprecate-submit').classList.remove('bg-amber-600', 'hover:bg-amber-700');
|
||||
document.getElementById('deprecate-submit').classList.add('bg-green-600', 'hover:bg-green-700');
|
||||
deprecateAction = `/dashboard/api/tools/${owner}/${name}/undeprecate`;
|
||||
} else {
|
||||
document.getElementById('deprecate-title').textContent = 'Deprecate Tool';
|
||||
document.getElementById('deprecate-desc').textContent = 'Mark this tool as deprecated. Users will see a warning.';
|
||||
document.getElementById('deprecate-fields').classList.remove('hidden');
|
||||
document.getElementById('deprecate-submit').textContent = 'Deprecate';
|
||||
document.getElementById('deprecate-submit').classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||
document.getElementById('deprecate-submit').classList.add('bg-amber-600', 'hover:bg-amber-700');
|
||||
deprecateAction = `/dashboard/api/tools/${owner}/${name}/deprecate`;
|
||||
}
|
||||
|
||||
document.getElementById('deprecate-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDeprecateModal() {
|
||||
document.getElementById('deprecate-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('deprecate-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const message = document.getElementById('deprecated_message')?.value || '';
|
||||
const replacement = document.getElementById('replacement')?.value || '';
|
||||
|
||||
try {
|
||||
const response = await fetch(deprecateAction, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({deprecated_message: message, replacement: replacement})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert((error.error && error.error.message) || 'Failed to update tool status');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to update tool status. Please try again.');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page Not Found - CmdForge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[60vh] flex items-center justify-center px-4">
|
||||
<div class="text-center max-w-md">
|
||||
<p class="text-6xl font-bold text-indigo-600">404</p>
|
||||
<h1 class="mt-4 text-3xl font-bold text-gray-900">Page not found</h1>
|
||||
<p class="mt-4 text-gray-600">
|
||||
Sorry, we couldn't find the page you're looking for. It may have been moved or deleted.
|
||||
</p>
|
||||
<div class="mt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a href="{{ url_for('web.home') }}"
|
||||
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Go Home
|
||||
</a>
|
||||
<a href="{{ url_for('web.tools') }}"
|
||||
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Browse Tools
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Server Error - CmdForge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[60vh] flex items-center justify-center px-4">
|
||||
<div class="text-center max-w-md">
|
||||
<p class="text-6xl font-bold text-red-600">500</p>
|
||||
<h1 class="mt-4 text-3xl font-bold text-gray-900">Something went wrong</h1>
|
||||
<p class="mt-4 text-gray-600">
|
||||
We're sorry, but something went wrong on our end. Please try again later or contact support if the problem persists.
|
||||
</p>
|
||||
<div class="mt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a href="{{ url_for('web.home') }}"
|
||||
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Go Home
|
||||
</a>
|
||||
<a href="https://github.com/your-org/cmdforge/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Report Issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ category.name }} - Forum{% endblock %}
|
||||
|
||||
{% block meta_description %}{{ category.description }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Header -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="text-sm text-gray-500 mb-4">
|
||||
<a href="{{ url_for('forum.index') }}" class="hover:text-indigo-600">Forum</a>
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-gray-900">{{ category.name }}</span>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">{{ category.name }}</h1>
|
||||
<p class="mt-1 text-gray-600">{{ category.description }}</p>
|
||||
</div>
|
||||
<a href="{{ url_for('forum.new_topic', slug=category.slug) }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
New Topic
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
{% if topics %}
|
||||
<!-- Topics list -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg divide-y divide-gray-100">
|
||||
{% for topic in topics %}
|
||||
<a href="{{ url_for('forum.topic', topic_id=topic.id, slug=topic.slug) }}"
|
||||
class="block p-4 hover:bg-gray-50 transition-colors">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
{% if topic.is_pinned %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">
|
||||
Pinned
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if topic.is_locked %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Locked
|
||||
</span>
|
||||
{% endif %}
|
||||
<h3 class="font-medium text-gray-900 truncate">{{ topic.title }}</h3>
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-500">
|
||||
by {{ topic.author_name }} · {{ topic.created_at | timeago }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-shrink-0 text-right">
|
||||
<div class="text-sm text-gray-900">{{ topic.reply_count }} replies</div>
|
||||
<div class="text-xs text-gray-500">{{ topic.view_count }} views</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="mt-6 flex items-center justify-center gap-2">
|
||||
{% if page > 1 %}
|
||||
<a href="{{ url_for('forum.category', slug=category.slug, page=page-1) }}"
|
||||
class="px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 text-sm text-gray-700">
|
||||
Page {{ page }} of {{ total_pages }}
|
||||
</span>
|
||||
|
||||
{% if page < total_pages %}
|
||||
<a href="{{ url_for('forum.category', slug=category.slug, page=page+1) }}"
|
||||
class="px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Next
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- Empty state -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-8 text-center">
|
||||
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No topics yet</h3>
|
||||
<p class="mt-2 text-gray-600">Be the first to start a discussion in this category.</p>
|
||||
<a href="{{ url_for('forum.new_topic', slug=category.slug) }}"
|
||||
class="mt-4 inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
|
||||
Create the first topic
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,136 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Community Forum{% endblock %}
|
||||
|
||||
{% block meta_description %}Join the CmdForge community. Ask questions, share your projects, and connect with other users.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Hero -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Community Forum</h1>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Ask questions, share your projects, and connect with other CmdForge users.
|
||||
</p>
|
||||
<div class="mt-4 flex items-center gap-6 text-sm text-gray-500">
|
||||
<span>{{ stats.topics }} topics</span>
|
||||
<span>{{ stats.replies }} replies</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="lg:grid lg:grid-cols-3 lg:gap-8">
|
||||
<!-- Categories -->
|
||||
<div class="lg:col-span-2">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Categories</h2>
|
||||
<div class="space-y-3">
|
||||
{% for cat in categories %}
|
||||
<a href="{{ url_for('forum.category', slug=cat.slug) }}"
|
||||
class="block bg-white border border-gray-200 rounded-lg p-4 hover:border-indigo-300 hover:shadow-sm transition-all">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start space-x-3">
|
||||
<!-- Icon -->
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center
|
||||
{% if cat.icon == 'chat' %}bg-blue-100 text-blue-600
|
||||
{% elif cat.icon == 'question' %}bg-amber-100 text-amber-600
|
||||
{% elif cat.icon == 'star' %}bg-purple-100 text-purple-600
|
||||
{% elif cat.icon == 'lightbulb' %}bg-green-100 text-green-600
|
||||
{% elif cat.icon == 'book' %}bg-indigo-100 text-indigo-600
|
||||
{% else %}bg-gray-100 text-gray-600{% endif %}">
|
||||
{% if cat.icon == 'chat' %}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
||||
</svg>
|
||||
{% elif cat.icon == 'question' %}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
{% elif cat.icon == 'star' %}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
|
||||
</svg>
|
||||
{% elif cat.icon == 'lightbulb' %}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{% elif cat.icon == 'book' %}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">{{ cat.name }}</h3>
|
||||
<p class="text-sm text-gray-600 mt-0.5">{{ cat.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right text-sm text-gray-500 flex-shrink-0 ml-4">
|
||||
<div>{{ cat.topic_count }} topics</div>
|
||||
<div class="text-xs">{{ cat.reply_count }} replies</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="mt-8 lg:mt-0">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h2>
|
||||
{% if recent_topics %}
|
||||
<div class="bg-white border border-gray-200 rounded-lg divide-y divide-gray-100">
|
||||
{% for topic in recent_topics %}
|
||||
<a href="{{ url_for('forum.topic', topic_id=topic.id, slug=topic.slug) }}"
|
||||
class="block p-3 hover:bg-gray-50 transition-colors">
|
||||
<div class="font-medium text-gray-900 text-sm truncate">{{ topic.title }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
in {{ topic.category_name }} · {{ topic.reply_count }} replies
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 text-center text-gray-500">
|
||||
<p>No topics yet. Be the first to start a discussion!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="mt-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Quick Links</h2>
|
||||
<div class="space-y-2">
|
||||
<a href="{{ url_for('web.docs', path='getting-started') }}"
|
||||
class="flex items-center text-sm text-gray-600 hover:text-indigo-600">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Getting Started Guide
|
||||
</a>
|
||||
<a href="{{ url_for('web.tools') }}"
|
||||
class="flex items-center text-sm text-gray-600 hover:text-indigo-600">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
Browse Tools
|
||||
</a>
|
||||
<a href="https://gitea.brrd.tech/rob/CmdForge/issues"
|
||||
target="_blank" rel="noopener"
|
||||
class="flex items-center text-sm text-gray-600 hover:text-indigo-600">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Report a Bug
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,123 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Topic in {{ category.name }} - Forum{% endblock %}
|
||||
|
||||
{% block meta_description %}Start a new discussion in {{ category.name }}.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Header -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="text-sm text-gray-500 mb-4">
|
||||
<a href="{{ url_for('forum.index') }}" class="hover:text-indigo-600">Forum</a>
|
||||
<span class="mx-2">/</span>
|
||||
<a href="{{ url_for('forum.category', slug=category.slug) }}" class="hover:text-indigo-600">{{ category.name }}</a>
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-gray-900">New Topic</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-900">New Topic in {{ category.name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
{% if error %}
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md text-red-800">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<!-- Honeypot field (hidden, bots fill this) -->
|
||||
<div style="display: none;">
|
||||
<input type="text" name="website_url" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<!-- Author info -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label for="author_name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Your Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="author_name"
|
||||
name="author_name"
|
||||
required
|
||||
value="{{ form_data.author_name if form_data else '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="John Doe">
|
||||
</div>
|
||||
<div>
|
||||
<label for="author_email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email <span class="text-gray-400">(optional)</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
id="author_email"
|
||||
name="author_email"
|
||||
value="{{ form_data.author_email if form_data else '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="[email protected]">
|
||||
<p class="mt-1 text-xs text-gray-500">Used for Gravatar only, not displayed publicly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="mb-6">
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Topic Title <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
required
|
||||
value="{{ form_data.title if form_data else '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="How do I...?">
|
||||
<p class="mt-1 text-xs text-gray-500">Be specific and descriptive (5-200 characters).</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="mb-6">
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Content <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea id="content"
|
||||
name="content"
|
||||
rows="12"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm"
|
||||
placeholder="Describe your question, idea, or project in detail...">{{ form_data.content if form_data else '' }}</textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Markdown is supported: **bold**, *italic*, `code`, ```code blocks```, [links](url)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tips -->
|
||||
<div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<h3 class="text-sm font-medium text-blue-800 mb-2">Tips for a great post:</h3>
|
||||
<ul class="text-sm text-blue-700 space-y-1 list-disc list-inside">
|
||||
<li>Search first to avoid duplicate topics</li>
|
||||
<li>Use a clear, descriptive title</li>
|
||||
<li>Include relevant details and context</li>
|
||||
<li>For bugs, include steps to reproduce</li>
|
||||
<li>Format code with triple backticks: ```code```</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="{{ url_for('forum.category', slug=category.slug) }}"
|
||||
class="text-gray-600 hover:text-gray-900">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Create Topic
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,178 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ topic.title }} - Forum{% endblock %}
|
||||
|
||||
{% block meta_description %}{{ topic.content[:160] }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Header -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="text-sm text-gray-500 mb-4">
|
||||
<a href="{{ url_for('forum.index') }}" class="hover:text-indigo-600">Forum</a>
|
||||
<span class="mx-2">/</span>
|
||||
{% if category %}
|
||||
<a href="{{ url_for('forum.category', slug=category.slug) }}" class="hover:text-indigo-600">{{ category.name }}</a>
|
||||
<span class="mx-2">/</span>
|
||||
{% endif %}
|
||||
<span class="text-gray-900 truncate">{{ topic.title[:50] }}{% if topic.title|length > 50 %}...{% endif %}</span>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
{% if topic.is_pinned %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">
|
||||
Pinned
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if topic.is_locked %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Locked
|
||||
</span>
|
||||
{% endif %}
|
||||
<h1 class="text-2xl font-bold text-gray-900">{{ topic.title }}</h1>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
{{ topic.view_count }} views · {{ replies|length }} replies
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<!-- Flash messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-4 rounded-md {% if category == 'error' %}bg-red-50 text-red-800{% else %}bg-green-50 text-green-800{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Original post -->
|
||||
<article class="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<!-- Avatar placeholder -->
|
||||
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-semibold">
|
||||
{{ topic.author_name[0]|upper }}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div class="font-medium text-gray-900">{{ topic.author_name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ topic.created_at | timeago }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prose prose-sm max-w-none">
|
||||
{{ topic.content | markdown | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Replies -->
|
||||
{% if replies %}
|
||||
<div class="mt-6 space-y-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">{{ replies|length }} {% if replies|length == 1 %}Reply{% else %}Replies{% endif %}</h2>
|
||||
|
||||
{% for reply in replies %}
|
||||
<article class="bg-white border border-gray-200 rounded-lg overflow-hidden {% if reply.is_solution %}ring-2 ring-green-500{% endif %}" id="reply-{{ reply.id }}">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<!-- Avatar placeholder -->
|
||||
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 font-semibold">
|
||||
{{ reply.author_name[0]|upper }}
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-900">{{ reply.author_name }}</span>
|
||||
{% if reply.is_solution %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||||
Solution
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">{{ reply.created_at | timeago }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prose prose-sm max-w-none">
|
||||
{{ reply.content | markdown | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Reply form -->
|
||||
{% if not topic.is_locked %}
|
||||
<div class="mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Post a Reply</h2>
|
||||
<form method="POST" action="{{ url_for('forum.reply', topic_id=topic.id) }}" class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<!-- Honeypot field (hidden, bots fill this) -->
|
||||
<div style="display: none;">
|
||||
<input type="text" name="website_url" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label for="author_name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Your Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="author_name"
|
||||
name="author_name"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="John Doe">
|
||||
</div>
|
||||
<div>
|
||||
<label for="author_email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email <span class="text-gray-400">(optional)</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
id="author_email"
|
||||
name="author_email"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="[email protected]">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Your Reply <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea id="content"
|
||||
name="content"
|
||||
rows="6"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm"
|
||||
placeholder="Write your reply here... (Markdown supported)"></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Markdown is supported: **bold**, *italic*, `code`, ```code blocks```, [links](url)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Post Reply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-8 bg-gray-100 border border-gray-200 rounded-lg p-6 text-center">
|
||||
<svg class="mx-auto w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
<p class="mt-2 text-gray-600">This topic is locked and cannot receive new replies.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,138 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About - CmdForge{% endblock %}
|
||||
|
||||
{% block meta_description %}Learn about CmdForge, the open-source platform for building custom AI-powered command-line tools.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-white">
|
||||
<!-- Hero -->
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
|
||||
<h1 class="text-4xl font-bold text-gray-900">About CmdForge</h1>
|
||||
<p class="mt-6 text-xl text-gray-600">
|
||||
An open-source platform for building and sharing AI-powered command-line tools.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mission -->
|
||||
<div class="bg-gray-50 py-16">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Our Mission</h2>
|
||||
<div class="prose prose-lg prose-indigo max-w-none">
|
||||
<p>
|
||||
CmdForge was created with a simple belief: powerful AI tools should be accessible to everyone,
|
||||
not just those with extensive programming knowledge or expensive API subscriptions.
|
||||
</p>
|
||||
<p>
|
||||
We're building a <strong>universally accessible development ecosystem</strong> that empowers
|
||||
regular people to collaborate and build upon each other's progress rather than compete.
|
||||
</p>
|
||||
<p>
|
||||
Our platform follows the Unix philosophy: simple, composable tools that do one thing well.
|
||||
With CmdForge, you can create custom AI commands using simple YAML configuration,
|
||||
chain them together, and share them with the community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Values -->
|
||||
<div class="py-16">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-8">Our Values</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Open Source</h3>
|
||||
<p class="text-gray-600">
|
||||
CmdForge is MIT licensed and open source. We believe in transparency and community ownership.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div class="w-12 h-12 bg-cyan-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Community First</h3>
|
||||
<p class="text-gray-600">
|
||||
We prioritize collaboration over competition. Share tools, learn from others, build together.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Privacy Respecting</h3>
|
||||
<p class="text-gray-600">
|
||||
Your data stays yours. We collect minimal analytics and never sell user information.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Provider Agnostic</h3>
|
||||
<p class="text-gray-600">
|
||||
Works with any AI provider. Use Claude, GPT, local models, or any CLI-accessible AI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sustainability -->
|
||||
<div class="bg-gray-50 py-16">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Sustainability</h2>
|
||||
<div class="prose prose-lg prose-indigo max-w-none">
|
||||
<p>
|
||||
CmdForge is committed to long-term sustainability. Revenue from optional ads
|
||||
and donations supports:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Maintaining and expanding the project</li>
|
||||
<li>Hosting infrastructure for the registry</li>
|
||||
<li>Future hosting of AI models for users with less access to paid services</li>
|
||||
<li>Building a sustainable, community-first platform</li>
|
||||
</ul>
|
||||
<p>
|
||||
<a href="{{ url_for('web.donate') }}" class="text-indigo-600 hover:text-indigo-800">
|
||||
Support the project
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Open Source -->
|
||||
<div class="py-16">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Contribute</h2>
|
||||
<p class="text-lg text-gray-600 mb-8">
|
||||
CmdForge is open source and welcomes contributions of all kinds.
|
||||
</p>
|
||||
<a href="https://github.com/your-org/cmdforge"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center px-6 py-3 text-base font-medium text-white bg-gray-900 rounded-md hover:bg-gray-800">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,183 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Community - CmdForge{% endblock %}
|
||||
|
||||
{% block meta_description %}Connect with CmdForge - report issues, request features, and contribute to the project.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Hero -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Community</h1>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
CmdForge is open source. Get help, report issues, and contribute.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<!-- Forum CTA -->
|
||||
<section class="mb-12">
|
||||
<a href="{{ url_for('forum.index') }}"
|
||||
class="block bg-indigo-600 rounded-lg p-6 hover:bg-indigo-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-indigo-500 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-white">Community Forum</h2>
|
||||
<p class="text-indigo-100">Ask questions, share projects, and connect with other users</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<!-- Community hubs -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Source Code</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Browse the source, fork the project, and submit pull requests.
|
||||
</p>
|
||||
<a href="https://gitea.brrd.tech/rob/CmdForge"
|
||||
target="_blank" rel="noopener"
|
||||
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
|
||||
View Repository
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Report Issues</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Found a bug? Have a feature request? Open an issue.
|
||||
</p>
|
||||
<a href="https://gitea.brrd.tech/rob/CmdForge/issues"
|
||||
target="_blank" rel="noopener"
|
||||
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
|
||||
Open Issue
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Documentation</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Learn how to build tools, configure providers, and publish to the registry.
|
||||
</p>
|
||||
<a href="{{ url_for('web.docs', path='getting-started') }}"
|
||||
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
|
||||
Read the Docs
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Get Involved -->
|
||||
<section class="bg-white rounded-lg border border-gray-200 p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 text-center">Ways to Contribute</h2>
|
||||
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-indigo-600 font-bold">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">Share Your Tools</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Built something useful? <a href="{{ url_for('web.docs', path='publishing') }}" class="text-indigo-600 hover:underline">Publish it to the registry</a> so others can use it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-indigo-600 font-bold">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">Improve the Docs</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Found something confusing? Submit a PR to improve the documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-indigo-600 font-bold">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">Report Bugs</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Help make CmdForge better by reporting issues with clear reproduction steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-indigo-600 font-bold">4</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">Add Features</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Check the issues for feature requests or propose your own ideas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<section class="mt-8 text-center">
|
||||
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||
<a href="{{ url_for('forum.index') }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Visit Forum
|
||||
</a>
|
||||
<a href="https://gitea.brrd.tech/rob/CmdForge#installation"
|
||||
target="_blank" rel="noopener"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Installation
|
||||
</a>
|
||||
<a href="{{ url_for('web.tools') }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Browse Tools
|
||||
</a>
|
||||
<a href="{{ url_for('web.tutorials') }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Tutorials
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ title }} - CmdForge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="bg-white py-16">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ title }}</h1>
|
||||
<p class="mt-4 text-gray-600">{{ body }}</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,208 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/callouts.html" import info, warning, tip %}
|
||||
|
||||
{% block title %}{{ page.title }} - CmdForge Docs{% endblock %}
|
||||
|
||||
{% block meta_description %}{{ page.description or page.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="lg:grid lg:grid-cols-12 lg:gap-8">
|
||||
<!-- Left Sidebar - TOC -->
|
||||
<aside class="hidden lg:block lg:col-span-3">
|
||||
<nav class="sticky top-4 bg-white rounded-lg border border-gray-200 p-4 max-h-[calc(100vh-6rem)] overflow-y-auto">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">Documentation</h3>
|
||||
|
||||
<ul class="space-y-1">
|
||||
{% for section in toc %}
|
||||
<li>
|
||||
<a href="{{ url_for('web.docs', path=section.slug) }}"
|
||||
class="block px-3 py-2 text-sm rounded-md {{ 'bg-indigo-50 text-indigo-700 font-medium' if current_path == section.slug else 'text-gray-600 hover:bg-gray-50' }}">
|
||||
{{ section.title }}
|
||||
</a>
|
||||
|
||||
{% if section.children %}
|
||||
<ul class="ml-4 mt-1 space-y-1">
|
||||
{% for child in section.children %}
|
||||
<li>
|
||||
<a href="{{ url_for('web.docs', path=child.slug) }}"
|
||||
class="block px-3 py-1.5 text-sm rounded-md {{ 'bg-indigo-50 text-indigo-700 font-medium' if current_path == child.slug else 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' }}">
|
||||
{{ child.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="lg:col-span-6">
|
||||
<!-- Mobile TOC Toggle -->
|
||||
<div class="lg:hidden mb-6">
|
||||
<button type="button"
|
||||
onclick="toggleMobileToc()"
|
||||
class="w-full flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-lg">
|
||||
<span class="font-medium text-gray-700">On this page</span>
|
||||
<svg id="toc-chevron" class="w-5 h-5 text-gray-500 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<nav id="mobile-toc" class="hidden mt-2 bg-white border border-gray-200 rounded-lg p-4">
|
||||
<ul class="space-y-2">
|
||||
{% for section in toc %}
|
||||
<li>
|
||||
<a href="{{ url_for('web.docs', path=section.slug) }}"
|
||||
class="block text-sm {{ 'text-indigo-600 font-medium' if current_path == section.slug else 'text-gray-600' }}">
|
||||
{{ section.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6 text-sm text-gray-500" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2">
|
||||
<li><a href="{{ url_for('web.docs', path='') }}" class="hover:text-gray-700">Docs</a></li>
|
||||
{% if page.parent %}
|
||||
<li class="flex items-center">
|
||||
<svg class="w-4 h-4 mx-1" 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.docs', path=page.parent.slug) }}" class="hover:text-gray-700">{{ page.parent.title }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="flex items-center">
|
||||
<svg class="w-4 h-4 mx-1" 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">{{ page.title }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Article Content -->
|
||||
<article class="bg-white rounded-lg border border-gray-200 p-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-6">{{ page.title }}</h1>
|
||||
|
||||
<div class="prose prose-lg prose-indigo max-w-none">
|
||||
{{ page.content_html|safe }}
|
||||
</div>
|
||||
|
||||
<!-- Page Navigation -->
|
||||
<div class="mt-12 pt-8 border-t border-gray-200 flex items-center justify-between">
|
||||
{% if prev_page %}
|
||||
<a href="{{ url_for('web.docs', path=prev_page.slug) }}"
|
||||
class="flex items-center text-indigo-600 hover:text-indigo-800">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
{{ prev_page.title }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div></div>
|
||||
{% endif %}
|
||||
|
||||
{% if next_page %}
|
||||
<a href="{{ url_for('web.docs', path=next_page.slug) }}"
|
||||
class="flex items-center text-indigo-600 hover:text-indigo-800">
|
||||
{{ next_page.title }}
|
||||
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Edit on GitHub -->
|
||||
<div class="mt-6 text-center">
|
||||
<a href="https://github.com/your-org/cmdforge/edit/main/docs/{{ current_path }}.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center text-sm text-gray-500 hover:text-gray-700">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
|
||||
</svg>
|
||||
Edit this page on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Right Sidebar - On This Page + Ads -->
|
||||
<aside class="hidden lg:block lg:col-span-3">
|
||||
<div class="sticky top-4 space-y-6">
|
||||
<!-- On This Page -->
|
||||
{% if page.headings %}
|
||||
<nav class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-3">On this page</h3>
|
||||
<ul class="space-y-2 text-sm" id="page-toc">
|
||||
{% for heading in page.headings %}
|
||||
<li class="{{ 'ml-4' if heading.level > 2 else '' }}">
|
||||
<a href="#{{ heading.id }}"
|
||||
class="text-gray-500 hover:text-gray-900 transition-colors"
|
||||
data-heading="{{ heading.id }}">
|
||||
{{ heading.text }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- Ad Zone -->
|
||||
{% if show_ads %}
|
||||
<div class="bg-blue-50 rounded-lg border border-blue-100 p-4">
|
||||
<p class="text-xs text-gray-500 mb-2">Advertisement</p>
|
||||
<div id="docs-sidebar-ad" class="min-h-[250px]">
|
||||
<!-- Ad content -->
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleMobileToc() {
|
||||
const toc = document.getElementById('mobile-toc');
|
||||
const chevron = document.getElementById('toc-chevron');
|
||||
toc.classList.toggle('hidden');
|
||||
chevron.classList.toggle('rotate-180');
|
||||
}
|
||||
|
||||
// Scroll spy for page TOC
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headings = document.querySelectorAll('article h2, article h3');
|
||||
const tocLinks = document.querySelectorAll('#page-toc a');
|
||||
|
||||
if (headings.length === 0 || tocLinks.length === 0) return;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
tocLinks.forEach(link => {
|
||||
link.classList.remove('text-indigo-600', 'font-medium');
|
||||
link.classList.add('text-gray-500');
|
||||
if (link.dataset.heading === entry.target.id) {
|
||||
link.classList.remove('text-gray-500');
|
||||
link.classList.add('text-indigo-600', 'font-medium');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, {rootMargin: '-100px 0px -66%'});
|
||||
|
||||
headings.forEach(heading => observer.observe(heading));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Support CmdForge{% endblock %}
|
||||
|
||||
{% block meta_description %}Support CmdForge by contributing code, tools, or feedback.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Hero -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Support CmdForge</h1>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
CmdForge is a passion project. The best way to support it is to use it and share what you build.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<!-- Ways to help -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Build Tools</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Create and publish useful tools to the registry. A growing library helps everyone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Spread the Word</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Tell a friend or colleague who might find CmdForge useful.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Give Feedback</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Report bugs, suggest features, or just share what you're building with it.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main CTA -->
|
||||
<section class="bg-white rounded-lg border border-gray-200 p-8 text-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Get Involved</h2>
|
||||
<p class="mt-3 text-gray-600 max-w-2xl mx-auto">
|
||||
CmdForge is open source. The code, docs, and registry are all open for contributions.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a href="https://gitea.brrd.tech/rob/CmdForge"
|
||||
target="_blank" rel="noopener"
|
||||
class="inline-flex items-center px-6 py-3 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
View on Gitea
|
||||
</a>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="inline-flex items-center px-6 py-3 text-sm font-medium text-indigo-600 border border-indigo-600 rounded-md hover:bg-indigo-50">
|
||||
Publish a Tool
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Thank you -->
|
||||
<section class="mt-12 text-center">
|
||||
<p class="text-gray-500">
|
||||
Thanks for using CmdForge!
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,200 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/tool_card.html" import tool_card %}
|
||||
{% from "components/tutorial_card.html" import tutorial_card %}
|
||||
{% from "components/contributor_card.html" import contributor_card %}
|
||||
|
||||
{% block title %}CmdForge - Build Custom AI Commands in YAML{% endblock %}
|
||||
|
||||
{% block meta_description %}Create Unix-style pipeable AI tools with simple YAML configuration. Provider-agnostic, composable, and community-driven.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Hero Section -->
|
||||
<section class="bg-white py-16 md:py-24">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 leading-tight">
|
||||
Build Custom AI Commands in YAML
|
||||
</h1>
|
||||
<p class="mt-6 text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Create Unix-style pipeable tools that work with any AI provider.
|
||||
Provider-agnostic and composable for ultimate flexibility.
|
||||
</p>
|
||||
|
||||
<!-- Install Link -->
|
||||
<div class="mt-10">
|
||||
<a href="https://gitea.brrd.tech/rob/CmdForge#installation"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-2 text-indigo-600 hover:text-indigo-800 font-medium transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
</svg>
|
||||
View Installation Options (pip, Docker, 60-second setup)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- CTAs -->
|
||||
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a href="{{ url_for('web.docs', path='getting-started') }}"
|
||||
class="w-full sm:w-auto inline-flex justify-center items-center px-8 py-4 text-lg font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors shadow-md">
|
||||
Get Started
|
||||
</a>
|
||||
<a href="{{ url_for('web.tutorials') }}"
|
||||
class="w-full sm:w-auto inline-flex justify-center items-center px-8 py-4 text-lg font-medium text-cyan-700 border-2 border-cyan-600 rounded-md hover:bg-cyan-50 transition-colors">
|
||||
View Tutorials
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Three Pillars Section -->
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12">
|
||||
Why CmdForge?
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<!-- Pillar 1: Easy -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center hover:shadow-lg transition-shadow">
|
||||
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-3">Easy to Use</h3>
|
||||
<p class="text-gray-600">
|
||||
Simple YAML configuration for quick setup. No complex programming required to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Pillar 2: Powerful -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center hover:shadow-lg transition-shadow">
|
||||
<div class="w-16 h-16 bg-cyan-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-8 h-8 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-3">Powerful</h3>
|
||||
<p class="text-gray-600">
|
||||
Leverage any AI provider, compose complex multi-step workflows with Python code integration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Pillar 3: Community -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center hover:shadow-lg transition-shadow">
|
||||
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-3">Community</h3>
|
||||
<p class="text-gray-600">
|
||||
Share, discover, and contribute to a growing ecosystem of tools built by developers like you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Tools Section -->
|
||||
<section class="py-16 bg-white">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between mb-12">
|
||||
<h2 class="text-3xl font-bold text-gray-900">
|
||||
Featured Tools & Projects
|
||||
</h2>
|
||||
<a href="{{ url_for('web.tools') }}" class="text-indigo-600 hover:text-indigo-800 font-medium flex items-center">
|
||||
View All
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for tool in featured_tools %}
|
||||
{{ tool_card(
|
||||
owner=tool.owner,
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
category=tool.category,
|
||||
downloads=tool.downloads,
|
||||
version=tool.version
|
||||
) }}
|
||||
{% else %}
|
||||
<!-- Placeholder when no tools -->
|
||||
<div class="col-span-full text-center py-12 text-gray-500">
|
||||
<p class="mb-4">No featured tools yet. Be the first to publish!</p>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
|
||||
Learn how to publish a tool
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Getting Started Section -->
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12">
|
||||
Getting Started
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{{ tutorial_card(
|
||||
title="Basic Setup",
|
||||
description="Learn how to install CmdForge and configure your first AI provider.",
|
||||
href=url_for('web.docs', path='getting-started'),
|
||||
step_number=1
|
||||
) }}
|
||||
|
||||
{{ tutorial_card(
|
||||
title="Your First Tool",
|
||||
description="Create a simple AI-powered command that you can use from your terminal.",
|
||||
href=url_for('web.tutorials_path', path='first-tool'),
|
||||
step_number=2
|
||||
) }}
|
||||
|
||||
{{ tutorial_card(
|
||||
title="Advanced Workflows",
|
||||
description="Combine multiple steps and providers to build powerful automation.",
|
||||
href=url_for('web.tutorials_path', path='advanced-workflows'),
|
||||
step_number=3
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Contributor Section -->
|
||||
{% if featured_contributor %}
|
||||
<section class="py-16 bg-white">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12">
|
||||
Featured Contributor
|
||||
</h2>
|
||||
|
||||
{{ contributor_card(
|
||||
display_name=featured_contributor.display_name,
|
||||
slug=featured_contributor.slug,
|
||||
bio=featured_contributor.bio_override or featured_contributor.bio,
|
||||
verified=featured_contributor.verified
|
||||
) }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Ad Zone (Optional) -->
|
||||
{% if show_ads %}
|
||||
<section class="py-8 bg-blue-50">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<p class="text-xs text-gray-500 mb-2">Advertisement</p>
|
||||
<div id="footer-ad-zone" class="min-h-[90px] flex items-center justify-center">
|
||||
<!-- Ad content loaded dynamically -->
|
||||
<p class="text-gray-400 text-sm">Support CmdForge Development</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/forms.html" import text_input, button_primary, form_errors %}
|
||||
|
||||
{% block title %}Sign In - CmdForge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[70vh] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<a href="{{ url_for('web.home') }}" class="inline-flex items-center justify-center">
|
||||
<svg class="w-10 h-10 text-indigo-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path 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>
|
||||
</a>
|
||||
<h1 class="mt-4 text-2xl font-bold text-gray-900">Sign in to CmdForge</h1>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Access your dashboard and manage your tools
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-8 shadow-sm">
|
||||
{{ form_errors(errors) }}
|
||||
|
||||
<form action="{{ url_for('web.login') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if next_url %}
|
||||
<input type="hidden" name="next" value="{{ next_url }}">
|
||||
{% endif %}
|
||||
|
||||
{{ text_input(
|
||||
name='email',
|
||||
label='Email address',
|
||||
type='email',
|
||||
placeholder='[email protected]',
|
||||
required=true,
|
||||
value=email or ''
|
||||
) }}
|
||||
|
||||
{{ text_input(
|
||||
name='password',
|
||||
label='Password',
|
||||
type='password',
|
||||
placeholder='Your password',
|
||||
required=true
|
||||
) }}
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="remember"
|
||||
id="remember"
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
<label for="remember" class="ml-2 text-sm text-gray-600">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<a href="{{ url_for('web.forgot_password') }}" class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{ button_primary('Sign in', full_width=true) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-sm text-gray-600">
|
||||
Don't have an account?
|
||||
<a href="{{ url_for('web.register') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
|
||||
Create one
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,130 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Privacy Policy - CmdForge{% endblock %}
|
||||
|
||||
{% block meta_description %}CmdForge privacy policy. Learn how we collect, use, and protect your data.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-white py-16">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Privacy Policy</h1>
|
||||
<p class="text-gray-500 mb-8">Last updated: January 2025</p>
|
||||
|
||||
<div class="prose prose-lg prose-indigo max-w-none">
|
||||
<h2>Introduction</h2>
|
||||
<p>
|
||||
CmdForge ("we", "our", or "us") respects your privacy and is committed to protecting
|
||||
your personal data. This privacy policy explains how we collect, use, and safeguard
|
||||
your information when you use our website and services.
|
||||
</p>
|
||||
|
||||
<h2>Information We Collect</h2>
|
||||
|
||||
<h3>Account Information</h3>
|
||||
<p>When you create an account, we collect:</p>
|
||||
<ul>
|
||||
<li>Email address</li>
|
||||
<li>Username (slug)</li>
|
||||
<li>Display name</li>
|
||||
<li>Password (stored securely hashed)</li>
|
||||
<li>Optional: bio and website URL</li>
|
||||
</ul>
|
||||
|
||||
<h3>Usage Data</h3>
|
||||
<p>We automatically collect certain information when you use our services:</p>
|
||||
<ul>
|
||||
<li>Tool download counts (anonymized)</li>
|
||||
<li>Page views and navigation patterns (if analytics consent given)</li>
|
||||
<li>Browser type and version</li>
|
||||
<li>IP address (for security and rate limiting)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Cookies</h3>
|
||||
<p>We use cookies for:</p>
|
||||
<ul>
|
||||
<li><strong>Essential cookies:</strong> Required for the website to function (session management, CSRF protection)</li>
|
||||
<li><strong>Analytics cookies:</strong> Help us understand how visitors use our site (optional, requires consent)</li>
|
||||
<li><strong>Advertising cookies:</strong> Used to show relevant ads (optional, requires consent)</li>
|
||||
</ul>
|
||||
<p>
|
||||
You can manage your cookie preferences at any time through our cookie consent banner
|
||||
or by contacting us.
|
||||
</p>
|
||||
|
||||
<h2>How We Use Your Information</h2>
|
||||
<p>We use your information to:</p>
|
||||
<ul>
|
||||
<li>Provide and maintain our services</li>
|
||||
<li>Authenticate your identity and secure your account</li>
|
||||
<li>Display your public profile and published tools</li>
|
||||
<li>Send important service notifications</li>
|
||||
<li>Improve our services based on usage patterns</li>
|
||||
<li>Prevent abuse and enforce our terms of service</li>
|
||||
</ul>
|
||||
|
||||
<h2>Information Sharing</h2>
|
||||
<p>We do not sell your personal information. We may share data with:</p>
|
||||
<ul>
|
||||
<li><strong>Service providers:</strong> Hosting, analytics, and email services that help us operate</li>
|
||||
<li><strong>Legal requirements:</strong> When required by law or to protect our rights</li>
|
||||
<li><strong>Public information:</strong> Your username, display name, bio, and published tools are publicly visible</li>
|
||||
</ul>
|
||||
|
||||
<h2>Data Security</h2>
|
||||
<p>
|
||||
We implement appropriate security measures to protect your personal information,
|
||||
including:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Encryption of data in transit (HTTPS)</li>
|
||||
<li>Secure password hashing (bcrypt)</li>
|
||||
<li>Regular security audits</li>
|
||||
<li>Limited access to personal data</li>
|
||||
</ul>
|
||||
|
||||
<h2>Your Rights</h2>
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<li><strong>Access:</strong> Request a copy of your personal data</li>
|
||||
<li><strong>Correction:</strong> Update inaccurate information in your account settings</li>
|
||||
<li><strong>Deletion:</strong> Delete your account and associated data</li>
|
||||
<li><strong>Portability:</strong> Export your data in a machine-readable format</li>
|
||||
<li><strong>Withdraw consent:</strong> Opt out of analytics and advertising cookies</li>
|
||||
</ul>
|
||||
|
||||
<h2>Data Retention</h2>
|
||||
<p>
|
||||
We retain your account data for as long as your account is active. If you delete
|
||||
your account, we will remove your personal data within 30 days, except where we
|
||||
are required to retain it for legal purposes.
|
||||
</p>
|
||||
<p>
|
||||
Published tools may remain in the registry after account deletion for continuity,
|
||||
but will be disassociated from your personal information.
|
||||
</p>
|
||||
|
||||
<h2>Children's Privacy</h2>
|
||||
<p>
|
||||
Our services are not intended for children under 13. We do not knowingly collect
|
||||
personal information from children under 13.
|
||||
</p>
|
||||
|
||||
<h2>Changes to This Policy</h2>
|
||||
<p>
|
||||
We may update this privacy policy from time to time. We will notify you of any
|
||||
significant changes by posting a notice on our website or sending you an email.
|
||||
</p>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<p>
|
||||
If you have questions about this privacy policy or your personal data, please
|
||||
contact us at:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Email: [email protected]</li>
|
||||
<li>GitHub: <a href="https://github.com/your-org/cmdforge/issues">Open an issue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/tool_card.html" import tool_card %}
|
||||
|
||||
{% block title %}{{ publisher.display_name }} (@{{ publisher.slug }}) - CmdForge{% endblock %}
|
||||
|
||||
{% block meta_description %}{{ publisher.bio or 'CmdForge publisher profile for ' ~ publisher.display_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Profile Header -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div class="flex items-start gap-6">
|
||||
<!-- Avatar -->
|
||||
<div class="w-24 h-24 bg-indigo-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-3xl font-bold text-indigo-600">{{ publisher.display_name[0]|upper }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-2xl font-bold text-gray-900 truncate">{{ publisher.display_name }}</h1>
|
||||
{% if publisher.verified %}
|
||||
<svg class="w-6 h-6 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" title="Verified publisher">
|
||||
<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 %}
|
||||
</div>
|
||||
<p class="mt-1 text-gray-500">@{{ publisher.slug }}</p>
|
||||
|
||||
{% if publisher.bio %}
|
||||
<p class="mt-4 text-gray-600">{{ publisher.bio }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4 flex items-center gap-4 text-sm text-gray-500">
|
||||
{% if publisher.website %}
|
||||
<a href="{{ publisher.website }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-1 hover:text-indigo-600">
|
||||
<svg 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="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
||||
</svg>
|
||||
Website
|
||||
</a>
|
||||
{% endif %}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg 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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
{{ tools|length }} tool{{ 's' if tools|length != 1 else '' }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg 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 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Joined {{ publisher.created_at|date_format }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools Section -->
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Published Tools</h2>
|
||||
|
||||
{% if tools %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{% for tool in tools %}
|
||||
{{ tool_card(
|
||||
owner=tool.owner,
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
category=tool.category,
|
||||
downloads=tool.downloads,
|
||||
version=tool.version
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12 bg-white rounded-lg border border-gray-200">
|
||||
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<p class="mt-4 text-gray-600">No tools published yet.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/forms.html" import text_input, button_primary, form_errors, checkbox %}
|
||||
|
||||
{% block title %}Create Account - CmdForge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[70vh] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<a href="{{ url_for('web.home') }}" class="inline-flex items-center justify-center">
|
||||
<svg class="w-10 h-10 text-indigo-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path 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>
|
||||
</a>
|
||||
<h1 class="mt-4 text-2xl font-bold text-gray-900">Create your account</h1>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Start publishing tools and join the community
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-8 shadow-sm">
|
||||
{{ form_errors(errors) }}
|
||||
|
||||
<form action="{{ url_for('web.register') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
{{ text_input(
|
||||
name='email',
|
||||
label='Email address',
|
||||
type='email',
|
||||
placeholder='[email protected]',
|
||||
required=true,
|
||||
value=email or '',
|
||||
help='We\'ll send a verification email to this address'
|
||||
) }}
|
||||
|
||||
{{ text_input(
|
||||
name='slug',
|
||||
label='Username',
|
||||
type='text',
|
||||
placeholder='your-username',
|
||||
required=true,
|
||||
value=slug or '',
|
||||
help='This will be your unique identifier (e.g., your-username/tool-name)'
|
||||
) }}
|
||||
|
||||
{{ text_input(
|
||||
name='display_name',
|
||||
label='Display name',
|
||||
type='text',
|
||||
placeholder='Your Name',
|
||||
required=true,
|
||||
value=display_name or ''
|
||||
) }}
|
||||
|
||||
{{ text_input(
|
||||
name='password',
|
||||
label='Password',
|
||||
type='password',
|
||||
placeholder='At least 8 characters',
|
||||
required=true,
|
||||
help='Use a mix of letters, numbers, and symbols'
|
||||
) }}
|
||||
|
||||
{{ text_input(
|
||||
name='password_confirm',
|
||||
label='Confirm password',
|
||||
type='password',
|
||||
placeholder='Confirm your password',
|
||||
required=true
|
||||
) }}
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex items-start">
|
||||
<input type="checkbox"
|
||||
name="terms"
|
||||
id="terms"
|
||||
required
|
||||
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
<label for="terms" class="ml-3 text-sm text-gray-600">
|
||||
I agree to the
|
||||
<a href="{{ url_for('web.terms') }}" class="text-indigo-600 hover:text-indigo-800">Terms of Service</a>
|
||||
and
|
||||
<a href="{{ url_for('web.privacy') }}" class="text-indigo-600 hover:text-indigo-800">Privacy Policy</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ button_primary('Create account', full_width=true) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-sm text-gray-600">
|
||||
Already have an account?
|
||||
<a href="{{ url_for('web.login') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,143 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/tool_card.html" import tool_card %}
|
||||
|
||||
{% block title %}Search: {{ query }} - CmdForge Registry{% endblock %}
|
||||
|
||||
{% block meta_description %}Search results for "{{ query }}" in the CmdForge Registry.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Search Header -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<form action="{{ url_for('web.search') }}" method="GET" class="max-w-2xl mx-auto">
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
name="q"
|
||||
value="{{ query or '' }}"
|
||||
placeholder="Search for tools..."
|
||||
aria-label="Search for tools"
|
||||
autofocus
|
||||
class="w-full pl-12 pr-4 py-4 text-lg border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<svg class="absolute left-4 top-4.5 w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<button type="submit"
|
||||
class="absolute right-2 top-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{% if query %}
|
||||
<!-- Results Count -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
{% if results|length == 1 %}
|
||||
1 result for "{{ query }}"
|
||||
{% else %}
|
||||
{{ results|length }} results for "{{ query }}"
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{% if results %}
|
||||
<!-- Search Results -->
|
||||
<div class="space-y-4">
|
||||
{% for tool in results %}
|
||||
{{ tool_card(
|
||||
owner=tool.owner,
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
category=tool.category,
|
||||
downloads=tool.downloads,
|
||||
version=tool.version
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination and pagination.pages > 1 %}
|
||||
<nav class="mt-12 flex items-center justify-center" aria-label="Pagination">
|
||||
<div class="flex items-center gap-2">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('web.search', q=query, page=pagination.prev_num) }}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-4 py-2 text-sm text-gray-700">
|
||||
Page {{ pagination.page }} of {{ pagination.pages }}
|
||||
</span>
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('web.search', q=query, page=pagination.next_num) }}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Next
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- No Results -->
|
||||
<div class="text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<h2 class="mt-4 text-lg font-medium text-gray-900">No results found</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
We couldn't find any tools matching "{{ query }}".
|
||||
</p>
|
||||
|
||||
<div class="mt-8">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-4">Suggestions:</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-2">
|
||||
<li>Check your spelling</li>
|
||||
<li>Try more general keywords</li>
|
||||
<li>Use fewer keywords</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<a href="{{ url_for('web.tools') }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-indigo-600 border border-indigo-600 rounded-md hover:bg-indigo-50">
|
||||
Browse All Tools
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- Initial Search State -->
|
||||
<div class="text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<h2 class="mt-4 text-lg font-medium text-gray-900">Search the Registry</h2>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Find tools by name, description, category, or tags.
|
||||
</p>
|
||||
|
||||
<!-- Popular Categories -->
|
||||
<div class="mt-8">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-4">Popular Categories:</h3>
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
{% for cat in popular_categories %}
|
||||
<a href="{{ url_for('web.category', name=cat.name) }}"
|
||||
class="px-3 py-1.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-full hover:border-indigo-500 hover:text-indigo-600 transition-colors">
|
||||
{{ cat.display_name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,158 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Terms of Service - CmdForge{% endblock %}
|
||||
|
||||
{% block meta_description %}CmdForge terms of service. Read our terms and conditions for using the platform.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-white py-16">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Terms of Service</h1>
|
||||
<p class="text-gray-500 mb-8">Last updated: January 2025</p>
|
||||
|
||||
<div class="prose prose-lg prose-indigo max-w-none">
|
||||
<h2>1. Acceptance of Terms</h2>
|
||||
<p>
|
||||
By accessing or using CmdForge ("the Service"), you agree to be bound by these
|
||||
Terms of Service ("Terms"). If you do not agree to these Terms, you may not use
|
||||
the Service.
|
||||
</p>
|
||||
|
||||
<h2>2. Description of Service</h2>
|
||||
<p>
|
||||
CmdForge is a platform for creating, publishing, and sharing AI-powered
|
||||
command-line tools. The Service includes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>The CmdForge CLI application</li>
|
||||
<li>The CmdForge Registry (tool hosting and discovery)</li>
|
||||
<li>Documentation and tutorials</li>
|
||||
<li>Community features</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. User Accounts</h2>
|
||||
<p>
|
||||
To publish tools or access certain features, you must create an account. You agree to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Provide accurate and complete information</li>
|
||||
<li>Maintain the security of your account credentials</li>
|
||||
<li>Notify us immediately of any unauthorized access</li>
|
||||
<li>Accept responsibility for all activities under your account</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. User Content</h2>
|
||||
|
||||
<h3>4.1 Your Content</h3>
|
||||
<p>
|
||||
You retain ownership of tools and content you publish ("User Content"). By publishing
|
||||
to the Registry, you grant CmdForge a non-exclusive, worldwide license to host,
|
||||
distribute, and display your User Content.
|
||||
</p>
|
||||
|
||||
<h3>4.2 Content Standards</h3>
|
||||
<p>You agree not to publish content that:</p>
|
||||
<ul>
|
||||
<li>Contains malware, viruses, or malicious code</li>
|
||||
<li>Infringes on intellectual property rights</li>
|
||||
<li>Is illegal, harmful, or promotes illegal activities</li>
|
||||
<li>Harasses, threatens, or harms others</li>
|
||||
<li>Contains spam or deceptive content</li>
|
||||
<li>Violates the privacy of others</li>
|
||||
</ul>
|
||||
|
||||
<h3>4.3 Content Removal</h3>
|
||||
<p>
|
||||
We reserve the right to remove any content that violates these Terms or that we
|
||||
determine to be harmful to users or the community.
|
||||
</p>
|
||||
|
||||
<h2>5. Acceptable Use</h2>
|
||||
<p>You agree not to:</p>
|
||||
<ul>
|
||||
<li>Use the Service for any unlawful purpose</li>
|
||||
<li>Attempt to gain unauthorized access to our systems</li>
|
||||
<li>Interfere with or disrupt the Service</li>
|
||||
<li>Scrape or collect data without permission</li>
|
||||
<li>Impersonate others or misrepresent your affiliation</li>
|
||||
<li>Use automated systems to access the Service excessively</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. API and CLI Usage</h2>
|
||||
<p>
|
||||
Access to the CmdForge API and CLI is subject to rate limits. Excessive use
|
||||
that impacts service availability for others may result in temporary or permanent
|
||||
restrictions.
|
||||
</p>
|
||||
|
||||
<h2>7. Third-Party Services</h2>
|
||||
<p>
|
||||
CmdForge is designed to work with various AI providers and external services.
|
||||
Your use of these third-party services is subject to their respective terms and
|
||||
conditions. We are not responsible for third-party services.
|
||||
</p>
|
||||
|
||||
<h2>8. Intellectual Property</h2>
|
||||
<p>
|
||||
The CmdForge software is open source and licensed under the MIT License.
|
||||
The CmdForge name, logo, and branding are trademarks of CmdForge.
|
||||
</p>
|
||||
|
||||
<h2>9. Disclaimer of Warranties</h2>
|
||||
<p>
|
||||
THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED.
|
||||
WE DO NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED, ERROR-FREE, OR SECURE.
|
||||
</p>
|
||||
<p>
|
||||
Tools published by third parties are not endorsed by CmdForge. You use third-party
|
||||
tools at your own risk.
|
||||
</p>
|
||||
|
||||
<h2>10. Limitation of Liability</h2>
|
||||
<p>
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY LAW, CMDFORGE SHALL NOT BE LIABLE FOR ANY
|
||||
INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM
|
||||
YOUR USE OF THE SERVICE.
|
||||
</p>
|
||||
|
||||
<h2>11. Indemnification</h2>
|
||||
<p>
|
||||
You agree to indemnify and hold harmless CmdForge and its contributors from
|
||||
any claims, damages, or expenses arising from your use of the Service or
|
||||
violation of these Terms.
|
||||
</p>
|
||||
|
||||
<h2>12. Termination</h2>
|
||||
<p>
|
||||
We may terminate or suspend your access to the Service at any time, with or
|
||||
without cause. Upon termination, your right to use the Service ceases immediately.
|
||||
</p>
|
||||
<p>
|
||||
You may delete your account at any time through your account settings.
|
||||
</p>
|
||||
|
||||
<h2>13. Changes to Terms</h2>
|
||||
<p>
|
||||
We may modify these Terms at any time. We will notify you of significant changes
|
||||
by posting a notice on our website. Continued use of the Service after changes
|
||||
constitutes acceptance of the new Terms.
|
||||
</p>
|
||||
|
||||
<h2>14. Governing Law</h2>
|
||||
<p>
|
||||
These Terms are governed by the laws of the jurisdiction in which CmdForge
|
||||
operates, without regard to conflict of law principles.
|
||||
</p>
|
||||
|
||||
<h2>15. Contact</h2>
|
||||
<p>
|
||||
For questions about these Terms, please contact us at:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Email: [email protected]</li>
|
||||
<li>GitHub: <a href="https://github.com/your-org/cmdforge/issues">Open an issue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,284 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/callouts.html" import warning, info %}
|
||||
|
||||
{% block title %}{{ tool.owner }}/{{ tool.name }} - CmdForge Registry{% endblock %}
|
||||
|
||||
{% block meta_description %}{{ tool.description or 'A CmdForge command-line tool' }}{% endblock %}
|
||||
|
||||
{% block og_title %}{{ tool.owner }}/{{ tool.name }}{% endblock %}
|
||||
{% block og_description %}{{ tool.description or 'A CmdForge 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>cmdforge 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">cmdforge 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>
|
||||
<button type="button"
|
||||
onclick="submitReport('{{ tool.owner }}', '{{ tool.name }}')"
|
||||
class="mt-4 w-full inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
|
||||
Report Tool
|
||||
</button>
|
||||
</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="submitReport('{{ tool.owner }}', '{{ tool.name }}')"
|
||||
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_owner" id="report-owner" value="{{ tool.owner }}">
|
||||
<input type="hidden" name="tool_name" id="report-name" value="{{ tool.name }}">
|
||||
|
||||
<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="spam">Spam</option>
|
||||
<option value="malicious">Malicious</option>
|
||||
<option value="duplicate">Duplicate</option>
|
||||
<option value="inappropriate">Inappropriate</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description (optional)</label>
|
||||
<textarea name="description" id="description" 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 = 'cmdforge 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);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,232 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/tool_card.html" import tool_card %}
|
||||
|
||||
{% block title %}Browse Tools - CmdForge Registry{% endblock %}
|
||||
|
||||
{% block meta_description %}Discover and install community-built AI tools. Browse by category, search by name, or explore the most popular tools.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Page Header -->
|
||||
<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-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Browse Tools</h1>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Discover community-built AI tools for your command line.
|
||||
</p>
|
||||
</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-4 lg:gap-8">
|
||||
<!-- Sidebar Filters -->
|
||||
<aside class="hidden lg:block">
|
||||
<div class="sticky top-4 space-y-6">
|
||||
<!-- Search -->
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Search
|
||||
</label>
|
||||
<form action="{{ url_for('web.search') }}" method="GET">
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
name="q"
|
||||
id="search"
|
||||
value="{{ query or '' }}"
|
||||
placeholder="Search tools..."
|
||||
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<button type="submit" class="absolute left-3 top-2.5" aria-label="Search">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">Categories</h3>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href="{{ url_for('web.tools') }}"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-lg {{ 'bg-indigo-50 text-indigo-600 font-medium' if not current_category else 'text-gray-600 hover:bg-gray-100' }}">
|
||||
<span>All Tools</span>
|
||||
<span class="text-sm text-gray-600">{{ total_count }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% for cat in categories %}
|
||||
<li>
|
||||
<a href="{{ url_for('web.category', name=cat.name) }}"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-lg {{ 'bg-indigo-50 text-indigo-600 font-medium' if current_category == cat.name else 'text-gray-600 hover:bg-gray-100' }}">
|
||||
<span>{{ cat.display_name }}</span>
|
||||
<span class="text-sm text-gray-600">{{ cat.count }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<div>
|
||||
<label for="sort-select" class="block text-sm font-medium text-gray-700 mb-3">Sort By</label>
|
||||
<select id="sort-select"
|
||||
onchange="updateSort(this.value)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white">
|
||||
<option value="downloads" {{ 'selected' if sort == 'downloads' }}>Most Downloads</option>
|
||||
<option value="published_at" {{ 'selected' if sort == 'published_at' }}>Recently Published</option>
|
||||
<option value="name" {{ 'selected' if sort == 'name' }}>Name (A-Z)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile Filter Toggle -->
|
||||
<div class="lg:hidden mb-6">
|
||||
<button type="button"
|
||||
onclick="toggleMobileFilters()"
|
||||
class="w-full flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-lg">
|
||||
<span class="font-medium text-gray-700">Filters & Sort</span>
|
||||
<svg id="filter-chevron" class="w-5 h-5 text-gray-500 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Filters Panel -->
|
||||
<div id="mobile-filters" class="hidden mt-4 p-4 bg-white border border-gray-200 rounded-lg space-y-4">
|
||||
<!-- Mobile Search -->
|
||||
<form action="{{ url_for('web.search') }}" method="GET" class="flex gap-2">
|
||||
<input type="text"
|
||||
name="q"
|
||||
value="{{ query or '' }}"
|
||||
placeholder="Search tools..."
|
||||
aria-label="Search tools"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700" aria-label="Search">
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Mobile Categories -->
|
||||
<div>
|
||||
<label for="mobile-category-select" class="block text-sm font-medium text-gray-700 mb-2">Category</label>
|
||||
<select id="mobile-category-select"
|
||||
onchange="window.location.href=this.value"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white">
|
||||
<option value="{{ url_for('web.tools') }}" {{ 'selected' if not current_category }}>All Tools</option>
|
||||
{% for cat in categories %}
|
||||
<option value="{{ url_for('web.category', name=cat.name) }}" {{ 'selected' if current_category == cat.name }}>
|
||||
{{ cat.display_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Sort -->
|
||||
<div>
|
||||
<label for="mobile-sort-select" class="block text-sm font-medium text-gray-700 mb-2">Sort By</label>
|
||||
<select id="mobile-sort-select"
|
||||
onchange="updateSort(this.value)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white">
|
||||
<option value="downloads" {{ 'selected' if sort == 'downloads' }}>Most Downloads</option>
|
||||
<option value="published_at" {{ 'selected' if sort == 'published_at' }}>Recently Published</option>
|
||||
<option value="name" {{ 'selected' if sort == 'name' }}>Name (A-Z)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools Grid -->
|
||||
<main class="lg:col-span-3">
|
||||
<!-- Results Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-sm text-gray-600">
|
||||
{% if query %}
|
||||
{{ tools|length }} results for "{{ query }}"
|
||||
{% elif current_category %}
|
||||
{{ tools|length }} tools in {{ current_category }}
|
||||
{% else %}
|
||||
{{ tools|length }} tools available
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if tools %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{% for tool in tools %}
|
||||
{{ tool_card(
|
||||
owner=tool.owner,
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
category=tool.category,
|
||||
downloads=tool.downloads,
|
||||
version=tool.version
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination %}
|
||||
<nav class="mt-12 flex items-center justify-center" aria-label="Pagination">
|
||||
<div class="flex items-center gap-2">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('web.tools', page=pagination.prev_num, sort=sort, category=current_category) }}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-4 py-2 text-sm text-gray-700">
|
||||
Page {{ pagination.page }} of {{ pagination.pages }}
|
||||
</span>
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('web.tools', page=pagination.next_num, sort=sort, category=current_category) }}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Next
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No tools found</h3>
|
||||
<p class="mt-2 text-gray-600">
|
||||
{% if query %}
|
||||
No tools match your search. Try different keywords.
|
||||
{% else %}
|
||||
Be the first to publish a tool in this category!
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Learn to Publish
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleMobileFilters() {
|
||||
const filters = document.getElementById('mobile-filters');
|
||||
const chevron = document.getElementById('filter-chevron');
|
||||
filters.classList.toggle('hidden');
|
||||
chevron.classList.toggle('rotate-180');
|
||||
}
|
||||
|
||||
function updateSort(value) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('sort', value);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,195 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/tutorial_card.html" import tutorial_card %}
|
||||
|
||||
{% block title %}Tutorials - CmdForge{% endblock %}
|
||||
|
||||
{% block meta_description %}Learn how to use CmdForge with step-by-step tutorials. From basic setup to advanced workflows.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Hero -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Tutorials</h1>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
Learn CmdForge from the ground up with step-by-step guides.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<!-- Getting Started Section -->
|
||||
<section class="mb-16">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Getting Started</h2>
|
||||
<p class="text-gray-600 mb-8">
|
||||
New to CmdForge? Start here with the fundamentals.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{{ tutorial_card(
|
||||
title="Installation & Setup",
|
||||
description="Install CmdForge and configure your first AI provider.",
|
||||
href=url_for('web.tutorials_path', path='installation'),
|
||||
step_number=1
|
||||
) }}
|
||||
|
||||
{{ tutorial_card(
|
||||
title="Your First Tool",
|
||||
description="Create a simple AI-powered command in under 5 minutes.",
|
||||
href=url_for('web.tutorials_path', path='first-tool'),
|
||||
step_number=2
|
||||
) }}
|
||||
|
||||
{{ tutorial_card(
|
||||
title="Understanding YAML Config",
|
||||
description="Learn the structure of CmdForge configuration files.",
|
||||
href=url_for('web.tutorials_path', path='yaml-config'),
|
||||
step_number=3
|
||||
) }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Core Concepts Section -->
|
||||
<section class="mb-16">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Core Concepts</h2>
|
||||
<p class="text-gray-600 mb-8">
|
||||
Understand the key concepts that power CmdForge.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{% for tutorial in core_tutorials %}
|
||||
<a href="{{ url_for('web.tutorials_path', path=tutorial.slug) }}"
|
||||
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ tutorial.title }}</h3>
|
||||
<p class="text-gray-600 text-sm">{{ tutorial.description }}</p>
|
||||
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
|
||||
Read tutorial
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('web.tutorials_path', path='providers') }}"
|
||||
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Working with Providers</h3>
|
||||
<p class="text-gray-600 text-sm">Configure and use different AI providers with your tools.</p>
|
||||
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
|
||||
Read tutorial
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('web.tutorials_path', path='arguments') }}"
|
||||
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Custom Arguments</h3>
|
||||
<p class="text-gray-600 text-sm">Add flags and options to make your tools flexible.</p>
|
||||
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
|
||||
Read tutorial
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('web.tutorials_path', path='multi-step') }}"
|
||||
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Multi-Step Workflows</h3>
|
||||
<p class="text-gray-600 text-sm">Chain prompts and code steps together.</p>
|
||||
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
|
||||
Read tutorial
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('web.tutorials_path', path='code-steps') }}"
|
||||
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Code Steps</h3>
|
||||
<p class="text-gray-600 text-sm">Add Python code processing between AI calls.</p>
|
||||
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
|
||||
Read tutorial
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Section -->
|
||||
<section class="mb-16">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Advanced Topics</h2>
|
||||
<p class="text-gray-600 mb-8">
|
||||
Take your tools to the next level with advanced techniques.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<a href="{{ url_for('web.tutorials_path', path='publishing') }}"
|
||||
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Publishing Tools</h3>
|
||||
<p class="text-gray-600 text-sm">Share your tools with the community via the registry.</p>
|
||||
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
|
||||
Read tutorial
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('web.tutorials_path', path='advanced-workflows') }}"
|
||||
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Advanced Workflows</h3>
|
||||
<p class="text-gray-600 text-sm">Complex multi-provider and branching workflows.</p>
|
||||
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
|
||||
Read tutorial
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Video Tutorials -->
|
||||
{% if video_tutorials %}
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Video Tutorials</h2>
|
||||
<p class="text-gray-600 mb-8">
|
||||
Prefer watching? Check out our video guides.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{% for video in video_tutorials %}
|
||||
<a href="{{ video.url }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="block bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div class="relative">
|
||||
<img src="{{ video.thumbnail }}"
|
||||
alt="{{ video.title }}"
|
||||
class="w-full h-48 object-cover">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-16 h-16 bg-black bg-opacity-60 rounded-full flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-white ml-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold text-gray-900">{{ video.title }}</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ video.duration }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user