Add survey capability

This commit is contained in:
nemunaire 2025-10-24 18:10:41 +07:00
commit a741570a36
9 changed files with 140 additions and 4 deletions

View file

@ -38,6 +38,7 @@ func declareFlags(o *Config) {
flag.DurationVar(&o.Analysis.HTTPTimeout, "http-timeout", o.Analysis.HTTPTimeout, "Timeout when performing HTTP query")
flag.Var(&StringArray{&o.Analysis.RBLs}, "rbl", "Append a RBL (use this option multiple time to append multiple RBLs)")
flag.DurationVar(&o.ReportRetention, "report-retention", o.ReportRetention, "How long to keep reports (e.g., 720h, 30d). 0 = keep forever")
flag.Var(&URL{&o.SurveyURL}, "survey-url", "URL for user feedback survey")
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
}

View file

@ -25,6 +25,7 @@ import (
"flag"
"fmt"
"log"
"net/url"
"os"
"path"
"strings"
@ -41,6 +42,7 @@ type Config struct {
Email EmailConfig
Analysis AnalysisConfig
ReportRetention time.Duration // How long to keep reports. 0 = keep forever
SurveyURL url.URL // URL for user feedback survey
}
// DatabaseConfig contains database connection settings

View file

@ -23,6 +23,7 @@ package config
import (
"fmt"
"net/url"
"strings"
)
@ -43,3 +44,25 @@ func (i *StringArray) Set(value string) error {
return nil
}
type URL struct {
URL *url.URL
}
func (i *URL) String() string {
if i.URL != nil {
return i.URL.String()
} else {
return ""
}
}
func (i *URL) Set(value string) error {
u, err := url.Parse(value)
if err != nil {
return err
}
*i.URL = *u
return nil
}

View file

@ -58,6 +58,10 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) {
appConfig["report_retention"] = cfg.ReportRetention
}
if cfg.SurveyURL.Host != "" {
appConfig["survey_url"] = cfg.SurveyURL.String()
}
if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil {
log.Println("Unable to generate JSON config to inject in web application")
} else {

View file

@ -14,10 +14,11 @@
}
interface Props {
children?: import("svelte").Snippet;
report: Report;
}
let { report }: Props = $props();
let { children, report }: Props = $props();
function buildSummary(): TextSegment[] {
const segments: TextSegment[] = [];
@ -455,7 +456,7 @@
<i class="bi bi-card-text me-2"></i>
Summary
</h5>
<p class="card-text text-muted mb-0" style="line-height: 1.8;">
<p class="card-text text-muted" class:mb-0={!children} style="line-height: 1.8;">
{#each summarySegments as segment}
{#if segment.link}
<a
@ -474,6 +475,7 @@
{/each}
Overall, your email received a grade <GradeDisplay grade={report.grade} score={report.score} size="inline" />{#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}: you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}: you could have delivery issues with common providers.{:else if report.grade == "F"}: it will most likely be rejected by most providers.{:else}!{/if} Check the details below 🔽
</p>
{@render children?.()}
</div>
</div>

View file

@ -0,0 +1,96 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { ClassValue } from "svelte/elements";
import { appConfig } from "$lib/config";
interface Props {
class: ClassValue;
question?: Snippet;
source?: string;
}
let { class: className, question, source }: Props = $props();
let step = $state<number>(0);
interface Responses {
id: string;
stars: number;
source?: string;
avis?: string;
}
const responses = $state<Responses>({
id: btoa(String(Math.random() * 100)),
stars: 1,
});
function submit(e: SubmitEvent): void {
e.preventDefault();
step += 1;
if (source) {
responses.source = source;
}
if ($appConfig.surveyUrl) {
fetch($appConfig.surveyUrl, {
method: "POST",
body: JSON.stringify(responses),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
}
}
</script>
{#if $appConfig.surveyUrl}
<form class={className} onsubmit={submit}>
{#if step === 0}
{#if question}{@render question()}{:else}
<p class="mb-1 small">Help us to design a better tool, rate this report!</p>
{/if}
<div class="btn-group" role="group" aria-label="Rate your level of happyness">
{#each [...Array(5).keys()] as i}
<button
class="btn btn-lg px-1 pb-2 pt-1"
class:btn-outline-success={responses.stars <= i}
class:text-dark={responses.stars <= i}
class:btn-success={responses.stars > i}
style="line-height: 1em"
onfocusin={() => (responses.stars = i + 1)}
onmouseenter={() => (responses.stars = i + 1)}
aria-label={`${i + 1} star${i + 1 > 1 ? "s" : ""}`}
>
<i class="bi bi-star-fill"></i>
</button>
{/each}
</div>
{:else if step === 1}
<p>
{#if responses.stars == 5}Thank you! Would you like to tell us more?
{:else if responses.stars == 4}What are we missing to earn 5 stars?
{:else}How could we improve?
{/if}
</p>
<!-- svelte-ignore a11y_autofocus -->
<textarea
autofocus
class="form-control"
placeholder="Your thoughts..."
id="q6"
rows="2"
bind:value={responses.avis}
></textarea>
<button class="btn btn-success mt-1"> Send! </button>
{:else if step === 2}
<p class="fw-bold mb-0">
Thank you so much for taking the time to share your feedback!
</p>
{/if}
</form>
{/if}

View file

@ -13,3 +13,4 @@ export { default as ContentAnalysisCard } from "./ContentAnalysisCard.svelte";
export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte";
export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
export { default as TinySurvey } from "./TinySurvey.svelte";

View file

@ -23,10 +23,12 @@ import { writable } from "svelte/store";
interface AppConfig {
report_retention?: number;
surveyUrl?: string;
}
const defaultConfig: AppConfig = {
report_retention: 0,
surveyUrl: "",
};
function getConfigFromScriptTag(): AppConfig | null {

View file

@ -12,7 +12,8 @@
DnsRecordsCard,
BlacklistCard,
ContentAnalysisCard,
HeaderAnalysisCard
HeaderAnalysisCard,
TinySurvey,
} from "$lib/components";
let testId = $derived(page.params.test);
@ -236,7 +237,11 @@
<!-- Summary -->
<div class="row mb-4">
<div class="col-12">
<SummaryCard {report} />
<SummaryCard {report}>
<div class="d-flex justify-content-end me-lg-5">
<TinySurvey class="bg-primary-subtle rounded-4 p-3 text-center" />
</div>
</SummaryCard>
</div>
</div>