- Add database schema for reviews, issues, and stats caching - Add API endpoints for reviews (CRUD, voting, flagging) - Add API endpoints for issues (report, list, resolve) - Add publisher stats and badge system - Add trust score calculation (0-100 scale) - Update tool detail page with ratings, reviews, issues sections - Update publisher profile with stats, badges, trust score - Add dedicated reviews and issues pages with filtering - Update dashboard tools table with rating/issues columns - Update tool cards with inline rating display Co-Authored-By: Claude Opus 4.5 <[email protected]>
178 lines
12 KiB
HTML
178 lines
12 KiB
HTML
{% 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 flex-wrap 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>
|
|
{% if stats.total_downloads %}
|
|
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
|
</svg>
|
|
{{ stats.total_downloads_formatted or stats.total_downloads }} downloads
|
|
</span>
|
|
{% endif %}
|
|
{% if stats.average_rating and stats.total_reviews %}
|
|
<span class="flex items-center gap-1">
|
|
<svg class="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
|
</svg>
|
|
{{ "%.1f"|format(stats.average_rating) }} ({{ stats.total_reviews }} review{{ 's' if stats.total_reviews != 1 else '' }})
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Badges -->
|
|
{% if stats.badges %}
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
{% for badge in stats.badges %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium
|
|
{{ 'bg-blue-100 text-blue-800' if badge.color == 'blue' else
|
|
'bg-yellow-100 text-yellow-800' if badge.color == 'gold' or badge.color == 'yellow' else
|
|
'bg-purple-100 text-purple-800' if badge.color == 'purple' else
|
|
'bg-green-100 text-green-800' if badge.color == 'green' else
|
|
'bg-cyan-100 text-cyan-800' if badge.color == 'cyan' else
|
|
'bg-gray-100 text-gray-800' }}"
|
|
title="{{ badge.description }}">
|
|
{% if badge.icon == 'shield-check' %}
|
|
<svg class="w-3.5 h-3.5 mr-1" 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>
|
|
{% elif badge.icon == 'award' %}
|
|
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/>
|
|
</svg>
|
|
{% elif badge.icon == 'layers' %}
|
|
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
</svg>
|
|
{% elif badge.icon == 'clock' %}
|
|
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
{% elif badge.icon == 'trending-up' %}
|
|
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
|
</svg>
|
|
{% elif badge.icon == 'message-circle' %}
|
|
<svg class="w-3.5 h-3.5 mr-1" 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 badge.icon == 'star' %}
|
|
<svg class="w-3.5 h-3.5 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
|
</svg>
|
|
{% endif %}
|
|
{{ badge.name }}
|
|
</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Trust Score -->
|
|
{% if stats.trust_score and stats.trust_score > 0 %}
|
|
<div class="mt-4">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-gray-500">Trust Score:</span>
|
|
<div class="flex-1 max-w-xs h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div class="h-full rounded-full
|
|
{{ 'bg-green-500' if stats.trust_score >= 80 else
|
|
'bg-yellow-500' if stats.trust_score >= 50 else
|
|
'bg-red-500' }}"
|
|
style="width: {{ stats.trust_score }}%"></div>
|
|
</div>
|
|
<span class="text-sm font-medium
|
|
{{ 'text-green-600' if stats.trust_score >= 80 else
|
|
'text-yellow-600' if stats.trust_score >= 50 else
|
|
'text-red-600' }}">
|
|
{{ stats.trust_score|int }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</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 %}
|