diff --git a/src/app/route-tree.gen.ts b/src/app/route-tree.gen.ts
index 1e788bbc..76219431 100644
--- a/src/app/route-tree.gen.ts
+++ b/src/app/route-tree.gen.ts
@@ -23,7 +23,6 @@ import { Route as With_menuSportRouteImport } from "./routes/_with_menu/sport";
import { Route as With_menuScholarshipRouteImport } from "./routes/_with_menu/scholarship";
import { Route as With_menuMenuRouteImport } from "./routes/_with_menu/menu";
import { Route as With_menuMapsRouteImport } from "./routes/_with_menu/maps";
-import { Route as With_menuGuardRouteImport } from "./routes/_with_menu/guard";
import { Route as With_menuFormsRouteImport } from "./routes/_with_menu/forms";
import { Route as With_menuExtensionRouteImport } from "./routes/_with_menu/extension";
import { Route as With_menuDormsRouteImport } from "./routes/_with_menu/dorms";
@@ -39,6 +38,7 @@ import { Route as With_menuScheduleIndexRouteImport } from "./routes/_with_menu/
import { Route as With_menuRoomBookingIndexRouteImport } from "./routes/_with_menu/room-booking/index";
import { Route as With_menuPrintersIndexRouteImport } from "./routes/_with_menu/printers/index";
import { Route as With_menuMusicRoomIndexRouteImport } from "./routes/_with_menu/music-room.index";
+import { Route as With_menuGuardIndexRouteImport } from "./routes/_with_menu/guard/index";
import { Route as With_menuEventsIndexRouteImport } from "./routes/_with_menu/events/index";
import { Route as With_menuClubsIndexRouteImport } from "./routes/_with_menu/clubs/index";
import { Route as With_menuCatalogueIndexRouteImport } from "./routes/_with_menu/catalogue/index";
@@ -55,6 +55,9 @@ import { Route as With_menuRoomBookingListRouteImport } from "./routes/_with_men
import { Route as With_menuPrintersScanRouteImport } from "./routes/_with_menu/printers/scan";
import { Route as With_menuPrintersPrintRouteImport } from "./routes/_with_menu/printers/print";
import { Route as With_menuMusicRoomInstructionsRouteImport } from "./routes/_with_menu/music-room.instructions";
+import { Route as With_menuGuardFilesRouteImport } from "./routes/_with_menu/guard/files";
+import { Route as With_menuGuardCreateRouteImport } from "./routes/_with_menu/guard/create";
+import { Route as With_menuGuardCopyRouteImport } from "./routes/_with_menu/guard/copy";
import { Route as With_menuEventsCalendarRouteImport } from "./routes/_with_menu/events/calendar";
import { Route as With_menuEventsArchiveRouteImport } from "./routes/_with_menu/events/archive";
import { Route as With_menuEventsApprovalRouteImport } from "./routes/_with_menu/events/approval";
@@ -145,11 +148,6 @@ const With_menuMapsRoute = With_menuMapsRouteImport.update({
path: "/maps",
getParentRoute: () => With_menuRouteRoute,
} as any);
-const With_menuGuardRoute = With_menuGuardRouteImport.update({
- id: "/guard",
- path: "/guard",
- getParentRoute: () => With_menuRouteRoute,
-} as any);
const With_menuFormsRoute = With_menuFormsRouteImport.update({
id: "/forms",
path: "/forms",
@@ -228,6 +226,11 @@ const With_menuMusicRoomIndexRoute = With_menuMusicRoomIndexRouteImport.update({
path: "/music-room/",
getParentRoute: () => With_menuRouteRoute,
} as any);
+const With_menuGuardIndexRoute = With_menuGuardIndexRouteImport.update({
+ id: "/guard/",
+ path: "/guard/",
+ getParentRoute: () => With_menuRouteRoute,
+} as any);
const With_menuEventsIndexRoute = With_menuEventsIndexRouteImport.update({
id: "/events/",
path: "/events/",
@@ -315,6 +318,21 @@ const With_menuMusicRoomInstructionsRoute =
path: "/music-room/instructions",
getParentRoute: () => With_menuRouteRoute,
} as any);
+const With_menuGuardFilesRoute = With_menuGuardFilesRouteImport.update({
+ id: "/guard/files",
+ path: "/guard/files",
+ getParentRoute: () => With_menuRouteRoute,
+} as any);
+const With_menuGuardCreateRoute = With_menuGuardCreateRouteImport.update({
+ id: "/guard/create",
+ path: "/guard/create",
+ getParentRoute: () => With_menuRouteRoute,
+} as any);
+const With_menuGuardCopyRoute = With_menuGuardCopyRouteImport.update({
+ id: "/guard/copy",
+ path: "/guard/copy",
+ getParentRoute: () => With_menuRouteRoute,
+} as any);
const With_menuEventsCalendarRoute = With_menuEventsCalendarRouteImport.update({
id: "/events/calendar",
path: "/events/calendar",
@@ -428,7 +446,6 @@ export interface FileRoutesByFullPath {
"/dorms": typeof With_menuDormsRoute;
"/extension": typeof With_menuExtensionRoute;
"/forms": typeof With_menuFormsRoute;
- "/guard": typeof With_menuGuardRoute;
"/maps": typeof With_menuMapsRoute;
"/menu": typeof With_menuMenuRoute;
"/scholarship": typeof With_menuScholarshipRoute;
@@ -449,6 +466,9 @@ export interface FileRoutesByFullPath {
"/events/approval": typeof With_menuEventsApprovalRoute;
"/events/archive": typeof With_menuEventsArchiveRoute;
"/events/calendar": typeof With_menuEventsCalendarRoute;
+ "/guard/copy": typeof With_menuGuardCopyRoute;
+ "/guard/create": typeof With_menuGuardCreateRoute;
+ "/guard/files": typeof With_menuGuardFilesRoute;
"/music-room/instructions": typeof With_menuMusicRoomInstructionsRoute;
"/printers/print": typeof With_menuPrintersPrintRoute;
"/printers/scan": typeof With_menuPrintersScanRoute;
@@ -465,6 +485,7 @@ export interface FileRoutesByFullPath {
"/catalogue/": typeof With_menuCatalogueIndexRoute;
"/clubs/": typeof With_menuClubsIndexRoute;
"/events/": typeof With_menuEventsIndexRoute;
+ "/guard/": typeof With_menuGuardIndexRoute;
"/music-room/": typeof With_menuMusicRoomIndexRoute;
"/printers/": typeof With_menuPrintersIndexRoute;
"/room-booking/": typeof With_menuRoomBookingIndexRoute;
@@ -493,7 +514,6 @@ export interface FileRoutesByTo {
"/dorms": typeof With_menuDormsRoute;
"/extension": typeof With_menuExtensionRoute;
"/forms": typeof With_menuFormsRoute;
- "/guard": typeof With_menuGuardRoute;
"/maps": typeof With_menuMapsRoute;
"/menu": typeof With_menuMenuRoute;
"/scholarship": typeof With_menuScholarshipRoute;
@@ -514,6 +534,9 @@ export interface FileRoutesByTo {
"/events/approval": typeof With_menuEventsApprovalRoute;
"/events/archive": typeof With_menuEventsArchiveRoute;
"/events/calendar": typeof With_menuEventsCalendarRoute;
+ "/guard/copy": typeof With_menuGuardCopyRoute;
+ "/guard/create": typeof With_menuGuardCreateRoute;
+ "/guard/files": typeof With_menuGuardFilesRoute;
"/music-room/instructions": typeof With_menuMusicRoomInstructionsRoute;
"/printers/print": typeof With_menuPrintersPrintRoute;
"/printers/scan": typeof With_menuPrintersScanRoute;
@@ -530,6 +553,7 @@ export interface FileRoutesByTo {
"/catalogue": typeof With_menuCatalogueIndexRoute;
"/clubs": typeof With_menuClubsIndexRoute;
"/events": typeof With_menuEventsIndexRoute;
+ "/guard": typeof With_menuGuardIndexRoute;
"/music-room": typeof With_menuMusicRoomIndexRoute;
"/printers": typeof With_menuPrintersIndexRoute;
"/room-booking": typeof With_menuRoomBookingIndexRoute;
@@ -561,7 +585,6 @@ export interface FileRoutesById {
"/_with_menu/dorms": typeof With_menuDormsRoute;
"/_with_menu/extension": typeof With_menuExtensionRoute;
"/_with_menu/forms": typeof With_menuFormsRoute;
- "/_with_menu/guard": typeof With_menuGuardRoute;
"/_with_menu/maps": typeof With_menuMapsRoute;
"/_with_menu/menu": typeof With_menuMenuRoute;
"/_with_menu/scholarship": typeof With_menuScholarshipRoute;
@@ -582,6 +605,9 @@ export interface FileRoutesById {
"/_with_menu/events/approval": typeof With_menuEventsApprovalRoute;
"/_with_menu/events/archive": typeof With_menuEventsArchiveRoute;
"/_with_menu/events/calendar": typeof With_menuEventsCalendarRoute;
+ "/_with_menu/guard/copy": typeof With_menuGuardCopyRoute;
+ "/_with_menu/guard/create": typeof With_menuGuardCreateRoute;
+ "/_with_menu/guard/files": typeof With_menuGuardFilesRoute;
"/_with_menu/music-room/instructions": typeof With_menuMusicRoomInstructionsRoute;
"/_with_menu/printers/print": typeof With_menuPrintersPrintRoute;
"/_with_menu/printers/scan": typeof With_menuPrintersScanRoute;
@@ -598,6 +624,7 @@ export interface FileRoutesById {
"/_with_menu/catalogue/": typeof With_menuCatalogueIndexRoute;
"/_with_menu/clubs/": typeof With_menuClubsIndexRoute;
"/_with_menu/events/": typeof With_menuEventsIndexRoute;
+ "/_with_menu/guard/": typeof With_menuGuardIndexRoute;
"/_with_menu/music-room/": typeof With_menuMusicRoomIndexRoute;
"/_with_menu/printers/": typeof With_menuPrintersIndexRoute;
"/_with_menu/room-booking/": typeof With_menuRoomBookingIndexRoute;
@@ -629,7 +656,6 @@ export interface FileRouteTypes {
| "/dorms"
| "/extension"
| "/forms"
- | "/guard"
| "/maps"
| "/menu"
| "/scholarship"
@@ -650,6 +676,9 @@ export interface FileRouteTypes {
| "/events/approval"
| "/events/archive"
| "/events/calendar"
+ | "/guard/copy"
+ | "/guard/create"
+ | "/guard/files"
| "/music-room/instructions"
| "/printers/print"
| "/printers/scan"
@@ -666,6 +695,7 @@ export interface FileRouteTypes {
| "/catalogue/"
| "/clubs/"
| "/events/"
+ | "/guard/"
| "/music-room/"
| "/printers/"
| "/room-booking/"
@@ -694,7 +724,6 @@ export interface FileRouteTypes {
| "/dorms"
| "/extension"
| "/forms"
- | "/guard"
| "/maps"
| "/menu"
| "/scholarship"
@@ -715,6 +744,9 @@ export interface FileRouteTypes {
| "/events/approval"
| "/events/archive"
| "/events/calendar"
+ | "/guard/copy"
+ | "/guard/create"
+ | "/guard/files"
| "/music-room/instructions"
| "/printers/print"
| "/printers/scan"
@@ -731,6 +763,7 @@ export interface FileRouteTypes {
| "/catalogue"
| "/clubs"
| "/events"
+ | "/guard"
| "/music-room"
| "/printers"
| "/room-booking"
@@ -761,7 +794,6 @@ export interface FileRouteTypes {
| "/_with_menu/dorms"
| "/_with_menu/extension"
| "/_with_menu/forms"
- | "/_with_menu/guard"
| "/_with_menu/maps"
| "/_with_menu/menu"
| "/_with_menu/scholarship"
@@ -782,6 +814,9 @@ export interface FileRouteTypes {
| "/_with_menu/events/approval"
| "/_with_menu/events/archive"
| "/_with_menu/events/calendar"
+ | "/_with_menu/guard/copy"
+ | "/_with_menu/guard/create"
+ | "/_with_menu/guard/files"
| "/_with_menu/music-room/instructions"
| "/_with_menu/printers/print"
| "/_with_menu/printers/scan"
@@ -798,6 +833,7 @@ export interface FileRouteTypes {
| "/_with_menu/catalogue/"
| "/_with_menu/clubs/"
| "/_with_menu/events/"
+ | "/_with_menu/guard/"
| "/_with_menu/music-room/"
| "/_with_menu/printers/"
| "/_with_menu/room-booking/"
@@ -927,13 +963,6 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof With_menuMapsRouteImport;
parentRoute: typeof With_menuRouteRoute;
};
- "/_with_menu/guard": {
- id: "/_with_menu/guard";
- path: "/guard";
- fullPath: "/guard";
- preLoaderRoute: typeof With_menuGuardRouteImport;
- parentRoute: typeof With_menuRouteRoute;
- };
"/_with_menu/forms": {
id: "/_with_menu/forms";
path: "/forms";
@@ -1039,6 +1068,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof With_menuMusicRoomIndexRouteImport;
parentRoute: typeof With_menuRouteRoute;
};
+ "/_with_menu/guard/": {
+ id: "/_with_menu/guard/";
+ path: "/guard";
+ fullPath: "/guard/";
+ preLoaderRoute: typeof With_menuGuardIndexRouteImport;
+ parentRoute: typeof With_menuRouteRoute;
+ };
"/_with_menu/events/": {
id: "/_with_menu/events/";
path: "/events";
@@ -1151,6 +1187,27 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof With_menuMusicRoomInstructionsRouteImport;
parentRoute: typeof With_menuRouteRoute;
};
+ "/_with_menu/guard/files": {
+ id: "/_with_menu/guard/files";
+ path: "/guard/files";
+ fullPath: "/guard/files";
+ preLoaderRoute: typeof With_menuGuardFilesRouteImport;
+ parentRoute: typeof With_menuRouteRoute;
+ };
+ "/_with_menu/guard/create": {
+ id: "/_with_menu/guard/create";
+ path: "/guard/create";
+ fullPath: "/guard/create";
+ preLoaderRoute: typeof With_menuGuardCreateRouteImport;
+ parentRoute: typeof With_menuRouteRoute;
+ };
+ "/_with_menu/guard/copy": {
+ id: "/_with_menu/guard/copy";
+ path: "/guard/copy";
+ fullPath: "/guard/copy";
+ preLoaderRoute: typeof With_menuGuardCopyRouteImport;
+ parentRoute: typeof With_menuRouteRoute;
+ };
"/_with_menu/events/calendar": {
id: "/_with_menu/events/calendar";
path: "/events/calendar";
@@ -1295,7 +1352,6 @@ interface With_menuRouteRouteChildren {
With_menuDormsRoute: typeof With_menuDormsRoute;
With_menuExtensionRoute: typeof With_menuExtensionRoute;
With_menuFormsRoute: typeof With_menuFormsRoute;
- With_menuGuardRoute: typeof With_menuGuardRoute;
With_menuMapsRoute: typeof With_menuMapsRoute;
With_menuMenuRoute: typeof With_menuMenuRoute;
With_menuScholarshipRoute: typeof With_menuScholarshipRoute;
@@ -1311,6 +1367,9 @@ interface With_menuRouteRouteChildren {
With_menuEventsApprovalRoute: typeof With_menuEventsApprovalRoute;
With_menuEventsArchiveRoute: typeof With_menuEventsArchiveRoute;
With_menuEventsCalendarRoute: typeof With_menuEventsCalendarRoute;
+ With_menuGuardCopyRoute: typeof With_menuGuardCopyRoute;
+ With_menuGuardCreateRoute: typeof With_menuGuardCreateRoute;
+ With_menuGuardFilesRoute: typeof With_menuGuardFilesRoute;
With_menuMusicRoomInstructionsRoute: typeof With_menuMusicRoomInstructionsRoute;
With_menuPrintersPrintRoute: typeof With_menuPrintersPrintRoute;
With_menuPrintersScanRoute: typeof With_menuPrintersScanRoute;
@@ -1326,6 +1385,7 @@ interface With_menuRouteRouteChildren {
With_menuCatalogueIndexRoute: typeof With_menuCatalogueIndexRoute;
With_menuClubsIndexRoute: typeof With_menuClubsIndexRoute;
With_menuEventsIndexRoute: typeof With_menuEventsIndexRoute;
+ With_menuGuardIndexRoute: typeof With_menuGuardIndexRoute;
With_menuMusicRoomIndexRoute: typeof With_menuMusicRoomIndexRoute;
With_menuPrintersIndexRoute: typeof With_menuPrintersIndexRoute;
With_menuRoomBookingIndexRoute: typeof With_menuRoomBookingIndexRoute;
@@ -1352,7 +1412,6 @@ const With_menuRouteRouteChildren: With_menuRouteRouteChildren = {
With_menuDormsRoute: With_menuDormsRoute,
With_menuExtensionRoute: With_menuExtensionRoute,
With_menuFormsRoute: With_menuFormsRoute,
- With_menuGuardRoute: With_menuGuardRoute,
With_menuMapsRoute: With_menuMapsRoute,
With_menuMenuRoute: With_menuMenuRoute,
With_menuScholarshipRoute: With_menuScholarshipRoute,
@@ -1368,6 +1427,9 @@ const With_menuRouteRouteChildren: With_menuRouteRouteChildren = {
With_menuEventsApprovalRoute: With_menuEventsApprovalRoute,
With_menuEventsArchiveRoute: With_menuEventsArchiveRoute,
With_menuEventsCalendarRoute: With_menuEventsCalendarRoute,
+ With_menuGuardCopyRoute: With_menuGuardCopyRoute,
+ With_menuGuardCreateRoute: With_menuGuardCreateRoute,
+ With_menuGuardFilesRoute: With_menuGuardFilesRoute,
With_menuMusicRoomInstructionsRoute: With_menuMusicRoomInstructionsRoute,
With_menuPrintersPrintRoute: With_menuPrintersPrintRoute,
With_menuPrintersScanRoute: With_menuPrintersScanRoute,
@@ -1383,6 +1445,7 @@ const With_menuRouteRouteChildren: With_menuRouteRouteChildren = {
With_menuCatalogueIndexRoute: With_menuCatalogueIndexRoute,
With_menuClubsIndexRoute: With_menuClubsIndexRoute,
With_menuEventsIndexRoute: With_menuEventsIndexRoute,
+ With_menuGuardIndexRoute: With_menuGuardIndexRoute,
With_menuMusicRoomIndexRoute: With_menuMusicRoomIndexRoute,
With_menuPrintersIndexRoute: With_menuPrintersIndexRoute,
With_menuRoomBookingIndexRoute: With_menuRoomBookingIndexRoute,
diff --git a/src/app/routes/_with_menu/guard/copy.tsx b/src/app/routes/_with_menu/guard/copy.tsx
new file mode 100644
index 00000000..4f825cdf
--- /dev/null
+++ b/src/app/routes/_with_menu/guard/copy.tsx
@@ -0,0 +1,30 @@
+import { RequireAuth } from "@/components/common/AuthWall.tsx";
+import { CopySheetPage } from "@/components/guard/CopySheetPage.tsx";
+import { GuardTabs } from "@/components/guard/GuardTabs.tsx";
+import { Topbar } from "@/components/layout/Topbar.tsx";
+import { Helmet } from "@dr.pogodin/react-helmet";
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/_with_menu/guard/copy")({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ return (
+ <>
+
+ Copy sheet — Guard
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/routes/_with_menu/guard/create.tsx b/src/app/routes/_with_menu/guard/create.tsx
new file mode 100644
index 00000000..6c47ee48
--- /dev/null
+++ b/src/app/routes/_with_menu/guard/create.tsx
@@ -0,0 +1,30 @@
+import { RequireAuth } from "@/components/common/AuthWall.tsx";
+import { CreateSheetPage } from "@/components/guard/CreateSheetPage.tsx";
+import { GuardTabs } from "@/components/guard/GuardTabs.tsx";
+import { Topbar } from "@/components/layout/Topbar.tsx";
+import { Helmet } from "@dr.pogodin/react-helmet";
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/_with_menu/guard/create")({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ return (
+ <>
+
+ Create sheet — Guard
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/routes/_with_menu/guard.tsx b/src/app/routes/_with_menu/guard/files.tsx
similarity index 51%
rename from src/app/routes/_with_menu/guard.tsx
rename to src/app/routes/_with_menu/guard/files.tsx
index 85bf9fa1..dec41092 100644
--- a/src/app/routes/_with_menu/guard.tsx
+++ b/src/app/routes/_with_menu/guard/files.tsx
@@ -1,10 +1,11 @@
import { RequireAuth } from "@/components/common/AuthWall.tsx";
-import { GuardPage } from "@/components/guard/GuardPage.tsx";
+import { FilesPage } from "@/components/guard/FilesPage.tsx";
+import { GuardTabs } from "@/components/guard/GuardTabs.tsx";
import { Topbar } from "@/components/layout/Topbar.tsx";
-import { createFileRoute } from "@tanstack/react-router";
import { Helmet } from "@dr.pogodin/react-helmet";
+import { createFileRoute } from "@tanstack/react-router";
-export const Route = createFileRoute("/_with_menu/guard")({
+export const Route = createFileRoute("/_with_menu/guard/files")({
component: RouteComponent,
});
@@ -12,12 +13,17 @@ function RouteComponent() {
return (
<>
- Guard
+ Your sheets — Guard
+
+
-
+
>
);
diff --git a/src/app/routes/_with_menu/guard/index.tsx b/src/app/routes/_with_menu/guard/index.tsx
new file mode 100644
index 00000000..a3106e18
--- /dev/null
+++ b/src/app/routes/_with_menu/guard/index.tsx
@@ -0,0 +1,27 @@
+import { GuardLandingPage } from "@/components/guard/GuardLandingPage.tsx";
+import { GuardTabs } from "@/components/guard/GuardTabs.tsx";
+import { Topbar } from "@/components/layout/Topbar.tsx";
+import { Helmet } from "@dr.pogodin/react-helmet";
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/_with_menu/guard/")({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ return (
+ <>
+
+ Guard
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/routes/guard.google.files.$slug.join.tsx b/src/app/routes/guard.google.files.$slug.join.tsx
index 8b34d261..ceeaae4f 100644
--- a/src/app/routes/guard.google.files.$slug.join.tsx
+++ b/src/app/routes/guard.google.files.$slug.join.tsx
@@ -69,7 +69,6 @@ function RouteComponent() {
<>
Sign In - Join InNoHassle Guard File
- {/* Do not scan this page */}
@@ -83,10 +82,9 @@ function RouteComponent() {
Join InNoHassle Guard File
- {/* Do not scan this page */}
-
+
Join InNoHassle Guard File
@@ -95,14 +93,9 @@ function RouteComponent() {
{!isSuccess ? (
-
) : null}
- {isPending && (
-
-
-
-
-
Adding you to the file...
-
- )}
-
{isSuccess && (
-
-
-
✅ Success!
+
+
+
Success!
+
+ {gmail} has been added to the spreadsheet.
+
+
+ Redirecting to spreadsheet in{" "}
+ {countdown} seconds...
+
-
- {gmail} has been added to the spreadsheet.
-
-
- Redirecting to spreadsheet in{" "}
- {countdown} seconds...
-
)}
{error && (
-
-
❌ Error:
-
- {errorMessage}
+
+
+ Error: {errorMessage}
+
)}
diff --git a/src/components/guard/BannedList.tsx b/src/components/guard/BannedList.tsx
index e5d7079f..14604fd4 100644
--- a/src/components/guard/BannedList.tsx
+++ b/src/components/guard/BannedList.tsx
@@ -1,6 +1,6 @@
-import { useState, useMemo } from "react";
-import { formatDate, filterByFields } from "./utils";
-import { MESSAGES } from "./consts";
+import { useToast } from "@/components/toast";
+import { useMemo, useState } from "react";
+import { filterByFields, formatDate } from "./utils";
type BannedItem = {
user_id: string;
@@ -9,26 +9,41 @@ type BannedItem = {
banned_at: string;
};
-interface BannedListProps {
+export function BannedList({
+ banned,
+ search,
+ onUnban,
+}: {
banned: BannedItem[];
search: string;
onUnban: (userId: string) => Promise
;
-}
-
-export function BannedList({ banned, search, onUnban }: BannedListProps) {
+}) {
const filtered = useMemo(
() => filterByFields(banned, search, ["gmail", "innomail"]),
[banned, search],
);
if (!banned || banned.length === 0) {
- return No banned users.
;
+ return (
+
+ );
+ }
+
+ if (filtered.length === 0) {
+ return (
+
+ No banned users match your search
+
+ );
}
return (
-
+
{filtered.map((bannedUser) => (
-
Promise;
-}
-
-function BannedItem({ banned, onUnban }: BannedItemProps) {
+}) {
return (
-
+
-
+
- banned at {formatDate(banned.banned_at)}
+ banned {formatDate(banned.banned_at)}
-
onUnban(banned.user_id)} />
+ onUnban(banned.user_id)}
+ />
);
}
function Email({ email }: { email: string }) {
+ const [local, domain] = email.split("@");
return (
-
- {email.split("@")[0]}
- @{email.split("@")[1]}
+
+ {local}
+ @{domain}
);
}
-function UnbanButton({ onClick }: { onClick: () => Promise }) {
+function UnbanButton({
+ onClick,
+ email,
+}: {
+ onClick: () => Promise;
+ email: string;
+}) {
+ const { showConfirm } = useToast();
const [pending, setPending] = useState(false);
const handleClick = async () => {
- if (!confirm(MESSAGES.unbanConfirm)) return;
+ const confirmed = await showConfirm({
+ title: "Unban user",
+ message: `Unban ${email}? They will be able to join this sheet again via the join link.`,
+ confirmText: "Unban",
+ cancelText: "Cancel",
+ type: "info",
+ });
+ if (!confirmed) return;
+
setPending(true);
try {
await onClick();
@@ -84,12 +119,17 @@ function UnbanButton({ onClick }: { onClick: () => Promise }) {
return (
- {pending ? "Unbanning..." : "Unban"}
+ {pending ? (
+
+ ) : (
+
+ )}
+ Unban
);
}
diff --git a/src/components/guard/CleanupAlert.tsx b/src/components/guard/CleanupAlert.tsx
deleted file mode 100644
index d1546bd1..00000000
--- a/src/components/guard/CleanupAlert.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { MESSAGES } from "./consts";
-
-interface CleanupAlertProps {
- onCleanup: () => void;
- isPending?: boolean;
-}
-
-export function CleanupAlert({ onCleanup, isPending }: CleanupAlertProps) {
- return (
-
-
{MESSAGES.cleanupRecommended}
-
- Running cleanup will revoke access from unverified users. You will need
- to re-join using the Guard link afterwards.
-
-
-
- {isPending ? "Cleaning up..." : "Cleanup access"}
-
-
-
- );
-}
diff --git a/src/components/guard/CopyLinkButton.tsx b/src/components/guard/CopyLinkButton.tsx
index 8928a781..6fae3c04 100644
--- a/src/components/guard/CopyLinkButton.tsx
+++ b/src/components/guard/CopyLinkButton.tsx
@@ -1,17 +1,14 @@
import { useState } from "react";
import { MESSAGES } from "./consts";
-
-interface CopyLinkButtonProps {
- text: string;
- variant?: "primary" | "secondary";
- size?: "sm" | "md" | "lg";
-}
+import { cn } from "@/lib/ui/cn";
export function CopyLinkButton({
text,
- variant = "primary",
- size = "md",
-}: CopyLinkButtonProps) {
+ className,
+}: {
+ text: string;
+ className?: string;
+}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
@@ -24,28 +21,17 @@ export function CopyLinkButton({
}
};
- const baseClasses = "rounded-field font-medium transition-all";
- const sizeClasses = {
- sm: "px-2.5 py-1.5 text-xs",
- md: "px-3.5 py-2.5 text-sm",
- lg: "px-4 py-3 text-base",
- };
- const variantClasses = {
- primary: copied
- ? "bg-green-500 text-white"
- : "bg-primary text-white hover:bg-[#6600CC]",
- secondary: copied
- ? "bg-green-500 text-white"
- : "border-2 border-base-content/20 hover:border-base-content/40",
- };
-
return (
- {copied ? MESSAGES.copySuccess : "Copy Join link"}
+ {copied ? MESSAGES.copySuccess : "Copy join link"}
);
}
diff --git a/src/components/guard/CopySheetPage.tsx b/src/components/guard/CopySheetPage.tsx
new file mode 100644
index 00000000..974e8247
--- /dev/null
+++ b/src/components/guard/CopySheetPage.tsx
@@ -0,0 +1,210 @@
+import { guardTypes } from "@/api/guard";
+import { formatApiErrorMessage } from "@/api/helpers/create-query-client";
+import { useEffect, useState } from "react";
+import { CopyInstructions } from "./Instructions";
+import { RolesSwitch } from "./JoinsList";
+import { SetupResult } from "./SetupResult";
+import { FileRole, useGuardMutations, useServiceAccountEmail } from "./hooks";
+import { extractSpreadsheetId, loadGmail, saveGmail } from "./utils";
+
+export function CopySheetPage() {
+ const { serviceEmail, isPending: isEmailPending } = useServiceAccountEmail();
+ const [spreadsheetId, setSpreadsheetId] = useState("");
+ const [gmail, setGmail] = useState("");
+ const [respondentRole, setRespondentRole] = useState(
+ FileRole.writer,
+ );
+ const [error, setError] = useState("");
+ const [setupResult, setSetupResult] = useState<{
+ title: string;
+ fileId: string;
+ fileType: string;
+ guardingMethod: string;
+ roleDisplay: string;
+ joinLink: string;
+ } | null>(null);
+
+ const mutations = useGuardMutations();
+
+ useEffect(() => {
+ const savedGmail = loadGmail();
+ if (savedGmail) {
+ setGmail(savedGmail);
+ }
+ }, []);
+
+ const validateSpreadsheetId = (input: string): string => {
+ if (!input.trim()) return "";
+
+ const extractedId = extractSpreadsheetId(input);
+
+ if (!extractedId) {
+ return "Invalid Google Spreadsheet URL";
+ }
+
+ if (extractedId.length < 10) {
+ return "Spreadsheet ID seems too short";
+ }
+
+ return "";
+ };
+
+ const handleBlur = () => {
+ if (spreadsheetId.trim()) {
+ setError(validateSpreadsheetId(spreadsheetId));
+ }
+ };
+
+ const handleSubmit = () => {
+ setError("");
+
+ if (!gmail.trim()) {
+ setError("Gmail is required");
+ return;
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(gmail.trim())) {
+ setError("Please enter a valid Gmail address");
+ return;
+ }
+
+ if (!spreadsheetId.trim()) {
+ setError("Spreadsheet URL is required");
+ return;
+ }
+
+ const validationError = validateSpreadsheetId(spreadsheetId);
+ if (validationError) {
+ setError(validationError);
+ return;
+ }
+
+ const extractedId = extractSpreadsheetId(spreadsheetId);
+ if (!extractedId) {
+ setError("Failed to extract Spreadsheet ID");
+ return;
+ }
+
+ const copyRole =
+ respondentRole === FileRole.writer
+ ? guardTypes.CopyFileRequestDefault_role.writer
+ : guardTypes.CopyFileRequestDefault_role.reader;
+
+ saveGmail(gmail.trim());
+
+ mutations.copyFile.mutate(
+ {
+ body: {
+ file_id: extractedId,
+ default_role: copyRole,
+ owner_gmail: gmail.trim(),
+ },
+ },
+ {
+ onSuccess: (data) => {
+ setSetupResult({
+ title: data.title,
+ fileId: data.file_id,
+ fileType: data.file_type,
+ guardingMethod: "copy",
+ roleDisplay: data.default_role,
+ joinLink: data.join_link,
+ });
+ setSpreadsheetId("");
+ mutations.invalidateFiles();
+ },
+ onError: (err) => {
+ setError(formatApiErrorMessage(err));
+ },
+ },
+ );
+ };
+
+ const isSubmitting = mutations.copyFile.isPending;
+
+ return (
+
+ {isEmailPending ? (
+
+ ) : (
+
+ )}
+
+
{
+ e.preventDefault();
+ handleSubmit();
+ }}
+ >
+
+ Spreadsheet URL
+ {
+ setSpreadsheetId(e.target.value);
+ if (error) setError("");
+ }}
+ onBlur={handleBlur}
+ placeholder="https://docs.google.com/spreadsheets/d/..."
+ className="input input-bordered w-full"
+ />
+
+
+
+ Owner Gmail
+ {
+ setGmail(e.target.value);
+ if (error) setError("");
+ }}
+ placeholder="your.email@gmail.com"
+ className="input input-bordered w-full"
+ />
+
+
+
+ Guests Role
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {isSubmitting ? (
+ <>
+
+ Copying...
+ >
+ ) : (
+ "Copy sheet"
+ )}
+
+
+
+ {setupResult && (
+
setSetupResult(null)}
+ />
+ )}
+
+ );
+}
diff --git a/src/components/guard/CreateSheetPage.tsx b/src/components/guard/CreateSheetPage.tsx
new file mode 100644
index 00000000..de095efe
--- /dev/null
+++ b/src/components/guard/CreateSheetPage.tsx
@@ -0,0 +1,163 @@
+import { guardTypes } from "@/api/guard";
+import { formatApiErrorMessage } from "@/api/helpers/create-query-client";
+import { useEffect, useState } from "react";
+import { RolesSwitch } from "./JoinsList";
+import { SetupResult } from "./SetupResult";
+import { FileRole, useGuardMutations } from "./hooks";
+import { loadGmail, saveGmail } from "./utils";
+
+export function CreateSheetPage() {
+ const [title, setTitle] = useState("");
+ const [gmail, setGmail] = useState("");
+ const [respondentRole, setRespondentRole] = useState(
+ FileRole.writer,
+ );
+ const [error, setError] = useState("");
+ const [setupResult, setSetupResult] = useState<{
+ title: string;
+ fileId: string;
+ fileType: string;
+ guardingMethod: string;
+ roleDisplay: string;
+ joinLink: string;
+ } | null>(null);
+
+ const mutations = useGuardMutations();
+
+ useEffect(() => {
+ const savedGmail = loadGmail();
+ if (savedGmail) {
+ setGmail(savedGmail);
+ }
+ }, []);
+
+ const handleSubmit = () => {
+ setError("");
+
+ if (!gmail.trim()) {
+ setError("Gmail is required");
+ return;
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(gmail.trim())) {
+ setError("Please enter a valid Gmail address");
+ return;
+ }
+
+ if (!title.trim()) {
+ setError("Title is required");
+ return;
+ }
+
+ saveGmail(gmail.trim());
+
+ mutations.createFile.mutate(
+ {
+ body: {
+ file_type: guardTypes.CreateFileRequestFile_type.spreadsheet,
+ title: title.trim(),
+ default_role: respondentRole,
+ owner_gmail: gmail.trim(),
+ },
+ },
+ {
+ onSuccess: (data) => {
+ setSetupResult({
+ title: data.title,
+ fileId: data.file_id,
+ fileType: "spreadsheet",
+ guardingMethod: "create",
+ roleDisplay: data.default_role,
+ joinLink: data.join_link,
+ });
+ setTitle("");
+ mutations.invalidateFiles();
+ },
+ onError: (err) => {
+ setError(formatApiErrorMessage(err));
+ },
+ },
+ );
+ };
+
+ const isSubmitting = mutations.createFile.isPending;
+
+ return (
+
+ );
+}
diff --git a/src/components/guard/EditTitleButton.tsx b/src/components/guard/EditTitleButton.tsx
index 3ac8a6b9..90fee566 100644
--- a/src/components/guard/EditTitleButton.tsx
+++ b/src/components/guard/EditTitleButton.tsx
@@ -1,29 +1,29 @@
-import { useState } from "react";
-
-interface EditTitleButtonProps {
- currentTitle: string;
- onSave: (newTitle: string) => Promise;
-}
+import { useEffect, useState } from "react";
export function EditTitleButton({
currentTitle,
onSave,
-}: EditTitleButtonProps) {
- const [isEditing, setIsEditing] = useState(false);
+}: {
+ currentTitle: string;
+ onSave: (newTitle: string) => Promise;
+}) {
const [title, setTitle] = useState(currentTitle);
const [isPending, setIsPending] = useState(false);
- const handleSave = async () => {
- if (!title.trim() || title === currentTitle) {
- setIsEditing(false);
+ useEffect(() => {
+ setTitle(currentTitle);
+ }, [currentTitle]);
+
+ const handleBlur = async () => {
+ const nextTitle = title.trim();
+ if (!nextTitle || nextTitle === currentTitle) {
setTitle(currentTitle);
return;
}
setIsPending(true);
try {
- await onSave(title.trim());
- setIsEditing(false);
+ await onSave(nextTitle);
} catch (error) {
console.error("Failed to update title:", error);
setTitle(currentTitle);
@@ -32,57 +32,29 @@ export function EditTitleButton({
}
};
- const handleCancel = () => {
- setIsEditing(false);
- setTitle(currentTitle);
- };
-
- if (isEditing) {
- return (
-
- setTitle(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- handleSave();
- } else if (e.key === "Escape") {
- handleCancel();
- }
- }}
- autoFocus
- disabled={isPending}
- className="border-base-content/20 bg-base-200/5 focus:border-primary flex-1 rounded-sm border-2 px-2 py-1 text-base outline-hidden transition-colors disabled:opacity-50"
- />
-
- {isPending ? "..." : "✓"}
-
-
- ✕
-
-
- );
- }
-
return (
-
-
{currentTitle || "Untitled"}
-
setIsEditing(true)}
- className="text-base-content/60 hover:bg-base-200/10 hover:text-base-content rounded-sm px-2 py-1 text-sm"
- title="Edit title"
- >
-
-
+
+ setTitle(e.target.value)}
+ onBlur={handleBlur}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.currentTarget.blur();
+ } else if (e.key === "Escape") {
+ setTitle(currentTitle);
+ e.currentTarget.blur();
+ }
+ }}
+ disabled={isPending}
+ size={Math.max(title.length, 1)}
+ placeholder="Untitled"
+ className="text-base-content placeholder:text-base-content/40 field-sizing-content max-w-full min-w-0 bg-transparent p-0 text-lg font-semibold outline-none disabled:opacity-50"
+ />
+ {isPending && (
+
+ )}
);
}
diff --git a/src/components/guard/FileDetails.tsx b/src/components/guard/FileDetails.tsx
index ff633a22..ac3d6ebf 100644
--- a/src/components/guard/FileDetails.tsx
+++ b/src/components/guard/FileDetails.tsx
@@ -1,11 +1,12 @@
+import { useToast } from "@/components/toast";
+import { cn } from "@/lib/ui/cn";
import { useState } from "react";
-import { buildJoinLink, buildSheetsUrl, formatDate } from "./utils";
-import { CopyLinkButton } from "./CopyLinkButton";
-import { JoinsList } from "./JoinsList";
import { BannedList } from "./BannedList";
+import { CopyLinkButton } from "./CopyLinkButton";
import { EditTitleButton } from "./EditTitleButton";
-import { MESSAGES } from "./consts";
+import { JoinsList, RolesSwitch } from "./JoinsList";
import { FileRole } from "./hooks";
+import { buildJoinLink, buildSheetsUrl, formatDate } from "./utils";
type FileDetailsData = {
title: string;
@@ -27,20 +28,7 @@ type FileDetailsData = {
}>;
};
-interface FileDetailsProps {
- slug: string;
- file: FileDetailsData | undefined;
- isLoading: boolean;
- search: string;
- onSearchChange: (search: string) => void;
- onBack: () => void;
- onDelete: () => Promise
;
- onUpdateTitle: (newTitle: string) => Promise;
- onBan: (userId: string) => Promise;
- onUnban: (userId: string) => Promise;
- onUpdateDefaultRole: (role: string) => Promise;
- onUpdateUserRole: (userId: string, role: string) => Promise;
-}
+type PeopleTab = "joins" | "banned";
export function FileDetails({
slug,
@@ -55,12 +43,41 @@ export function FileDetails({
onUnban,
onUpdateDefaultRole,
onUpdateUserRole,
-}: FileDetailsProps) {
+}: {
+ slug: string;
+ file: FileDetailsData | undefined;
+ isLoading: boolean;
+ search: string;
+ onSearchChange: (search: string) => void;
+ onBack: () => void;
+ onDelete: () => Promise;
+ onUpdateTitle: (newTitle: string) => Promise;
+ onBan: (userId: string) => Promise;
+ onUnban: (userId: string) => Promise;
+ onUpdateDefaultRole: (role: string) => Promise;
+ onUpdateUserRole: (userId: string, role: string) => Promise;
+}) {
+ const { showConfirm } = useToast();
const [isDeleting, setIsDeleting] = useState(false);
const [isUpdatingRole, setIsUpdatingRole] = useState(false);
+ const [peopleTab, setPeopleTab] = useState("joins");
+
+ const joinsCount = file?.sso_joins?.length ?? 0;
+ const bannedCount = file?.sso_banned?.length ?? 0;
+ const currentRole =
+ file?.default_role === FileRole.reader ? FileRole.reader : FileRole.writer;
+ const sheetTitle = file?.title || "Untitled";
const handleDelete = async () => {
- if (!confirm(MESSAGES.deleteConfirm)) return;
+ const confirmed = await showConfirm({
+ title: "Delete sheet",
+ message: `Delete "${sheetTitle}"? The join link will stop working, and access managed by Guard will be removed. This cannot be undone.`,
+ confirmText: "Delete",
+ cancelText: "Cancel",
+ type: "error",
+ });
+ if (!confirmed) return;
+
setIsDeleting(true);
try {
await onDelete();
@@ -69,15 +86,24 @@ export function FileDetails({
}
};
- const handleUpdateDefaultRole = async (newRole: FileRole) => {
- if (!file) return;
+ const handleGuestsRoleSwitch = async (newRole: FileRole) => {
+ if (!file || newRole === currentRole) return;
const affectedCount = file.sso_joins?.length || 0;
const roleLabel = newRole === FileRole.writer ? "Writer" : "Reader";
+ const message =
+ affectedCount > 0
+ ? `New guests will get ${roleLabel} access. Permissions for ${affectedCount} already joined user${affectedCount === 1 ? "" : "s"} will be updated too.`
+ : `New guests will get ${roleLabel} access when they join via the link.`;
- const message = `Update all roles to ${roleLabel}?\n\nThis will update the default role to ${roleLabel} and change permissions for ${affectedCount} user${affectedCount === 1 ? "" : "s"}. Are you sure?`;
-
- if (!confirm(message)) return;
+ const confirmed = await showConfirm({
+ title: `Set guests role to ${roleLabel}`,
+ message,
+ confirmText: `Set to ${roleLabel}`,
+ cancelText: "Cancel",
+ type: "warning",
+ });
+ if (!confirmed) return;
setIsUpdatingRole(true);
try {
@@ -88,113 +114,155 @@ export function FileDetails({
};
return (
-
-
-
-
-
- created at {file?.created_at ? formatDate(file.created_at) : "—"}
-
-
-
-
- Back to files
-
-
-
-
-
-
-
+
+
{
- if (!file?.file_id) return;
- window.location.href = buildSheetsUrl(file.file_id);
- }}
- disabled={!file?.file_id}
- className="border-base-content/20 hover:border-base-content/40 rounded-field border-2 px-3 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
+ type="button"
+ onClick={onBack}
+ className="btn btn-ghost btn-sm w-fit gap-1 px-2"
>
- Open spreadsheet
+
+ Sheets
-
- {isDeleting ? "Deleting..." : "Delete"}
-
-
+
+
+
+
+
+ {file?.created_at ? formatDate(file.created_at) : "—"}
+
+ {!isLoading && (
+ <>
+
+ {joinsCount} {joinsCount === 1 ? "join" : "joins"}
+
+ {bannedCount > 0 && (
+
+ {bannedCount} banned
+
+ )}
+ >
+ )}
+
+
- {file && (
-
-
- Default Role:{" "}
-
- {file?.default_role}
-
-
-
+
+
+
{
+ if (!file?.file_id) e.preventDefault();
+ }}
+ >
+
+ Open
+
- handleUpdateDefaultRole(
- file.default_role === FileRole.writer
- ? FileRole.reader
- : FileRole.writer,
- )
- }
- disabled={isUpdatingRole}
- className={`border-base-content/20 hover:border-base-content/40 rounded-field border-2 px-3 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50`}
- title={`Update all roles to ${file.default_role === FileRole.writer ? "Reader" : "Writer"}`}
+ type="button"
+ onClick={handleDelete}
+ disabled={isDeleting}
+ className="btn btn-ghost btn-sm btn-error gap-1"
>
- {isUpdatingRole
- ? "Updating..."
- : `Update all roles to ${file.default_role === FileRole.writer ? "Reader" : "Writer"}`}
+ {isDeleting ? (
+
+ ) : (
+
+ )}
+ Delete
- )}
-
-
- onSearchChange(e.target.value)}
- placeholder="Search by gmail or innomail"
- className="border-base-content/20 bg-base-200/5 focus:border-base-content/40 focus:bg-base-200/10 rounded-field w-full border-2 px-4 py-3 outline-hidden transition-colors"
- />
- {isLoading ? (
-
Loading...
- ) : (
- <>
-
-
Joins
-
+
+
+
Guests Role
+
+ Role granted when someone joins via the link
+
+
+ {isUpdatingRole ? (
+
+ ) : file ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ setPeopleTab("joins")}
+ >
+ Joins
+ {joinsCount}
+
+ setPeopleTab("banned")}
+ >
+ Banned
+ {bannedCount}
+
-
-
Banned
-
+
+ onSearchChange(e.target.value)}
+ placeholder="Search email"
+ className="grow"
/>
+
+
+
+ {isLoading ? (
+
- >
- )}
+ ) : peopleTab === "joins" ? (
+
+ ) : (
+
+ )}
+
);
}
diff --git a/src/components/guard/FilesList.tsx b/src/components/guard/FilesList.tsx
index e3736024..68829818 100644
--- a/src/components/guard/FilesList.tsx
+++ b/src/components/guard/FilesList.tsx
@@ -1,5 +1,5 @@
import { useMemo } from "react";
-import { buildJoinLink, parseSlugFromJoinLink, formatDate } from "./utils";
+import { buildJoinLink, formatDate, parseSlugFromJoinLink } from "./utils";
import { CopyLinkButton } from "./CopyLinkButton";
type FileItem = {
@@ -10,13 +10,15 @@ type FileItem = {
default_role: string;
};
-interface FilesListProps {
+export function FilesList({
+ files,
+ search,
+ onShowDetails,
+}: {
files: FileItem[];
search: string;
onShowDetails: (slug: string) => void;
-}
-
-export function FilesList({ files, search, onShowDetails }: FilesListProps) {
+}) {
const normalizedSearch = search.trim().toLowerCase();
const searchSlug = useMemo(() => {
@@ -35,42 +37,56 @@ export function FilesList({ files, search, onShowDetails }: FilesListProps) {
}, [files, normalizedSearch, searchSlug]);
if (files.length === 0) {
- return No files found.
;
+ return No files found.
;
+ }
+
+ if (filtered.length === 0) {
+ return No files match your search.
;
}
return (
{filtered.map((file) => (
-
+
))}
);
}
-interface FileItemProps {
+function FileItemCard({
+ file,
+ onShowDetails,
+}: {
file: FileItem;
onShowDetails: (slug: string) => void;
-}
-
-function FileItem({ file, onShowDetails }: FileItemProps) {
+}) {
return (
-
+
onShowDetails(file.slug)}
+ className="bg-base-200 hover:bg-base-300 rounded-box flex cursor-pointer flex-col gap-3 px-4 py-3 transition-colors sm:flex-row sm:items-center sm:justify-between"
+ >
{file.title || "Untitled"}
-
- {file.default_role}, {file.sso_joins_count}{" "}
- {file.sso_joins_count === 1 ? "join" : "joins"}, created at{" "}
- {formatDate(file.created_at)}
+
+
+
+ {file.sso_joins_count}
+
+
+
+ {formatDate(file.created_at)}
+
-
-
onShowDetails(file.slug)}
- className="border-base-content/20 hover:border-base-content/40 rounded-field border-2 px-3 py-2 text-sm font-medium"
- >
- Show more
-
-
+
e.stopPropagation()}
+ >
+
);
diff --git a/src/components/guard/GuardPage.tsx b/src/components/guard/FilesPage.tsx
similarity index 58%
rename from src/components/guard/GuardPage.tsx
rename to src/components/guard/FilesPage.tsx
index 387923ee..9139d6f6 100644
--- a/src/components/guard/GuardPage.tsx
+++ b/src/components/guard/FilesPage.tsx
@@ -1,28 +1,22 @@
+import { guardTypes } from "@/api/guard";
import { formatApiErrorMessage } from "@/api/helpers/create-query-client";
import { useState } from "react";
-import {
- useServiceAccountEmail,
- useGuardFiles,
- useGuardFile,
- useGuardMutations,
-} from "./hooks";
-import { SetupContainer } from "./SetupContainer";
-import { FilesSection } from "./FilesSection";
-import { FilesList } from "./FilesList";
import { FileDetails } from "./FileDetails";
+import { FilesList } from "./FilesList";
+import { FilesSection } from "./FilesSection";
+import { useGuardFile, useGuardFiles, useGuardMutations } from "./hooks";
-export function GuardPage() {
+export function FilesPage() {
const [selectedSlug, setSelectedSlug] = useState
(null);
const [filesSearch, setFilesSearch] = useState("");
const [detailsSearch, setDetailsSearch] = useState("");
- const { serviceEmail } = useServiceAccountEmail();
const {
files,
- isLoading: isLoadingFiles,
+ isPending: isLoadingFiles,
error: filesError,
} = useGuardFiles();
- const { file: selectedFile, isLoading: isLoadingFile } =
+ const { file: selectedFile, isPending: isLoadingFile } =
useGuardFile(selectedSlug);
const mutations = useGuardMutations();
@@ -85,7 +79,7 @@ export function GuardPage() {
await mutations.updateDefaultRole.mutateAsync({
params: { path: { slug: selectedSlug } },
- body: { role: role as any },
+ body: { role: role as guardTypes.UpdateDefaultRoleRequestRole },
});
mutations.invalidateFile(selectedSlug);
@@ -97,54 +91,54 @@ export function GuardPage() {
await mutations.updateUserRole.mutateAsync({
params: { path: { slug: selectedSlug, user_id: userId } },
- body: { role: role as any },
+ body: { role: role as guardTypes.UpdateUserRoleRequestRole },
});
mutations.invalidateFile(selectedSlug);
};
return (
-
-
-
-
-
-
- {selectedSlug ? (
-
- ) : (
-
- {isLoadingFiles ? (
- Loading...
- ) : filesError ? (
-
+
+ {selectedSlug ? (
+
+ ) : (
+
+ {isLoadingFiles ? (
+
+ ) : filesError ? (
+
+
Error loading files: {formatApiErrorMessage(filesError)}
-
- ) : files.length > 0 ? (
-
- ) : (
- No files found.
- )}
-
- )}
-
+
+
+ ) : files.length > 0 ? (
+
+ ) : (
+ No files found.
+ )}
+
+ )}
);
}
diff --git a/src/components/guard/FilesSection.tsx b/src/components/guard/FilesSection.tsx
index 8a463cac..a2f3e6f2 100644
--- a/src/components/guard/FilesSection.tsx
+++ b/src/components/guard/FilesSection.tsx
@@ -1,27 +1,26 @@
-interface FilesSectionProps {
- search: string;
- onSearchChange: (search: string) => void;
- children: React.ReactNode;
-}
-
export function FilesSection({
search,
onSearchChange,
children,
-}: FilesSectionProps) {
+}: {
+ search: string;
+ onSearchChange: (search: string) => void;
+ children: React.ReactNode;
+}) {
return (
-
-
Your Files
+
+
Your sheets
-
+
+
onSearchChange(e.target.value)}
- placeholder="Search by title or paste full join link or slug"
- className="border-base-content/20 bg-base-200/5 focus:border-base-content/40 focus:bg-base-200/10 rounded-field w-full border-2 px-4 py-3 outline-hidden transition-colors"
+ placeholder="Search by title, join link, or slug"
+ className="grow"
/>
-
+
{children}
diff --git a/src/components/guard/GuardLandingPage.tsx b/src/components/guard/GuardLandingPage.tsx
new file mode 100644
index 00000000..2cbf98b6
--- /dev/null
+++ b/src/components/guard/GuardLandingPage.tsx
@@ -0,0 +1,72 @@
+import { Link } from "@tanstack/react-router";
+
+export function GuardLandingPage() {
+ return (
+
+
+
+ Guard protects Google Spreadsheets with SSO join links — only
+ signed-in university users get access, and you stay in control of who
+ can edit.
+
+
+
+
+ Spreadsheets owned by a service account so only Guard can grant
+ access
+
+
+
+ Share a join link; respondents sign in via SSO and get the role you
+ choose
+
+
+
+ Manage joins, roles, and bans from one place
+
+
+
+
+
+
+
+
+
Create sheet
+
+ Create a new protected spreadsheet from scratch
+
+
+
+
+
+
+
+
Copy sheet
+
+ Make a protected copy of an existing spreadsheet
+
+
+
+
+
+
+
+
Your sheets
+
+ View join links, roles, and access for your sheets
+
+
+
+
+
+ );
+}
diff --git a/src/components/guard/GuardTabs.tsx b/src/components/guard/GuardTabs.tsx
new file mode 100644
index 00000000..5035653f
--- /dev/null
+++ b/src/components/guard/GuardTabs.tsx
@@ -0,0 +1,23 @@
+import { Link, ValidateLinkOptions } from "@tanstack/react-router";
+
+export function GuardTabs() {
+ return (
+
+ Info
+ Create Sheet
+ Copy Sheet
+ Your Sheets
+
+ );
+}
+
+function TabLink(props: ValidateLinkOptions) {
+ return (
+
+ );
+}
diff --git a/src/components/guard/Instructions.tsx b/src/components/guard/Instructions.tsx
index 574ac36e..0c776c6c 100644
--- a/src/components/guard/Instructions.tsx
+++ b/src/components/guard/Instructions.tsx
@@ -1,10 +1,7 @@
import { useState } from "react";
+import { cn } from "@/lib/ui/cn";
-interface ServiceAccountEmailProps {
- email: string;
-}
-
-export function ServiceAccountEmail({ email }: ServiceAccountEmailProps) {
+export function ServiceAccountEmail({ email }: { email: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
@@ -20,77 +17,27 @@ export function ServiceAccountEmail({ email }: ServiceAccountEmailProps) {
};
return (
-
- {copied ? "✓ Copied!" : email}
-
+ {copied ? "Copied!" : email || "…"}
+
);
}
-interface CreateInstructionsProps {
- serviceEmail: string;
-}
-
-export function CreateInstructions({ serviceEmail }: CreateInstructionsProps) {
- return (
-
-
-
- We will create a new Google Spreadsheet using the title you provide.
-
-
- The service account will
- own the file.
-
-
- You will get a join link to share with respondents, they will get
- access after signing in via SSO.
-
-
-
-
- 💡 Spreadsheet must be owned by the service account so only it can add
- editors. Otherwise, anyone can add editors and you lose control over
- who has access to the spreadsheet.
-
-
-
- );
-}
-
-interface CopyInstructionsProps {
- serviceEmail: string;
-}
-
-export function CopyInstructions({ serviceEmail }: CopyInstructionsProps) {
+export function CopyInstructions({ serviceEmail }: { serviceEmail: string }) {
return (
-
-
-
- Open your spreadsheet and give Editor access to{" "}
-
-
- Paste the spreadsheet URL below.
- We will create a copy of your file owned by the service account
-
- You will get a join link to share with respondents, they will get
- access after signing in via SSO.
-
- Your original file remains unchanged and under your control.
-
-
-
- 💡 The protected copy is owned by the service account so only it can
- add editors. This ensures you have full control over who has access.
-
-
+
+
+ Give Editor access to the service account{" "}
+ , then paste the spreadsheet
+ URL below.
+
);
}
diff --git a/src/components/guard/JoinsList.tsx b/src/components/guard/JoinsList.tsx
index 8b8b5f1e..cafbaf2d 100644
--- a/src/components/guard/JoinsList.tsx
+++ b/src/components/guard/JoinsList.tsx
@@ -1,7 +1,8 @@
-import { useState, useMemo } from "react";
-import { filterByFields } from "./utils";
-import { MESSAGES } from "./consts";
+import { useToast } from "@/components/toast";
+import { cn } from "@/lib/ui/cn";
+import { useMemo, useState } from "react";
import { FileRole } from "./hooks";
+import { filterByFields } from "./utils";
type JoinItem = {
user_id: string;
@@ -11,32 +12,44 @@ type JoinItem = {
role: string;
};
-interface JoinsListProps {
- joins: JoinItem[];
- search: string;
- onBan: (userId: string) => Promise
;
- onUpdateRole: (userId: string, role: string) => Promise;
-}
-
export function JoinsList({
joins,
search,
onBan,
onUpdateRole,
-}: JoinsListProps) {
+}: {
+ joins: JoinItem[];
+ search: string;
+ onBan: (userId: string) => Promise;
+ onUpdateRole: (userId: string, role: string) => Promise;
+}) {
const filtered = useMemo(
() => filterByFields(joins, search, ["gmail", "innomail"]),
[joins, search],
);
if (!joins || joins.length === 0) {
- return No joins yet.
;
+ return (
+
+
+
No one has joined yet
+
Share the join link to grant access
+
+ );
+ }
+
+ if (filtered.length === 0) {
+ return (
+
+ No joins match your search
+
+ );
}
return (
-
+
{filtered.map((join) => (
-
Promise;
onUpdateRole: (userId: string, role: string) => Promise;
-}
-
-function JoinItem({ join, onBan, onUpdateRole }: JoinItemProps) {
+}) {
return (
-
+
- {/*
- joined at {formatDate(join.joined_at)}
-
*/}
-
+
onUpdateRole(join.user_id, role)}
/>
- onBan(join.user_id)} />
+ onBan(join.user_id)}
+ />
);
}
function Email({ email }: { email: string }) {
+ const [local, domain] = email.split("@");
return (
-
- {email.split("@")[0]}
- @{email.split("@")[1]}
+
+ {local}
+ @{domain}
);
}
@@ -92,57 +108,65 @@ export function RolesSwitch({
}) {
const [pending, setPending] = useState(false);
const isWriter = currentRole === FileRole.writer;
- const handleSwitch = async () => {
- if (pending) return;
- const nextRole = isWriter ? FileRole.reader : FileRole.writer;
+
+ const handleSwitch = async (role: FileRole) => {
+ if (pending || role === currentRole) return;
setPending(true);
try {
- await onSwitch(nextRole);
+ await onSwitch(role);
} finally {
setPending(false);
}
};
return (
-
-
+ handleSwitch(FileRole.writer)}
+ disabled={pending}
>
Writer
-
-
+ handleSwitch(FileRole.reader)}
+ disabled={pending}
>
Reader
-
- {/* {pending && (
- Switching...
- )} */}
-
+
+
);
}
-function BanButton({ onClick }: { onClick: () => Promise
}) {
+function BanButton({
+ onClick,
+ email,
+}: {
+ onClick: () => Promise;
+ email: string;
+}) {
+ const { showConfirm } = useToast();
const [pending, setPending] = useState(false);
const handleClick = async () => {
- if (!confirm(MESSAGES.banConfirm)) return;
+ const confirmed = await showConfirm({
+ title: "Ban user",
+ message: `Ban ${email}? They will lose access to this sheet and will not be able to join again until unbanned.`,
+ confirmText: "Ban",
+ cancelText: "Cancel",
+ type: "error",
+ });
+ if (!confirmed) return;
+
setPending(true);
try {
await onClick();
@@ -153,12 +177,16 @@ function BanButton({ onClick }: { onClick: () => Promise }) {
return (
- {pending ? "Banning..." : "Ban"}
+ {pending ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/src/components/guard/ModeToggle.tsx b/src/components/guard/ModeToggle.tsx
deleted file mode 100644
index 95f8172e..00000000
--- a/src/components/guard/ModeToggle.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { MODE_LABELS } from "./consts";
-
-interface ModeToggleProps {
- mode: "create" | "copy";
- onChange: (mode: "create" | "copy") => void;
-}
-
-export function ModeToggle({ mode, onChange }: ModeToggleProps) {
- return (
-
- onChange("create")}
- className={`flex-1 rounded px-4 py-2 text-sm font-medium transition-colors ${
- mode === "create"
- ? "bg-primary text-white"
- : "text-base-content/70 hover:bg-base-200/5"
- }`}
- >
- {MODE_LABELS.create}
-
- onChange("copy")}
- className={`flex-1 rounded px-4 py-2 text-sm font-medium transition-colors ${
- mode === "copy"
- ? "bg-primary text-white"
- : "text-base-content/70 hover:bg-base-200/5"
- }`}
- >
- {MODE_LABELS.copy}
-
-
- );
-}
diff --git a/src/components/guard/SetupContainer.tsx b/src/components/guard/SetupContainer.tsx
deleted file mode 100644
index 89aba0d9..00000000
--- a/src/components/guard/SetupContainer.tsx
+++ /dev/null
@@ -1,323 +0,0 @@
-import { useState, useEffect } from "react";
-import { guardTypes } from "@/api/guard";
-import { formatApiErrorMessage } from "@/api/helpers/create-query-client";
-import { useGuardMutations, FileRole } from "./hooks";
-import { extractSpreadsheetId, loadGmail, saveGmail } from "./utils";
-import { ModeToggle } from "./ModeToggle";
-import { CreateInstructions, CopyInstructions } from "./Instructions";
-import { SetupResult } from "./SetupResult";
-import { DEFAULT_MODE } from "./consts";
-import { RolesSwitch } from "./JoinsList";
-
-interface SetupContainerProps {
- serviceEmail: string;
-}
-
-export function SetupContainer({ serviceEmail }: SetupContainerProps) {
- const [mode, setMode] = useState<"create" | "copy">(DEFAULT_MODE);
- const [spreadsheetId, setSpreadsheetId] = useState("");
- const [title, setTitle] = useState("");
- const [gmail, setGmail] = useState("");
- const [respondentRole, setRespondentRole] = useState(
- FileRole.writer,
- );
- const [error, setError] = useState("");
- const [setupResult, setSetupResult] = useState<{
- title: string;
- fileId: string;
- fileType: string;
- guardingMethod: string;
- roleDisplay: string;
- joinLink: string;
- } | null>(null);
-
- const mutations = useGuardMutations();
-
- // Load gmail from localStorage on mount
- useEffect(() => {
- const savedGmail = loadGmail();
- if (savedGmail) {
- setGmail(savedGmail);
- }
- }, []);
-
- const validateSpreadsheetId = (input: string): string => {
- if (!input.trim()) return "";
-
- const extractedId = extractSpreadsheetId(input);
-
- if (!extractedId) {
- return "Invalid Google Spreadsheet URL";
- }
-
- if (extractedId.length < 10) {
- return "Spreadsheet ID seems too short";
- }
-
- return "";
- };
-
- const handleSubmit = async () => {
- setError("");
-
- if (!gmail.trim()) {
- setError("Gmail is required");
- return;
- }
-
- // Basic email validation
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- if (!emailRegex.test(gmail.trim())) {
- setError("Please enter a valid Gmail address");
- return;
- }
-
- if (mode === "create") {
- if (!title.trim()) {
- setError("Title is required");
- return;
- }
-
- // Save gmail to localStorage
- saveGmail(gmail.trim());
-
- mutations.createFile.mutate(
- {
- body: {
- file_type: guardTypes.CreateFileRequestFile_type.spreadsheet,
- title: title.trim(),
- default_role: respondentRole,
- owner_gmail: gmail.trim(),
- },
- },
- {
- onSuccess: (data) => {
- setSetupResult({
- title: data.title,
- fileId: data.file_id,
- fileType: "spreadsheet",
- guardingMethod: mode,
- roleDisplay: data.default_role,
- joinLink: data.join_link,
- });
- setTitle("");
- mutations.invalidateFiles();
- },
- onError: (error) => {
- setError(formatApiErrorMessage(error));
- },
- },
- );
- return;
- }
-
- // Copy mode
- if (!spreadsheetId.trim()) {
- setError("Spreadsheet URL is required");
- return;
- }
-
- const validationError = validateSpreadsheetId(spreadsheetId);
- if (validationError) {
- setError(validationError);
- return;
- }
-
- const extractedId = extractSpreadsheetId(spreadsheetId);
- if (!extractedId) {
- setError("Failed to extract Spreadsheet ID");
- return;
- }
-
- const copyRole =
- respondentRole === FileRole.writer
- ? guardTypes.CopyFileRequestDefault_role.writer
- : guardTypes.CopyFileRequestDefault_role.reader;
-
- // Save gmail to localStorage
- saveGmail(gmail.trim());
-
- mutations.copyFile.mutate(
- {
- body: {
- file_id: extractedId,
- default_role: copyRole,
- owner_gmail: gmail.trim(),
- },
- },
- {
- onSuccess: (data) => {
- setSetupResult({
- title: data.title,
- fileId: data.file_id,
- fileType: data.file_type,
- guardingMethod: mode,
- roleDisplay: data.default_role,
- joinLink: data.join_link,
- });
- setSpreadsheetId("");
- mutations.invalidateFiles();
- },
- onError: (error) => {
- setError(formatApiErrorMessage(error));
- },
- },
- );
- };
-
- const handleBlur = () => {
- if (mode === "copy" && spreadsheetId.trim()) {
- const validationError = validateSpreadsheetId(spreadsheetId);
- setError(validationError);
- }
- };
-
- const isSubmitting =
- mode === "create"
- ? mutations.createFile.isPending
- : mutations.copyFile.isPending;
-
- return (
-
-
-
- {mode === "create" ? (
-
- ) : (
-
- )}
-
-
{
- e.preventDefault();
- handleSubmit();
- }}
- >
- {mode === "create" && (
-
-
- Title:
-
- {
- setTitle(e.target.value);
- if (error) setError("");
- }}
- placeholder="e.g., CSE electives"
- className="border-base-content/20 bg-base-200/5 focus:border-base-200 focus:bg-base-200/10 rounded-field w-full border-2 px-4 py-2 outline-hidden transition-colors"
- />
-
- )}
-
- {mode === "copy" && (
-
-
- Spreadsheet URL:
-
- {
- setSpreadsheetId(e.target.value);
- if (error) setError("");
- }}
- onBlur={handleBlur}
- placeholder="https://docs.google.com/spreadsheets/d/1gqfGNkl_nLI5vXc5br8-J__33kj8Kr3fI2xqrMBK2JA"
- className="border-base-content/20 bg-base-200/5 focus:border-base-200 focus:bg-base-200/10 rounded-field w-full border-2 px-4 py-2 outline-hidden transition-colors"
- />
-
- )}
-
-
-
- Owner Gmail:
-
- {
- setGmail(e.target.value);
- if (error) setError("");
- }}
- placeholder="your.email@gmail.com"
- className="border-base-content/20 bg-base-200/5 focus:border-base-200 focus:bg-base-200/10 rounded-field w-full border-2 px-4 py-2 outline-hidden transition-colors"
- />
-
-
-
-
- Default Role:
-
-
- {/*
- setRespondentRole(FileRole.writer)}
- className={`rounded-field border-2 px-3 py-2 text-sm font-medium ${
- respondentRole === FileRole.writer
- ? "border-primary bg-primary/10"
- : "border-base-content/20 hover:border-base-content/40"
- }`}
- >
- {ROLE_LABELS.writer}
-
- setRespondentRole(FileRole.reader)}
- className={`rounded-field border-2 px-3 py-2 text-sm font-medium ${
- respondentRole === FileRole.reader
- ? "border-primary bg-primary/10"
- : "border-base-content/20 hover:border-base-content/40"
- }`}
- >
- {ROLE_LABELS.reader}
-
-
*/}
-
-
- {error && (
-
- {error}
-
- )}
-
-
- {isSubmitting
- ? mode === "create"
- ? "Creating..."
- : "Copying..."
- : mode === "create"
- ? "Create InNoHassle Guard Sheet"
- : "Copy Existing Sheet"}
-
-
-
- {setupResult && (
-
setSetupResult(null)}
- />
- )}
-
- );
-}
diff --git a/src/components/guard/SetupResult.tsx b/src/components/guard/SetupResult.tsx
index c79c41a4..87104d68 100644
--- a/src/components/guard/SetupResult.tsx
+++ b/src/components/guard/SetupResult.tsx
@@ -1,10 +1,12 @@
-// import { useState } from "react";
-import { buildSheetsUrl, buildDocsUrl } from "./utils";
+import { buildDocsUrl, buildSheetsUrl } from "./utils";
import { CopyLinkButton } from "./CopyLinkButton";
import { useServiceAccountEmail } from "./hooks";
import { ServiceAccountEmail } from "./Instructions";
-interface SetupResultProps {
+export function SetupResult({
+ result,
+ onDismiss,
+}: {
result: {
title: string;
fileId: string;
@@ -14,20 +16,7 @@ interface SetupResultProps {
joinLink: string;
};
onDismiss?: () => void;
-}
-
-export function SetupResult({ result, onDismiss }: SetupResultProps) {
- // const [copied, setCopied] = useState(false);
-
- // const handleCopy = async () => {
- // try {
- // await navigator.clipboard.writeText(result.joinLink);
- // setCopied(true);
- // setTimeout(() => setCopied(false), 2000);
- // } catch (_) {
- // console.log("Failed to copy to clipboard");
- // }
- // };
+}) {
const { serviceEmail } = useServiceAccountEmail();
const fileUrl =
@@ -36,41 +25,34 @@ export function SetupResult({ result, onDismiss }: SetupResultProps) {
: buildDocsUrl(result.fileId);
return (
-
- {/*
*/}
-
- ✅ Setup Complete!
-
-
+
-
+
Setup complete
+
New file was created under{" "}
- ownership
-
- You was added to the file with editor role
+ ownership. You were added
+ with editor role.
-
- You can edit spreadsheet or share join link with the students!
+
+ Edit the spreadsheet or share the join link with students.
-
-
-
+
+
+
+ Open spreadsheet
+
{onDismiss ? (
Dismiss
diff --git a/src/components/guard/consts.ts b/src/components/guard/consts.ts
index feb34201..ada4c1e0 100644
--- a/src/components/guard/consts.ts
+++ b/src/components/guard/consts.ts
@@ -1,26 +1,14 @@
export const GMAIL_STORAGE_KEY = "guard_gmail";
-export const DEFAULT_MODE = "create" as const;
-
export const ROLE_LABELS = {
writer: "Writer",
reader: "Reader",
} as const;
-export const MODE_LABELS = {
- create: "Create Protected Spreadsheet",
- copy: "Copy Existing Spreadsheet",
-} as const;
-
export const MESSAGES = {
setupSuccess: "Setup Complete!",
copySuccess: "Copied!",
- deleteConfirm: "Are you sure you want to delete this file?",
- banConfirm: "Are you sure you want to ban this user?",
- unbanConfirm: "Are you sure you want to unban this user?",
cleanupRecommended:
"Cleanup recommended: unverified users have access to this file.",
noAccess: "No access to the document",
} as const;
-
-export const MAX_LIST_HEIGHT = "max-h-[400px]";
diff --git a/src/components/guard/hooks.ts b/src/components/guard/hooks.ts
index 7fb78f30..2d43040f 100644
--- a/src/components/guard/hooks.ts
+++ b/src/components/guard/hooks.ts
@@ -25,7 +25,7 @@ export function useServiceAccountEmail() {
const query = $guard.useQuery("get", "/google/service-account-email");
return {
serviceEmail: query.data?.email || "",
- isLoading: query.isLoading,
+ isPending: query.isPending,
};
}
@@ -37,7 +37,7 @@ export function useGuardFiles() {
return {
files: query.data || [],
- isLoading: query.isLoading,
+ isPending: query.isPending,
error: query.error,
};
}
@@ -59,7 +59,7 @@ export function useGuardFile(slug: string | null) {
return {
file: query.data,
- isLoading: query.isLoading,
+ isPending: query.isPending,
};
}
diff --git a/src/components/guard/index.ts b/src/components/guard/index.ts
index 10bedccc..81c96758 100644
--- a/src/components/guard/index.ts
+++ b/src/components/guard/index.ts
@@ -1,19 +1,16 @@
-export { GuardPage } from "./GuardPage";
-export { SetupContainer } from "./SetupContainer";
+export { GuardLandingPage } from "./GuardLandingPage";
+export { GuardTabs } from "./GuardTabs";
+export { CreateSheetPage } from "./CreateSheetPage";
+export { CopySheetPage } from "./CopySheetPage";
+export { FilesPage } from "./FilesPage";
export { SetupResult } from "./SetupResult";
export { FilesSection } from "./FilesSection";
export { FilesList } from "./FilesList";
export { FileDetails } from "./FileDetails";
export { JoinsList } from "./JoinsList";
export { BannedList } from "./BannedList";
-export { ModeToggle } from "./ModeToggle";
-export { CleanupAlert } from "./CleanupAlert";
export { CopyLinkButton } from "./CopyLinkButton";
-export {
- CreateInstructions,
- CopyInstructions,
- ServiceAccountEmail,
-} from "./Instructions";
+export { CopyInstructions, ServiceAccountEmail } from "./Instructions";
export { EditTitleButton } from "./EditTitleButton";
export * from "./hooks";
export * from "./utils";