({
+ mode: "uncontrolled",
+ initialValues: {
+ fileType: "csv",
+ },
+ });
+
+ const studies = useSuspenseQuery($api.queryOptions("get", "/studies"));
const { isDownloading, downloadFile } = $api.useDownload("/export", "dump.sql");
return (
-
-
-
+
);
}
export const Route = createFileRoute("/_auth/administration/export/")({
+ loader: ({ context: { queryClient } }) => {
+ queryClient.ensureQueryData($api.queryOptions("get", "/studies"));
+ },
component: AdministrationExportIndex,
});
diff --git a/apps/frontend/src/routes/_auth/administration/index.tsx b/apps/frontend/src/routes/_auth/administration/index.tsx
index ac8a5481..ca70bb61 100644
--- a/apps/frontend/src/routes/_auth/administration/index.tsx
+++ b/apps/frontend/src/routes/_auth/administration/index.tsx
@@ -1,10 +1,15 @@
-import { Title } from "@quassel/ui";
import { createFileRoute } from "@tanstack/react-router";
+import { i18n } from "../../../stores/i18n";
-export const Route = createFileRoute("/_auth/administration/")({
- component: Index,
+const messages = i18n("AdministrationDashboardRoute", {
+ title: "Dashboard",
});
function Index() {
- return Welcome to the administration interface!;
+ return null;
}
+
+export const Route = createFileRoute("/_auth/administration/")({
+ beforeLoad: () => ({ title: messages.get().title }),
+ component: Index,
+});
diff --git a/apps/frontend/src/routes/_auth/administration/languages.tsx b/apps/frontend/src/routes/_auth/administration/languages.tsx
index 7238ee13..1f8ed974 100644
--- a/apps/frontend/src/routes/_auth/administration/languages.tsx
+++ b/apps/frontend/src/routes/_auth/administration/languages.tsx
@@ -1,17 +1,11 @@
-import { Title, Paper } from "@quassel/ui";
import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { i18n } from "../../../stores/i18n";
-function AdministrationLanguages() {
- return (
- <>
- Languages
-
-
-
- >
- );
-}
+const messages = i18n("AdministrationLanguagesRoute", {
+ title: "Languages",
+});
export const Route = createFileRoute("/_auth/administration/languages")({
- component: AdministrationLanguages,
+ beforeLoad: () => ({ title: messages.get().title }),
+ component: Outlet,
});
diff --git a/apps/frontend/src/routes/_auth/administration/languages/edit.$id.tsx b/apps/frontend/src/routes/_auth/administration/languages/edit.$id.tsx
index eb46fd9c..a8f1e0c4 100644
--- a/apps/frontend/src/routes/_auth/administration/languages/edit.$id.tsx
+++ b/apps/frontend/src/routes/_auth/administration/languages/edit.$id.tsx
@@ -44,16 +44,14 @@ function AdministrationLanguagesEdit() {
}, [isSuccess, data]);
return (
- <>
-
- >
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/languages/index.tsx b/apps/frontend/src/routes/_auth/administration/languages/index.tsx
index 2d01953d..2c97c22a 100644
--- a/apps/frontend/src/routes/_auth/administration/languages/index.tsx
+++ b/apps/frontend/src/routes/_auth/administration/languages/index.tsx
@@ -13,50 +13,52 @@ function AdministrationLanguageIndex() {
});
return (
- <>
-
-
-
-
- Id
- Name
- IETF BCP 47
-
-
-
- {languages.data?.map((l) => (
-
- {l.id}
- {l.name}
- {l.ietfBcp47}
-
-
+
+ ))}
+
+
);
}
export const Route = createFileRoute("/_auth/administration/languages/")({
+ beforeLoad: () => ({
+ actions: [
+ }>
+ New language
+ ,
+ ],
+ }),
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData($api.queryOptions("get", "/languages")),
component: AdministrationLanguageIndex,
});
diff --git a/apps/frontend/src/routes/_auth/administration/languages/new.tsx b/apps/frontend/src/routes/_auth/administration/languages/new.tsx
index 34a26241..9a464e32 100644
--- a/apps/frontend/src/routes/_auth/administration/languages/new.tsx
+++ b/apps/frontend/src/routes/_auth/administration/languages/new.tsx
@@ -24,16 +24,14 @@ function AdministrationLanguagesNew() {
};
return (
- <>
-
- >
+
+ Create
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/participants.tsx b/apps/frontend/src/routes/_auth/administration/participants.tsx
index f0b70121..df3de215 100644
--- a/apps/frontend/src/routes/_auth/administration/participants.tsx
+++ b/apps/frontend/src/routes/_auth/administration/participants.tsx
@@ -1,17 +1,11 @@
-import { Paper, Title } from "@quassel/ui";
import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { i18n } from "../../../stores/i18n";
-function AdministrationParticipants() {
- return (
- <>
- Participants
-
-
-
- >
- );
-}
+const messages = i18n("AdministrationParticipantsRoute", {
+ title: "Participants",
+});
export const Route = createFileRoute("/_auth/administration/participants")({
- component: AdministrationParticipants,
+ beforeLoad: () => ({ title: messages.get().title }),
+ component: Outlet,
});
diff --git a/apps/frontend/src/routes/_auth/administration/participants/index.tsx b/apps/frontend/src/routes/_auth/administration/participants/index.tsx
index 77adfe19..6fcbcfea 100644
--- a/apps/frontend/src/routes/_auth/administration/participants/index.tsx
+++ b/apps/frontend/src/routes/_auth/administration/participants/index.tsx
@@ -13,52 +13,54 @@ function AdministrationParticipantsIndex() {
});
return (
- <>
- }>
- New participant
-
- }>
- Import participants
-
-
-
-
- Id
- Birthday
- Actions
-
-
-
- {participants.data?.map((p) => (
-
- {p.id}
- {p.birthday}
-
- }>
- Edit
+
+
+
+ Id
+ Birthday
+ Actions
+
+
+
+ {participants.data?.map((p) => (
+
+ {p.id}
+ {p.birthday}
+
+ }>
+ Edit
+
+ {sessionStore.role === "ADMIN" && (
+
+ deleteParticipantMutation.mutate({
+ params: { path: { id: p.id.toString() } },
+ })
+ }
+ >
+ Delete
- {sessionStore.role === "ADMIN" && (
-
- deleteParticipantMutation.mutate({
- params: { path: { id: p.id.toString() } },
- })
- }
- >
- Delete
-
- )}
-
-
- ))}
-
-
- >
+ )}
+
+
+ ))}
+
+
);
}
export const Route = createFileRoute("/_auth/administration/participants/")({
+ beforeLoad: () => ({
+ actions: [
+ }>
+ New participant
+ ,
+ }>
+ Import participants
+ ,
+ ],
+ }),
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData($api.queryOptions("get", "/participants")),
component: () => ,
});
diff --git a/apps/frontend/src/routes/_auth/administration/participants/new.tsx b/apps/frontend/src/routes/_auth/administration/participants/new.tsx
index 9363f72e..774febbc 100644
--- a/apps/frontend/src/routes/_auth/administration/participants/new.tsx
+++ b/apps/frontend/src/routes/_auth/administration/participants/new.tsx
@@ -24,16 +24,14 @@ function AdministrationParticipantsNew() {
};
return (
- <>
-
- >
+
+ Create
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/questionnaires.tsx b/apps/frontend/src/routes/_auth/administration/questionnaires.tsx
index ec1ea40c..aa58ee4f 100644
--- a/apps/frontend/src/routes/_auth/administration/questionnaires.tsx
+++ b/apps/frontend/src/routes/_auth/administration/questionnaires.tsx
@@ -1,17 +1,11 @@
-import { Title, Paper } from "@quassel/ui";
import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { i18n } from "../../../stores/i18n";
-function AdministrationQuestionnaires() {
- return (
- <>
- Questionnaires
-
-
-
- >
- );
-}
+const messages = i18n("AdministrationQuestionnairesRoute", {
+ title: "Questionnaires",
+});
export const Route = createFileRoute("/_auth/administration/questionnaires")({
- component: AdministrationQuestionnaires,
+ beforeLoad: () => ({ title: messages.get().title }),
+ component: Outlet,
});
diff --git a/apps/frontend/src/routes/_auth/administration/questionnaires/edit.$id.tsx b/apps/frontend/src/routes/_auth/administration/questionnaires/edit.$id.tsx
index 054ae38f..5a3adf35 100644
--- a/apps/frontend/src/routes/_auth/administration/questionnaires/edit.$id.tsx
+++ b/apps/frontend/src/routes/_auth/administration/questionnaires/edit.$id.tsx
@@ -40,15 +40,13 @@ function AdministrationQuestionnairesEdit() {
}, [isSuccess, data]);
return (
- <>
-
- >
+
+ Change
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/questionnaires/index.tsx b/apps/frontend/src/routes/_auth/administration/questionnaires/index.tsx
index d19ed77a..5bedbadd 100644
--- a/apps/frontend/src/routes/_auth/administration/questionnaires/index.tsx
+++ b/apps/frontend/src/routes/_auth/administration/questionnaires/index.tsx
@@ -12,40 +12,38 @@ function AdministrationQuestionnairesIndex() {
});
return (
- <>
-
-
-
- Id
- Name
-
-
-
- {data?.map((q) => (
-
- {q.id}
-
- }>
- Edit
+
+
+
+ Id
+ Name
+
+
+
+ {data?.map((q) => (
+
+ {q.id}
+
+ }>
+ Edit
+
+ {sessionStore.role === "ADMIN" && (
+
+ deleteQuestionnaireMutation.mutate({
+ params: { path: { id: q.id.toString() } },
+ })
+ }
+ >
+ Delete
- {sessionStore.role === "ADMIN" && (
-
- deleteQuestionnaireMutation.mutate({
- params: { path: { id: q.id.toString() } },
- })
- }
- >
- Delete
-
- )}
-
-
- ))}
-
-
- >
+ )}
+
+
+ ))}
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/studies.tsx b/apps/frontend/src/routes/_auth/administration/studies.tsx
index bfa698f2..09a5f26c 100644
--- a/apps/frontend/src/routes/_auth/administration/studies.tsx
+++ b/apps/frontend/src/routes/_auth/administration/studies.tsx
@@ -1,17 +1,11 @@
-import { Title, Paper } from "@quassel/ui";
import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { i18n } from "../../../stores/i18n";
-function AdministrationStudies() {
- return (
- <>
- Studies
-
-
-
- >
- );
-}
+const messages = i18n("AdministrationStudiesRoute", {
+ title: "Studies",
+});
export const Route = createFileRoute("/_auth/administration/studies")({
- component: AdministrationStudies,
+ beforeLoad: () => ({ title: messages.get().title }),
+ component: Outlet,
});
diff --git a/apps/frontend/src/routes/_auth/administration/studies/edit.$id.tsx b/apps/frontend/src/routes/_auth/administration/studies/edit.$id.tsx
index 78dd4c0e..c103ae97 100644
--- a/apps/frontend/src/routes/_auth/administration/studies/edit.$id.tsx
+++ b/apps/frontend/src/routes/_auth/administration/studies/edit.$id.tsx
@@ -40,16 +40,14 @@ function AdministrationStudiesEdit() {
}, [study.isSuccess, study.data]);
return (
- <>
-
- >
+
+ Change
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/studies/index.tsx b/apps/frontend/src/routes/_auth/administration/studies/index.tsx
index c68b83b1..b098ced2 100644
--- a/apps/frontend/src/routes/_auth/administration/studies/index.tsx
+++ b/apps/frontend/src/routes/_auth/administration/studies/index.tsx
@@ -13,48 +13,50 @@ function AdministrationStudiesIndex() {
});
return (
- <>
- }>
- New study
-
-
-
-
- Id
- Title
-
-
-
- {studies.data?.map((s) => (
-
- {s.id}
- {s.title}
-
- }>
- Edit
+
+
+
+ Id
+ Title
+
+
+
+ {studies.data?.map((s) => (
+
+ {s.id}
+ {s.title}
+
+ }>
+ Edit
+
+ {sessionStore.role === "ADMIN" && (
+
+ deleteStudyMutation.mutate({
+ params: { path: { id: s.id.toString() } },
+ })
+ }
+ >
+ Delete
- {sessionStore.role === "ADMIN" && (
-
- deleteStudyMutation.mutate({
- params: { path: { id: s.id.toString() } },
- })
- }
- >
- Delete
-
- )}
-
-
- ))}
-
-
- >
+ )}
+
+
+ ))}
+
+
);
}
export const Route = createFileRoute("/_auth/administration/studies/")({
+ beforeLoad: () => ({
+ actions: [
+ }>
+ New study
+ ,
+ ],
+ }),
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData($api.queryOptions("get", "/studies")),
component: () => ,
});
diff --git a/apps/frontend/src/routes/_auth/administration/studies/new.tsx b/apps/frontend/src/routes/_auth/administration/studies/new.tsx
index 2a1debd9..098468f5 100644
--- a/apps/frontend/src/routes/_auth/administration/studies/new.tsx
+++ b/apps/frontend/src/routes/_auth/administration/studies/new.tsx
@@ -25,16 +25,14 @@ function AdministrationStudiesNew() {
};
return (
- <>
-
- >
+
+ Create
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/users.tsx b/apps/frontend/src/routes/_auth/administration/users.tsx
index 3538a62d..a8d3b945 100644
--- a/apps/frontend/src/routes/_auth/administration/users.tsx
+++ b/apps/frontend/src/routes/_auth/administration/users.tsx
@@ -1,17 +1,11 @@
-import { Paper, Title } from "@quassel/ui";
import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { i18n } from "../../../stores/i18n";
-function AdministrationUsers() {
- return (
- <>
- Users
-
-
-
- >
- );
-}
+const messages = i18n("AdministrationUsersRoute", {
+ title: "Users",
+});
export const Route = createFileRoute("/_auth/administration/users")({
- component: AdministrationUsers,
+ beforeLoad: () => ({ title: messages.get().title }),
+ component: Outlet,
});
diff --git a/apps/frontend/src/routes/_auth/administration/users/edit.$id.tsx b/apps/frontend/src/routes/_auth/administration/users/edit.$id.tsx
index f1353f6c..7e393cf1 100644
--- a/apps/frontend/src/routes/_auth/administration/users/edit.$id.tsx
+++ b/apps/frontend/src/routes/_auth/administration/users/edit.$id.tsx
@@ -35,17 +35,15 @@ function AdministrationUsersEdit() {
}, [user.isSuccess, user.data]);
return (
- <>
-
- >
+
+ Change
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/administration/users/index.tsx b/apps/frontend/src/routes/_auth/administration/users/index.tsx
index 1f702b23..feb394f4 100644
--- a/apps/frontend/src/routes/_auth/administration/users/index.tsx
+++ b/apps/frontend/src/routes/_auth/administration/users/index.tsx
@@ -11,44 +11,46 @@ function AdministrationUsersIndex() {
const deleteUserMutation = $api.useMutation("delete", "/users/{id}", { onSuccess: () => users.refetch() });
return (
- <>
- }>
- New user
-
-
-
-
- Id
- Email
- Role
- Actions
-
-
-
- {users.data?.map((u) => (
-
- {u.id}
- {u.email}
- {u.role}
-
- }>
- Edit
+
+
+
+ Id
+ Email
+ Role
+ Actions
+
+
+
+ {users.data?.map((u) => (
+
+ {u.id}
+ {u.email}
+ {u.role}
+
+ }>
+ Edit
+
+ {sessionStore.role === "ADMIN" && (
+ deleteUserMutation.mutate({ params: { path: { id: u.id.toString() } } })}>
+ Delete
- {sessionStore.role === "ADMIN" && (
- deleteUserMutation.mutate({ params: { path: { id: u.id.toString() } } })}>
- Delete
-
- )}
-
-
- ))}
-
-
- >
+ )}
+
+
+ ))}
+
+
);
}
export const Route = createFileRoute("/_auth/administration/users/")({
+ beforeLoad: () => ({
+ actions: [
+ }>
+ New user
+ ,
+ ],
+ }),
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData($api.queryOptions("get", "/users")),
component: AdministrationUsersIndex,
});
diff --git a/apps/frontend/src/routes/_auth/administration/users/new.tsx b/apps/frontend/src/routes/_auth/administration/users/new.tsx
index afdcf84b..1a1d7b67 100644
--- a/apps/frontend/src/routes/_auth/administration/users/new.tsx
+++ b/apps/frontend/src/routes/_auth/administration/users/new.tsx
@@ -24,17 +24,15 @@ function AdministrationUsersNew() {
};
return (
- <>
-
- >
+
+ Create
+
+
);
}
diff --git a/apps/frontend/src/routes/_auth/index.tsx b/apps/frontend/src/routes/_auth/index.tsx
index e500327c..cd83ccac 100644
--- a/apps/frontend/src/routes/_auth/index.tsx
+++ b/apps/frontend/src/routes/_auth/index.tsx
@@ -2,11 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { HeroSection } from "../../sections/HeroSection";
function Index() {
- return (
- <>
-
- >
- );
+ return ;
}
export const Route = createFileRoute("/_auth/")({
diff --git a/apps/frontend/src/types.d.ts b/apps/frontend/src/types.d.ts
index 08fc88a9..109fc83f 100644
--- a/apps/frontend/src/types.d.ts
+++ b/apps/frontend/src/types.d.ts
@@ -1,4 +1,7 @@
import "@tanstack/react-query";
+import "@tanstack/react-router";
+import { QueryClient } from "@tanstack/react-query";
+import { ReactElement } from "react";
declare module "@tanstack/react-query" {
interface Register {
@@ -10,6 +13,14 @@ declare module "@tanstack/react-query" {
}
}
+declare module "@tanstack/react-router" {
+ interface RouteContext {
+ queryClient: QueryClient;
+ title?: string;
+ actions?: ReactElement[];
+ }
+}
+
declare global {
interface Window {
env?: {
diff --git a/libs/ui/src/components/ContentShell.tsx b/libs/ui/src/components/ContentShell.tsx
new file mode 100644
index 00000000..2f363637
--- /dev/null
+++ b/libs/ui/src/components/ContentShell.tsx
@@ -0,0 +1,21 @@
+import { Flex, Paper, Title } from "@mantine/core";
+import { PropsWithChildren, ReactElement } from "react";
+
+type Props = PropsWithChildren<{
+ title: string;
+ actions?: ReactElement[];
+ breadcrumbs?: ReactElement;
+}>;
+
+export function ContentShell({ title, breadcrumbs, actions, children }: Props) {
+ return (
+ <>
+ {breadcrumbs}
+
+ {title}
+ {actions}
+
+ {children}
+ >
+ );
+}
diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts
index 8226bad0..3aa6953f 100644
--- a/libs/ui/src/index.ts
+++ b/libs/ui/src/index.ts
@@ -29,6 +29,7 @@ import "@mantine/core/styles/Title.css";
import "@mantine/core/styles/ActionIcon.css";
import "@mantine/core/styles/Combobox.css";
import "@mantine/core/styles/Stack.css";
+import "@mantine/core/styles/Radio.css";
import "@mantine/dates/styles.css";
@@ -41,6 +42,7 @@ export { formatDate, getTime, getDateFromTimeAndWeekday, getNext, isSameOrAfter,
// custom components
export { Brand } from "./components/Brand";
+export { ContentShell } from "./components/ContentShell";
export { FooterLogos } from "./components/FooterLogos";
export { ImportInput } from "./components/ImportInput";
export { ImportPreview } from "./components/ImportPreview";
@@ -51,6 +53,7 @@ export {
ActionIcon,
Anchor,
AppShell,
+ Breadcrumbs,
Button,
Checkbox,
Combobox,
@@ -61,10 +64,12 @@ export {
Flex,
Group,
InputError,
+ InputLabel,
Modal,
NavLink,
Paper,
PasswordInput,
+ Radio,
Select,
type SelectProps,
Stack,
diff --git a/libs/ui/vite.config.ts b/libs/ui/vite.config.ts
index f71a88bb..4bb6a8f8 100644
--- a/libs/ui/vite.config.ts
+++ b/libs/ui/vite.config.ts
@@ -27,6 +27,7 @@ export default defineConfig({
"@mantine/dates": "mantineDates",
"@mantine/hooks": "mantineHooks",
"@mantine/form": "mantineForm",
+ "@mantine/notifications": "mantineNotifications",
"@tabler/icons-react/dist/esm/icons/index.mjs": "tablerIconsReact",
"react-dsv-import": "reactDsvImport",
},