Add JavaScript interactivity and consent API

- Complete main.js with all interactive functions:
  - Mobile menu toggle with icon switching
  - Search modal with live search
  - Dashboard modals (tokens, deprecate, settings)
  - Tool detail page (copy install, report modal)
  - Toast notifications
- Add /api/v1/consent endpoint for cookie preferences
- Session-based consent storage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2025-12-31 21:06:40 -04:00
parent e029166ba6
commit 9a33d57de1
5 changed files with 404 additions and 18 deletions

View File

@ -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:

View File

@ -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")

View File

@ -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));
}

View File

@ -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? Wed 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 %}

View File

@ -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 %}