diff --git a/.changeset/fifty-seas-thank.md b/.changeset/fifty-seas-thank.md new file mode 100644 index 00000000..78895a0c --- /dev/null +++ b/.changeset/fifty-seas-thank.md @@ -0,0 +1,6 @@ +--- +"@quassel/frontend": patch +"@quassel/ui": patch +--- + +Add export ui diff --git a/.changeset/many-snails-try.md b/.changeset/many-snails-try.md new file mode 100644 index 00000000..6855da5a --- /dev/null +++ b/.changeset/many-snails-try.md @@ -0,0 +1,6 @@ +--- +"@quassel/frontend": patch +"@quassel/ui": patch +--- + +Add content shell diff --git a/apps/frontend/src/components/Breadcrumbs.tsx b/apps/frontend/src/components/Breadcrumbs.tsx new file mode 100644 index 00000000..fbb0c049 --- /dev/null +++ b/apps/frontend/src/components/Breadcrumbs.tsx @@ -0,0 +1,29 @@ +import { isMatch, Link, useLocation, useMatches } from "@tanstack/react-router"; +import { Anchor, Breadcrumbs } from "@quassel/ui"; + +// Inspired by https://github.com/TanStack/router/blob/main/examples/react/kitchen-sink-file-based/src/components/Breadcrumbs.tsx +export function BreadcrumbsNavigation() { + const matches = useMatches(); + const location = useLocation(); + + if (matches.some((match) => match.status === "pending")) return null; + + const matchesWithTitle = matches.filter((match) => isMatch(match, "context.title")); + const entries = matchesWithTitle.map((match) => ({ label: match.context.title, to: match.fullPath })); + const uniqueEntries = entries.filter((entry, index) => entries.findIndex((e) => e.label === entry.label) === index); + + // Remove the last entry if it's the current page + if (uniqueEntries.length > 0 && uniqueEntries[uniqueEntries.length - 1].to.startsWith(location.pathname)) { + uniqueEntries.pop(); + } + + return ( + + {uniqueEntries.map((e) => ( + }> + {e.label} + + ))} + + ); +} diff --git a/apps/frontend/src/routes/__root.tsx b/apps/frontend/src/routes/__root.tsx index 32810867..876bac82 100644 --- a/apps/frontend/src/routes/__root.tsx +++ b/apps/frontend/src/routes/__root.tsx @@ -17,13 +17,18 @@ import { Divider, FooterLogos, } from "@quassel/ui"; -import { createRootRouteWithContext, Link, Outlet, useNavigate } from "@tanstack/react-router"; +import { createRootRouteWithContext, Link, Outlet, RouteContext, useNavigate } from "@tanstack/react-router"; import { version } from "../../package.json"; import { $session } from "../stores/session"; import { useStore } from "@nanostores/react"; import { $layout } from "../stores/layout"; import { $api } from "../stores/api"; -import { DefaultError, QueryClient, useQueryClient } from "@tanstack/react-query"; +import { DefaultError, useQueryClient } from "@tanstack/react-query"; +import { i18n } from "../stores/i18n"; + +const messages = i18n("RootRoute", { + title: "Home", +}); function Root() { const n = useNavigate(); @@ -106,6 +111,7 @@ function Root() { ); } -export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ +export const Route = createRootRouteWithContext()({ + beforeLoad: () => ({ title: messages.get().title }), component: Root, }); diff --git a/apps/frontend/src/routes/_auth/administration.tsx b/apps/frontend/src/routes/_auth/administration.tsx index c748c29e..573bb8ef 100644 --- a/apps/frontend/src/routes/_auth/administration.tsx +++ b/apps/frontend/src/routes/_auth/administration.tsx @@ -1,6 +1,13 @@ -import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { createFileRoute, Outlet, useMatches } from "@tanstack/react-router"; import { useEffect } from "react"; import { $layout } from "../../stores/layout"; +import { i18n } from "../../stores/i18n"; +import { ContentShell } from "@quassel/ui"; +import { BreadcrumbsNavigation } from "../../components/Breadcrumbs"; + +const messages = i18n("AdministrationRoute", { + title: "Administration", +}); function AdministrationLayout() { useEffect(() => { @@ -8,13 +15,18 @@ function AdministrationLayout() { return () => $layout.set({ admin: false }); }, []); + const matches = useMatches(); + const title = matches[matches.length - 2]?.context.title; + const actions = matches[matches.length - 1]?.context.actions; + return ( - <> + } actions={actions}> - + ); } export const Route = createFileRoute("/_auth/administration")({ + beforeLoad: () => ({ title: messages.get().title }), component: AdministrationLayout, }); diff --git a/apps/frontend/src/routes/_auth/administration/carers.tsx b/apps/frontend/src/routes/_auth/administration/carers.tsx index 8bddace3..7e533308 100644 --- a/apps/frontend/src/routes/_auth/administration/carers.tsx +++ b/apps/frontend/src/routes/_auth/administration/carers.tsx @@ -1,17 +1,11 @@ -import { Paper, Title } from "@quassel/ui"; import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { i18n } from "../../../stores/i18n"; -function AdministrationCarers() { - return ( - <> - Carers - - - - - ); -} +const messages = i18n("AdministrationCarersRoute", { + title: "Carers", +}); export const Route = createFileRoute("/_auth/administration/carers")({ - component: AdministrationCarers, + beforeLoad: () => ({ title: messages.get().title }), + component: Outlet, }); diff --git a/apps/frontend/src/routes/_auth/administration/carers/index.tsx b/apps/frontend/src/routes/_auth/administration/carers/index.tsx index e8f19f53..5ecfe6d9 100644 --- a/apps/frontend/src/routes/_auth/administration/carers/index.tsx +++ b/apps/frontend/src/routes/_auth/administration/carers/index.tsx @@ -12,50 +12,52 @@ function AdministrationCarersIndex() { }); return ( - <> - - - - - Id - Name - Color - - - - {carers.data?.map((c) => ( - - {c.id} - {c.name} - {c.color && } - -
+ + + Id + Name + Color + + + + {carers.data?.map((c) => ( + + {c.id} + {c.name} + {c.color && } + + + {sessionStore.role === "ADMIN" && ( + - {sessionStore.role === "ADMIN" && ( - - )} - - - ))} - -
- + )} + + + ))} + + ); } export const Route = createFileRoute("/_auth/administration/carers/")({ + beforeLoad: () => ({ + actions: [ + , + ], + }), loader: ({ context: { queryClient } }) => queryClient.ensureQueryData($api.queryOptions("get", "/carers")), component: () => , }); diff --git a/apps/frontend/src/routes/_auth/administration/export.tsx b/apps/frontend/src/routes/_auth/administration/export.tsx index e3372625..d14a3dcb 100644 --- a/apps/frontend/src/routes/_auth/administration/export.tsx +++ b/apps/frontend/src/routes/_auth/administration/export.tsx @@ -1,17 +1,11 @@ -import { Paper, Title } from "@quassel/ui"; import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { i18n } from "../../../stores/i18n"; -function AdministrationExport() { - return ( - <> - Export - - - - - ); -} +const messages = i18n("AdministrationExportRoute", { + title: "Export", +}); export const Route = createFileRoute("/_auth/administration/export")({ - component: AdministrationExport, + beforeLoad: () => ({ title: messages.get().title }), + component: Outlet, }); diff --git a/apps/frontend/src/routes/_auth/administration/export/index.tsx b/apps/frontend/src/routes/_auth/administration/export/index.tsx index 53b9cea0..1ed05632 100644 --- a/apps/frontend/src/routes/_auth/administration/export/index.tsx +++ b/apps/frontend/src/routes/_auth/administration/export/index.tsx @@ -1,18 +1,61 @@ -import { Button } from "@quassel/ui"; +import { Button, Group, Radio, Select, Stack, useForm } from "@quassel/ui"; import { createFileRoute } from "@tanstack/react-router"; import { $api } from "../../../../stores/api"; +import { i18n } from "../../../../stores/i18n"; +import { useStore } from "@nanostores/react"; +import { useSuspenseQuery } from "@tanstack/react-query"; + +const messages = i18n("AdministrationExportIndexRoute", { + title: "Carers", + studyLabel: "Study", + studyPlaceholder: "Select a study", + formatLabel: "File format", + csvLabel: "Comma-separated values (CSV)", + sqlLabel: "Database export (SQL)", + formAction: "Download", +}); + +type FormValues = { + fileType: "csv" | "sql"; + studyId?: string; +}; function AdministrationExportIndex() { + const t = useStore(messages); + const f = useForm({ + mode: "uncontrolled", + initialValues: { + fileType: "csv", + }, + }); + + const studies = useSuspenseQuery($api.queryOptions("get", "/studies")); const { isDownloading, downloadFile } = $api.useDownload("/export", "dump.sql"); return ( -
- -
+
downloadFile())}> + + + + + + + + + +