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 Pagination from "./pagination";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 useTranscriptList from "../transcripts/useTranscriptList";
|
||||||
|
import { formatTime } from "../../lib/time";
|
||||||
|
import getApi from "../../api";
|
||||||
|
import { useError } from "../../(errors)/errorContext";
|
||||||
|
|
||||||
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 [transcriptToDeleteId, setTranscriptToDeleteId] = useState("");
|
||||||
|
const [deletionLoading, setDeletionLoading] = useState(false);
|
||||||
|
const [deletedItems, setDeletedItems] = useState<string[]>([]);
|
||||||
|
const api = getApi();
|
||||||
|
const { setError } = useError();
|
||||||
|
|
||||||
return (
|
if (loading && !response)
|
||||||
<div className="grid grid-rows-layout-topbar gap-2 lg:gap-4 h-full max-h-full">
|
return (
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="h-full flex flex-col items-center justify-center">
|
||||||
<Title className="mb-5 mt-5 flex-1">Past transcripts</Title>
|
<FontAwesomeIcon
|
||||||
<Pagination
|
icon={faGear}
|
||||||
page={page}
|
className="animate-spin-slow h-14 w-14 md:h-20 md:w-20 text-gray-400"
|
||||||
setPage={setPage}
|
|
||||||
total={response?.total || 0}
|
|
||||||
size={response?.size || 0}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
{loading && (
|
if (!loading && !response)
|
||||||
<div className="full-screen flex flex-col items-center justify-center">
|
return (
|
||||||
<FontAwesomeIcon
|
<div className="text-gray-500">
|
||||||
icon={faGear}
|
No transcripts found, but you can
|
||||||
className="animate-spin-slow h-14 w-14 md:h-20 md:w-20"
|
<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>
|
</div>
|
||||||
)}
|
<div className="grid grid-cols-1 gap-2 lg:gap-4 h-full">
|
||||||
{!loading && !response && (
|
{response?.items
|
||||||
<div className="text-gray-500">
|
.filter((item) => !deletedItems.includes(item.id))
|
||||||
No transcripts found, but you can
|
.map((item: GetTranscript) => (
|
||||||
<Link href="/transcripts/new" className="underline">
|
<Link
|
||||||
record a meeting
|
key={item.id}
|
||||||
</Link>
|
href={`/transcripts/${item.id}`}
|
||||||
to get started.
|
className="flex flex-col bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4"
|
||||||
</div>
|
>
|
||||||
)}
|
<div className="flex flex-col">
|
||||||
<div /** center and max 900px wide */ className="overflow-y-scroll">
|
<div className="flex flex-row gap-2 items-start">
|
||||||
<div className="grid grid-cols-1 gap-2 lg:gap-4 h-full mx-auto max-w-[900px]">
|
<h2 className="text-1xl font-semibold flex-1">
|
||||||
{response?.items.map((item: GetTranscript) => (
|
{item.title || item.name}
|
||||||
<div
|
</h2>
|
||||||
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>
|
|
||||||
|
|
||||||
{item.locked ? (
|
{item.locked && (
|
||||||
<div className="inline-block bg-red-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
<div className="inline-block bg-red-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
||||||
Locked
|
Locked
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{item.source_language ? (
|
{item.status == "ended" && (
|
||||||
<div className="inline-block bg-blue-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
<FontAwesomeIcon
|
||||||
{item.source_language}
|
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>
|
</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>
|
||||||
<div className="text-xs text-gray-700">
|
</Link>
|
||||||
{new Date(item.created_at).toLocaleDateString("en-US")}
|
))}
|
||||||
</div>
|
|
||||||
<div className="text-sm">{item.short_summary}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
type PaginationProps = {
|
type PaginationProps = {
|
||||||
page: number;
|
page: number;
|
||||||
setPage: (page: number) => void;
|
setPage: (page: number) => void;
|
||||||
@@ -40,13 +43,13 @@ export default function Pagination(props: PaginationProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-4 my-4">
|
<div className="flex justify-center space-x-4 my-4">
|
||||||
<button
|
<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"
|
canGoPrevious ? "text-gray-500" : "text-gray-300"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handlePageChange(page - 1)}
|
onClick={() => handlePageChange(page - 1)}
|
||||||
disabled={!canGoPrevious}
|
disabled={!canGoPrevious}
|
||||||
>
|
>
|
||||||
<i className="fa fa-chevron-left"><</i>
|
<FontAwesomeIcon icon={faArrowLeft} className="h-5 w-auto" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{pageNumbers.map((pageNumber) => (
|
{pageNumbers.map((pageNumber) => (
|
||||||
@@ -62,13 +65,13 @@ export default function Pagination(props: PaginationProps) {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<button
|
<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"
|
canGoNext ? "text-gray-500" : "text-gray-300"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handlePageChange(page + 1)}
|
onClick={() => handlePageChange(page + 1)}
|
||||||
disabled={!canGoNext}
|
disabled={!canGoNext}
|
||||||
>
|
>
|
||||||
<i className="fa fa-chevron-right">></i>
|
<FontAwesomeIcon icon={faArrowRight} className="h-5 w-auto" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 } 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,15 @@ 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(() => {
|
||||||
|
setResponse(null);
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
@@ -32,9 +42,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;
|
||||||
|
|||||||
Reference in New Issue
Block a user