mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
improve list view
This commit is contained in:
@@ -6,81 +6,176 @@ import { Title } from "../../lib/textComponents";
|
||||
import Pagination from "./pagination";
|
||||
import Link from "next/link";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGear } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faCheck,
|
||||
faGear,
|
||||
faMicrophone,
|
||||
faStar,
|
||||
faTrash,
|
||||
faX,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import useTranscriptList from "../transcripts/useTranscriptList";
|
||||
import { formatTime } from "../../lib/time";
|
||||
import getApi from "../../api";
|
||||
import { useError } from "../../(errors)/errorContext";
|
||||
|
||||
export default function TranscriptBrowser() {
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const { loading, response } = useTranscriptList(page);
|
||||
const { loading, response, refetch } = useTranscriptList(page);
|
||||
const [transcriptToDeleteId, setTranscriptToDeleteId] = useState("");
|
||||
const [deletionLoading, setDeletionLoading] = useState(false);
|
||||
const [deletedItems, setDeletedItems] = useState<string[]>([]);
|
||||
const api = getApi();
|
||||
const { setError } = useError();
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-layout-topbar gap-2 lg:gap-4 h-full max-h-full">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Title className="mb-5 mt-5 flex-1">Past transcripts</Title>
|
||||
<Pagination
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
total={response?.total || 0}
|
||||
size={response?.size || 0}
|
||||
if (loading && !response)
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="animate-spin-slow h-14 w-14 md:h-20 md:w-20 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
{loading && (
|
||||
<div className="full-screen flex flex-col items-center justify-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="animate-spin-slow h-14 w-14 md:h-20 md:w-20"
|
||||
if (!loading && !response)
|
||||
return (
|
||||
<div className="text-gray-500">
|
||||
No transcripts found, but you can
|
||||
<Link href="/transcripts/new" className="underline">
|
||||
record a meeting
|
||||
</Link>
|
||||
to get started.
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleDelete =
|
||||
(id: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setTranscriptToDeleteId(id);
|
||||
};
|
||||
|
||||
const deleteTranscript = () => (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!deletionLoading) {
|
||||
api
|
||||
?.v1TranscriptDelete({ transcriptId: transcriptToDeleteId })
|
||||
.then(() => {
|
||||
setDeletionLoading(false);
|
||||
setDeletedItems([...deletedItems, transcriptToDeleteId]);
|
||||
setTranscriptToDeleteId("");
|
||||
refetch();
|
||||
})
|
||||
.catch((err) => {
|
||||
setDeletionLoading(false);
|
||||
setError(err, "There was an error deleting the transcript");
|
||||
});
|
||||
}
|
||||
};
|
||||
const cancelDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setTranscriptToDeleteId("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center justify-center flex-grow">
|
||||
<div className="max-w-5xl h-full">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Title className="mb-5 mt-5 flex-1">Past transcripts</Title>
|
||||
<Pagination
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
total={response?.total || 0}
|
||||
size={response?.size || 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!loading && !response && (
|
||||
<div className="text-gray-500">
|
||||
No transcripts found, but you can
|
||||
<Link href="/transcripts/new" className="underline">
|
||||
record a meeting
|
||||
</Link>
|
||||
to get started.
|
||||
</div>
|
||||
)}
|
||||
<div /** center and max 900px wide */ className="overflow-y-scroll">
|
||||
<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
|
||||
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"
|
||||
>
|
||||
{item.title || item.name}
|
||||
</Link>
|
||||
<div className="grid grid-cols-1 gap-2 lg:gap-4 h-full">
|
||||
{response?.items
|
||||
.filter((item) => !deletedItems.includes(item.id))
|
||||
.map((item: GetTranscript) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/transcripts/${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">
|
||||
<h2 className="text-1xl font-semibold flex-1">
|
||||
{item.title || item.name}
|
||||
</h2>
|
||||
|
||||
{item.locked ? (
|
||||
<div className="inline-block bg-red-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
||||
Locked
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{item.locked && (
|
||||
<div className="inline-block bg-red-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
||||
Locked
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.source_language ? (
|
||||
<div className="inline-block bg-blue-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
||||
{item.source_language}
|
||||
{item.status == "ended" && (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="mt-1 text-green-500"
|
||||
/>
|
||||
)}
|
||||
{item.status == "error" && (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className="mt-1 text-red-500"
|
||||
/>
|
||||
)}
|
||||
{item.status == "idle" && (
|
||||
<FontAwesomeIcon
|
||||
icon={faStar}
|
||||
className="mt-1 text-yellow-500"
|
||||
/>
|
||||
)}
|
||||
{item.status == "processing" && (
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="mt-1 text-grey-500"
|
||||
/>
|
||||
)}
|
||||
{item.status == "recording" && (
|
||||
<FontAwesomeIcon
|
||||
icon={faMicrophone}
|
||||
className="mt-1 text-blue-500"
|
||||
/>
|
||||
)}
|
||||
|
||||
{item.sourceLanguage && (
|
||||
<div className="inline-block bg-blue-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
||||
{item.sourceLanguage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 items-start">
|
||||
<div className="text-xs text-gray-700 flex-1">
|
||||
{new Date(item.createdAt).toLocaleDateString("en-US")}
|
||||
{"\u00A0"}-{"\u00A0"}
|
||||
{formatTime(Math.floor(item.duration))}
|
||||
<div className="text-sm">{item.shortSummary}</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{item.status !== "ended" && (
|
||||
<button
|
||||
className="self-end p-2"
|
||||
disabled={deletionLoading}
|
||||
onClick={handleDelete(item.id)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
|
||||
</button>
|
||||
)}
|
||||
<dialog open={transcriptToDeleteId == item.id}>
|
||||
<p>Are you sure you want to delete {item.title} ?</p>
|
||||
<p>This action is not reversible.</p>
|
||||
<button onClick={cancelDelete}>Cancel</button>
|
||||
<button onClick={deleteTranscript()}>Confirm</button>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-700">
|
||||
{new Date(item.created_at).toLocaleDateString("en-US")}
|
||||
</div>
|
||||
<div className="text-sm">{item.short_summary}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
type PaginationProps = {
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -40,13 +43,13 @@ export default function Pagination(props: PaginationProps) {
|
||||
return (
|
||||
<div className="flex justify-center space-x-4 my-4">
|
||||
<button
|
||||
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
||||
className={`w-10 h-10 rounded-full p-2 ${
|
||||
canGoPrevious ? "text-gray-500" : "text-gray-300"
|
||||
}`}
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
disabled={!canGoPrevious}
|
||||
>
|
||||
<i className="fa fa-chevron-left"><</i>
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="h-5 w-auto" />
|
||||
</button>
|
||||
|
||||
{pageNumbers.map((pageNumber) => (
|
||||
@@ -62,13 +65,13 @@ export default function Pagination(props: PaginationProps) {
|
||||
))}
|
||||
|
||||
<button
|
||||
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
||||
className={`w-10 h-10 rounded-full p-2 ${
|
||||
canGoNext ? "text-gray-500" : "text-gray-300"
|
||||
}`}
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
disabled={!canGoNext}
|
||||
>
|
||||
<i className="fa fa-chevron-right">></i>
|
||||
<FontAwesomeIcon icon={faArrowRight} className="h-5 w-auto" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useError } from "../../(errors)/errorContext";
|
||||
import { GetTranscript, CreateTranscript } from "../../api";
|
||||
import { CreateTranscript } from "../../api";
|
||||
import useApi from "../../lib/useApi";
|
||||
|
||||
type UseTranscript = {
|
||||
type UseCreateTranscript = {
|
||||
transcript: GetTranscript | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
create: (transcriptCreationDetails: CreateTranscript) => void;
|
||||
};
|
||||
|
||||
const useCreateTranscript = (): UseTranscript => {
|
||||
const useCreateTranscript = (): UseCreateTranscript => {
|
||||
const [transcript, setTranscript] = useState<GetTranscript | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setErrorState] = useState<Error | null>(null);
|
||||
|
||||
@@ -7,6 +7,7 @@ type TranscriptList = {
|
||||
response: Page_GetTranscript_ | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
//always protected
|
||||
@@ -16,6 +17,15 @@ const useTranscriptList = (page: number): TranscriptList => {
|
||||
const [error, setErrorState] = useState<Error | null>(null);
|
||||
const { setError } = useError();
|
||||
const api = useApi();
|
||||
const [refetchCount, setRefetchCount] = useState(0);
|
||||
|
||||
const refetch = () => {
|
||||
setRefetchCount(refetchCount + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setResponse(null);
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
@@ -32,9 +42,9 @@ const useTranscriptList = (page: number): TranscriptList => {
|
||||
setError(err);
|
||||
setErrorState(err);
|
||||
});
|
||||
}, [!api, page]);
|
||||
}, [!api, page, refetchCount]);
|
||||
|
||||
return { response, loading, error };
|
||||
return { response, loading, error, refetch };
|
||||
};
|
||||
|
||||
export default useTranscriptList;
|
||||
|
||||
Reference in New Issue
Block a user