feat: migrate from chakra 2 to chakra 3 (#500)

* feat: separate page into different component, greatly improving the loading and reactivity

* fix: various fixes

* feat: migrate to Chakra UI v3 - update theme, fix deprecated props

- Add whiteAlpha color palette with semantic tokens
- Update button recipe with fontWeight 600 and hover states
- Move Poppins font from theme to HTML tag className
- Fix deprecated props: isDisabled→disabled, align→alignItems/textAlign
- Remove button.css as styles are now handled by Chakra v3

* fix: complete Chakra UI v3 deprecated prop migrations

- Replace all isDisabled with disabled
- Replace all isChecked with checked
- Replace all isLoading with loading
- Replace all isOpen with open
- Replace all noOfLines with lineClamp
- Replace all align with alignItems on Flex/Stack components
- Replace all justify with justifyContent on Flex/Stack components
- Update temporary Select components to use new prop names
- Update REFACTOR2.md with completion status

* fix: add value prop to Menu.Item for proper hover states in Chakra v3

* fix: update browse page components for Chakra UI v3 compatibility

- Fix FilterSidebar status filter styling and prop usage
- Update Pagination component to use new Chakra v3 props and structure
- Refactor TranscriptTable to use modern Chakra patterns
- Clean up browse page layout and props
- Remove unused import from transcripts API view
- Enhance theme with additional semantic color tokens

* fix: polish browse page UI for Chakra v3

- Add rounded corners to FilterSidebar
- Adjust responsive breakpoints from md to lg for table/card view
- Add consistent font weights to table headers
- Improve card view typography and spacing
- Fix padding and margins for better mobile experience
- Remove unused table recipe from theme

* fix: padding

* fix: rework transcript page

* fix: more tidy layout for topic

* fix: share and privacy using chakra3 select

* fix: fix share and privacy select, now working, with closing dialog

* fix: complete Chakra UI v3 migration for share components and fix all TypeScript errors

- Refactor shareZulip.tsx to integrate modal content directly
- Replace react-select-search with Chakra UI v3 Select components using collection pattern
- Convert all Checkbox components to use v3 composable structure (Checkbox.Root, etc.)
- Fix Card components to use Card.Root and Card.Body
- Replace deprecated textColor prop with color prop
- Update Menu components to use v3 namespace pattern (Menu.Root, Menu.Trigger, etc.)
- Remove unused AlertDialog imports
- Fix useDisclosure hook changes (isOpen -> open)
- Replace UnorderedList with List.Root and ListItem with List.Item
- Fix Skeleton components by removing isLoaded prop and using conditional rendering
- Update Button variants to valid v3 options
- Fix Spinner props (remove thickness, speed, emptyColor)
- Update toast API to use custom toaster component
- Fix Progress components and FormControl to Field.Root
- Update Alert to use compound component pattern
- Remove shareModal.tsx file after integration

* fix: bring back topic list

* fix: normalize menu item

* fix: migrate rooms page to Chakra UI v3 pattern

- Updated layout to match browse page with Flex container and proper spacing
- Migrated add/edit room modal from custom HTML to Chakra UI v3 Dialog component
- Replaced all Select components with Chakra UI v3 Select using createListCollection
- Replaced FormControl/FormLabel/FormHelperText with Field.Root/Field.Label/Field.HelperText
- Removed inline styles and used Chakra props (mr={2} instead of style={{ marginRight: "8px" }})
- Fixed TypeScript interfaces removing OptionBase extension
- Fixed theme.ts accordion anatomy import issue

* refactor: convert rooms list to table view with responsive design

- Create RoomTable component for desktop view showing room details in columns
- Create RoomCards component for mobile/tablet responsive view
- Refactor RoomList to use table/card components based on screen size
- Display Zulip configuration, room size, and recording settings in table
- Remove unused RoomItem component
- Import Room type from API for proper typing

* refactor: extract RoomActionsMenu component to eliminate duplication

- Create RoomActionsMenu component for consistent room action menus
- Update RoomCards and RoomTable to use the new shared component
- Remove duplicated menu code from both components

* feat: add icons to TranscriptActionsMenu for consistency

- Add FaTrash icon for Delete action with red color
- Add FaArrowsRotate icon for Reprocess action
- Matches the pattern established in RoomActionsMenu

* refactor: update icons from Font Awesome to Lucide React

- Replace FaEllipsisVertical with LuMenu in menu triggers
- Replace FaLink with LuLink for copy URL buttons
- Replace FaPencil with LuPen for edit actions
- Replace FaTrash with LuTrash for delete actions
- Replace FaArrowsRotate with LuRotateCw for reprocess action
- Consistent icon library usage across all components

* refactor: little pass on the icons

* fix: lu icon

* fix: primary for button

* fix: recording page with mic selection

* fix: also fix duration

* fix: use combobox for share zulip

* fix: use proper theming for button, variant was not recognized

* fix: room actions menu

* fix: remove other variant primary left.
This commit is contained in:
2025-07-21 16:16:12 -06:00
committed by GitHub
parent d77b5611f8
commit 901a239952
66 changed files with 3061 additions and 2437 deletions

View File

@@ -1,13 +1,6 @@
import React from "react";
import {
Button,
AlertDialog,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogBody,
AlertDialogFooter,
} from "@chakra-ui/react";
import { Button } from "@chakra-ui/react";
// import { Dialog } from "@chakra-ui/react";
interface DeleteTranscriptDialogProps {
isOpen: boolean;
@@ -22,30 +15,34 @@ export default function DeleteTranscriptDialog({
onConfirm,
cancelRef,
}: DeleteTranscriptDialogProps) {
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
// Temporarily return null to fix import issues
return null;
/* return (
<Dialog.Root
open={isOpen}
onOpenChange={(e) => !e.open && onClose()}
initialFocusEl={() => cancelRef.current}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header fontSize="lg" fontWeight="bold">
Delete Transcript
</AlertDialogHeader>
<AlertDialogBody>
</Dialog.Header>
<Dialog.Body>
Are you sure? You can't undo this action afterwards.
</AlertDialogBody>
<AlertDialogFooter>
</Dialog.Body>
<Dialog.Footer>
<Button ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button colorScheme="red" onClick={onConfirm} ml={3}>
<Button colorPalette="red" onClick={onConfirm} ml={3}>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
); */
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Box, Stack, Link, Heading, Divider } from "@chakra-ui/react";
import { Box, Stack, Link, Heading } from "@chakra-ui/react";
import NextLink from "next/link";
import { Room, SourceKind } from "../../../api";
@@ -20,24 +20,23 @@ export default function FilterSidebar({
const sharedRooms = rooms.filter((room) => room.is_shared);
return (
<Box w={{ base: "full", md: "300px" }} p={4} bg="gray.100">
<Stack spacing={3}>
<Box w={{ base: "full", md: "300px" }} p={4} bg="gray.100" rounded="md">
<Stack gap={3}>
<Link
as={NextLink}
href="#"
onClick={() => onFilterChange(null, "")}
color={selectedSourceKind === null ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === null ? "bold" : "normal"}
>
All Transcripts
</Link>
<Divider />
<Box borderBottomWidth="1px" my={2} />
{myRooms.length > 0 && (
<>
<Heading size="sm">My Rooms</Heading>
<Heading size="md">My Rooms</Heading>
{myRooms.map((room) => (
<Link
@@ -50,7 +49,6 @@ export default function FilterSidebar({
? "blue.500"
: "gray.600"
}
_hover={{ color: "blue.300" }}
fontWeight={
selectedSourceKind === "room" && selectedRoomId === room.id
? "bold"
@@ -66,7 +64,7 @@ export default function FilterSidebar({
{sharedRooms.length > 0 && (
<>
<Heading size="sm">Shared Rooms</Heading>
<Heading size="md">Shared Rooms</Heading>
{sharedRooms.map((room) => (
<Link
@@ -79,7 +77,6 @@ export default function FilterSidebar({
? "blue.500"
: "gray.600"
}
_hover={{ color: "blue.300" }}
fontWeight={
selectedSourceKind === "room" && selectedRoomId === room.id
? "bold"
@@ -93,7 +90,7 @@ export default function FilterSidebar({
</>
)}
<Divider />
<Box borderBottomWidth="1px" my={2} />
<Link
as={NextLink}
href="#"

View File

@@ -0,0 +1,47 @@
import React from "react";
import { Pagination, IconButton, ButtonGroup } from "@chakra-ui/react";
import { LuChevronLeft, LuChevronRight } from "react-icons/lu";
type PaginationProps = {
page: number;
setPage: (page: number) => void;
total: number;
size: number;
};
export default function PaginationComponent(props: PaginationProps) {
const { page, setPage, total, size } = props;
const totalPages = Math.ceil(total / size);
if (totalPages <= 1) return null;
return (
<Pagination.Root
count={total}
pageSize={size}
page={page}
onPageChange={(details) => setPage(details.page)}
style={{ display: "flex", justifyContent: "center" }}
>
<ButtonGroup variant="ghost" size="xs">
<Pagination.PrevTrigger asChild>
<IconButton>
<LuChevronLeft />
</IconButton>
</Pagination.PrevTrigger>
<Pagination.Items
render={(page) => (
<IconButton variant={{ base: "ghost", _selected: "solid" }}>
{page.value}
</IconButton>
)}
/>
<Pagination.NextTrigger asChild>
<IconButton>
<LuChevronRight />
</IconButton>
</Pagination.NextTrigger>
</ButtonGroup>
</Pagination.Root>
);
}

View File

@@ -19,7 +19,7 @@ export default function SearchBar({ onSearch }: SearchBarProps) {
};
return (
<Flex mb={4} alignItems="center">
<Flex alignItems="center">
<Input
placeholder="Search transcriptions..."
value={searchInputValue}

View File

@@ -1,13 +1,6 @@
import React from "react";
import {
Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
Icon,
} from "@chakra-ui/react";
import { FaEllipsisVertical } from "react-icons/fa6";
import { IconButton, Icon, Menu } from "@chakra-ui/react";
import { LuMenu, LuTrash, LuRotateCw } from "react-icons/lu";
interface TranscriptActionsMenuProps {
transcriptId: string;
@@ -21,19 +14,25 @@ export default function TranscriptActionsMenu({
onReprocess,
}: TranscriptActionsMenuProps) {
return (
<Menu closeOnSelect={true} isLazy={true}>
<MenuButton
as={IconButton}
icon={<Icon as={FaEllipsisVertical} />}
variant="outline"
aria-label="Options"
/>
<MenuList>
<MenuItem onClick={(e) => onDelete(transcriptId)(e)}>Delete</MenuItem>
<MenuItem onClick={(e) => onReprocess(transcriptId)(e)}>
Reprocess
</MenuItem>
</MenuList>
</Menu>
<Menu.Root closeOnSelect={true} lazyMount={true}>
<Menu.Trigger asChild>
<IconButton aria-label="Options" size="sm" variant="ghost">
<LuMenu />
</IconButton>
</Menu.Trigger>
<Menu.Positioner>
<Menu.Content>
<Menu.Item
value="reprocess"
onClick={(e) => onReprocess(transcriptId)(e)}
>
<LuRotateCw /> Reprocess
</Menu.Item>
<Menu.Item value="delete" onClick={(e) => onDelete(transcriptId)(e)}>
<LuTrash /> Delete
</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Menu.Root>
);
}

View File

@@ -20,7 +20,7 @@ export default function TranscriptCards({
loading,
}: TranscriptCardsProps) {
return (
<Box display={{ base: "block", md: "none" }} position="relative">
<Box display={{ base: "block", lg: "none" }} position="relative">
{loading && (
<Flex
position="absolute"
@@ -33,7 +33,7 @@ export default function TranscriptCards({
align="center"
justify="center"
>
<Spinner size="xl" color="gray.700" thickness="4px" />
<Spinner size="xl" color="gray.700" />
</Flex>
)}
<Box
@@ -41,9 +41,15 @@ export default function TranscriptCards({
pointerEvents={loading ? "none" : "auto"}
transition="opacity 0.2s ease-in-out"
>
<Stack spacing={2}>
<Stack gap={2}>
{transcripts.map((item) => (
<Box key={item.id} borderWidth={1} p={4} borderRadius="md">
<Box
key={item.id}
borderWidth={1}
p={4}
borderRadius="md"
fontSize="sm"
>
<Flex justify="space-between" alignItems="flex-start" gap="2">
<Box>
<TranscriptStatusIcon status={item.status} />
@@ -52,7 +58,7 @@ export default function TranscriptCards({
<Link
as={NextLink}
href={`/transcripts/${item.id}`}
fontWeight="bold"
fontWeight="600"
display="block"
>
{item.title || "Unnamed Transcript"}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Icon, Tooltip } from "@chakra-ui/react";
import { Icon, Box } from "@chakra-ui/react";
import {
FaCheck,
FaTrash,
@@ -18,43 +18,33 @@ export default function TranscriptStatusIcon({
switch (status) {
case "ended":
return (
<Tooltip label="Processing done">
<span>
<Icon color="green" as={FaCheck} />
</span>
</Tooltip>
<Box as="span" title="Processing done">
<Icon color="green" as={FaCheck} />
</Box>
);
case "error":
return (
<Tooltip label="Processing error">
<span>
<Icon color="red.500" as={FaTrash} />
</span>
</Tooltip>
<Box as="span" title="Processing error">
<Icon color="red.500" as={FaTrash} />
</Box>
);
case "idle":
return (
<Tooltip label="New meeting, no recording">
<span>
<Icon color="yellow.500" as={FaStar} />
</span>
</Tooltip>
<Box as="span" title="New meeting, no recording">
<Icon color="yellow.500" as={FaStar} />
</Box>
);
case "processing":
return (
<Tooltip label="Processing in progress">
<span>
<Icon color="gray.500" as={FaGear} />
</span>
</Tooltip>
<Box as="span" title="Processing in progress">
<Icon color="gray.500" as={FaGear} />
</Box>
);
case "recording":
return (
<Tooltip label="Recording in progress">
<span>
<Icon color="blue.500" as={FaMicrophone} />
</span>
</Tooltip>
<Box as="span" title="Recording in progress">
<Icon color="blue.500" as={FaMicrophone} />
</Box>
);
default:
return null;

View File

@@ -1,16 +1,5 @@
import React from "react";
import {
Box,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Link,
Flex,
Spinner,
} from "@chakra-ui/react";
import { Box, Table, Link, Flex, Spinner } from "@chakra-ui/react";
import NextLink from "next/link";
import { GetTranscriptMinimal } from "../../../api";
import { formatTimeMs, formatLocalDate } from "../../../lib/time";
@@ -31,7 +20,7 @@ export default function TranscriptTable({
loading,
}: TranscriptTableProps) {
return (
<Box display={{ base: "none", md: "block" }} position="relative">
<Box display={{ base: "none", lg: "block" }} position="relative">
{loading && (
<Flex
position="absolute"
@@ -39,12 +28,10 @@ export default function TranscriptTable({
left={0}
right={0}
bottom={0}
bg="rgba(255, 255, 255, 0.8)"
zIndex={10}
align="center"
justify="center"
>
<Spinner size="xl" color="gray.700" thickness="4px" />
<Spinner size="xl" color="gray.700" />
</Flex>
)}
<Box
@@ -52,47 +39,60 @@ export default function TranscriptTable({
pointerEvents={loading ? "none" : "auto"}
transition="opacity 0.2s ease-in-out"
>
<Table colorScheme="gray">
<Thead>
<Tr>
<Th pl={12} width="400px">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeader
width="16px"
fontWeight="600"
></Table.ColumnHeader>
<Table.ColumnHeader width="400px" fontWeight="600">
Transcription Title
</Th>
<Th width="150px">Source</Th>
<Th width="200px">Date</Th>
<Th width="100px">Duration</Th>
<Th width="50px"></Th>
</Tr>
</Thead>
<Tbody>
</Table.ColumnHeader>
<Table.ColumnHeader width="150px" fontWeight="600">
Source
</Table.ColumnHeader>
<Table.ColumnHeader width="200px" fontWeight="600">
Date
</Table.ColumnHeader>
<Table.ColumnHeader width="100px" fontWeight="600">
Duration
</Table.ColumnHeader>
<Table.ColumnHeader
width="50px"
fontWeight="600"
></Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{transcripts.map((item) => (
<Tr key={item.id}>
<Td>
<Flex alignItems="start">
<TranscriptStatusIcon status={item.status} />
<Link as={NextLink} href={`/transcripts/${item.id}`} ml={2}>
{item.title || "Unnamed Transcript"}
</Link>
</Flex>
</Td>
<Td>
<Table.Row key={item.id}>
<Table.Cell>
<TranscriptStatusIcon status={item.status} />
</Table.Cell>
<Table.Cell>
<Link as={NextLink} href={`/transcripts/${item.id}`}>
{item.title || "Unnamed Transcript"}
</Link>
</Table.Cell>
<Table.Cell>
{item.source_kind === "room"
? item.room_name
: item.source_kind}
</Td>
<Td>{formatLocalDate(item.created_at)}</Td>
<Td>{formatTimeMs(item.duration)}</Td>
<Td>
</Table.Cell>
<Table.Cell>{formatLocalDate(item.created_at)}</Table.Cell>
<Table.Cell>{formatTimeMs(item.duration)}</Table.Cell>
<Table.Cell>
<TranscriptActionsMenu
transcriptId={item.id}
onDelete={onDelete}
onReprocess={onReprocess}
/>
</Td>
</Tr>
</Table.Cell>
</Table.Row>
))}
</Tbody>
</Table>
</Table.Body>
</Table.Root>
</Box>
</Box>
);