mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
Merge pull request #335 from Monadical-SAS/sara/UI-improvements
Sara/UI improvements & fix transcript deletion
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -188,7 +190,8 @@ class Transcript(BaseModel):
|
|||||||
return [participant.model_dump(mode=mode) for participant in self.participants]
|
return [participant.model_dump(mode=mode) for participant in self.participants]
|
||||||
|
|
||||||
def unlink(self):
|
def unlink(self):
|
||||||
self.data_path.unlink(missing_ok=True)
|
if os.path.exists(self.data_path) and os.path.isdir(self.data_path):
|
||||||
|
shutil.rmtree(self.data_path)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data_path(self):
|
def data_path(self):
|
||||||
|
|||||||
@@ -118,3 +118,16 @@ async def test_transcript_audio_download_range_with_seek(
|
|||||||
assert response.status_code == 206
|
assert response.status_code == 206
|
||||||
assert response.headers["content-type"] == content_type
|
assert response.headers["content-type"] == content_type
|
||||||
assert response.headers["content-range"].startswith("bytes 100-")
|
assert response.headers["content-range"].startswith("bytes 100-")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_transcript_delete_with_audio(fake_transcript):
|
||||||
|
from reflector.app import app
|
||||||
|
|
||||||
|
ac = AsyncClient(app=app, base_url="http://test/v1")
|
||||||
|
response = await ac.delete(f"/transcripts/{fake_transcript.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "ok"
|
||||||
|
|
||||||
|
response = await ac.get(f"/transcripts/{fake_transcript.id}")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|||||||
@@ -1,88 +1,288 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { GetTranscript } from "../../api";
|
import { GetTranscript } from "../../api";
|
||||||
import { Title } from "../../lib/textComponents";
|
|
||||||
import Pagination from "./pagination";
|
import Pagination from "./pagination";
|
||||||
import Link from "next/link";
|
import NextLink from "next/link";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FaGear } from "react-icons/fa6";
|
||||||
import { faGear } from "@fortawesome/free-solid-svg-icons";
|
import { FaCheck, FaTrash, FaStar, FaMicrophone } from "react-icons/fa";
|
||||||
|
import { MdError } from "react-icons/md";
|
||||||
import useTranscriptList from "../transcripts/useTranscriptList";
|
import useTranscriptList from "../transcripts/useTranscriptList";
|
||||||
|
import { formatTime } from "../../lib/time";
|
||||||
|
import useApi from "../../lib/useApi";
|
||||||
|
import { useError } from "../../(errors)/errorContext";
|
||||||
|
import { FaEllipsisVertical } from "react-icons/fa6";
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Spinner,
|
||||||
|
Heading,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Link,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
Grid,
|
||||||
|
Divider,
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverCloseButton,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverFooter,
|
||||||
|
IconButton,
|
||||||
|
Spacer,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogFooter,
|
||||||
|
keyframes,
|
||||||
|
Tooltip,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { PlusSquareIcon } from "@chakra-ui/icons";
|
||||||
|
import { ExpandableText } from "../../lib/expandableText";
|
||||||
|
// import { useFiefUserinfo } from "@fief/fief/nextjs/react";
|
||||||
|
|
||||||
export default function TranscriptBrowser() {
|
export default function TranscriptBrowser() {
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const { loading, response } = useTranscriptList(page);
|
const { loading, response, refetch } = useTranscriptList(page);
|
||||||
|
const [deletionLoading, setDeletionLoading] = useState(false);
|
||||||
|
const api = useApi();
|
||||||
|
const { setError } = useError();
|
||||||
|
const cancelRef = React.useRef(null);
|
||||||
|
const [transcriptToDeleteId, setTranscriptToDeleteId] =
|
||||||
|
React.useState<string>();
|
||||||
|
const [deletedItemIds, setDeletedItemIds] = React.useState<string[]>();
|
||||||
|
|
||||||
|
// Todo: fief add name field to userinfo
|
||||||
|
// const user = useFiefUserinfo();
|
||||||
|
// console.log(user);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDeletedItemIds([]);
|
||||||
|
}, [page, response]);
|
||||||
|
|
||||||
|
if (loading && !response)
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" align="center" justify="center" h="100%">
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!loading && !response)
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" align="center" justify="center" h="100%">
|
||||||
|
<Text>
|
||||||
|
No transcripts found, but you can
|
||||||
|
<Link href="/transcripts/new" className="underline">
|
||||||
|
record a meeting
|
||||||
|
</Link>
|
||||||
|
to get started.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
const onCloseDeletion = () => setTranscriptToDeleteId(undefined);
|
||||||
|
|
||||||
|
const handleDeleteTranscript = (transcriptToDeleteId) => (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (api && !deletionLoading) {
|
||||||
|
setDeletionLoading(true);
|
||||||
|
api
|
||||||
|
.v1TranscriptDelete(transcriptToDeleteId)
|
||||||
|
.then(() => {
|
||||||
|
setDeletionLoading(false);
|
||||||
|
refetch();
|
||||||
|
onCloseDeletion();
|
||||||
|
setDeletedItemIds((deletedItemIds) => [
|
||||||
|
deletedItemIds,
|
||||||
|
...transcriptToDeleteId,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setDeletionLoading(false);
|
||||||
|
setError(err, "There was an error deleting the transcript");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-layout-topbar gap-2 lg:gap-4 h-full max-h-full">
|
<Flex
|
||||||
<div className="flex flex-row gap-2 items-center">
|
maxW="container.xl"
|
||||||
<Title className="mb-5 mt-5 flex-1">Past transcripts</Title>
|
flexDir="column"
|
||||||
|
margin="auto"
|
||||||
|
gap={2}
|
||||||
|
overflowY="scroll"
|
||||||
|
maxH="100%"
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
flexDir="row"
|
||||||
|
justify="flex-end"
|
||||||
|
align="center"
|
||||||
|
flexWrap={"wrap-reverse"}
|
||||||
|
>
|
||||||
|
{/* <Heading>{user?.fields?.name}'s Meetings</Heading> */}
|
||||||
|
<Heading>Your Meetings</Heading>
|
||||||
|
{loading || (deletionLoading && <Spinner></Spinner>)}
|
||||||
|
|
||||||
|
<Spacer />
|
||||||
<Pagination
|
<Pagination
|
||||||
page={page}
|
page={page}
|
||||||
setPage={setPage}
|
setPage={setPage}
|
||||||
total={response?.total || 0}
|
total={response?.total || 0}
|
||||||
size={response?.size || 0}
|
size={response?.size || 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading && (
|
<Button colorScheme="blue" rightIcon={<PlusSquareIcon />}>
|
||||||
<div className="full-screen flex flex-col items-center justify-center">
|
New Meeting
|
||||||
<FontAwesomeIcon
|
</Button>
|
||||||
icon={faGear}
|
</Flex>
|
||||||
className="animate-spin-slow h-14 w-14 md:h-20 md:w-20"
|
|
||||||
|
<Grid
|
||||||
|
templateColumns={{
|
||||||
|
base: "repeat(1, 1fr)",
|
||||||
|
md: "repeat(2, 1fr)",
|
||||||
|
lg: "repeat(3, 1fr)",
|
||||||
|
}}
|
||||||
|
gap={{
|
||||||
|
base: 2,
|
||||||
|
lg: 4,
|
||||||
|
}}
|
||||||
|
maxH="100%"
|
||||||
|
overflowY={"scroll"}
|
||||||
|
mb="4"
|
||||||
|
>
|
||||||
|
{response?.items
|
||||||
|
.filter((i) => !deletedItemIds?.includes(i.id))
|
||||||
|
.map((item: GetTranscript) => (
|
||||||
|
<Card key={item.id} border="gray.light" variant="outline">
|
||||||
|
<CardBody>
|
||||||
|
<Flex align={"center"} ml="-6px">
|
||||||
|
{item.status == "ended" && (
|
||||||
|
<Tooltip label="Processing done">
|
||||||
|
<span>
|
||||||
|
<Icon color="green" as={FaCheck} mr="2" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{item.status == "error" && (
|
||||||
|
<Tooltip label="Processing error">
|
||||||
|
<span>
|
||||||
|
<Icon color="red.primary" as={MdError} mr="2" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{item.status == "idle" && (
|
||||||
|
<Tooltip label="New meeting, no recording">
|
||||||
|
<span>
|
||||||
|
<Icon color="yellow.500" as={FaStar} mr="2" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{item.status == "processing" && (
|
||||||
|
<Tooltip label="Processing in progress">
|
||||||
|
<span>
|
||||||
|
<Icon
|
||||||
|
color="grey.primary"
|
||||||
|
as={FaGear}
|
||||||
|
mr="2"
|
||||||
|
transition={"all 2s ease"}
|
||||||
|
transform={"rotate(0deg)"}
|
||||||
|
_hover={{ transform: "rotate(360deg)" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</span>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!loading && !response && (
|
{item.status == "recording" && (
|
||||||
<div className="text-gray-500">
|
<Tooltip label="Recording in progress">
|
||||||
No transcripts found, but you can
|
<span>
|
||||||
<Link href="/transcripts/new" className="underline">
|
<Icon color="blue.primary" as={FaMicrophone} mr="2" />
|
||||||
record a meeting
|
</span>
|
||||||
</Link>
|
</Tooltip>
|
||||||
to get started.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div /** center and max 900px wide */ className="overflow-y-scroll">
|
<Heading size="md">
|
||||||
<div className="grid grid-cols-1 gap-2 lg:gap-4 h-full mx-auto max-w-[900px]">
|
|
||||||
{response?.items.map((item: GetTranscript) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="flex flex-col bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex flex-row gap-2 items-start">
|
|
||||||
<Link
|
<Link
|
||||||
|
as={NextLink}
|
||||||
href={`/transcripts/${item.id}`}
|
href={`/transcripts/${item.id}`}
|
||||||
className="text-1xl flex-1 pl-0 hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
|
noOfLines={2}
|
||||||
>
|
>
|
||||||
{item.title || item.name}
|
{item.title || item.name || "Unamed Transcript"}
|
||||||
</Link>
|
</Link>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
{item.locked ? (
|
<Spacer />
|
||||||
<div className="inline-block bg-red-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
<Menu closeOnSelect={false}>
|
||||||
Locked
|
<MenuButton
|
||||||
</div>
|
as={IconButton}
|
||||||
) : (
|
icon={<FaEllipsisVertical />}
|
||||||
<></>
|
aria-label="actions"
|
||||||
)}
|
/>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem
|
||||||
|
disabled={deletionLoading}
|
||||||
|
onClick={() => setTranscriptToDeleteId(item.id)}
|
||||||
|
icon={<FaTrash color={"red.500"} />}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
<AlertDialog
|
||||||
|
isOpen={transcriptToDeleteId === item.id}
|
||||||
|
leastDestructiveRef={cancelRef}
|
||||||
|
onClose={onCloseDeletion}
|
||||||
|
>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
Delete{" "}
|
||||||
|
{item.title || item.name || "Unamed Transcript"}
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
{item.source_language ? (
|
<AlertDialogBody>
|
||||||
<div className="inline-block bg-blue-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
Are you sure? You can't undo this action
|
||||||
{item.source_language}
|
afterwards.
|
||||||
</div>
|
</AlertDialogBody>
|
||||||
) : (
|
|
||||||
<></>
|
<AlertDialogFooter>
|
||||||
)}
|
<Button ref={cancelRef} onClick={onCloseDeletion}>
|
||||||
</div>
|
Cancel
|
||||||
<div className="text-xs text-gray-700">
|
</Button>
|
||||||
{new Date(item.created_at).toLocaleDateString("en-US")}
|
<Button
|
||||||
</div>
|
colorScheme="red"
|
||||||
<div className="text-sm">{item.short_summary}</div>
|
onClick={handleDeleteTranscript(item.id)}
|
||||||
</div>
|
ml={3}
|
||||||
</div>
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Flex>
|
||||||
|
<Stack mt="6" spacing="3">
|
||||||
|
<Text fontSize="small">
|
||||||
|
{new Date(item.created_at).toLocaleString("en-US")}
|
||||||
|
{"\u00A0"}-{"\u00A0"}
|
||||||
|
{formatTime(Math.floor(item.duration / 1000))}
|
||||||
|
</Text>
|
||||||
|
<ExpandableText noOfLines={5}>
|
||||||
|
{item.short_summary}
|
||||||
|
</ExpandableText>
|
||||||
|
</Stack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Flex>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { Button, Flex, IconButton } from "@chakra-ui/react";
|
||||||
|
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
|
||||||
|
|
||||||
type PaginationProps = {
|
type PaginationProps = {
|
||||||
page: number;
|
page: number;
|
||||||
setPage: (page: number) => void;
|
setPage: (page: number) => void;
|
||||||
@@ -36,40 +39,41 @@ export default function Pagination(props: PaginationProps) {
|
|||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-4 my-4">
|
<Flex justify="center" align="center" gap="2" mx="2">
|
||||||
<button
|
<IconButton
|
||||||
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
isRound={true}
|
||||||
canGoPrevious ? "text-gray-500" : "text-gray-300"
|
variant="text"
|
||||||
}`}
|
color={!canGoPrevious ? "gray" : "dark"}
|
||||||
|
mb="1"
|
||||||
|
icon={<FaChevronLeft />}
|
||||||
onClick={() => handlePageChange(page - 1)}
|
onClick={() => handlePageChange(page - 1)}
|
||||||
disabled={!canGoPrevious}
|
disabled={!canGoPrevious}
|
||||||
>
|
aria-label="Previous page"
|
||||||
<i className="fa fa-chevron-left"><</i>
|
/>
|
||||||
</button>
|
|
||||||
|
|
||||||
{pageNumbers.map((pageNumber) => (
|
{pageNumbers.map((pageNumber) => (
|
||||||
<button
|
<Button
|
||||||
key={pageNumber}
|
key={pageNumber}
|
||||||
className={`w-10 h-10 rounded-full p-2 border ${
|
variant="text"
|
||||||
page === pageNumber ? "border-gray-600" : "border-gray-300"
|
color={page === pageNumber ? "gray" : "dark"}
|
||||||
} rounded`}
|
|
||||||
onClick={() => handlePageChange(pageNumber)}
|
onClick={() => handlePageChange(pageNumber)}
|
||||||
|
disabled={page === pageNumber}
|
||||||
>
|
>
|
||||||
{pageNumber}
|
{pageNumber}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<button
|
<IconButton
|
||||||
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
isRound={true}
|
||||||
canGoNext ? "text-gray-500" : "text-gray-300"
|
variant="text"
|
||||||
}`}
|
color={!canGoNext ? "gray" : "dark"}
|
||||||
|
icon={<FaChevronRight />}
|
||||||
|
mb="1"
|
||||||
onClick={() => handlePageChange(page + 1)}
|
onClick={() => handlePageChange(page + 1)}
|
||||||
disabled={!canGoNext}
|
disabled={!canGoNext}
|
||||||
>
|
aria-label="Next page"
|
||||||
<i className="fa fa-chevron-right">></i>
|
/>
|
||||||
</button>
|
</Flex>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import UserInfo from "../(auth)/userInfo";
|
|||||||
import { ErrorProvider } from "../(errors)/errorContext";
|
import { ErrorProvider } from "../(errors)/errorContext";
|
||||||
import ErrorMessage from "../(errors)/errorMessage";
|
import ErrorMessage from "../(errors)/errorMessage";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
|
||||||
import About from "../(aboutAndPrivacy)/about";
|
import About from "../(aboutAndPrivacy)/about";
|
||||||
import Privacy from "../(aboutAndPrivacy)/privacy";
|
import Privacy from "../(aboutAndPrivacy)/privacy";
|
||||||
import { DomainContextProvider } from "./domainContext";
|
import { DomainContextProvider } from "./domainContext";
|
||||||
@@ -15,6 +14,8 @@ import { ErrorBoundary } from "@sentry/nextjs";
|
|||||||
import { cookies } from "next/dist/client/components/headers";
|
import { cookies } from "next/dist/client/components/headers";
|
||||||
import { SESSION_COOKIE_NAME } from "../lib/fief";
|
import { SESSION_COOKIE_NAME } from "../lib/fief";
|
||||||
import { Providers } from "../providers";
|
import { Providers } from "../providers";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
import { Container, Flex, Link } from "@chakra-ui/react";
|
||||||
|
|
||||||
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
|
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
|
||||||
|
|
||||||
@@ -91,13 +92,25 @@ export default async function RootLayout({ children, params }: LayoutProps) {
|
|||||||
<ErrorProvider>
|
<ErrorProvider>
|
||||||
<ErrorMessage />
|
<ErrorMessage />
|
||||||
<Providers>
|
<Providers>
|
||||||
<div
|
<Container
|
||||||
id="container"
|
minW="100vw"
|
||||||
className="items-center h-[100svh] w-[100svw] p-2 md:p-4 grid grid-rows-layout-topbar gap-2 md:gap-4"
|
maxH="100vh"
|
||||||
|
minH="100vh"
|
||||||
|
maxW="container.xl"
|
||||||
|
display="grid"
|
||||||
|
gridTemplateRows="auto minmax(0,1fr)"
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
as="header"
|
||||||
|
justify="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
w="100%"
|
||||||
|
py="2"
|
||||||
|
px="0"
|
||||||
>
|
>
|
||||||
<header className="flex justify-between items-center w-full">
|
|
||||||
{/* Logo on the left */}
|
{/* Logo on the left */}
|
||||||
<Link
|
<Link
|
||||||
|
as={NextLink}
|
||||||
href="/"
|
href="/"
|
||||||
className="flex outline-blue-300 md:outline-none focus-visible:underline underline-offset-2 decoration-[.5px] decoration-gray-500"
|
className="flex outline-blue-300 md:outline-none focus-visible:underline underline-offset-2 decoration-[.5px] decoration-gray-500"
|
||||||
>
|
>
|
||||||
@@ -120,6 +133,7 @@ export default async function RootLayout({ children, params }: LayoutProps) {
|
|||||||
<div>
|
<div>
|
||||||
{/* Text link on the right */}
|
{/* Text link on the right */}
|
||||||
<Link
|
<Link
|
||||||
|
as={NextLink}
|
||||||
href="/transcripts/new"
|
href="/transcripts/new"
|
||||||
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
|
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
|
||||||
>
|
>
|
||||||
@@ -130,6 +144,7 @@ export default async function RootLayout({ children, params }: LayoutProps) {
|
|||||||
·
|
·
|
||||||
<Link
|
<Link
|
||||||
href="/browse"
|
href="/browse"
|
||||||
|
as={NextLink}
|
||||||
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
|
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
>
|
>
|
||||||
@@ -158,10 +173,10 @@ export default async function RootLayout({ children, params }: LayoutProps) {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</Flex>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Container>
|
||||||
</Providers>
|
</Providers>
|
||||||
</ErrorProvider>
|
</ErrorProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useError } from "../../(errors)/errorContext";
|
import { useError } from "../../(errors)/errorContext";
|
||||||
import { GetTranscript, CreateTranscript } from "../../api";
|
import { CreateTranscript, GetTranscript } from "../../api";
|
||||||
import useApi from "../../lib/useApi";
|
import useApi from "../../lib/useApi";
|
||||||
|
|
||||||
type UseTranscript = {
|
type UseCreateTranscript = {
|
||||||
transcript: GetTranscript | null;
|
transcript: GetTranscript | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
create: (transcriptCreationDetails: CreateTranscript) => void;
|
create: (transcriptCreationDetails: CreateTranscript) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useCreateTranscript = (): UseTranscript => {
|
const useCreateTranscript = (): UseCreateTranscript => {
|
||||||
const [transcript, setTranscript] = useState<GetTranscript | null>(null);
|
const [transcript, setTranscript] = useState<GetTranscript | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setErrorState] = useState<Error | null>(null);
|
const [error, setErrorState] = useState<Error | null>(null);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type TranscriptList = {
|
|||||||
response: Page_GetTranscript_ | null;
|
response: Page_GetTranscript_ | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
|
refetch: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
//always protected
|
//always protected
|
||||||
@@ -16,6 +17,11 @@ const useTranscriptList = (page: number): TranscriptList => {
|
|||||||
const [error, setErrorState] = useState<Error | null>(null);
|
const [error, setErrorState] = useState<Error | null>(null);
|
||||||
const { setError } = useError();
|
const { setError } = useError();
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const [refetchCount, setRefetchCount] = useState(0);
|
||||||
|
|
||||||
|
const refetch = () => {
|
||||||
|
setRefetchCount(refetchCount + 1);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
@@ -32,9 +38,9 @@ const useTranscriptList = (page: number): TranscriptList => {
|
|||||||
setError(err);
|
setError(err);
|
||||||
setErrorState(err);
|
setErrorState(err);
|
||||||
});
|
});
|
||||||
}, [!api, page]);
|
}, [!api, page, refetchCount]);
|
||||||
|
|
||||||
return { response, loading, error };
|
return { response, loading, error, refetch };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useTranscriptList;
|
export default useTranscriptList;
|
||||||
|
|||||||
46
www/app/lib/expandableText.tsx
Normal file
46
www/app/lib/expandableText.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { BoxProps } from "@chakra-ui/react";
|
||||||
|
import { Box, Button, Text } from "@chakra-ui/react";
|
||||||
|
import React, { forwardRef, useState } from "react";
|
||||||
|
|
||||||
|
interface Props extends BoxProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
noOfLines: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExpandableText = forwardRef<HTMLDivElement, Props>(
|
||||||
|
({ children, noOfLines, ...rest }, ref) => {
|
||||||
|
const [expandedCount, setExpandedCount] = useState<number | undefined>(
|
||||||
|
noOfLines,
|
||||||
|
);
|
||||||
|
const [isClicked, setIsClicked] = useState(false);
|
||||||
|
const handleToggle = () => {
|
||||||
|
setIsClicked(true);
|
||||||
|
setExpandedCount(expandedCount ? undefined : noOfLines);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const isTextClamped =
|
||||||
|
(inputRef.current?.scrollHeight as number) >
|
||||||
|
(inputRef.current?.clientHeight as number) || isClicked;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={ref} {...rest}>
|
||||||
|
<Box ref={inputRef} noOfLines={expandedCount}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
display={isTextClamped ? "block" : "none"}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
onClick={handleToggle}
|
||||||
|
mt={2}
|
||||||
|
>
|
||||||
|
<Text>{!expandedCount ? "Show less" : "Read more"}</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ExpandableText.displayName = "ExpandableText";
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChakraProvider } from "@chakra-ui/react";
|
import { ChakraProvider } from "@chakra-ui/react";
|
||||||
|
import theme from "./theme";
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
return <ChakraProvider>{children}</ChakraProvider>;
|
return <ChakraProvider theme={theme}>{children}</ChakraProvider>;
|
||||||
}
|
}
|
||||||
|
|||||||
34
www/app/theme.ts
Normal file
34
www/app/theme.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// 1. Import `extendTheme`
|
||||||
|
import { extendTheme } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
// 2. Call `extendTheme` and pass your custom values
|
||||||
|
const theme = extendTheme({
|
||||||
|
colors: {
|
||||||
|
blue: {
|
||||||
|
primary: "#3158E2",
|
||||||
|
500: "#3158E2",
|
||||||
|
light: "#B1CBFF",
|
||||||
|
200: "#B1CBFF",
|
||||||
|
dark: "#0E1B48",
|
||||||
|
900: "#0E1B48",
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
primary: "#DF7070",
|
||||||
|
500: "#DF7070",
|
||||||
|
light: "#FBD5D5",
|
||||||
|
200: "#FBD5D5",
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
bg: "#F4F4F4",
|
||||||
|
100: "#F4F4F4",
|
||||||
|
light: "#D5D5D5",
|
||||||
|
200: "#D5D5D5",
|
||||||
|
primary: "#838383",
|
||||||
|
500: "#838383",
|
||||||
|
},
|
||||||
|
light: "#FFFFFF",
|
||||||
|
dark: "#0C0D0E",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropdown": "^1.11.0",
|
"react-dropdown": "^1.11.0",
|
||||||
|
"react-icons": "^5.0.1",
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
"react-qr-code": "^2.0.12",
|
"react-qr-code": "^2.0.12",
|
||||||
"react-select-search": "^4.1.7",
|
"react-select-search": "^4.1.7",
|
||||||
|
|||||||
@@ -4674,6 +4674,11 @@ react-focus-lock@^2.9.4:
|
|||||||
use-callback-ref "^1.3.0"
|
use-callback-ref "^1.3.0"
|
||||||
use-sidecar "^1.1.2"
|
use-sidecar "^1.1.2"
|
||||||
|
|
||||||
|
react-icons@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.0.1.tgz#1694e11bfa2a2888cab47dcc30154ce90485feee"
|
||||||
|
integrity sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.7.0:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user