Add SmartTools Registry with Web UI (Phase 1-7) #18
|
|
@ -1369,6 +1369,31 @@ def create_app() -> Flask:
|
|||
|
||||
return jsonify({"data": {"status": "submitted"}})
|
||||
|
||||
@app.route("/api/v1/consent", methods=["POST"])
|
||||
def save_consent() -> Response:
|
||||
"""Save user consent preferences for analytics/ads."""
|
||||
try:
|
||||
data = request.get_json(force=True) or {}
|
||||
except Exception:
|
||||
data = {}
|
||||
|
||||
analytics = bool(data.get("analytics", False))
|
||||
ads = bool(data.get("ads", False))
|
||||
|
||||
# Store consent in session (works with our SQLite session interface)
|
||||
from flask import session
|
||||
session["consent_analytics"] = analytics
|
||||
session["consent_ads"] = ads
|
||||
session["consent_given"] = True
|
||||
|
||||
return jsonify({
|
||||
"data": {
|
||||
"analytics": analytics,
|
||||
"ads": ads,
|
||||
"saved": True
|
||||
}
|
||||
})
|
||||
|
||||
@app.route("/api/v1/webhook/gitea", methods=["POST"])
|
||||
def webhook_gitea() -> Response:
|
||||
if request.content_length and request.content_length > MAX_BODY_BYTES:
|
||||
|
|
|
|||
|
|
@ -508,11 +508,7 @@ web_bp.add_url_rule("/tutorials/<path:path>", endpoint="tutorial", view_func=tut
|
|||
|
||||
@web_bp.route("/community", endpoint="community")
|
||||
def community():
|
||||
return render_template(
|
||||
"pages/content.html",
|
||||
title="Community",
|
||||
body="Community features will live here. For now, join the discussion and share ideas.",
|
||||
)
|
||||
return render_template("pages/community.html")
|
||||
|
||||
|
||||
@web_bp.route("/about", endpoint="about")
|
||||
|
|
@ -522,11 +518,7 @@ def about():
|
|||
|
||||
@web_bp.route("/donate", endpoint="donate")
|
||||
def donate():
|
||||
return render_template(
|
||||
"pages/content.html",
|
||||
title="Support SmartTools",
|
||||
body="Donations help fund infrastructure, development, and broader access to AI tools.",
|
||||
)
|
||||
return render_template("pages/donate.html")
|
||||
|
||||
|
||||
@web_bp.route("/privacy", endpoint="privacy")
|
||||
|
|
|
|||
|
|
@ -61,21 +61,51 @@ function debounce(func, wait) {
|
|||
// Mobile menu toggle
|
||||
function toggleMobileMenu() {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
const overlay = document.getElementById('mobile-menu-overlay');
|
||||
if (menu && overlay) {
|
||||
const openIcon = document.getElementById('menu-icon-open');
|
||||
const closeIcon = document.getElementById('menu-icon-close');
|
||||
|
||||
if (menu) {
|
||||
const isHidden = menu.classList.contains('hidden');
|
||||
menu.classList.toggle('hidden');
|
||||
overlay.classList.toggle('hidden');
|
||||
document.body.classList.toggle('overflow-hidden');
|
||||
|
||||
// Toggle icons
|
||||
if (openIcon && closeIcon) {
|
||||
if (isHidden) {
|
||||
openIcon.classList.add('hidden');
|
||||
closeIcon.classList.remove('hidden');
|
||||
} else {
|
||||
openIcon.classList.remove('hidden');
|
||||
closeIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileMenu() {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
const overlay = document.getElementById('mobile-menu-overlay');
|
||||
if (menu && overlay) {
|
||||
const openIcon = document.getElementById('menu-icon-open');
|
||||
const closeIcon = document.getElementById('menu-icon-close');
|
||||
|
||||
if (menu) {
|
||||
menu.classList.add('hidden');
|
||||
overlay.classList.add('hidden');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
if (openIcon) openIcon.classList.remove('hidden');
|
||||
if (closeIcon) closeIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile filters toggle (tools page)
|
||||
function toggleMobileFilters() {
|
||||
const filters = document.getElementById('mobile-filters');
|
||||
if (filters) {
|
||||
filters.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile TOC toggle (docs page)
|
||||
function toggleMobileToc() {
|
||||
const toc = document.getElementById('mobile-toc');
|
||||
if (toc) {
|
||||
toc.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,3 +241,190 @@ function createToastContainer() {
|
|||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Tool Detail Page Functions
|
||||
// ============================================
|
||||
|
||||
function copyInstall() {
|
||||
const command = document.getElementById('install-command');
|
||||
if (command) {
|
||||
const text = command.textContent || command.innerText;
|
||||
navigator.clipboard.writeText(text.trim()).then(() => {
|
||||
showToast('Install command copied!', 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openReportModal() {
|
||||
const modal = document.getElementById('report-modal');
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeReportModal() {
|
||||
const modal = document.getElementById('report-modal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Dashboard - Token Management
|
||||
// ============================================
|
||||
|
||||
function openCreateTokenModal() {
|
||||
const modal = document.getElementById('create-token-modal');
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeCreateTokenModal() {
|
||||
const modal = document.getElementById('create-token-modal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function closeTokenCreatedModal() {
|
||||
const modal = document.getElementById('token-created-modal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
// Reload to show new token in list
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function copyNewToken() {
|
||||
const tokenEl = document.getElementById('new-token-value');
|
||||
if (tokenEl) {
|
||||
navigator.clipboard.writeText(tokenEl.textContent.trim()).then(() => {
|
||||
showToast('Token copied to clipboard!', 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function revokeToken(tokenId, tokenName) {
|
||||
if (confirm(`Are you sure you want to revoke the token "${tokenName}"? This cannot be undone.`)) {
|
||||
fetch(`/api/v1/tokens/${tokenId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
showToast('Token revoked successfully', 'success');
|
||||
window.location.reload();
|
||||
} else {
|
||||
showToast('Failed to revoke token', 'error');
|
||||
}
|
||||
}).catch(() => {
|
||||
showToast('Failed to revoke token', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Dashboard - Tool Management
|
||||
// ============================================
|
||||
|
||||
let deprecateToolOwner = '';
|
||||
let deprecateToolName = '';
|
||||
|
||||
function openDeprecateModal(owner, name, isDeprecated) {
|
||||
deprecateToolOwner = owner;
|
||||
deprecateToolName = name;
|
||||
const modal = document.getElementById('deprecate-modal');
|
||||
const title = document.getElementById('deprecate-modal-title');
|
||||
const btn = document.getElementById('deprecate-submit-btn');
|
||||
|
||||
if (modal) {
|
||||
if (title) {
|
||||
title.textContent = isDeprecated ? `Restore ${name}` : `Deprecate ${name}`;
|
||||
}
|
||||
if (btn) {
|
||||
btn.textContent = isDeprecated ? 'Restore Tool' : 'Deprecate Tool';
|
||||
btn.classList.toggle('bg-red-600', !isDeprecated);
|
||||
btn.classList.toggle('bg-green-600', isDeprecated);
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeDeprecateModal() {
|
||||
const modal = document.getElementById('deprecate-modal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
deprecateToolOwner = '';
|
||||
deprecateToolName = '';
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Dashboard - Settings
|
||||
// ============================================
|
||||
|
||||
function resendVerification() {
|
||||
fetch('/api/v1/me/resend-verification', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
showToast('Verification email sent!', 'success');
|
||||
} else {
|
||||
showToast('Failed to send verification email', 'error');
|
||||
}
|
||||
}).catch(() => {
|
||||
showToast('Failed to send verification email', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDeleteAccount() {
|
||||
if (confirm('Are you absolutely sure you want to delete your account? This will permanently delete all your tools and cannot be undone.')) {
|
||||
const confirmInput = prompt('Type "DELETE" to confirm:');
|
||||
if (confirmInput === 'DELETE') {
|
||||
fetch('/api/v1/me', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
showToast('Failed to delete account', 'error');
|
||||
}
|
||||
}).catch(() => {
|
||||
showToast('Failed to delete account', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Search Functionality
|
||||
// ============================================
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', debounce(function() {
|
||||
const query = this.value.trim();
|
||||
if (query.length < 2) {
|
||||
searchResults.innerHTML = '<p class="text-sm text-gray-500 text-center py-8">Start typing to search...</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/tools/search?q=${encodeURIComponent(query)}&limit=10`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.data && data.data.length > 0) {
|
||||
searchResults.innerHTML = data.data.map(tool => `
|
||||
<a href="/tools/${tool.owner}/${tool.name}"
|
||||
class="block p-3 hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<div class="font-medium text-gray-900">${tool.owner}/${tool.name}</div>
|
||||
<div class="text-sm text-gray-500 truncate">${tool.description || 'No description'}</div>
|
||||
</a>
|
||||
`).join('');
|
||||
} else {
|
||||
searchResults.innerHTML = '<p class="text-sm text-gray-500 text-center py-8">No tools found</p>';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
searchResults.innerHTML = '<p class="text-sm text-red-500 text-center py-8">Search failed</p>';
|
||||
});
|
||||
}, 300));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Community - SmartTools{% endblock %}
|
||||
|
||||
{% block meta_description %}Connect with the SmartTools community, share tools, and collaborate on new ideas.{% 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">
|
||||
Collaboration over competition. Share tools, ask questions, and build together.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<!-- 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">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Discussions</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Ask for help, share feedback, and discover best practices.
|
||||
</p>
|
||||
<a href="#" class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
|
||||
Coming soon
|
||||
<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="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Contributor Spotlight</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Celebrate creators who publish tools and help others succeed.
|
||||
</p>
|
||||
<a href="#" class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
|
||||
Coming soon
|
||||
<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="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Project Showcase</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
See how teams use SmartTools in real projects and workflows.
|
||||
</p>
|
||||
<a href="#" class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
|
||||
Coming soon
|
||||
<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>
|
||||
|
||||
<!-- Calls to action -->
|
||||
<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">
|
||||
Want to contribute tools, documentation, or tutorials? We’d love to hear from you.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a href="{{ url_for('web.docs', path='contributing') }}"
|
||||
class="inline-flex items-center px-6 py-3 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Contribution Guide
|
||||
</a>
|
||||
<a href="{{ url_for('web.tools') }}"
|
||||
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">
|
||||
Browse Tools
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Support SmartTools - Donate{% endblock %}
|
||||
|
||||
{% block meta_description %}Support SmartTools development and help keep AI tools accessible for everyone.{% 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 SmartTools</h1>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
Your support keeps the registry running and funds new features for the community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<!-- Impact -->
|
||||
<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">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Infrastructure</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Keep the registry fast, reliable, and available for everyone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Open Access</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Fund future hosting of shared AI models and public demos.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Community Growth</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Support tutorials, examples, and recognition for contributors.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Donation options -->
|
||||
<section class="bg-white rounded-lg border border-gray-200 p-8 text-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Choose a Way to Contribute</h2>
|
||||
<p class="mt-3 text-gray-600">
|
||||
Placeholder links for donation providers. Replace with real endpoints when ready.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a href="#"
|
||||
class="inline-flex items-center px-6 py-3 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Donate via GitHub Sponsors
|
||||
</a>
|
||||
<a href="#"
|
||||
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">
|
||||
Donate via Ko-fi
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Transparency -->
|
||||
<section class="mt-12 bg-gray-100 rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">How funds are used</h3>
|
||||
<ul class="mt-3 text-sm text-gray-600 space-y-2">
|
||||
<li>Registry hosting, backups, and monitoring</li>
|
||||
<li>Documentation, tutorials, and example projects</li>
|
||||
<li>Ongoing development and community support</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue