Building a Voice and Eye-Controlled To-Do App - Part 2
Tags: webdev, javascript, nextjs, frontend
Published: 2024-03-21 19:23:23

In the last part we created the basic functionality with adding and removing tasks, now let's make the TODO application a bit prettier. I sketched the layout in figma using ChakraUI Figma KIT, here are two themes light and dark which we will implement.

![UI Template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c317mllpkqmx1b97lo2c.png)

First set the dependencies for the icons.
`npm i --save @chakra-ui/icons`

## Write the code

Create a new `Menu` component, which will have control buttons. Right now there is only a theme switch, but in the future we will add buttons for microphone, speakers, camera and possibly biometric authentication.

`src/components/Menu/index.tsx`
```tsx
import { MoonIcon, SunIcon, } from "@chakra-ui/icons"
import { HStack, IconButton, StackProps, useColorMode } from "@chakra-ui/react"

export const Menu: React.FC<StackProps> = (props) => {
    const { colorMode, toggleColorMode } = useColorMode()

    return (
        <HStack justify='end' {...props}>
            <IconButton variant='ghost' aria-label="change theme"
                onClick={toggleColorMode}
                icon={colorMode === 'light' ? <SunIcon color='yellow.500' /> : <MoonIcon />} />
        </HStack>
    )
}
```

Stylize the existing `TaskList` and `TaskCreator` components.

`src/components/TaskList/index.tsx`
```tsx
const TaskCard: React.FC<TaskCardProps> = ({ id, name, onDelete }) => {
    const handleDelete = () => {
        onDelete(id)
    }

    return (
        <Card
            direction={{ base: 'column', sm: 'row' }}
            overflow='hidden'
        >
            <CardBody>
                <Heading size='md'>{name}</Heading>
            </CardBody>
            <IconButton
                aria-label='remove task'
                colorScheme="red"
                height='auto'
                variant='insideCard'
                borderTopLeftRadius='0'
                borderBottomLeftRadius='0'
                icon={<DeleteIcon color="white" />}
                onClick={handleDelete} />
        </Card>
    )
}

export const TaskList = () => {
...
if (error) {
        return (
            <Center>
                <Text color='red' fontSize={'xl'}>
                    {'Network Error'}
                </Text>
            </Center>
        )
    }

    if (isLoadingDelayed) {
        return (
            <Center>
                <Spinner size='xl' />
            </Center>
        )
    }

    return (
        <Stack spacing={4} pr={'8'}>
            {...tasks.map((task, idx) => (
                <TaskCard key={task.id || `${task.name}_${idx}`} {...task} onDelete={handleDeleteTask} />
            ))}
        </Stack>
    )
}
```

`src/components/TaskCreator/index.tsx`
```tsx
return (
        <form onSubmit={handleAddTask} {...props}>
            <Card
                direction={{ base: 'column', sm: 'row' }}
                overflow='hidden'
            >
                <CardBody pt='3' pb='3' pl='3' pr='8'>
                    <InputGroup>
                        <Input variant='outline' placeholder="Input a task" onChange={handleChange} />
                    </InputGroup>
                </CardBody>
                <IconButton
                    type="submit"
                    aria-label='add task'
                    colorScheme="green"
                    height='auto'
                    variant='insideCard'
                    borderTopLeftRadius='0'
                    borderBottomLeftRadius='0'
                    icon={<AddIcon color="white" />} />
            </Card>
        </form>
    )
```

### Basic layout. 
Let's make it so that the task input field was initially a little lower than the first quarter of the screen, and when adding tasks and appearing scroll, there is an opportunity to scroll it up after which it will stick to the top edge and the tasks will go as if under it.

