Implement web ui
This commit is contained in:
parent
0889dce85b
commit
a58cfa5f96
13 changed files with 1141 additions and 0 deletions
74
web/src/lib/components/CheckCard.svelte
Normal file
74
web/src/lib/components/CheckCard.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import type { Check } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
check: Check;
|
||||
}
|
||||
|
||||
let { check }: Props = $props();
|
||||
|
||||
function getCheckIcon(status: string): string {
|
||||
switch (status) {
|
||||
case "pass":
|
||||
return "bi-check-circle-fill text-success";
|
||||
case "fail":
|
||||
return "bi-x-circle-fill text-danger";
|
||||
case "warn":
|
||||
return "bi-exclamation-triangle-fill text-warning";
|
||||
case "info":
|
||||
return "bi-info-circle-fill text-info";
|
||||
default:
|
||||
return "bi-question-circle-fill text-secondary";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="fs-4">
|
||||
<i class={getCheckIcon(check.status)}></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h5 class="fw-bold mb-1">{check.name}</h5>
|
||||
<span class="badge bg-secondary text-capitalize">{check.category}</span>
|
||||
</div>
|
||||
<span class="badge bg-light text-dark">{check.score.toFixed(1)} pts</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 mb-2">{check.message}</p>
|
||||
|
||||
{#if check.advice}
|
||||
<div class="alert alert-light border mb-2" role="alert">
|
||||
<i class="bi bi-lightbulb me-2"></i>
|
||||
<strong>Recommendation:</strong>
|
||||
{check.advice}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if check.details}
|
||||
<details class="small text-muted">
|
||||
<summary class="cursor-pointer">Technical Details</summary>
|
||||
<pre class="mt-2 mb-0 small bg-light p-2 rounded">{check.details}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details summary {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details summary:hover {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
</style>
|
||||
46
web/src/lib/components/EmailAddressDisplay.svelte
Normal file
46
web/src/lib/components/EmailAddressDisplay.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
email: string;
|
||||
}
|
||||
|
||||
let { email }: Props = $props();
|
||||
let copied = $state(false);
|
||||
|
||||
async function copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(email);
|
||||
copied = true;
|
||||
setTimeout(() => (copied = false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-light rounded p-4">
|
||||
<div class="d-flex align-items-center justify-content-center gap-3">
|
||||
<code class="fs-5 text-primary fw-bold">{email}</code>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary clipboard-btn"
|
||||
onclick={copyToClipboard}
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<i class={copied ? "bi bi-check2" : "bi bi-clipboard"}></i>
|
||||
</button>
|
||||
</div>
|
||||
{#if copied}
|
||||
<small class="text-success d-block mt-2">
|
||||
<i class="bi bi-check2"></i> Copied to clipboard!
|
||||
</small>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.clipboard-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.clipboard-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
33
web/src/lib/components/FeatureCard.svelte
Normal file
33
web/src/lib/components/FeatureCard.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
variant?: "primary" | "success" | "warning" | "danger" | "info" | "secondary";
|
||||
}
|
||||
|
||||
let { icon, title, description, variant = "primary" }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card h-100 text-center p-4">
|
||||
<div class="feature-icon bg-{variant} bg-opacity-10 text-{variant} mx-auto">
|
||||
<i class="bi {icon}"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold">{title}</h5>
|
||||
<p class="text-muted small mb-0">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feature-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
17
web/src/lib/components/HowItWorksStep.svelte
Normal file
17
web/src/lib/components/HowItWorksStep.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
step: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
let { step, title, description }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card h-100 text-center p-4">
|
||||
<div class="display-1 text-primary fw-bold opacity-25">{step}</div>
|
||||
<h5 class="fw-bold mt-3">{title}</h5>
|
||||
<p class="text-muted mb-0">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
115
web/src/lib/components/PendingState.svelte
Normal file
115
web/src/lib/components/PendingState.svelte
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<script lang="ts">
|
||||
import type { Test } from "$lib/api/types.gen";
|
||||
import EmailAddressDisplay from "./EmailAddressDisplay.svelte";
|
||||
|
||||
interface Props {
|
||||
test: Test;
|
||||
}
|
||||
|
||||
let { test }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 fade-in">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-body p-5 text-center">
|
||||
<div class="pulse mb-4">
|
||||
<i class="bi bi-envelope-paper display-1 text-primary"></i>
|
||||
</div>
|
||||
|
||||
<h2 class="fw-bold mb-3">Waiting for Your Email</h2>
|
||||
<p class="text-muted mb-4">Send your test email to the address below:</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<EmailAddressDisplay email={test.email} />
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mb-4" role="alert">
|
||||
<i class="bi bi-lightbulb me-2"></i>
|
||||
<strong>Tip:</strong> Send an email that represents your actual use case (newsletters,
|
||||
transactional emails, etc.) for the most accurate results.
|
||||
</div>
|
||||
|
||||
{#if test.status === "received"}
|
||||
<div class="alert alert-success" role="alert">
|
||||
<i class="bi bi-check-circle me-2"></i>
|
||||
Email received! Analyzing...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="d-flex align-items-center justify-content-center gap-2 text-muted">
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
<small>Checking for email every 3 seconds...</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions Card -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3">
|
||||
<i class="bi bi-info-circle me-2"></i>What we'll check:
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check2 text-success me-2"></i> SPF, DKIM, DMARC
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check2 text-success me-2"></i> DNS Records
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check2 text-success me-2"></i> SpamAssassin Score
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check2 text-success me-2"></i> Blacklist Status
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check2 text-success me-2"></i> Content Quality
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check2 text-success me-2"></i> Header Validation
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
web/src/lib/components/ScoreCard.svelte
Normal file
71
web/src/lib/components/ScoreCard.svelte
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<script lang="ts">
|
||||
import type { ScoreSummary } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
score: number;
|
||||
summary?: ScoreSummary;
|
||||
}
|
||||
|
||||
let { score, summary }: Props = $props();
|
||||
|
||||
function getScoreClass(score: number): string {
|
||||
if (score >= 9) return "score-excellent";
|
||||
if (score >= 7) return "score-good";
|
||||
if (score >= 5) return "score-warning";
|
||||
if (score >= 3) return "score-poor";
|
||||
return "score-bad";
|
||||
}
|
||||
|
||||
function getScoreLabel(score: number): string {
|
||||
if (score >= 9) return "Excellent";
|
||||
if (score >= 7) return "Good";
|
||||
if (score >= 5) return "Fair";
|
||||
if (score >= 3) return "Poor";
|
||||
return "Critical";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card shadow-lg bg-white">
|
||||
<div class="card-body p-5 text-center">
|
||||
<h1 class="display-1 fw-bold mb-3 {getScoreClass(score)}">
|
||||
{score.toFixed(1)}/10
|
||||
</h1>
|
||||
<h3 class="fw-bold mb-2">{getScoreLabel(score)}</h3>
|
||||
<p class="text-muted mb-4">Overall Deliverability Score</p>
|
||||
|
||||
{#if summary}
|
||||
<div class="row g-3 text-start">
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-3 bg-light rounded">
|
||||
<small class="text-muted d-block">Authentication</small>
|
||||
<strong class="fs-5">{summary.authentication_score.toFixed(1)}/3</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-3 bg-light rounded">
|
||||
<small class="text-muted d-block">Spam Score</small>
|
||||
<strong class="fs-5">{summary.spam_score.toFixed(1)}/2</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-3 bg-light rounded">
|
||||
<small class="text-muted d-block">Blacklists</small>
|
||||
<strong class="fs-5">{summary.blacklist_score.toFixed(1)}/2</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-3 bg-light rounded">
|
||||
<small class="text-muted d-block">Content</small>
|
||||
<strong class="fs-5">{summary.content_score.toFixed(1)}/2</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-3 bg-light rounded">
|
||||
<small class="text-muted d-block">Headers</small>
|
||||
<strong class="fs-5">{summary.header_score.toFixed(1)}/1</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
65
web/src/lib/components/SpamAssassinCard.svelte
Normal file
65
web/src/lib/components/SpamAssassinCard.svelte
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import type { SpamAssassinResult } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
spamassassin: SpamAssassinResult;
|
||||
}
|
||||
|
||||
let { spamassassin }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-warning bg-opacity-10">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="bi bi-bug me-2"></i>SpamAssassin Analysis
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Score:</strong>
|
||||
<span class={spamassassin.is_spam ? "text-danger" : "text-success"}>
|
||||
{spamassassin.score.toFixed(2)} / {spamassassin.required_score.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Classified as:</strong>
|
||||
<span class="badge {spamassassin.is_spam ? 'bg-danger' : 'bg-success'} ms-2">
|
||||
{spamassassin.is_spam ? "SPAM" : "HAM"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if spamassassin.tests && spamassassin.tests.length > 0}
|
||||
<div class="mb-2">
|
||||
<strong>Tests Triggered:</strong>
|
||||
<div class="mt-2">
|
||||
{#each spamassassin.tests as test}
|
||||
<span class="badge bg-light text-dark me-1 mb-1">{test}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if spamassassin.report}
|
||||
<details class="mt-3">
|
||||
<summary class="cursor-pointer fw-bold">Full Report</summary>
|
||||
<pre class="mt-2 small bg-light p-3 rounded">{spamassassin.report}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details summary {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details summary:hover {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
</style>
|
||||
8
web/src/lib/components/index.ts
Normal file
8
web/src/lib/components/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Component exports
|
||||
export { default as FeatureCard } from "./FeatureCard.svelte";
|
||||
export { default as HowItWorksStep } from "./HowItWorksStep.svelte";
|
||||
export { default as ScoreCard } from "./ScoreCard.svelte";
|
||||
export { default as CheckCard } from "./CheckCard.svelte";
|
||||
export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte";
|
||||
export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte";
|
||||
export { default as PendingState } from "./PendingState.svelte";
|
||||
Loading…
Add table
Add a link
Reference in a new issue