Skip to content

Commit

Permalink
Merge pull request #35 from MiracleHorizon/feat/file-name-input
Browse files Browse the repository at this point in the history
feat: option (input) to add the name of the output file
  • Loading branch information
MiracleHorizon authored Dec 20, 2023
2 parents fcb1998 + 3b77738 commit 1ff44e1
Show file tree
Hide file tree
Showing 26 changed files with 402 additions and 58 deletions.
6 changes: 3 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { FooterPanel } from '@components/FooterPanel'
import { SettingsPanel } from '@components/SettingsPanel'
import { UploadedFileSkeleton } from '@components/UploadedFile/UploadedFileSkeleton'
import { FileUploadZone } from '@components/FileUploadZone'
import { useConvertStore } from '@stores/convert'
import { useOutputStore } from '@stores/output'
import { useConvertImage } from '@hooks/useConvertImage'
import { ALLOWED_IMAGE_FORMATS } from '@libs/Sharp'
import type { FlexDirection } from '@libs/radix'
Expand Down Expand Up @@ -46,8 +46,8 @@ const contentPadding: PaddingProps = {
}

export default function HomePage() {
const file = useConvertStore(state => state.file)
const setFile = useConvertStore(state => state.setFile)
const file = useOutputStore(state => state.file)
const setFile = useOutputStore(state => state.setFile)

const { handleConvertImage, isPending, error, reset } = useConvertImage()

Expand Down
4 changes: 2 additions & 2 deletions src/components/ButtonDownload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { useRef } from 'react'
import { Button, Link } from '@radix-ui/themes'
import { DownloadIcon } from '@radix-ui/react-icons'

import { useConvertStore } from '@stores/convert'
import { useOutputStore } from '@stores/output'

export function ButtonDownload() {
const linkRef = useRef<HTMLAnchorElement>(null)

const downloadPayload = useConvertStore(state => state.downloadPayload)
const downloadPayload = useOutputStore(state => state.downloadPayload)

const handleButtonClick = () => {
if (!linkRef.current) return
Expand Down
8 changes: 4 additions & 4 deletions src/components/FooterPanel/FooterPanelContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ import { Button, Flex, Separator } from '@radix-ui/themes'
import { LockClosedIcon, SymbolIcon } from '@radix-ui/react-icons'

import { ButtonDownload } from '@components/ButtonDownload'
import { useConvertStore } from '@stores/convert'
import { useOutputStore } from '@stores/output'
import { useConvertImage } from '@hooks/useConvertImage'

export function FooterPanelContent({ isPending, handleConvertImage }: Props) {
const file = useConvertStore(state => state.file)
const isFileUploaded = useOutputStore(state => state.isFileUploaded())

return (
<Flex align='center' justify='end' gap='3' height='100%' width='100%'>
<ButtonDownload />
<Separator orientation='vertical' size='2' />
<Button size='3' disabled={!file || isPending} onClick={handleConvertImage}>
<Button size='3' disabled={!isFileUploaded || isPending} onClick={handleConvertImage}>
Convert
{!file || isPending ? (
{!isFileUploaded || isPending ? (
<LockClosedIcon width='20px' height='20px' />
) : (
<SymbolIcon width='20px' height='20px' />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.root {
max-width: 500px;
}

.textFieldRoot {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import dynamic from 'next/dynamic'
import { type ChangeEvent, memo, useCallback, useMemo, useRef, useState } from 'react'
import { Flex, TextField } from '@radix-ui/themes'

import { ButtonClear } from '@ui/ButtonClear'
import { ButtonInfoSkeleton } from '@ui/skeletons/ButtonInfoSkeleton'
import { useOutputStore } from '@stores/output'
import { useEscapeBlur } from '@hooks/useEscapeBlur'
import {
isValidFileName,
MAX_FILE_NAME_LENGTH,
MIN_FILE_NAME_LENGTH
} from '@helpers/isValidFileName'
import type { TextFieldInputProps } from '@libs/radix'
import styles from './InputOutputFileName.module.css'

const PopoverOutputFileName = dynamic(
() => import('./PopoverOutputFileName').then(mod => mod.PopoverOutputFileName),
{
ssr: false,
loading: () => <ButtonInfoSkeleton />
}
)

function InputOutputFileName() {
const [isError, setIsError] = useState<boolean>(false)
const inputRef = useRef<HTMLInputElement>(null)

const outputFileName = useOutputStore(state => state.outputFileName)
const setOutputFileName = useOutputStore(state => state.setOutputFileName)

const inputProps = useMemo(() => {
if (!isError) return

return {
color: 'red',
variant: 'soft'
} as TextFieldInputProps
}, [isError])

const handleChange = useCallback(
(ev: ChangeEvent<HTMLInputElement>) => {
const value = ev.target.value

if (value.length === 0) {
setOutputFileName(ev.target.value)
return setIsError(false)
}

const isValid = isValidFileName(value)

if (!isValid) {
setIsError(true)
} else {
if (isError !== null) {
setIsError(false)
}
}

setOutputFileName(ev.target.value)
},
[isError, setOutputFileName]
)

const handleClear = useCallback(() => {
setOutputFileName('')
setIsError(false)
}, [setOutputFileName])

useEscapeBlur({
ref: inputRef
})

return (
<Flex gap='2' align='center' width='100%' mb='2' className={styles.root}>
<TextField.Root className={styles.textFieldRoot}>
<TextField.Input
{...inputProps}
ref={inputRef}
mr='1'
type='text'
placeholder='Output file name'
minLength={MIN_FILE_NAME_LENGTH}
maxLength={MAX_FILE_NAME_LENGTH}
value={outputFileName}
onChange={handleChange}
/>

{outputFileName.length > 0 && (
<TextField.Slot>
<ButtonClear onClick={handleClear} />
</TextField.Slot>
)}
</TextField.Root>

<PopoverOutputFileName />
</Flex>
)
}

const Memoized = memo(InputOutputFileName)

export { Memoized as InputOutputFileName }
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.content {
max-width: 370px;
padding: var(--space-2);
}

.content li {
list-style: disc outside;
padding-left: 4px;
}

.content li span {
margin-left: -8px;
}

.spanOptional {
text-decoration: underline;
text-underline-offset: 3px;
}

.contentText {
line-height: 21px;
}

@media screen and (max-width: 520px) {
.content {
max-width: 80dvw;
}

.content li {
list-style: none;
padding: 0;
}

.content li span {
margin-left: 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client'

import { Fragment } from 'react'
import { Code, Popover, Separator, Text } from '@radix-ui/themes'

import { ButtonInfo } from '@ui/ButtonInfo'
import {
MAX_FILE_NAME_LENGTH,
MIN_FILE_NAME_LENGTH,
notAllowedChars
} from '@helpers/isValidFileName'
import styles from './PopoverOutputFileName.module.css'

export function PopoverOutputFileName() {
return (
<Popover.Root>
<Popover.Trigger>
<ButtonInfo />
</Popover.Trigger>

<Popover.Content size='1' side='bottom' align='center' className={styles.content}>
<Text as='p' size='2' className={styles.contentText}>
The{' '}
<Text as='span' className={styles.spanOptional}>
optional
</Text>{' '}
name of the output file{' '}
<Text as='span' weight='medium'>
(without extension)
</Text>
.
</Text>

<Separator orientation='horizontal' size='4' mt='2' mb='1' />

<li>
<Text size='2'>
Not allowed characters:{' '}
{notAllowedChars.map((chars, index) => (
<Fragment key={chars}>
<Code variant='ghost'>{chars}</Code>
{index < notAllowedChars.length - 1 && ' '}
</Fragment>
))}
</Text>
</li>

<li>
<Text size='2'>
Length:{' '}
<Code variant='ghost'>
{MIN_FILE_NAME_LENGTH} - {MAX_FILE_NAME_LENGTH}
</Code>{' '}
characters
</Text>
</li>
</Popover.Content>
</Popover.Root>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PopoverOutputFileName } from './PopoverOutputFileName'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InputOutputFileName } from './InputOutputFileName'
9 changes: 6 additions & 3 deletions src/components/SettingsPanel/TabDefault/options/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { Gamma } from './Gamma'
import { Tint } from './Tint'
import { Normalise } from './Normalise'
import { Format } from './Format'
import { useConvertStore } from '@stores/convert'
import { InputOutputFileName } from '../InputOutputFileName'
import { useOutputStore } from '@stores/output'

const options = [
{ key: 'basic', Component: BasicOptions },
Expand All @@ -32,18 +33,20 @@ const padding: PaddingProps = {
}

export function Options() {
const file = useConvertStore(state => state.file)
const isFileUploaded = useOutputStore(state => state.isFileUploaded())

return (
<Flex direction='column' gap='2' {...padding}>
<InputOutputFileName />

{options.map(({ key, Component }, index) => (
<Fragment key={key}>
<Component />
{index < options.length - 1 && <Separator my='1' size='4' />}
</Fragment>
))}

{file && (
{isFileUploaded && (
<Fragment>
<Separator mt='1' size='4' />
<Format />
Expand Down
4 changes: 2 additions & 2 deletions src/components/UploadedFile/UploadedFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { EnterFullScreenIcon } from '@radix-ui/react-icons'

import { UploadedFileCard } from './UploadedFileCard'
import { UploadedFileLightbox } from './UploadedFileLightbox'
import { useConvertStore } from '@stores/convert'
import { useOutputStore } from '@stores/output'
import styles from './UploadedFile.module.css'

export function UploadedFile(props: Props) {
const [lightboxOpen, setLightboxOpen] = useState(false)

const downloadPayload = useConvertStore(state => state.downloadPayload)
const downloadPayload = useOutputStore(state => state.downloadPayload)

const handleOpenLightbox = () => setLightboxOpen(true)
const handleCloseLightbox = () => setLightboxOpen(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import cn from 'classnames'
import { LoadingSpinner } from '@ui/LoadingSpinner'
import { ButtonDelete } from '@ui/ButtonDelete'
import { ButtonFileUpload } from '@components/ButtonFileUpload'
import { useConvertStore } from '@stores/convert'
import { useOutputStore } from '@stores/output'
import { ALLOWED_IMAGE_FORMATS } from '@libs/Sharp'
import styles from './UploadedFileCard.module.css'

export function UploadedFileCard({ file, isLoading }: Props) {
const setFile = useConvertStore(state => state.setFile)
const removeFile = useConvertStore(state => state.removeFile)
const setFile = useOutputStore(state => state.setFile)
const removeFile = useOutputStore(state => state.removeFile)

const handleRemoveFile = useCallback(() => removeFile(), [removeFile])

Expand Down
7 changes: 3 additions & 4 deletions src/hoc/withFileUploader/withFileUploader.types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { DragEvent, PropsWithChildren } from 'react'

import type { Actions as ConvertActions } from '@stores/convert'

export interface Props extends Pick<ConvertActions, 'setFile'> {
/* eslint no-unused-vars: 0 */
export interface Props {
accept: string
setFile: (file: File | null) => void
}

/* eslint no-unused-vars: 0 */
export type ComponentProps = PropsWithChildren<{
isDragOver: boolean
onClick: VoidFunction
Expand Down
11 changes: 7 additions & 4 deletions src/hooks/useConvertImage.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useCallback } from 'react'

import { useConvertStore } from '@stores/convert'
import { useConvertMutation } from './useConvertMutation'
import { useConvertSettings } from '@stores/hooks/useConvertSettings'
import { useOutputStore } from '@stores/output'

export function useConvertImage() {
const file = useConvertStore(state => state.file)
const file = useOutputStore(state => state.file)
const outputFileName = useOutputStore(state => state.outputFileName)
const isValidOutputFileName = useOutputStore(state => state.isValidOutputFileName())
const convertSettings = useConvertSettings()

const { mutate, ...rest } = useConvertMutation()
Expand All @@ -15,9 +17,10 @@ export function useConvertImage() {

mutate({
file,
settings: convertSettings
settings: convertSettings,
outputFileName: isValidOutputFileName ? outputFileName : null
})
}, [mutate, file, convertSettings])
}, [file, mutate, convertSettings, isValidOutputFileName, outputFileName])

return {
handleConvertImage,
Expand Down
Loading

0 comments on commit 1ff44e1

Please sign in to comment.