251 lines
12 KiB
HTML
251 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Website Analyzer - Scan Your Website{% endblock %}
|
|
|
|
{% block content %}
|
|
<div x-data="scanApp()" x-cloak class="space-y-8">
|
|
<!-- Hero Section -->
|
|
<div class="text-center py-12">
|
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
|
Analyze Your Website
|
|
</h1>
|
|
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
|
Get comprehensive insights on performance, security, accessibility, and SEO.
|
|
Powered by Lighthouse, OWASP ZAP, and advanced browser analysis.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- URL Input Form -->
|
|
<div class="max-w-2xl mx-auto">
|
|
<form @submit.prevent="startScan" class="bg-white rounded-xl shadow-lg p-6">
|
|
<div class="flex flex-col sm:flex-row gap-4">
|
|
<div class="flex-1">
|
|
<label for="url" class="sr-only">Website URL</label>
|
|
<input
|
|
type="url"
|
|
id="url"
|
|
x-model="url"
|
|
placeholder="https://example.com"
|
|
required
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
|
|
:disabled="loading"
|
|
>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
:disabled="loading || !url"
|
|
class="px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
<span x-show="!loading">Scan Now</span>
|
|
<span x-show="loading" class="flex items-center">
|
|
<svg class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Starting...
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<div x-show="error" x-transition class="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
</svg>
|
|
<span class="text-red-700" x-text="error"></span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Features Grid -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mt-12">
|
|
<!-- Performance -->
|
|
<div class="bg-white rounded-xl shadow p-6 border-t-4 border-blue-500">
|
|
<div class="flex items-center mb-4">
|
|
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-blue-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="ml-3 font-semibold text-gray-900">Performance</h3>
|
|
</div>
|
|
<p class="text-gray-600 text-sm">
|
|
Core Web Vitals, load times, bundle sizes, and render-blocking resources.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Security -->
|
|
<div class="bg-white rounded-xl shadow p-6 border-t-4 border-red-500">
|
|
<div class="flex items-center mb-4">
|
|
<div class="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-red-600" 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>
|
|
</div>
|
|
<h3 class="ml-3 font-semibold text-gray-900">Security</h3>
|
|
</div>
|
|
<p class="text-gray-600 text-sm">
|
|
OWASP ZAP scanning, security headers, TLS/SSL, and cookie analysis.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Accessibility -->
|
|
<div class="bg-white rounded-xl shadow p-6 border-t-4 border-green-500">
|
|
<div class="flex items-center mb-4">
|
|
<div class="w-10 h-10 rounded-lg bg-green-100 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="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="ml-3 font-semibold text-gray-900">Accessibility</h3>
|
|
</div>
|
|
<p class="text-gray-600 text-sm">
|
|
WCAG compliance, ARIA labels, color contrast, and keyboard navigation.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- SEO -->
|
|
<div class="bg-white rounded-xl shadow p-6 border-t-4 border-purple-500">
|
|
<div class="flex items-center mb-4">
|
|
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-purple-600" 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>
|
|
</div>
|
|
<h3 class="ml-3 font-semibold text-gray-900">SEO</h3>
|
|
</div>
|
|
<p class="text-gray-600 text-sm">
|
|
Meta tags, structured data, crawlability, and search engine optimization.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Scans -->
|
|
<div class="mt-12" x-show="recentScans.length > 0">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">Recent Scans</h2>
|
|
<div class="bg-white rounded-xl shadow overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Website</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Score</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<template x-for="scan in recentScans" :key="scan.id">
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="text-sm text-gray-900" x-text="scan.website_url"></span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
class="px-2 py-1 text-xs font-medium rounded-full"
|
|
:class="{
|
|
'bg-green-100 text-green-800': scan.status === 'done',
|
|
'bg-yellow-100 text-yellow-800': scan.status === 'running' || scan.status === 'pending',
|
|
'bg-red-100 text-red-800': scan.status === 'failed',
|
|
'bg-blue-100 text-blue-800': scan.status === 'partial'
|
|
}"
|
|
x-text="scan.status_display"
|
|
></span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
class="text-sm font-medium"
|
|
:class="{
|
|
'text-green-600': scan.overall_score >= 80,
|
|
'text-yellow-600': scan.overall_score >= 50 && scan.overall_score < 80,
|
|
'text-red-600': scan.overall_score < 50
|
|
}"
|
|
x-text="scan.overall_score !== null ? scan.overall_score + '/100' : '-'"
|
|
></span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<span x-text="new Date(scan.created_at).toLocaleString()"></span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
<a
|
|
:href="'/scan/' + scan.id + '/'"
|
|
class="text-blue-600 hover:text-blue-800 font-medium"
|
|
>
|
|
View Details
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function scanApp() {
|
|
return {
|
|
url: '',
|
|
loading: false,
|
|
error: null,
|
|
recentScans: [],
|
|
|
|
async init() {
|
|
await this.loadRecentScans();
|
|
},
|
|
|
|
async loadRecentScans() {
|
|
try {
|
|
const response = await fetch('/api/scans/?page_size=10');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.recentScans = data.results || [];
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load recent scans:', err);
|
|
}
|
|
},
|
|
|
|
async startScan() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const response = await fetch('/api/scans/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCsrfToken(),
|
|
},
|
|
body: JSON.stringify({ url: this.url })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || data.url?.[0] || 'Failed to start scan');
|
|
}
|
|
|
|
const scan = await response.json();
|
|
|
|
// Redirect to scan detail page
|
|
window.location.href = `/scan/${scan.id}/`;
|
|
|
|
} catch (err) {
|
|
this.error = err.message;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
getCsrfToken() {
|
|
return document.cookie.split('; ')
|
|
.find(row => row.startsWith('csrftoken='))
|
|
?.split('=')[1] || '';
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|