`src/pages/index.tsx`
```tsx
...
      <Container pt='25%' pb='5%' style={{ background: 'inherit' }}>
        <Stack spacing='8' style={{ background: 'inherit' }}>
          <Menu style={{
            position: 'sticky', top: '0rem', zIndex: '999',
            background: 'inherit',
            padding: '0.4rem 0',
            paddingLeft: '1rem',
            marginLeft: '-1rem',
          }} />
          <TaskCreator style={{
            position: 'sticky', top: '3.2rem', zIndex: '999',
            background: colorMode === 'light' ? 'inherit' : 'transparent',
            paddingLeft: '1rem',
            marginLeft: '-1rem',
        }}/>
          <TaskList />
        </Stack>
      </Container>
...
```

There appeared a little magic paddings and margins, they are necessary for that the shadow of cards did not appear when the card behind the input field and menu. Also need to propagate `background: inherit` through all components. A bit clumsy, but anyway.

`src/pages/_app.tsx`
```tsx
...
      <style jsx global>
        {`
          #__next {
            background: inherit;
          }
        `}
      </style>
...
```

### Theme switcher

In the `Menu` component was colorMode variable.     
`const { colorMode, toggleColorMode } = useColorMode()`

Now add the theme information to the Chakra provider.

`src/chakra.tsx`
```tsx
import {
    ChakraProvider,
    cookieStorageManagerSSR,
    localStorageManager,
} from '@chakra-ui/react'
import { GetServerSideProps } from 'next'
import { PropsWithChildren } from 'react'
import theme from './theme'

interface ChakraProps {
    cookies: string
}

export const Chakra: React.FC<PropsWithChildren<ChakraProps>> = ({ cookies, children }) => {
    const colorModeManager =
        typeof cookies === 'string'
            ? cookieStorageManagerSSR(cookies)
            : localStorageManager

    return (
        <ChakraProvider colorModeManager={colorModeManager} theme={theme}>
            {children}
        </ChakraProvider>
    )
}

export const getServerSideProps = (async ({ req }) => {
    return {
        props: {
            cookies: req.headers.cookie ?? '',
        },
    }
}) satisfies GetServerSideProps<ChakraProps>
```
`src/theme.tsx`
```ts
import { extendTheme, type ThemeConfig } from '@chakra-ui/react'

const config: ThemeConfig = {
    initialColorMode: 'dark',
    useSystemColorMode: false,
}

const theme = extendTheme({
    config,
})

export default theme
```

`src/pages/_document.tsx`
```tsx
import theme from "@/theme";
import { ColorModeScript } from "@chakra-ui/react";
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <ColorModeScript initialColorMode={theme.config.initialColorMode} type="cookie"/>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
```

`src/pages/_app.tsx`
```tsx
import type { AppProps } from "next/app";
import { SWRConfig } from "swr";
import { Chakra } from "@/chakra";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <SWRConfig
        value={{
          refreshInterval: 3000,
        }}
      >
        <Chakra cookies={pageProps.cookies}>
          <Component {...pageProps} />
        </Chakra>
      </SWRConfig>
      <style jsx global>
        {`
          #__next {
            background: inherit;
          }
        `}
      </style>
    </>
  )
}
```

`src/pages/index.tsx`
```tsx
...
export { getServerSideProps } from '@/chakra';
```

In order to keep the theme from blinking on page refresh, information about theme stored in cookies, so that Next.js on the pre-render knows what color to use.

And make the color of the buttons in the task card a little bit darker.

`src/theme.ts`
```ts
const theme = extendTheme({
    config,
    components: {
        Button: {
            variants: {
                insideCard: (props: StyleFunctionProps) => {
                    const c = props.theme.colors[props.colorScheme]
                    return {
                        ...props.theme.components.Button.variants.solid(props),
                        background: c['500'],
                    }
                }
            }
        }
    },
})
```
<center><b>Result</b></center>

![result without animations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7n35fr1ro0ndofd68tns.gif)

We are done with basic styling. Thank you for doing this with me. In the next part, we'll add animations for a task list and theme switching.

If you have any questions or suggestions please leave them in the comments.

The full code is available at [GitHub](https://github.com/Gerpea/todo-ai)