diff --git a/website/package-lock.json b/website/package-lock.json index 82a24151..9b369be4 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -27,6 +27,7 @@ "axios": "^1.2.1", "boolean": "^3.2.0", "clsx": "^1.2.1", + "date-fns": "^2.29.3", "eslint": "8.29.0", "eslint-config-next": "13.0.6", "eslint-plugin-simple-import-sort": "^8.0.0", @@ -17710,6 +17711,18 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", @@ -51947,6 +51960,11 @@ } } }, + "date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + }, "dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", diff --git a/website/package.json b/website/package.json index d685ac0f..bb09c986 100644 --- a/website/package.json +++ b/website/package.json @@ -45,6 +45,7 @@ "axios": "^1.2.1", "boolean": "^3.2.0", "clsx": "^1.2.1", + "date-fns": "^2.29.3", "eslint": "8.29.0", "eslint-config-next": "13.0.6", "eslint-plugin-simple-import-sort": "^8.0.0", diff --git a/website/src/components/DataTable/DataTableAction.tsx b/website/src/components/DataTable/DataTableAction.tsx new file mode 100644 index 00000000..de4f0b72 --- /dev/null +++ b/website/src/components/DataTable/DataTableAction.tsx @@ -0,0 +1,10 @@ +import { forwardRef, IconButton, IconButtonProps } from "@chakra-ui/react"; +import { LucideIcon } from "lucide-react"; + +export type DataTableActionProps = Omit & { icon: LucideIcon }; + +// need to use forwardRef from Charka to support `as` props +// https://chakra-ui.com/community/recipes/as-prop +export const DataTableAction = forwardRef((props: DataTableActionProps, ref) => { + return } ref={ref} />; +}); diff --git a/website/src/components/DataTable/useCursorPagination.ts b/website/src/components/DataTable/useCursorPagination.ts new file mode 100644 index 00000000..28e4396e --- /dev/null +++ b/website/src/components/DataTable/useCursorPagination.ts @@ -0,0 +1,40 @@ +import { useState } from "react"; + +export interface CursorPaginationState { + /** + * The user's `display_name` used for pagination. + */ + cursor: string; + + /** + * The pagination direction. + */ + direction: "forward" | "back"; +} + +export const useCursorPagination = () => { + const [pagination, setPagination] = useState({ cursor: "", direction: "forward" }); + + const toPreviousPage = (data: undefined | { prev?: string; next?: string }) => { + setPagination({ + cursor: data?.prev || "", + direction: "back", + }); + }; + + const toNextPage = (data: undefined | { prev?: string; next?: string }) => { + setPagination({ + cursor: data?.next || "", + direction: "forward", + }); + }; + + const resetCursor = () => setPagination((old) => ({ ...old, cursor: "" })); + + return { + pagination, + toNextPage, + toPreviousPage, + resetCursor, + }; +}; diff --git a/website/src/components/JsonCard.tsx b/website/src/components/JsonCard.tsx new file mode 100644 index 00000000..973977b4 --- /dev/null +++ b/website/src/components/JsonCard.tsx @@ -0,0 +1,12 @@ +import { Card, CardBody } from "@chakra-ui/card"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const JsonCard = ({ children }: { children: any }) => { + return ( + + +
{JSON.stringify(children, null, 2)}
+
+
+ ); +}; diff --git a/website/src/components/Messages/AdminMessageTable.tsx b/website/src/components/Messages/AdminMessageTable.tsx new file mode 100644 index 00000000..90866459 --- /dev/null +++ b/website/src/components/Messages/AdminMessageTable.tsx @@ -0,0 +1,110 @@ +import { Avatar } from "@chakra-ui/avatar"; +import { Badge, Flex } from "@chakra-ui/layout"; +import { Tooltip } from "@chakra-ui/react"; +import { createColumnHelper } from "@tanstack/table-core"; +import { formatDistanceToNow, formatISO9075 } from "date-fns"; +import { Eye } from "lucide-react"; +import NextLink from "next/link"; +import { ROUTES } from "src/lib/routes"; +import { Message } from "src/types/Conversation"; +import { isKnownEmoji } from "src/types/Emoji"; +import { StrictOmit } from "src/types/utils"; + +import { DataTable, DataTableProps } from "../DataTable/DataTable"; +import { DataTableAction } from "../DataTable/DataTableAction"; +import { MessageEmojiButton } from "./MessageEmojiButton"; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor("text", { + cell: ({ getValue, row }) => { + const limit = 80; + const text = getValue(); + const renderText = text.length > limit ? `${text.slice(0, limit)}...` : text; + return ( + + + {renderText} + {row.original.deleted && ( + + Deleted + + )} + + ); + }, + }), + columnHelper.accessor("lang", { + header: "Language", + cell: ({ getValue }) => {getValue()}, + }), + columnHelper.accessor("emojis", { + header: "Reactions", + cell: ({ getValue, row }) => { + const emojis = getValue(); + + emojis["+1"] = emojis["+1"] || 0; + emojis["-1"] = emojis["-1"] || 0; + + return ( + + {Object.entries(emojis) + .filter(([emoji]) => isKnownEmoji(emoji)) + .sort(([emoji]) => -emoji) + .map(([emoji, count]) => { + return ( + + ); + })} + + ); + }, + }), + columnHelper.accessor("created_date", { + header: "Date", + cell: ({ getValue }) => { + return {getValue()}; + }, + }), + columnHelper.accessor((row) => row.id, { + header: "Actions", + cell: ({ getValue }) => ( + + ), + }), +]; +// TODO move this to somewhere +const DateDiff = ({ children }: { children: string | Date | number }) => { + const date = new Date(children); + const diff = formatDistanceToNow(date, { addSuffix: true }); + return ( + + {diff} + + ); +}; + +export const AdminMessageTable = (props: StrictOmit, "columns">) => { + return ; +}; diff --git a/website/src/components/Messages/MessageTable.stories.tsx b/website/src/components/Messages/MessageConversation.stories.tsx similarity index 96% rename from website/src/components/Messages/MessageTable.stories.tsx rename to website/src/components/Messages/MessageConversation.stories.tsx index 772d1b52..c6d6acb3 100644 --- a/website/src/components/Messages/MessageTable.stories.tsx +++ b/website/src/components/Messages/MessageConversation.stories.tsx @@ -2,12 +2,12 @@ import { SessionProvider } from "next-auth/react"; import React from "react"; import { Message } from "src/types/Conversation"; -import { MessageTable } from "./MessageTable"; +import { MessageConversation } from "./MessageConversation"; // eslint-disable-next-line import/no-anonymous-default-export export default { title: "Messages/MessageTable", - component: MessageTable, + component: MessageConversation, }; const Template = ({ @@ -21,7 +21,7 @@ const Template = ({ }) => { return ( - ; + ; ); }; diff --git a/website/src/components/Messages/MessageTable.tsx b/website/src/components/Messages/MessageConversation.tsx similarity index 79% rename from website/src/components/Messages/MessageTable.tsx rename to website/src/components/Messages/MessageConversation.tsx index 2d39f346..4efdfb45 100644 --- a/website/src/components/Messages/MessageTable.tsx +++ b/website/src/components/Messages/MessageConversation.tsx @@ -2,13 +2,13 @@ import { Stack } from "@chakra-ui/react"; import { MessageTableEntry } from "src/components/Messages/MessageTableEntry"; import { Message } from "src/types/Conversation"; -interface MessageTableProps { +interface MessageConversationProps { messages: Message[]; enableLink?: boolean; highlightLastMessage?: boolean; } -export function MessageTable({ messages, enableLink, highlightLastMessage }: MessageTableProps) { +export function MessageConversation({ messages, enableLink, highlightLastMessage }: MessageConversationProps) { return ( {messages.map((message, idx) => ( diff --git a/website/src/components/Messages/MessageEmojiButton.tsx b/website/src/components/Messages/MessageEmojiButton.tsx index e3acb3c0..e2cfd569 100644 --- a/website/src/components/Messages/MessageEmojiButton.tsx +++ b/website/src/components/Messages/MessageEmojiButton.tsx @@ -1,4 +1,4 @@ -import { Button } from "@chakra-ui/react"; +import { Button, ButtonProps } from "@chakra-ui/react"; import { useHasRole } from "src/hooks/auth/useHasRole"; import { MessageEmoji } from "src/types/Conversation"; import { emojiIcons } from "src/types/Emoji"; @@ -6,10 +6,11 @@ import { emojiIcons } from "src/types/Emoji"; interface MessageEmojiButtonProps { emoji: MessageEmoji; checked?: boolean; - onClick: () => void; + onClick?: () => void; userIsAuthor: boolean; disabled?: boolean; userReacted: boolean; + sx?: ButtonProps["sx"]; } export const MessageEmojiButton = ({ @@ -19,6 +20,7 @@ export const MessageEmojiButton = ({ userIsAuthor, disabled, userReacted, + sx, }: MessageEmojiButtonProps) => { const EmojiIcon = emojiIcons.get(emoji.name); const isAdmin = useHasRole("admin"); @@ -42,8 +44,9 @@ export const MessageEmojiButton = ({ ":hover": { backgroundColor: isDisabled ? "transparent" : undefined, }, + ...sx, }} - color={isDisabled ? "gray.500" : undefined} + color={isDisabled ? (checked ? "gray.700" : "gray.500") : undefined} > {showCount && {emoji.count}} diff --git a/website/src/components/Tasks/CreateTask.tsx b/website/src/components/Tasks/CreateTask.tsx index 448ad287..ee9b71c1 100644 --- a/website/src/components/Tasks/CreateTask.tsx +++ b/website/src/components/Tasks/CreateTask.tsx @@ -1,7 +1,7 @@ import { Box, Stack, Text, useColorModeValue } from "@chakra-ui/react"; import { useTranslation } from "next-i18next"; import { useState } from "react"; -import { MessageTable } from "src/components/Messages/MessageTable"; +import { MessageConversation } from "src/components/Messages/MessageConversation"; import { TrackedTextarea } from "src/components/Survey/TrackedTextarea"; import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards"; import { TaskSurveyProps } from "src/components/Tasks/Task"; @@ -44,7 +44,7 @@ export const CreateTask = ({ {task.type !== TaskType.initial_prompt && ( - + )} diff --git a/website/src/components/Tasks/EvaluateTask.tsx b/website/src/components/Tasks/EvaluateTask.tsx index 2f24e6f1..ee0ba881 100644 --- a/website/src/components/Tasks/EvaluateTask.tsx +++ b/website/src/components/Tasks/EvaluateTask.tsx @@ -1,6 +1,6 @@ import { Box, useColorModeValue } from "@chakra-ui/react"; import { useEffect, useState } from "react"; -import { MessageTable } from "src/components/Messages/MessageTable"; +import { MessageConversation } from "src/components/Messages/MessageConversation"; import { Sortable } from "src/components/Sortable/Sortable"; import { SurveyCard } from "src/components/Survey/SurveyCard"; import { TaskSurveyProps } from "src/components/Tasks/Task"; @@ -47,7 +47,7 @@ export const EvaluateTask = ({ - + - + { - const url = path || "/api/messages/user"; - const { data: messages, isLoading } = useSWR(url, get, { - refreshInterval: 2000, - }); - // TODO(#651): This box coloring and styling is used in multiple places. We - // should factor it into a common ui component. - const boxBgColor = useColorModeValue("white", "gray.700"); - const boxAccentColor = useColorModeValue("gray.200", "gray.900"); - - return ( - - {isLoading ? : } - - ); -}; - -export { UserMessagesCell }; diff --git a/website/src/components/UserMessagesCell/index.tsx b/website/src/components/UserMessagesCell/index.tsx deleted file mode 100644 index c32b1c1f..00000000 --- a/website/src/components/UserMessagesCell/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from "./UserMessagesCell"; diff --git a/website/src/components/UserTable.tsx b/website/src/components/UserTable.tsx index 5c51c585..385c9d06 100644 --- a/website/src/components/UserTable.tsx +++ b/website/src/components/UserTable.tsx @@ -8,18 +8,7 @@ import type { FetchUsersResponse, User } from "src/types/Users"; import useSWR from "swr"; import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable/DataTable"; - -interface Pagination { - /** - * The user's `display_name` used for pagination. - */ - cursor: string; - - /** - * The pagination direction. - */ - direction: "forward" | "back"; -} +import { useCursorPagination } from "./DataTable/useCursorPagination"; const columnHelper = createColumnHelper(); @@ -56,12 +45,13 @@ const columns: DataTableColumnDef[] = [ ]; export const UserTable = memo(function UserTable() { - const [pagination, setPagination] = useState({ cursor: "", direction: "forward" }); + const { pagination, resetCursor, toNextPage, toPreviousPage } = useCursorPagination(); const [filterValues, setFilterValues] = useState([]); const handleFilterValuesChange = (values: FilterItem[]) => { setFilterValues(values); - setPagination((old) => ({ ...old, cursor: "" })); + resetCursor(); }; + // Fetch and save the users. // This follows useSWR's recommendation for simple pagination: // https://swr.vercel.app/docs/pagination#when-to-use-useswr @@ -74,20 +64,6 @@ export const UserTable = memo(function UserTable() { } ); - const toPreviousPage = () => { - setPagination({ - cursor: data?.prev || "", - direction: "back", - }); - }; - - const toNextPage = () => { - setPagination({ - cursor: data?.next || "", - direction: "forward", - }); - }; - return ( @@ -95,8 +71,8 @@ export const UserTable = memo(function UserTable() { data={data?.items || []} columns={columns} caption="Users" - onNextClick={toNextPage} - onPreviousClick={toPreviousPage} + onNextClick={() => toNextPage(data)} + onPreviousClick={() => toPreviousPage(data)} disableNext={!data?.next} disablePrevious={!data?.prev} filterValues={filterValues} diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts index 2ec76c85..a3607323 100644 --- a/website/src/lib/oasst_api_client.ts +++ b/website/src/lib/oasst_api_client.ts @@ -1,4 +1,4 @@ -import type { EmojiOp, Message } from "src/types/Conversation"; +import type { EmojiOp, FetchUserMessagesCursorResponse, Message } from "src/types/Conversation"; import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard"; import type { AvailableTasks } from "src/types/Task"; import { FetchTrollBoardResponse, TrollboardTimeFrame } from "src/types/Trollboard"; @@ -264,11 +264,33 @@ export class OasstApiClient { return this.get(`/api/v1/users/${user_id}/messages`); } + async fetch_user_messages_cursor( + user_id: string, + { + direction, + cursor, + ...rest + }: { include_deleted?: boolean; max_count?: number; cursor?: string; direction: "forward" | "back"; desc?: boolean } + ) { + return this.get(`/api/v1/users/${user_id}/messages/cursor`, { + ...rest, + after: direction === "forward" ? cursor : undefined, + before: direction === "back" ? cursor : undefined, + }); + } + /** * Updates the backend's knowledge about the `user_id`. */ - async set_user_status(user_id: string, is_enabled: boolean, notes: string): Promise { - await this.put(`/api/v1/users/${user_id}?enabled=${is_enabled}¬es=${notes}`); + async set_user_status( + user_id: string, + is_enabled: boolean, + notes: string, + show_on_leaderboard: boolean + ): Promise { + await this.put( + `/api/v1/users/${user_id}?enabled=${is_enabled}¬es=${notes}&show_on_leaderboard=${show_on_leaderboard}` + ); } /** diff --git a/website/src/lib/routes.ts b/website/src/lib/routes.ts new file mode 100644 index 00000000..f85b5a31 --- /dev/null +++ b/website/src/lib/routes.ts @@ -0,0 +1,3 @@ +export const ROUTES = { + ADMIN_MESSAGE_DETAIL: (id: string) => `/admin/messages/${id}`, +}; diff --git a/website/src/pages/admin/manage_user/[id].tsx b/website/src/pages/admin/manage_user/[id].tsx index a68bca16..ee7bcc66 100644 --- a/website/src/pages/admin/manage_user/[id].tsx +++ b/website/src/pages/admin/manage_user/[id].tsx @@ -1,19 +1,39 @@ -import { Button, Card, CardBody, Container, FormControl, FormLabel, Input, Stack, useToast } from "@chakra-ui/react"; -import { InferGetServerSidePropsType } from "next"; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Button, + Card, + CardBody, + CardHeader, + Checkbox, + CircularProgress, + FormControl, + FormLabel, + Input, + Stack, + useToast, +} from "@chakra-ui/react"; +import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import Head from "next/head"; -import { useRouter } from "next/router"; -import { useSession } from "next-auth/react"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { useEffect } from "react"; import { useForm } from "react-hook-form"; +import { AdminArea } from "src/components/AdminArea"; +import { useCursorPagination } from "src/components/DataTable/useCursorPagination"; +import { JsonCard } from "src/components/JsonCard"; import { getAdminLayout } from "src/components/Layout"; +import { AdminMessageTable } from "src/components/Messages/AdminMessageTable"; import { Role, RoleSelect } from "src/components/RoleSelect"; -import { UserMessagesCell } from "src/components/UserMessagesCell"; -import { post } from "src/lib/api"; +import { get, post } from "src/lib/api"; import { userlessApiClient } from "src/lib/oasst_client_factory"; import prisma from "src/lib/prismadb"; +import { FetchUserMessagesCursorResponse } from "src/types/Conversation"; +import { User } from "src/types/Users"; +import useSWRImmutable from "swr/immutable"; import useSWRMutation from "swr/mutation"; - interface UserForm { user_id: string; id: string; @@ -21,33 +41,17 @@ interface UserForm { display_name: string; role: Role; notes: string; + show_on_leaderboard: boolean; } const ManageUser = ({ user }: InferGetServerSidePropsType) => { const toast = useToast(); - const router = useRouter(); - const { data: session, status } = useSession(); - - // Check when the user session is loaded and re-route if the user is not an - // admin. This follows the suggestion by NextJS for handling private pages: - // https://nextjs.org/docs/api-reference/next/router#usage - // - // All admin pages should use the same check and routing steps. - useEffect(() => { - if (status === "loading") { - return; - } - if (session?.user?.role === "admin") { - return; - } - router.push("/"); - }, [router, session, status]); // Trigger to let us update the user's role. Triggers a toast when complete. const { trigger } = useSWRMutation("/api/admin/update_user", post, { onSuccess: () => { toast({ - title: "User Role Updated", + title: "Updated user", status: "success", duration: 1000, isClosable: true, @@ -76,8 +80,8 @@ const ManageUser = ({ user }: InferGetServerSidePropsType - - + +
trigger(data))}> @@ -88,32 +92,97 @@ const ManageUser = ({ user }: InferGetServerSidePropsTypeDisplay Name - + Role - + Notes + + Show on leaderboard + + + + + + + Raw JSON + + + + + {user} + + +
-
- -
+ + + {`User's messages`} + + + + + +
+ ); }; +const UserMessageTable = ({ id }: { id: User["id"] }) => { + const { pagination, toNextPage, toPreviousPage } = useCursorPagination(); + const { data, error, isLoading } = useSWRImmutable( + `/api/admin/user_messages?user=${id}&cursor=${encodeURIComponent(pagination.cursor)}&direction=${ + pagination.direction + }`, + get, + { + keepPreviousData: true, + } + ); + + if (isLoading && !data) { + return ; + } + + if (error) { + return <>Unable to load messages.; + } + + return ( + toNextPage(data)} + onPreviousClick={() => toPreviousPage(data)} + > + ); +}; + /** * Fetch the user's data on the server side when rendering. */ -export async function getServerSideProps({ query, locale }) { - const backend_user = await userlessApiClient.fetch_user(query.id); +export const getServerSideProps: GetServerSideProps<{ user: User }, { id: string }> = async ({ + params, + locale = "en", +}) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const backend_user = await userlessApiClient.fetch_user(params!.id as string); + + if (!backend_user) { + return { + notFound: true, + }; + } const local_user = await prisma.user.findUnique({ where: { id: backend_user.id }, select: { @@ -130,7 +199,7 @@ export async function getServerSideProps({ query, locale }) { ...(await serverSideTranslations(locale, ["common"])), }, }; -} +}; ManageUser.getLayout = getAdminLayout; diff --git a/website/src/pages/api/admin/update_user.ts b/website/src/pages/api/admin/update_user.ts index c71159ad..13ee97f5 100644 --- a/website/src/pages/api/admin/update_user.ts +++ b/website/src/pages/api/admin/update_user.ts @@ -6,7 +6,7 @@ import prisma from "src/lib/prismadb"; * Update's the user's data in the database. Accessible only to admins. */ const handler = withRole("admin", async (req, res, token) => { - const { id, auth_method, user_id, notes, role } = req.body; + const { id, auth_method, user_id, notes, role, show_on_leaderboard } = req.body; const oasstApiClient = await createApiClient(token); // If the user is authorized by the web, update their role. @@ -17,7 +17,7 @@ const handler = withRole("admin", async (req, res, token) => { }); } // Tell the backend the user's enabled or not enabled status. - await oasstApiClient.set_user_status(user_id, role !== "banned", notes); + await oasstApiClient.set_user_status(user_id, role !== "banned", notes, show_on_leaderboard); res.status(200).json({}); }); diff --git a/website/src/pages/api/admin/user_messages.ts b/website/src/pages/api/admin/user_messages.ts index 0223e8e3..bca4d0f2 100644 --- a/website/src/pages/api/admin/user_messages.ts +++ b/website/src/pages/api/admin/user_messages.ts @@ -1,15 +1,25 @@ import { withRole } from "src/lib/auth"; import { createApiClient } from "src/lib/oasst_client_factory"; -import type { Message } from "src/types/Conversation"; + +const LIMIT = 10; /** * Returns the messages recorded by the backend for a user. */ const handler = withRole("admin", async (req, res, token) => { - const { user } = req.query; + const { cursor, direction, user } = req.query; + const oasstApiClient = await createApiClient(token); - const messages: Message[] = await oasstApiClient.fetch_user_messages(user as string); - res.status(200).json(messages); + + const response = await oasstApiClient.fetch_user_messages_cursor(user as string, { + include_deleted: true, + direction: direction as "back", + cursor: cursor as string, + max_count: LIMIT, + desc: true, + }); + + res.status(200).json(response); }); export default handler; diff --git a/website/src/pages/messages/index.tsx b/website/src/pages/messages/index.tsx index 4cae792a..6c1d6f7e 100644 --- a/website/src/pages/messages/index.tsx +++ b/website/src/pages/messages/index.tsx @@ -3,7 +3,7 @@ import Head from "next/head"; import { useTranslation } from "next-i18next"; import { useCookies } from "react-cookie"; import { getDashboardLayout } from "src/components/Layout"; -import { MessageTable } from "src/components/Messages/MessageTable"; +import { MessageConversation } from "src/components/Messages/MessageConversation"; import { get } from "src/lib/api"; import useSWRImmutable from "swr/immutable"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; @@ -38,7 +38,7 @@ const MessagesDashboard = () => { borderRadius="xl" className="p-3 sm:p-4 shadow-sm" > - {messages ? : } + {messages ? : } @@ -52,7 +52,11 @@ const MessagesDashboard = () => { borderRadius="xl" className="p-6 shadow-sm" > - {userMessages ? : } + {userMessages ? ( + + ) : ( + + )} diff --git a/website/src/styles/Theme/components/Table.ts b/website/src/styles/Theme/components/Table.ts new file mode 100644 index 00000000..ebf9f9df --- /dev/null +++ b/website/src/styles/Theme/components/Table.ts @@ -0,0 +1,20 @@ +import { tableAnatomy } from "@chakra-ui/anatomy"; +import { createMultiStyleConfigHelpers } from "@chakra-ui/react"; + +const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(tableAnatomy.keys); + +export const tableTheme = defineMultiStyleConfig({ + variants: { + simple: definePartsStyle(({ colorMode }) => { + const isLightMode = colorMode === "light"; + return { + td: { + borderColor: isLightMode ? "gray.100" : "gray.800", + }, + th: { + borderColor: isLightMode ? "gray.100" : "gray.800", + }, + }; + }), + }, +}); diff --git a/website/src/styles/Theme/index.ts b/website/src/styles/Theme/index.ts index 91d54088..2fde5a8c 100644 --- a/website/src/styles/Theme/index.ts +++ b/website/src/styles/Theme/index.ts @@ -5,6 +5,7 @@ import { colors } from "./colors"; import { badgeTheme } from "./components/Badge"; import { cardTheme } from "./components/Card"; import { containerTheme } from "./components/Container"; +import { tableTheme } from "./components/Table"; const config: ThemeConfig = { initialColorMode: "light", @@ -16,6 +17,7 @@ const components = { Badge: badgeTheme, Container: containerTheme, Card: cardTheme, + Table: tableTheme, }; const breakpoints = { diff --git a/website/src/types/Conversation.ts b/website/src/types/Conversation.ts index 89713107..57a9efbb 100644 --- a/website/src/types/Conversation.ts +++ b/website/src/types/Conversation.ts @@ -32,3 +32,11 @@ export interface Message extends MessageEmojis { export interface Conversation { messages: Message[]; } + +export type FetchUserMessagesCursorResponse = { + next?: string; + prev?: string; + sort_key: string; + items: Message[]; + order: "asc" | "desc"; +}; diff --git a/website/src/types/Users.ts b/website/src/types/Users.ts index 5100dec4..6dd65e28 100644 --- a/website/src/types/Users.ts +++ b/website/src/types/Users.ts @@ -75,11 +75,11 @@ export interface BackendUser extends BackendUserCore { /** * An expanded User for the web. */ -export interface User extends BackendUser { +export interface User extends BackendUser { /** * The user's roles within the webapp. */ - role: string; + role: TRole; } export type FetchUsersParams = { diff --git a/website/src/types/utils.ts b/website/src/types/utils.ts index 82c35036..f3ac0af3 100644 --- a/website/src/types/utils.ts +++ b/website/src/types/utils.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // https://github.com/ts-essentials/ts-essentials/blob/25cae45c162f8784e3cdae8f43783d0c66370a57/lib/types.ts#L437 // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ElementOf = T extends readonly (infer ET)[] ? ET : never; +type AnyRecord = Record; +type KeyofBase = keyof any; + +export type AnyArray = Array | ReadonlyArray; + +export type StrictOmit = T extends AnyArray ? never : Omit;