Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:incident_subscriber #316

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions migrations/20250211040017_create_subscribers_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function up(knex) {
return knex.schema.createTable("subscribers", function (table) {
table.increments("id").primary();
table.string("email").notNullable();
table.integer("incident_id").notNullable().defaultTo(0);
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.string("status", 255).defaultTo("ACTIVE");
table.string("token").notNullable().unique();
table.unique(["email", "incident_id"]);
});
}

export function down(knex) {
return knex.schema.dropTableIfExists("subscribers");
}
12 changes: 12 additions & 0 deletions src/lib/components/IncidentNew.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { Button } from "$lib/components/ui/button";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { createEventDispatcher } from "svelte";
export let incident;
export let index;
export let lang;
Expand All @@ -21,6 +22,8 @@
const lastedFor = fd(startTime, endTime, selectedLang);
const startedAt = fdn(startTime, selectedLang);

export let showSubButton = false;

let isFuture = false;
//is future incident
if (nowTime < startTime) {
Expand Down Expand Up @@ -62,6 +65,10 @@
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
let dispatch = createEventDispatcher();
function showSubscribeFn() {
dispatch("subscribe");
}
</script>

<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
Expand Down Expand Up @@ -101,6 +108,11 @@
{/if}
</div>
</Accordion.Trigger>
{#if showSubButton}
<div class="flex justify-end py-2 pr-2">
<Button class="h-8 text-sm" variant="outline" on:click={showSubscribeFn}>Subscribe</Button>
</div>
{/if}
<Accordion.Content>
<div class="px-4 pt-2">
{#if incident.monitors.length > 0}
Expand Down
90 changes: 90 additions & 0 deletions src/lib/components/IncidentSubscribe.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script>
import { Button } from "$lib/components/ui/button";
import { base } from "$app/paths";
import { Loader } from "lucide-svelte";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
export let id;
export let showSubscribe = true;
let formState = "idle";
let validFormMessage = "";
let invalidFormMessage = "";
let newSubscriber = {
id: 0,
email: "",
incident_id: 0
};

async function addNewSubscriber() {
invalidFormMessage = "";
validFormMessage = "";
formState = "loading";

if (newSubscriber.email.trim() === "") {
invalidFormMessage = "Email is required";
formState = "idle";
return;
}

// Email validation using regex
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(newSubscriber.email)) {
invalidFormMessage = "Invalid email format";
formState = "idle";
return;
}

newSubscriber.incident_id = id;

try {
let response = await fetch(base + "/api/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "subscribeToIncidentID", data: newSubscriber })
});

let resp = await response.json();
if (resp.error) {
invalidFormMessage = resp.message;
}
validFormMessage = "Email successfully subscribed";
} catch (error) {
// Set the invalid form message based on the error thrown
invalidFormMessage = "Failed inserting email";
} finally {
formState = "idle";
setTimeout(() => {
showSubscribe = false;
}, 2000);
}
}
</script>

<div>
<div class="mt-2 w-full">
<Label class="text-sm">
Add Email Address
<span class="text-red-500">*</span>
</Label>
<Input class="mt-2" bind:value={newSubscriber.email} placeholder="[email protected]" />
</div>
<div class="py-2.5">
{#if invalidFormMessage != ""}
<div class="col-span-5 pt-2.5">
<p class="text-right text-xs font-medium text-red-500">{invalidFormMessage}</p>
</div>
{:else if validFormMessage != ""}
<div class="col-span-5 pt-2.5">
<p class="text-right text-xs font-medium text-green-500">{validFormMessage}</p>
</div>
{/if}
<Button class="h-8" on:click={addNewSubscriber}>
Submit
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
151 changes: 151 additions & 0 deletions src/lib/components/manage/subscriberInfo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<script>
import { Button } from "$lib/components/ui/button";
import { base } from "$app/paths";
import { Loader } from "lucide-svelte";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { onMount } from "svelte";

let formState = "idle";
let invalidFormMessage = "";
let subscribers = [];
let loadingData = false;

let newSubscriber = {
id: 0,
email: "",
incident_id: 0
};

async function addNewSubscriber() {
invalidFormMessage = "";
formState = "loading";

if (newSubscriber.email == "") {
invalidFormMessage = "Email is required";
formState = "idle";
return;
}
if (newSubscriber.email.trim() === "") {
invalidFormMessage = "Email is required";
formState = "idle";
return;
}
// Email validation using regex
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(newSubscriber.email)) {
invalidFormMessage = "Invalid email format";
formState = "idle";
return;
}
try {
let data = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "subscribeToIncidentID", data: newSubscriber })
});
let resp = await data.json();
if (resp.error) {
invalidFormMessage = resp.message;
}
} catch (error) {
invalidFormMessage = "Error while inserting email";
} finally {
formState = "idle";
}
}

async function loadData() {
//fetch data
loadingData = true;
try {
let resp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "getSubscribers" })
});
subscribers = await resp.json();
} catch (error) {
alert("Error: " + error);
} finally {
loadingData = false;
}
}

onMount(() => {
loadData();
});
</script>

<div>
<div class="mt-2 w-full">
<Label class="text-sm">
Add Email Address
<span class="text-red-500">*</span>
</Label>
<Input class="mt-2" bind:value={newSubscriber.email} placeholder="[email protected]" />
</div>
<div class="py-2.5">
<div class="col-span-5 pt-2.5">
<p class="text-right text-xs font-medium text-red-500">{invalidFormMessage}</p>
</div>
<Button class="h-8" on:click={addNewSubscriber}>
Submit
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
<div>
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
{#if loadingData}
<div class="flex items-center justify-center">
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
</div>
{/if}
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
<thead>
<tr>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-semibold uppercase text-gray-500 dark:text-neutral-500">id</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-semibold uppercase text-gray-500 dark:text-neutral-500">email</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-semibold uppercase text-gray-500 dark:text-neutral-500"
>incident_id</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-semibold uppercase text-gray-500 dark:text-neutral-500"
>status</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-semibold uppercase text-gray-500 dark:text-neutral-500">token</th
>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{#each subscribers as subscriber, i}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium">{subscriber.id}</td>
<td class=" px-6 py-4 text-xs font-semibold">{subscriber.email}</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold leading-5">{subscriber.incident_id}</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold leading-5">{subscriber.status}</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold leading-5">{subscriber.token}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
40 changes: 40 additions & 0 deletions src/lib/components/subscribeModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script>
import { Button, buttonVariants } from "$lib/components/ui/button";
import { fade } from "svelte/transition";
import IncidentSubscribe from "$lib/components/IncidentSubscribe.svelte";
import { X } from "lucide-svelte";
export let showSubscribe = true;
export let selectedIncident;
</script>

<div
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
transition:fade={{ duration: 100 }}
on:click={() => {
showSubscribe = false;
}}
>
<div
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
on:click|stopPropagation
>
<Button
variant="ghost"
on:click={() => {
showSubscribe = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content px-4 py-4">
<h2 class="text-lg font-semibold">Subscribe to {selectedIncident.title}</h2>
<p class="text-xs text-muted-foreground">
Subscribe to updates for {selectedIncident.title} via email. You'll receive email notifications when incidents are
updated.
</p>
<hr class="my-4" />
<IncidentSubscribe bind:showSubscribe id={selectedIncident.id} />
</div>
</div>
</div>
Loading