Skip to content

Commit 07d9bb3

Browse files
feat: toasts/copy tracking script
Signed-off-by: Henry Gressmann <[email protected]>
1 parent d4837e1 commit 07d9bb3

File tree

5 files changed

+120
-2
lines changed

5 files changed

+120
-2
lines changed

web/src/components/settings/dialogs.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type UserResponse,
2020
} from "../../api";
2121
import { InfoIcon } from "lucide-react";
22+
import { createToast } from "../toast";
2223

2324
const toTitleCase = (str: string) => str[0].toUpperCase() + str.slice(1);
2425

@@ -52,6 +53,7 @@ export const DeleteDialog = ({
5253
invalidateUsers();
5354
break;
5455
}
56+
createToast(`${toTitleCase(type)} deleted successfully`, "success");
5557
},
5658
onError: console.error,
5759
});
@@ -117,6 +119,7 @@ export const EditProjectEntities = ({ project, trigger }: { project: ProjectResp
117119
mutationFn: api["/api/dashboard/project/{project_id}"].put,
118120
onSuccess: () => {
119121
closeRef?.current?.click();
122+
createToast("Entities updated successfully", "success");
120123
invalidateProjects();
121124
},
122125
onError: console.error,
@@ -184,6 +187,7 @@ export const EditProject = ({ project, trigger }: { project: ProjectResponse; tr
184187
onSuccess: () => {
185188
closeRef?.current?.click();
186189
queryClient.invalidateQueries({ queryKey: ["projects"] });
190+
createToast("Project updated successfully", "success");
187191
},
188192
onError: console.error,
189193
});
@@ -256,6 +260,7 @@ export const CreateProject = () => {
256260
mutationFn: api["/api/dashboard/project/{project_id}"].post,
257261
onSuccess: () => {
258262
closeRef?.current?.click();
263+
createToast("Project created successfully", "success");
259264
invalidateProjects();
260265
},
261266
onError: console.error,
@@ -337,6 +342,7 @@ export const EditEntity = ({ entity, trigger }: { entity: EntityResponse; trigge
337342
mutationFn: api["/api/dashboard/entity/{entity_id}"].put,
338343
onSuccess: () => {
339344
closeRef?.current?.click();
345+
createToast("Entity updated successfully", "success");
340346
invalidateEntities();
341347
},
342348
onError: console.error,
@@ -422,6 +428,7 @@ export const CreateEntity = () => {
422428
mutationFn: api["/api/dashboard/entity"].post,
423429
onSuccess: () => {
424430
closeRef?.current?.click();
431+
createToast("Entity created successfully", "success");
425432
invalidateEntities();
426433
},
427434
onError: console.error,
@@ -497,6 +504,7 @@ export const EditPassword = ({ user, trigger }: { user: UserResponse; trigger: J
497504
const { mutate, error, reset } = useMutation({
498505
mutationFn: api["/api/dashboard/user/{username}/password"].put,
499506
onSuccess: () => {
507+
createToast("Password updated successfully", "success");
500508
closeRef?.current?.click();
501509
},
502510
onError: console.error,
@@ -564,6 +572,7 @@ export const EditUser = ({ user, trigger }: { user: UserResponse; trigger: JSX.E
564572
mutationFn: api["/api/dashboard/user/{username}"].put,
565573
onSuccess: () => {
566574
closeRef?.current?.click();
575+
createToast("User updated successfully", "success");
567576
invalidateUsers();
568577
},
569578
onError: console.error,
@@ -624,6 +633,7 @@ export const CreateUser = () => {
624633
mutationFn: api["/api/dashboard/user"].post,
625634
onSuccess: () => {
626635
closeRef?.current?.click();
636+
createToast("User created successfully", "success");
627637
invalidateUsers();
628638
},
629639
onError: console.error,

web/src/components/settings/tables.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
type ProjectResponse,
2525
type UserResponse,
2626
} from "../../api";
27+
import { createToast } from "../toast";
2728

2829
type DropdownOptions = Record<string, ((close: () => void) => JSX.Element) | null>;
2930

@@ -74,7 +75,6 @@ const ProjectDropdown = ({ project }: { project: ProjectResponse }) => {
7475
}
7576
/>
7677
),
77-
7878
delete: (close) => (
7979
<DeleteDialog
8080
id={project.id}
@@ -107,6 +107,7 @@ export const ProjectsTable = () => {
107107
icon: <WholeWordIcon size={18} />,
108108
header: "ID",
109109
render: (row) => <i>{row.id}</i>,
110+
nowrap: true,
110111
},
111112
{
112113
id: "public",
@@ -141,6 +142,24 @@ export const ProjectsTable = () => {
141142

142143
const EntityDropdown = ({ entity }: { entity: EntityResponse }) => {
143144
const options: DropdownOptions = {
145+
copy: (close) => (
146+
<button
147+
type="button"
148+
onClick={() => {
149+
navigator.clipboard
150+
.writeText(
151+
`<script type="module" data-entity=${entity.id} src="${window.location.origin}/script.js"></script>`,
152+
)
153+
.then(() => createToast("Snippet copied to clipboard", "info"))
154+
.catch(() => createToast("Failed to copy snippet to clipboard", "error"));
155+
156+
close();
157+
}}
158+
>
159+
<RectangleEllipsisIcon size={18} />
160+
Copy Snippet
161+
</button>
162+
),
144163
edit: (close) => (
145164
<EditEntity
146165
entity={entity}
@@ -184,6 +203,7 @@ export const EntitiesTable = () => {
184203
icon: <WholeWordIcon size={18} />,
185204
header: "ID",
186205
render: (row) => <i>{row.id}</i>,
206+
nowrap: true,
187207
},
188208
{
189209
id: "projects",
@@ -266,6 +286,7 @@ export const UsersTable = () => {
266286
header: "Username",
267287
icon: <UserIcon size={18} />,
268288
render: (row) => <span>{row.username}</span>,
289+
nowrap: true,
269290
},
270291
{
271292
id: "role",

web/src/components/table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const Table = <T extends { id: string }>({
1717
columns: Column<T>[];
1818
}) => {
1919
return (
20-
<div className="overflow-auto">
20+
<div className={styles.container}>
2121
<table className={styles.table}>
2222
<thead>
2323
<tr>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.toastContainer {
2+
position: fixed;
3+
bottom: 2rem;
4+
right: 2rem;
5+
display: flex;
6+
flex-direction: column;
7+
align-items: flex-end;
8+
gap: 0.5rem;
9+
z-index: 9999;
10+
}
11+
12+
.toast {
13+
padding: 0.8rem 1.1rem;
14+
border-radius: 0.5rem;
15+
font-size: 0.9rem;
16+
color: #fff;
17+
box-shadow: var(--pico-card-box-shadow);
18+
border-radius: var(--pico-border-radius);
19+
}
20+
21+
.success {
22+
background-color: #4caf50;
23+
}
24+
25+
.error {
26+
background-color: #f44336;
27+
}
28+
29+
.info {
30+
background-color: var(--pico-primary-background);
31+
}
32+
33+
.warning {
34+
background-color: #ff9800;
35+
}

web/src/components/toast.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import styles from "./toast.module.css";
2+
3+
type ToastType = "success" | "error" | "info" | "warning";
4+
5+
export const createToast = (message: string, type: ToastType = "info") => {
6+
let toastContainer = document.getElementById("toast-container");
7+
if (!toastContainer) {
8+
toastContainer = document.createElement("div");
9+
toastContainer.id = "toast-container";
10+
toastContainer.className = styles.toastContainer;
11+
toastContainer.setAttribute("role", "alert");
12+
toastContainer.setAttribute("aria-live", "assertive");
13+
toastContainer.setAttribute("aria-atomic", "true");
14+
document.body.appendChild(toastContainer);
15+
}
16+
17+
const toast = document.createElement("div");
18+
toast.className = `${styles.toast} ${styles[type]}`;
19+
toast.textContent = message;
20+
21+
toastContainer.appendChild(toast);
22+
toast.animate(
23+
[
24+
{ opacity: 0, transform: "translateY(0.5rem)" },
25+
{ opacity: 1, transform: "translateY(0)" },
26+
],
27+
{
28+
duration: 300,
29+
easing: "ease-out",
30+
},
31+
);
32+
33+
setTimeout(() => {
34+
const fadeOut = toast.animate(
35+
[
36+
{ opacity: 1, transform: "translateY(0)" },
37+
{ opacity: 0, transform: "translateY(0.5rem)" },
38+
],
39+
{
40+
duration: 500,
41+
easing: "ease-out",
42+
},
43+
);
44+
45+
fadeOut.onfinish = () => {
46+
toast.remove();
47+
if (toastContainer && toastContainer.childElementCount === 0) {
48+
toastContainer.remove();
49+
}
50+
};
51+
}, 2500);
52+
};

0 commit comments

Comments
 (0)