File Viewer
File Viewer UI components
File viewer UI
File viewer UI that displays a list of files and displays files in modal.
Carousel
FileViewerModal
PdfViewer
Provider
store
TextViewer
App.tsx
createProvider.tsx
FileViewerContent.tsx
FileViewerList.tsx
FileViewerListItem.tsx
getFileIcon.ts
splitByNumber.ts
import { Flex } from '@chakra-ui/react'
import { FileViewerContent } from './FileViewerContent'
import { FileViewerList } from './FileViewerList'
import { FileViewerModal } from './FileViewerModal'
import { Provider } from './Provider'
export function App() {
return (
<Provider>
<Flex
flex={1}
h="full"
flexDirection="column"
bg="transparent"
minH={{ base: 'auto', lg: '600px' }}
>
<FileViewerContent>
<FileViewerList />
</FileViewerContent>
</Flex>
<FileViewerModal />
</Provider>
)
}
import type { FlexProps } from '@chakra-ui/react'
import { Flex } from '@chakra-ui/react'
import React from 'react'
type Props = FlexProps
export function FileViewerContent(props: Props) {
return (
<Flex flex={1} position="relative" h="full" p={8} pb={0} {...props}>
<Flex flex={1} flexDirection="column">
{props.children}
</Flex>
</Flex>
)
}
import { Flex, Stack, useBreakpointValue } from '@chakra-ui/react'
import React, { useMemo } from 'react'
import { FileViewerListItem } from './FileViewerListItem'
import { useFileViewerContext } from './Provider/FileViewerProvider'
import { splitByNumber } from './splitByNumber'
export function FileViewerList() {
const { fileIds } = useFileViewerContext()
const splitNum = useBreakpointValue({ base: 1, lg: 2, '2xl': 3 }) as number
const columns = useMemo(
() => splitByNumber(fileIds, splitNum),
[fileIds, splitNum],
)
return (
<Flex flex={1} pb={4}>
<Stack maxW="90%" mx="auto" direction="row" spacing={8}>
{columns.map((ids, i) => (
<Stack spacing={8} key={i}>
{ids.map((id) => (
<FileViewerListItem fileId={id} key={id} />
))}
</Stack>
))}
</Stack>
</Flex>
)
}
import type { FlexProps } from '@chakra-ui/react'
import { Flex, Icon, Text, Image, Divider } from '@chakra-ui/react'
import React, { useCallback } from 'react'
import { useFileViewerModalContext } from './FileViewerModal'
import { useFileViewerContext } from './Provider'
import { getFileIcon } from './getFileIcon'
type Props = {
fileId: string
} & FlexProps
export function FileViewerListItem({ fileId, ...rest }: Props) {
const { fileIds, getFileById } = useFileViewerContext()
const file = getFileById(fileId)
const { onOpen, setState } = useFileViewerModalContext()
const FileIcon = getFileIcon(file?.fileType?.typeCode || 'IMAGE')
const handleOpenFileViewer = useCallback(() => {
setState({
fileIds,
currentFileId: fileId,
})
onOpen()
}, [fileId, fileIds, onOpen, setState])
return (
<Flex
border="1px"
borderRadius="md"
borderColor="gray.200"
_hover={{
borderColor: 'gray.400',
}}
flexDirection="column"
cursor="pointer"
w={{ base: '100%', lg: '420px' }}
maxW={{ base: '100%', lg: '420px' }}
maxH={{ base: '275px' }}
bg="transparent"
_dark={{
borderColor: 'gray.700',
_hover: {
borderColor: 'gray.400',
},
}}
boxShadow="md"
onClick={handleOpenFileViewer}
{...rest}
>
<Flex p={4} alignItems="center">
<Icon as={FileIcon} color="gray.500" w={8} h={8} />
<Flex ml={4} flexDirection="column" flex={1} minW={0}>
<Text fontSize="sm" noOfLines={1}>
{file?.name}
</Text>
</Flex>
</Flex>
{file?.fileType?.typeCode === 'IMAGE' && (
<>
<Divider />
<Image
width="auto"
maxW="100%"
src={file.src}
borderBottomRadius="md"
objectFit="contain"
overflow="hidden"
alt={file.name}
/>
</>
)}
</Flex>
)
}
import { forwardRef } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React, { createContext, memo } from 'react'
/**
* Creates a provider component for a given context.
*
* @param {function} useValue - A function that takes props and returns the context value.
*
* @return An object containing the provider component, the context object, and a hook to consume the context.
*/
export function createProvider<
ContextProps extends object,
Props extends object,
>(useValue: (props: Props) => ContextProps) {
const Context = createContext<ContextProps>({} as ContextProps)
const useContext = () => {
const context = React.useContext(Context)
if (!Object.keys(context).length) {
throw new Error(
`【${
(useValue as any).__PROVIDER__
}】Context needs to be consumed in Provider`,
)
}
return context
}
const Provider: React.FC<PropsWithChildren<Props>> = memo<
PropsWithChildren<Props>
>(
forwardRef((props, ref) => (
<Component {...props} ref={ref} {...useValue(props)} />
)) as React.FC<PropsWithChildren<Props>>,
)
Provider.displayName = 'Provider'
const Component: React.FC<PropsWithChildren<Props> & ContextProps> = memo<
PropsWithChildren<Props> & ContextProps
>(
forwardRef(({ children, ...rest }, ref) => {
return (
<Context.Provider value={{ ...(rest as unknown as ContextProps), ref }}>
{children}
</Context.Provider>
)
}) as React.FC<PropsWithChildren<Props> & ContextProps>,
)
return {
Provider,
Context,
useContext,
}
}
import { AiOutlineFilePdf, AiOutlineFileText } from 'react-icons/ai'
import { BiImageAlt } from 'react-icons/bi'
export const getFileIcon = (type: 'PDF' | 'TEXT' | 'IMAGE') => {
switch (type) {
case 'IMAGE':
return BiImageAlt
case 'PDF':
return AiOutlineFilePdf
case 'TEXT':
return AiOutlineFileText
}
}
/**
* Splits an array into subarrays based on a given number.
*
* @param {T[]} array - The array to be split.
* @param {number} num - The number of subarrays to create.
* @returns {T[][]} - An array of subarrays containing the elements from the original array.
*/
export const splitByNumber = <T>(array: T[], num: number) => {
return [...new Array(num)].map((_, resultIndex) => {
const result = []
for (let i = 0; i <= array.length; i += num) {
const index = i + resultIndex
if (array[index]) result.push(array[index])
}
return result as T[]
})
}
import { Flex, Icon, IconButton } from '@chakra-ui/react'
import React, { useCallback } from 'react'
import { BiChevronLeft } from 'react-icons/bi'
import { useCarouselContext } from './Provider'
export function CarouselLeftChevron() {
const { count, currentIndex, setCurrentIndex } = useCarouselContext()
const handleClick = useCallback(() => {
const nextIndex = currentIndex - 1
if (nextIndex < 0) {
setCurrentIndex(count - 1)
return
}
setCurrentIndex(nextIndex)
}, [count, currentIndex, setCurrentIndex])
return (
<Flex
hideBelow="md"
position="absolute"
top={0}
left={0}
w={24}
h="100%"
justifyContent="center"
alignItems="center"
zIndex="skipLink"
>
<IconButton
onClick={handleClick}
aria-label="next"
icon={<Icon as={BiChevronLeft} w={8} h={8} />}
variant="ghost"
/>
</Flex>
)
}
import { Flex, Icon, IconButton } from '@chakra-ui/react'
import React, { useCallback } from 'react'
import { BiChevronRight } from 'react-icons/bi'
import { useCarouselContext } from './Provider'
export function CarouselRightChevron() {
const { count, currentIndex, setCurrentIndex } = useCarouselContext()
const handleClick = useCallback(() => {
const nextIndex = currentIndex + 1
if (nextIndex === count) {
setCurrentIndex(0)
return
}
setCurrentIndex(nextIndex)
}, [count, currentIndex, setCurrentIndex])
return (
<Flex
hideBelow="md"
position="absolute"
top={0}
right={0}
w={24}
h="100%"
justifyContent="center"
alignItems="center"
zIndex="skipLink"
>
<IconButton
onClick={handleClick}
aria-label="next"
icon={<Icon as={BiChevronRight} w={8} h={8} />}
variant="ghost"
/>
</Flex>
)
}
import { Flex } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React from 'react'
import { Provider } from './Provider'
type Props = {
onChange?: (currentIndex: number) => void
defaultIndex?: number
}
export function Carousel(props: PropsWithChildren<Props>) {
return (
<Provider {...props}>
<Component {...props} />
</Provider>
)
}
function Component(props: PropsWithChildren<Props>) {
return (
<Flex
flex="1"
overflow="hidden"
position="relative"
height="100%"
flexDirection="column"
>
{props.children}
</Flex>
)
}
import { Flex } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React, { useEffect } from 'react'
import { useCarouselContext } from './Provider'
export function CarouselBody(props: PropsWithChildren) {
const { setCount } = useCarouselContext()
const count = React.Children.toArray(props.children).filter(
(c) => (c as any).type.name === 'CarouselItem',
).length
const children = React.Children.map(props.children, (child, index) => {
if (!React.isValidElement(child)) {
console.warn('Provide React element under Carousel component')
return null
}
return React.cloneElement(child, {
index,
} as {
index?: number
})
})
useEffect(() => {
setCount(count)
}, [count, setCount])
return (
<Flex flex="1" position="relative" height="100%">
{children}
</Flex>
)
}
import { Flex } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React, { useMemo } from 'react'
import { useCarouselContext } from './Provider'
type Props = {
index?: number
}
export function CarouselItem(props: PropsWithChildren<Props>) {
const { currentIndex } = useCarouselContext()
const show = useMemo(
() => currentIndex === props.index,
[currentIndex, props.index],
)
return (
<Flex
w="full"
h="full"
position="absolute"
top={0}
left={0}
justifyContent="center"
alignItems="center"
px={{ base: 0, md: 24 }}
opacity={show ? 1 : 0}
zIndex={show ? 'popover' : 'base'}
>
<Flex
w="full"
h="full"
justifyContent="center"
alignItems="center"
position="relative"
>
{props.children}
</Flex>
</Flex>
)
}
import type { WrapProps } from '@chakra-ui/react'
import { Flex } from '@chakra-ui/react'
import { Wrap } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React from 'react'
type Props = WrapProps
export function CarouselThumbnail({
children,
...props
}: PropsWithChildren<Props>) {
const components = React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) {
console.warn('Provide React element under Carousel component')
return null
}
return React.cloneElement(child, {
index,
} as {
index?: number
})
})
return (
<Flex
position="absolute"
bottom="-1px"
px={{ base: 0, md: 4 }}
pt={{ base: 6 }}
pb={{ base: 8 }}
width="100%"
alignItems="center"
justifyContent="center"
zIndex="tooltip"
{...props}
>
<Wrap spacing={8} alignItems="center" mx="auto">
{components}
</Wrap>
</Flex>
)
}
import { AspectRatio, WrapItem } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React, { useCallback, useMemo } from 'react'
import { useCarouselContext } from './Provider'
type Props = {
index?: number
}
export function CarouselThumbnailItem(props: PropsWithChildren<Props>) {
const { currentIndex, setCurrentIndex } = useCarouselContext()
const show = useMemo(
() => currentIndex === props.index,
[currentIndex, props.index],
)
const handleClick = useCallback(() => {
setCurrentIndex(props.index!)
}, [props.index, setCurrentIndex])
return (
<WrapItem
justifyContent="center"
alignItems="center"
opacity={show ? 1 : 0.5}
_hover={{
opacity: 1,
}}
transition="opacity .15s ease-out"
cursor="pointer"
borderRadius="md"
onClick={handleClick}
>
<AspectRatio w={16} ratio={4 / 3}>
{props.children}
</AspectRatio>
</WrapItem>
)
}
import type React from 'react'
import { useCallback, useState } from 'react'
import { createProvider } from '../createProvider'
type ContextProps = {
count: number
setCount: React.Dispatch<React.SetStateAction<number>>
currentIndex: number
setCurrentIndex: (currentIndex: number) => void
}
type Props = {
onChange?: (currentIndex: number) => void
defaultIndex?: number
}
const useValue = (props: Props): ContextProps => {
const [currentIndex, setCurrentIndex] = useState<number>(
props.defaultIndex ?? 0,
)
const [count, setCount] = useState<number>(0)
const handleSetCurrentIndex = useCallback(
(index: number) => {
setCurrentIndex(index)
props.onChange?.(index)
},
[setCurrentIndex, props],
)
return {
count,
setCount,
currentIndex,
setCurrentIndex: handleSetCurrentIndex,
}
}
useValue.__PROVIDER__ = 'Carousel'
export const { Provider, useContext: useCarouselContext } =
createProvider(useValue)
import React, { useCallback, useMemo } from 'react'
import {
Carousel,
CarouselItem,
CarouselRightChevron,
CarouselLeftChevron,
CarouselBody,
CarouselThumbnail,
CarouselThumbnailItem,
} from '../Carousel'
import { useFileViewerModalContext } from './FileViewerModalProvider'
import { ListItem } from './ListItem'
import { ThumbnailListItem } from './ThumbnailListItem'
export function Body() {
const { fileIds, setState, currentFileId } = useFileViewerModalContext()
const defaultIndex = useMemo(
() => fileIds.indexOf(currentFileId),
[fileIds, currentFileId],
)
const handleChangeCarousel = useCallback(
(currentIndex: number) => {
setState((s) => ({
...s,
currentFileId: fileIds[currentIndex] as string,
}))
},
[fileIds, setState],
)
return (
<Carousel defaultIndex={defaultIndex} onChange={handleChangeCarousel}>
<CarouselBody>
{fileIds.map((id) => (
<CarouselItem key={id}>
<ListItem fileId={id} />
</CarouselItem>
))}
</CarouselBody>
<CarouselThumbnail bg="white" _dark={{ bg: 'gray.900' }}>
{fileIds.map((id) => (
<CarouselThumbnailItem key={id}>
<ThumbnailListItem fileId={id} />
</CarouselThumbnailItem>
))}
</CarouselThumbnail>
<CarouselRightChevron />
<CarouselLeftChevron />
</Carousel>
)
}
import {
Modal,
ModalBody,
ModalContent,
ModalHeader,
Divider,
PortalManager,
} from '@chakra-ui/react'
import React from 'react'
import { Body } from './Body'
import { useFileViewerModalContext } from './FileViewerModalProvider'
import { Header } from './Header'
export function FileViewerModal() {
const { isOpen, onClose } = useFileViewerModalContext()
return (
<PortalManager zIndex={1800}>
<Modal isOpen={isOpen} onClose={onClose} size="full">
<ModalContent
_dark={{ bg: 'gray.900' }}
w="100vw"
h="100vh"
m={0}
borderRadius="none"
>
<ModalHeader p={0}>
<Header />
</ModalHeader>
<Divider />
<ModalBody pb={0} zIndex="tooltip">
{isOpen && <Body />}
</ModalBody>
</ModalContent>
</Modal>
</PortalManager>
)
}
import type { SetStateAction, Dispatch } from 'react'
import { useCallback, useState } from 'react'
import { createProvider } from '../createProvider'
type State = {
currentFileId: string
fileIds: string[]
}
type ContextProps = {
isOpen: boolean
currentFileId: string
fileIds: string[]
onOpen: () => void
onClose: () => void
setState: Dispatch<SetStateAction<State>>
}
const useValue = (): ContextProps => {
const [isOpen, setIsOpen] = useState(false)
const [state, setState] = useState<State>({
currentFileId: '',
fileIds: [],
})
const onClose = useCallback(() => {
setIsOpen(false)
setState({
currentFileId: '',
fileIds: [],
})
}, [setIsOpen])
const onOpen = useCallback(() => {
setIsOpen(true)
}, [setIsOpen])
return {
isOpen,
...state,
setState,
onClose,
onOpen,
}
}
useValue.__PROVIDER__ = 'FileViewerModalProvider'
export const {
Provider: FileViewerModalProvider,
useContext: useFileViewerModalContext,
} = createProvider(useValue)
import {
Button,
Divider,
Flex,
Icon,
IconButton,
Link,
Stack,
Text,
} from '@chakra-ui/react'
import React from 'react'
import { BiDownload, BiX } from 'react-icons/bi'
import { useFileViewerContext } from '../Provider'
import { useFileViewerModalContext } from './FileViewerModalProvider'
import { formatFileCreatedAt } from './formatFileCreatedAt'
export function Header() {
const { onClose, currentFileId } = useFileViewerModalContext()
const { getFileById } = useFileViewerContext()
const file = getFileById(currentFileId)
const formattedCreateAt = formatFileCreatedAt(file?.createdAt || '')
return (
<Flex h="full">
<Flex
flexDirection="column"
py={4}
px={6}
flex={{ base: 1, lg: 'initial' }}
>
<Text fontSize="md">{file?.name}</Text>
<Text fontSize="sm" color="gray.500">
{formattedCreateAt}
</Text>
</Flex>
<Stack direction="row" spacing={2} ml="auto" py={4} px={6} hideBelow="md">
<Link
href={file?.src}
download
_focusVisible={{ boxShadow: 'none', outline: 'none' }}
>
<Button
leftIcon={<Icon as={BiDownload} />}
iconSpacing={2}
variant="ghost"
>
Download
</Button>
</Link>
</Stack>
<Divider orientation="vertical" />
<Flex py={4} px={6} justifyContent="center" alignItems="center">
<IconButton
icon={<Icon as={BiX} w={6} h={6} />}
aria-label="close modal"
variant="ghost"
onClick={onClose}
_focusVisible={{ boxShadow: 'none', outline: 'none' }}
/>
</Flex>
</Flex>
)
}
import { Flex, Image, Text } from '@chakra-ui/react'
import React from 'react'
import { PdfViewer } from '../PdfViewer'
import { useFileViewerContext } from '../Provider'
import { TextViewer } from '../TextViewer'
type Props = {
fileId: string
}
export function ListItem({ fileId }: Props) {
const { getFileById } = useFileViewerContext()
const file = getFileById(fileId)
switch (file?.fileType?.typeCode) {
case 'IMAGE': {
return <Image src={file.src} objectFit="contain" alt={file?.name} />
}
case 'PDF': {
return <PdfViewer fileUrl={file.src} />
}
case 'TEXT': {
return <TextViewer src={file?.src} />
}
default: {
return (
<Flex
alignItems="center"
justifyContent="center"
flexDirection="column"
>
<Text fontSize="xl" mt={4}>
{"We're not able to preview this file"}
</Text>
<Text fontSize="sm" color="gray.500" mt={4}>
{file?.name}
</Text>
</Flex>
)
}
}
}
import dayjs from 'dayjs'
export const formatFileCreatedAt = (date: string): string => {
if (!date) return ''
const dayjsObj = dayjs(date)
if (dayjs().year() === dayjsObj.year()) {
return dayjsObj.format('MMM D, h:mma')
} else {
return dayjsObj.format('MMM D, YYYY h:mma')
}
}
import { Box } from '@chakra-ui/react'
import * as PDFViewer from '@react-pdf-viewer/core'
import type { PropsWithChildren } from 'react'
import React from 'react'
import '@react-pdf-viewer/core/lib/styles/index.css'
// The version should be same as the pdfjs-dist in package.json.
// @see https://react-pdf-viewer.dev/docs/getting-started/
const version = '2.9.359'
const characterMap: PDFViewer.CharacterMap = {
isCompressed: true,
// The url has to end with "/"
url: `https://unpkg.com/pdfjs-dist@${version}/cmaps/`,
}
type Props = PDFViewer.ViewerProps
const Worker = PDFViewer.Worker as unknown as React.FC<
PropsWithChildren<React.ComponentProps<typeof PDFViewer.Worker>>
>
const Viewer = PDFViewer.Viewer as unknown as React.FC<
PropsWithChildren<React.ComponentProps<typeof PDFViewer.Viewer>>
>
export function PdfViewer(props: Props) {
return (
<Worker
workerUrl={`https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${version}/pdf.worker.min.js`}
>
<Box w={{ base: '100%', lg: '70%' }} h="full" className="pdfViewer">
<Viewer characterMap={characterMap} {...props} />
</Box>
</Worker>
)
}
import { useCallback, useMemo, useState } from 'react'
import { createProvider } from '../createProvider'
import type { File } from '../store/file'
import { files as data } from './files'
type ContextProps = {
fileIds: string[]
getFileById: (id: string) => File | null
}
const useValue = (): ContextProps => {
// The files should come from external resources such as API or Database.
const [files] = useState<File[]>(data)
const fileIds = useMemo<string[]>(() => {
return files.map((f) => f.id)
}, [files])
const getFileById = useCallback(
(id: string) => {
return files.find((file) => file.id === id) ?? null
},
[files],
)
return {
fileIds,
getFileById,
}
}
useValue.__PROVIDER__ = 'CalendarInfiniteScroll'
export const {
Provider: FileViewerProvider,
useContext: useFileViewerContext,
} = createProvider(useValue)
import type { PropsWithChildren } from 'react'
import { FileViewerModalProvider } from '../FileViewerModal/FileViewerModalProvider'
import { FileViewerProvider } from './FileViewerProvider'
export function Provider({ children }: PropsWithChildren) {
return (
<FileViewerModalProvider>
<FileViewerProvider>{children}</FileViewerProvider>
</FileViewerModalProvider>
)
}
import type { File } from '../store/file'
export const files: File[] = [
{
id: '0BK01HMH3S270TQZ55BDW1ZMRMP7B',
name: 'cat_img.png',
src: '/files/cat-img.png',
fileTypeId: '0BJ01HMH3RYZ22GAM5EP4WQHCH023',
fileType: {
id: '0BJ01HMH3RYZ22GAM5EP4WQHCH023',
name: 'Image',
typeCode: 'IMAGE',
createdAt: '2024-01-20T00:00:06+09:00',
updatedAt: '2024-01-20T00:00:06+09:00',
},
createdAt: '2024-01-20T00:00:09+09:00',
updatedAt: '2024-01-20T00:00:09+09:00',
},
{
id: '0BK01HMH3S270TQZ55BDW230F2GBG',
name: 'pdf-test.pdf',
src: '/files/pdf-test.pdf',
fileTypeId: '0BJ01HMH3RYZ22GAM5EP4WRYNJERF',
fileType: {
id: '0BJ01HMH3RYZ22GAM5EP4WRYNJERF',
name: 'PDF',
typeCode: 'PDF',
createdAt: '2024-01-20T00:00:06+09:00',
updatedAt: '2024-01-20T00:00:06+09:00',
},
createdAt: '2024-01-20T00:00:09+09:00',
updatedAt: '2024-01-20T00:00:09+09:00',
},
{
id: '0BK01HMH3S270TQZ55BDW23YW7P6V',
name: 'pdf-test-2.pdf',
src: '/files/pdf-test-2.pdf',
fileTypeId: '0BJ01HMH3RYZ22GAM5EP4WRYNJERF',
fileType: {
id: '0BJ01HMH3RYZ22GAM5EP4WRYNJERF',
name: 'PDF',
typeCode: 'PDF',
createdAt: '2024-01-20T00:00:06+09:00',
updatedAt: '2024-01-20T00:00:06+09:00',
},
createdAt: '2024-01-20T00:00:09+09:00',
updatedAt: '2024-01-20T00:00:09+09:00',
},
{
id: '0BK01HMH3S270TQZ55BDW26EWEM2S',
name: 'test.js',
src: '/files/test.js',
fileTypeId: '0BJ01HMH3RYZ22GAM5EP4WWCCRE7S',
fileType: {
id: '0BJ01HMH3RYZ22GAM5EP4WWCCRE7S',
name: 'Text',
typeCode: 'TEXT',
createdAt: '2024-01-20T00:00:06+09:00',
updatedAt: '2024-01-20T00:00:06+09:00',
},
createdAt: '2024-01-20T00:00:09+09:00',
updatedAt: '2024-01-20T00:00:09+09:00',
},
]
import { Box } from '@chakra-ui/react'
import React, { useEffect, memo, useState } from 'react'
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx'
import vscDarkPlus from 'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus'
// Add any languages you want to support.
// @see https://github.com/react-syntax-highlighter/react-syntax-highlighter?tab=readme-ov-file#light-build
SyntaxHighlighter.registerLanguage('tsx', tsx)
type Props = {
src: string
}
export const TextViewer = memo<Props>(function TextViewer({ src }) {
const [data, setData] = useState<string>('')
useEffect(() => {
const fetchTextFile = async () => {
try {
const response = await fetch(src)
if (!response.ok) {
console.log(`HTTP error! status: ${response.status}`)
}
const text = await response.text()
setData(text)
} catch (error) {
console.error('Error fetching the text file. Error: ', error)
}
}
fetchTextFile()
}, [src])
return (
<Box
w={{ base: '100%', lg: '70%' }}
pb={{ base: '100px', md: '120px' }}
h="full"
overflow="scroll"
>
<SyntaxHighlighter
style={vscDarkPlus}
language="tsx"
customStyle={{
margin: 0,
background: 'rgb(16, 20, 24)',
minHeight: '600px',
}}
>
{data}
</SyntaxHighlighter>
</Box>
)
})
export type FileTypeCode = 'IMAGE' | 'PDF' | 'TEXT'
export type File = {
id: string
name: string
src: string
fileTypeId: string
fileType: {
id: string
name: string
typeCode: FileTypeCode
createdAt: string
updatedAt: string
}
createdAt: string
updatedAt: string
}
import { Tooltip } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React from 'react'
type Props = {
label: string
}
export function Container(props: PropsWithChildren<Props>) {
return (
<Tooltip
hasArrow
label={props.label}
aria-label="Attachment file name"
size="sm"
>
{props.children}
</Tooltip>
)
}
import type { ChakraProps } from '@chakra-ui/react'
import { Center, Image, Icon } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
import React, { useMemo } from 'react'
import { useFileViewerContext } from '../../Provider'
import { getFileIcon } from '../../getFileIcon'
import { Container } from './Container'
type Props = {
fileId: string
}
export function ThumbnailListItem({ fileId }: PropsWithChildren<Props>) {
const { getFileById } = useFileViewerContext()
const file = getFileById(fileId)
const style = useMemo<ChakraProps>(
() => ({
bg: 'gray.50',
borderRadius: 'md',
h: 'full',
w: 'full',
}),
[],
)
switch (file?.fileType?.typeCode) {
case 'IMAGE': {
return (
<Container label={file?.name}>
<Image
src={file?.src}
objectFit="cover"
alt={file?.name}
{...style}
/>
</Container>
)
}
case 'PDF':
case 'TEXT': {
const FileIcon = getFileIcon(file?.fileType.typeCode)
return (
<Container label={file?.name}>
<Center {...style}>
<Icon as={FileIcon} color="gray.700" />
</Center>
</Container>
)
}
}
return null
}