-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
M-6 Activity Create UI #12
Changes from 43 commits
237380b
aabcbb9
eb91e11
f62e107
d1f92fb
129db0c
54dad88
cef4e86
5a08291
dc8d608
2aee48d
39fc7ef
774986a
6ae4700
765b074
1f8125f
28657c1
afadc75
7ce5437
b87a8bc
1b13037
10bfead
0c560fd
3d7c8b6
6683985
ca25316
1299b01
94d63a0
4cecdda
067a8f1
128fbc4
551d754
3da07b1
7f83955
56d73d1
a644b0b
fac749d
2be24e0
edd889f
0aa506e
7b8b64a
07cad6f
8d06e7a
11fb113
8f213f6
2596b9a
512eabe
c44b7cf
a90971e
8bb09d9
cf3e1de
c1ca210
2495ae5
d815286
b3de7f1
013d8ba
c5e56e8
5826f5a
e366c06
0d7cbd1
6a0a663
bffa2c7
19481d7
dc61a00
3407a71
8dc5753
dfcb458
105ac44
2f3c1f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import React, { useCallback, useState } from 'react' | ||
import { useDropzone } from 'react-dropzone' | ||
import { useForm } from 'react-hook-form' | ||
import { | ||
Box, | ||
FormControl, | ||
FormLabel, | ||
FormErrorMessage, | ||
Button, | ||
Image, | ||
SimpleGrid, | ||
Text, | ||
Flex | ||
} from '@chakra-ui/react' | ||
import { PrimaryButton } from '@/components/button' | ||
import { CustomDateTimePicker } from '@/components/customDateTimePicker' | ||
import { InputForm, TextareaForm } from '@/components/input' | ||
import { createActivitySchema, createActivityResolver } from '../schema' | ||
|
||
type ValuePiece = Date | null | ||
type Value = ValuePiece | [ValuePiece, ValuePiece] | ||
|
||
export const FormActivity = () => { | ||
const [dateTo, setDateTo] = useState<Value>(null) | ||
const [dateFrom, setDateFrom] = useState<Value>(null) | ||
const [selectedImages, setSelectedImages] = useState<File[]>([]) | ||
const onDrop = useCallback((acceptedFiles: File[]) => { | ||
setSelectedImages((prevImages) => [...prevImages, ...acceptedFiles]) | ||
}, []) | ||
const { getRootProps, getInputProps } = useDropzone({ onDrop }) | ||
function removeImage(index: number) { | ||
setSelectedImages((prevImages) => { | ||
const newImages = [...prevImages] | ||
newImages.splice(index, 1) | ||
return newImages | ||
}) | ||
} | ||
|
||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors } | ||
} = useForm<createActivitySchema>({ | ||
resolver: createActivityResolver | ||
}) | ||
|
||
const createHandler = handleSubmit(async (data: createActivitySchema) => { | ||
console.log(data) | ||
// TODO append 'images', 'date from', 'date to' to formData and send to backend | ||
}) | ||
|
||
console.log('date to', dateTo, 'date from', dateFrom) | ||
|
||
return ( | ||
<Box as="form" onSubmit={createHandler} pt={{ base: '40px', md: '40px' }}> | ||
<FormControl isRequired isInvalid={!!errors.title}> | ||
<FormLabel>Title</FormLabel> | ||
<InputForm | ||
type="text" | ||
placeholder="Asakusa Temple" | ||
{...register('title')} | ||
/> | ||
{errors.title && ( | ||
<FormErrorMessage>{errors.title.message}</FormErrorMessage> | ||
)} | ||
</FormControl> | ||
|
||
<FormControl mt={{ base: '30px', md: '40px' }}> | ||
<FormLabel>Time From</FormLabel> | ||
<CustomDateTimePicker onChange={setDateFrom} value={dateFrom} /> | ||
</FormControl> | ||
|
||
<FormControl mt={{ base: '30px', md: '40px' }}> | ||
<FormLabel>Time To</FormLabel> | ||
<CustomDateTimePicker onChange={setDateTo} value={dateTo} /> | ||
</FormControl> | ||
|
||
<FormControl | ||
mt={{ base: '30px', md: '40px' }} | ||
isInvalid={!!errors.address} | ||
> | ||
<FormLabel>Address</FormLabel> | ||
<InputForm | ||
{...register('address')} | ||
type="text" | ||
placeholder="10-10 Shibuya, Tokyo, Japan" | ||
/> | ||
{errors.address && ( | ||
<FormErrorMessage>{errors.address.message}</FormErrorMessage> | ||
)} | ||
</FormControl> | ||
|
||
<FormControl isInvalid={!!errors.url} mt={{ base: '30px', md: '40px' }}> | ||
<FormLabel>URL</FormLabel> | ||
<InputForm | ||
{...register('url')} | ||
type="url" | ||
placeholder="https://www.google.com" | ||
/> | ||
{errors.url && ( | ||
<FormErrorMessage>{errors.url.message}</FormErrorMessage> | ||
)} | ||
</FormControl> | ||
|
||
<Text | ||
mt={{ base: '30px', md: '40px' }} | ||
mb={selectedImages.length !== 0 ? { base: '30px', md: '40px' } : '8px'} | ||
fontWeight="medium" | ||
> | ||
Image | ||
</Text> | ||
<SimpleGrid columns={{ base: 2, md: 3 }} spacing={4}> | ||
{selectedImages.map((image, index) => ( | ||
<Image | ||
key={index} | ||
src={URL.createObjectURL(image)} | ||
alt={`Selected Image ${image.name}`} | ||
objectFit="cover" | ||
width={{ base: '160px', md: '180px' }} | ||
height={{ base: '106px', md: '120px' }} | ||
margin="auto" | ||
onClick={() => removeImage(index)} | ||
/> | ||
))} | ||
</SimpleGrid> | ||
|
||
<Box | ||
{...getRootProps()} | ||
textAlign="center" | ||
mt={{ base: 0, md: selectedImages.length !== 0 ? '40px' : '0' }} | ||
> | ||
<PrimaryButton variant="outline"> | ||
<input {...getInputProps()} /> | ||
Add Image | ||
</PrimaryButton> | ||
</Box> | ||
|
||
<FormControl isInvalid={!!errors.memo} mt={{ base: '30px', md: '40px' }}> | ||
<FormLabel>Memo</FormLabel> | ||
<TextareaForm | ||
{...register('memo')} | ||
name="memo" | ||
placeholder="Type here..." | ||
/> | ||
{errors.memo && ( | ||
<FormErrorMessage>{errors.memo.message}</FormErrorMessage> | ||
)} | ||
</FormControl> | ||
|
||
<FormControl isInvalid={!!errors.cost} mt={{ base: '30px', md: '40px' }}> | ||
<FormLabel>Cost</FormLabel> | ||
<InputForm {...register('cost')} type="text" placeholder="$200" /> | ||
{errors.cost && ( | ||
<FormErrorMessage>{errors.cost.message}</FormErrorMessage> | ||
)} | ||
</FormControl> | ||
|
||
<Flex justifyContent="center" mt={{ base: '30px', md: '40px' }}> | ||
<Button colorScheme="teal" type="submit" margin="auto"> | ||
Create Activity | ||
</Button> | ||
</Flex> | ||
</Box> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { FormActivity } from './form' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
'use client' | ||
|
||
import { Box, Container, Heading, useColorModeValue } from '@chakra-ui/react' | ||
import { Header, Footer } from '@/components/navigation' | ||
import { FormActivity } from './components' | ||
|
||
export default function CreateActivityPage() { | ||
const bg = useColorModeValue('white', 'gray.800') | ||
const color = useColorModeValue('black', 'gray.300') | ||
|
||
return ( | ||
<> | ||
<Box as="main" minH="100vh" bg={bg} color={color}> | ||
<Header /> | ||
<Container | ||
pt={{ base: '20px', md: '40px' }} | ||
pb={{ base: '40px', md: '70px' }} | ||
> | ||
<Heading as={'h1'} fontSize={{ base: '2xl', md: '4xl' }}> | ||
Create Activity | ||
</Heading> | ||
<FormActivity /> | ||
</Container> | ||
<Footer /> | ||
</Box> | ||
</> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { zodResolver } from '@hookform/resolvers/zod' | ||
import * as z from 'zod' | ||
|
||
// TODO: refactor zod validation | ||
|
||
const createActivitySchema = z.object({ | ||
title: z.string().min(1).max(20).optional(), | ||
timeFrom: z.string().optional(), | ||
timeTo: z.string().optional(), | ||
address: z.string().min(0).max(50).optional(), | ||
url: z.union([z.string().url().nullish(), z.literal('')]), | ||
memo: z.string().min(0).max(300).optional(), | ||
cost: z.string().min(0).max(15).optional() | ||
}) | ||
|
||
export type createActivitySchema = z.infer<typeof createActivitySchema> | ||
export const createActivityResolver = zodResolver(createActivitySchema) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { DateTimePicker as ReactDateTimePicker } from 'react-datetime-picker' | ||
import { useColorMode } from '@chakra-ui/react' | ||
import { MdCalendarToday, MdClose } from 'react-icons/md' | ||
import 'react-clock/dist/Clock.css' | ||
import 'react-datetime-picker/dist/DateTimePicker.css' | ||
import 'react-calendar/dist/Calendar.css' | ||
|
||
type ValuePiece = Date | null | ||
type Value = ValuePiece | [ValuePiece, ValuePiece] | ||
|
||
export const CustomDateTimePicker = ({ | ||
onChange, | ||
value | ||
}: { | ||
onChange: (value: Value) => void | ||
value: Value | ||
}) => { | ||
const { colorMode } = useColorMode() | ||
|
||
return ( | ||
<> | ||
<style>{` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Screen.Recording.2024-01-04.at.16.34.59.movIt's fixed :) |
||
|
||
.react-calendar{ | ||
margin-top: 12%; | ||
} | ||
.react-calendar__tile--now { | ||
background-color: ${colorMode === 'dark' ? '#2D3748' : '#CBD5E0'}; | ||
} | ||
|
||
.react-calendar__tile--active:enabled:hover { | ||
background-color: ${colorMode === 'dark' && '#086F83'}; | ||
} | ||
|
||
.react-calendar__navigation__label:focus { | ||
color: ${colorMode === 'dark' && 'black'}; | ||
} | ||
|
||
.react-calendar__tile:enabled:hover { | ||
color: ${colorMode === 'dark' && 'black'}; | ||
} | ||
|
||
.react-calendar__navigation__arrow:focus, | ||
.react-calendar__navigation__arrow:hover, | ||
.react-calendar__navigation__label__labelText:hover { | ||
color: ${colorMode === 'dark' && 'black'}; | ||
} | ||
|
||
.react-calendar { | ||
background-color: ${colorMode === 'dark' && '#1a202c'}; | ||
} | ||
|
||
.react-datetime-picker { | ||
width: 100%; | ||
height: 2.5rem; | ||
} | ||
|
||
.react-datetime-picker__wrapper { | ||
background-color: ${colorMode === 'dark' ? '#2d3748' : '#f8f8f8'}; | ||
border: 1px solid ${colorMode === 'dark' ? '#718096' : '#CBD5E0'}; | ||
border-radius: 0.375rem; | ||
} | ||
|
||
.react-calendar__tile--active { | ||
background-color: #086F83; | ||
} | ||
|
||
.react-calendar__tile--hasActive, | ||
.react-calendar__tile--hasActive:enabled:hover { | ||
background: ${colorMode === 'dark' ? 'white' : '#0987a0'}; | ||
color: ${colorMode === 'dark' ? 'black' : 'white'}; | ||
} | ||
|
||
.react-calendar__navigation button:disabled { | ||
background: ${colorMode === 'dark' ? 'white' : '#0987a0'}; | ||
} | ||
|
||
.react-calendar__navigation button:enabled:hover { | ||
background: ${colorMode === 'dark' ? 'white' : '#0987a0'}; | ||
color: ${colorMode === 'dark' ? 'black' : 'white'}; | ||
} | ||
|
||
.react-datetime-picker__inputGroup__input:invalid { | ||
background: ${colorMode === 'dark' ? '#2d3748' : '#f8f8f8'}; | ||
} | ||
|
||
.react-calendar__tile--now:enabled:hover { | ||
background-color: ${colorMode === 'dark' ? '#CBD5E0' : '#EDF2F7'}; | ||
} | ||
|
||
.react-datetime-picker__wrapper{ | ||
position:relative; | ||
} | ||
|
||
.react-datetime-picker__calendar--open { | ||
inset:0 !important; | ||
} | ||
|
||
.react-datetime-picker__inputGroup{ | ||
padding:0 16px; | ||
} | ||
|
||
`}</style> | ||
|
||
<div style={{ position: 'relative' }}> | ||
<ReactDateTimePicker | ||
onChange={onChange} | ||
value={value} | ||
calendarIcon={<MdCalendarToday />} | ||
clearIcon={<MdClose />} | ||
format={'y-MM-dd h:mm a'} | ||
/> | ||
</div> | ||
</> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { CustomDateTimePicker } from './dateTimePickerWrapper' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { StoryObj, Meta } from '@storybook/react' | ||
import { CustomDateTimePicker } from '../index' // Adjust the import path accordingly | ||
|
||
const meta: Meta<typeof CustomDateTimePicker> = { | ||
title: 'CustomDateTimePicker', | ||
component: CustomDateTimePicker | ||
} | ||
|
||
export default meta | ||
|
||
type Story = StoryObj<typeof CustomDateTimePicker> | ||
|
||
export const Default: Story = { | ||
args: { | ||
onChange: (value) => console.log(value), | ||
value: new Date() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export { InputSearch } from './input-search' | ||
export { InputForm } from './input-form' | ||
export { TextareaForm } from './textarea-form' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { | ||
Input as ChakraFormInput, | ||
InputProps as ChakraInputProps, | ||
InputGroup, | ||
forwardRef, | ||
useColorModeValue | ||
} from '@chakra-ui/react' | ||
|
||
export const InputForm = forwardRef( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you also add to storybook as this is the UI for everywhere you can use? Textarea is same! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created a storybook for the custom date time picker, input form, text area as well as input search! |
||
(props: ChakraInputProps, ref: React.Ref<HTMLInputElement>) => { | ||
const bgColor = useColorModeValue('white', 'gray.700') | ||
const borderColor = useColorModeValue('gray.300', 'gray.500') | ||
const placeholdercolor = useColorModeValue('gray.400', 'gray.600') | ||
|
||
return ( | ||
<InputGroup minW={{ base: '100%', md: '23.75rem' }}> | ||
<ChakraFormInput | ||
{...props} | ||
ref={ref} | ||
focusBorderColor={'primary.600'} | ||
borderColor={borderColor} | ||
bgColor={bgColor} | ||
_placeholder={{ | ||
color: placeholdercolor | ||
}} | ||
/> | ||
</InputGroup> | ||
) | ||
} | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have made the title field required so that people can quickly add a new activity and add more information later.
I am unsure about how we are handling images. Are we storing images in the cloud and storing their CDN in the database? Currently, once images are added and the submit button is clicked, it stores image information like the example below: