From 7b5f2b8633dcda0ae2a2c6c81b08eaebd4e57950 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 10 Feb 2024 22:26:02 -0800 Subject: [PATCH 01/17] fix: use ActivityForm Component in create/edit pages --- .../[id]/components/activity-header.tsx | 7 +- app/activity/[id]/edit/page.tsx | 79 +++++++ .../graphql/query/activityCollection.graphql | 10 +- app/activity/[id]/page.tsx | 10 +- app/activity/components/activity-form.tsx | 216 ++++++++++++++++++ app/activity/components/index.ts | 1 + app/activity/create/page.tsx | 4 +- app/activity/schema.ts | 25 ++ graphql-codegen/generated/api.ts | 26 ++- graphql-codegen/generated/gql.ts | 6 +- graphql-codegen/generated/graphql.ts | 82 ++++++- .../generated/persisted-documents.json | 2 +- 12 files changed, 454 insertions(+), 14 deletions(-) create mode 100644 app/activity/[id]/edit/page.tsx create mode 100644 app/activity/components/activity-form.tsx create mode 100644 app/activity/components/index.ts create mode 100644 app/activity/schema.ts diff --git a/app/activity/[id]/components/activity-header.tsx b/app/activity/[id]/components/activity-header.tsx index 81b1219..ae4e327 100644 --- a/app/activity/[id]/components/activity-header.tsx +++ b/app/activity/[id]/components/activity-header.tsx @@ -1,12 +1,14 @@ 'use client' import { Heading, Box, Flex, Spacer, Text, IconButton } from '@chakra-ui/react' +import { useRouter } from 'next/navigation' import { FiClock, FiMapPin, FiLink2, FiEdit3, FiTrash2 } from 'react-icons/fi' import { Link } from '@/components/link' import { formatToDateTime } from '@/libs/utils' import { customColors } from '@/theme/color' type ActivityHeaderProps = { + id: string title: string time_from: string | null time_to?: string | null @@ -15,12 +17,15 @@ type ActivityHeaderProps = { } export const ActivityHeader = ({ + id, title, time_from, time_to, address, url }: ActivityHeaderProps) => { + const router = useRouter() + return ( <> @@ -31,7 +36,7 @@ export const ActivityHeader = ({ {}} + onClick={() => router.push(`/activity/${id}/edit`)} p={{ base: '6px', md: '10px' }} > diff --git a/app/activity/[id]/edit/page.tsx b/app/activity/[id]/edit/page.tsx new file mode 100644 index 0000000..8858c2f --- /dev/null +++ b/app/activity/[id]/edit/page.tsx @@ -0,0 +1,79 @@ +'use client' + +import { NetworkStatus } from '@apollo/client' +import { Box, Container, Heading, useColorModeValue } from '@chakra-ui/react' +import { ActivityForm } from '@/activity/components/activity-form' +import { Loading } from '@/components/loading' +import { useActivityCollectionQuery } from '@generated/api' + +export default function ActivityEditPage({ + params +}: { + params: { id: string } +}) { + const bg = useColorModeValue('white', 'gray.800') + const color = useColorModeValue('black', 'gray.300') + + // Activity Details + const { + data: activityData, + loading: activityLoading, + refetch: activityRefetch, + networkStatus: activityNetWorkStatus + } = useActivityCollectionQuery({ + variables: { + id: params.id + }, + notifyOnNetworkStatusChange: true + }) + + const dummyUrls: string[] = [ + 'https://images.unsplash.com/photo-1612852098516-55d01c75769a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDR8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60', + 'https://images.unsplash.com/photo-1627875764093-315831ac12f7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60', + 'https://images.unsplash.com/photo-1571432248690-7fd6980a1ae2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDl8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60' + ] + + const activityDataCollection = + activityData?.activityCollection?.edges[0]?.node + const activityDetailsRefetch = () => { + activityRefetch() + } + const activityDetailsRefetchLoading = + activityNetWorkStatus === NetworkStatus.refetch + + if (!activityData && !activityLoading) + throw new Error('No activity data found') + + return ( + + + + Edit Activity + + {!activityDataCollection || activityLoading ? ( + + ) : ( + + )} + + + ) +} diff --git a/app/activity/[id]/graphql/query/activityCollection.graphql b/app/activity/[id]/graphql/query/activityCollection.graphql index 1f9984e..c6b9ce9 100644 --- a/app/activity/[id]/graphql/query/activityCollection.graphql +++ b/app/activity/[id]/graphql/query/activityCollection.graphql @@ -12,7 +12,15 @@ query activityCollection($id: UUID!) { memo cost cost_unit - image_url + activity_uploaded_filesCollection { + edges { + node { + id + file_name + file_url + } + } + } } } } diff --git a/app/activity/[id]/page.tsx b/app/activity/[id]/page.tsx index 6621dfb..4e1f8c8 100644 --- a/app/activity/[id]/page.tsx +++ b/app/activity/[id]/page.tsx @@ -30,6 +30,11 @@ export default function ActivityDetails({ if (!loading && !data) throw new Error('No activity data found') const activityData = data?.activityCollection?.edges[0]?.node + const uploadedImageUrls = activityData?.activity_uploaded_filesCollection + ? activityData?.activity_uploaded_filesCollection?.edges.map( + (edge) => edge?.node?.file_url || '' + ) + : [] return ( @@ -44,6 +49,7 @@ export default function ActivityDetails({ pb={{ base: '40px', md: '80px' }} > {/* TODO: Fetch images from the database once available. */} - + 0 ? uploadedImageUrls : dummyUrls} + /> diff --git a/app/activity/components/activity-form.tsx b/app/activity/components/activity-form.tsx new file mode 100644 index 0000000..902b882 --- /dev/null +++ b/app/activity/components/activity-form.tsx @@ -0,0 +1,216 @@ +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 { extractTimeFromDate } from '@/libs/utils' +import { activitySchema, activityResolver } from '../schema' + +type ValuePiece = Date | null +type Value = ValuePiece | [ValuePiece, ValuePiece] + +export type ActivityDetails = { + id: string + title: string + time_from: string | null + time_to?: string | null + address?: string | null + url?: string | null + memo?: string | null + cost?: string | null + cost_unit?: string | null + image_urls?: string[] + refetch: () => void + refetchLoading: boolean +} + +type ActivityFormProps = { + activityDetails?: ActivityDetails +} + +export const ActivityForm = ({ activityDetails }: ActivityFormProps) => { + const [timeTo, setDateTo] = useState(null) + const [timeFrom, setDateFrom] = useState(null) + const [selectedImages, setSelectedImages] = useState([]) + 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({ + defaultValues: { + title: activityDetails?.title, + timeFrom: activityDetails?.time_from + ? extractTimeFromDate(activityDetails?.time_from) + : undefined, + timeTo: activityDetails?.time_to + ? extractTimeFromDate(activityDetails?.time_to) + : undefined, + address: activityDetails?.address || undefined, + url: activityDetails?.url || undefined, + memo: activityDetails?.memo || undefined, + cost: activityDetails?.cost || undefined, + costUnit: activityDetails?.cost_unit || undefined, + imageUrls: activityDetails?.image_urls || undefined + }, + resolver: activityResolver + }) + + const createHandler = handleSubmit(async (data: activitySchema) => { + console.log(data) + // TODO append 'images', 'date from', 'date to' to formData and send to backend + }) + + return ( + + + Title + + {errors.title && ( + {errors.title.message} + )} + + + + Time From + + + + + Time To + + + + + Address + + {errors.address && ( + {errors.address.message} + )} + + + + URL + + {errors.url && ( + {errors.url.message} + )} + + + + Image + + + {activityDetails?.image_urls?.map((url, index) => ( + {`Uploaded + ))} + {selectedImages.map((image, index) => ( + {`Selected removeImage(index)} + /> + ))} + + + 0 || + selectedImages.length !== 0 + ? '40px' + : '0' + }} + > + + + Add Image + + + + + Memo + + {errors.memo && ( + {errors.memo.message} + )} + + + + Cost + + {errors.cost && ( + {errors.cost.message} + )} + + + + + + + ) +} diff --git a/app/activity/components/index.ts b/app/activity/components/index.ts new file mode 100644 index 0000000..7213fb2 --- /dev/null +++ b/app/activity/components/index.ts @@ -0,0 +1 @@ +export { ActivityForm } from './activity-form' diff --git a/app/activity/create/page.tsx b/app/activity/create/page.tsx index 335dbb3..6202d81 100644 --- a/app/activity/create/page.tsx +++ b/app/activity/create/page.tsx @@ -1,7 +1,7 @@ 'use client' import { Box, Container, Heading, useColorModeValue } from '@chakra-ui/react' -import { FormActivity } from './components' +import { ActivityForm } from '../components' export default function CreateActivityPage() { const bg = useColorModeValue('white', 'gray.800') @@ -16,7 +16,7 @@ export default function CreateActivityPage() { Create Activity - + ) diff --git a/app/activity/schema.ts b/app/activity/schema.ts new file mode 100644 index 0000000..4cc2011 --- /dev/null +++ b/app/activity/schema.ts @@ -0,0 +1,25 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import * as z from 'zod' + +const activitySchema = 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(), + costUnit: z.string().min(0).max(15).optional(), + imageUrls: z.array(z.string()).optional() +}) + +export type activitySchema = z.infer +export const activityResolver = zodResolver(activitySchema) + +const imagesSchema = z.object({ + urls: z.array(z.string()), + uploadedFiles: z.array(z.instanceof(File)) +}) + +export type ImagesSchema = z.infer +export const imagesResolver = zodResolver(imagesSchema) diff --git a/graphql-codegen/generated/api.ts b/graphql-codegen/generated/api.ts index 8fb224a..510e0d8 100644 --- a/graphql-codegen/generated/api.ts +++ b/graphql-codegen/generated/api.ts @@ -1325,7 +1325,18 @@ export type ActivityCollectionQuery = { memo?: string | null cost?: string | null cost_unit?: string | null - image_url?: string | null + activity_uploaded_filesCollection?: { + __typename: 'activity_uploaded_filesConnection' + edges: Array<{ + __typename: 'activity_uploaded_filesEdge' + node: { + __typename: 'activity_uploaded_files' + id: string + file_name: string + file_url: string + } + }> + } | null } }> } | null @@ -1692,7 +1703,18 @@ export const ActivityCollectionDocument = gql` memo cost cost_unit - image_url + activity_uploaded_filesCollection { + __typename + edges { + __typename + node { + __typename + id + file_name + file_url + } + } + } } } } diff --git a/graphql-codegen/generated/gql.ts b/graphql-codegen/generated/gql.ts index 5136b13..a45d40b 100644 --- a/graphql-codegen/generated/gql.ts +++ b/graphql-codegen/generated/gql.ts @@ -15,7 +15,7 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- const documents = { 'query getUser($id: UUID!) {\n usersCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n email\n name\n profile_picture_url\n }\n }\n }\n}': types.GetUserDocument, - 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n image_url\n }\n }\n }\n}': + 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}': types.ActivityCollectionDocument, 'mutation createActivity($trip_id: UUID!, $title: String!, $time_from: Datetime, $time_to: Datetime, $address: String, $url: String, $memo: String, $cost: BigFloat, $cost_unit: String, $image_url: String) {\n insertIntoactivityCollection(\n objects: [{trip_id: $trip_id, title: $title, time_from: $time_from, time_to: $time_to, address: $address, url: $url, memo: $memo, cost: $cost, cost_unit: $cost_unit, image_url: $image_url}]\n ) {\n records {\n __typename\n id\n title\n }\n }\n}': types.CreateActivityDocument, @@ -67,8 +67,8 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n image_url\n }\n }\n }\n}' -): (typeof documents)['query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n image_url\n }\n }\n }\n}'] + source: 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}' +): (typeof documents)['query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}'] /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/graphql-codegen/generated/graphql.ts b/graphql-codegen/generated/graphql.ts index 1f9ad04..07c7abb 100644 --- a/graphql-codegen/generated/graphql.ts +++ b/graphql-codegen/generated/graphql.ts @@ -1333,7 +1333,18 @@ export type ActivityCollectionQuery = { memo?: string | null cost?: string | null cost_unit?: string | null - image_url?: string | null + activity_uploaded_filesCollection?: { + __typename: 'activity_uploaded_filesConnection' + edges: Array<{ + __typename: 'activity_uploaded_filesEdge' + node: { + __typename: 'activity_uploaded_files' + id: string + file_name: string + file_url: string + } + }> + } | null } }> } | null @@ -1715,7 +1726,7 @@ export const GetUserDocument = { ] } as unknown as DocumentNode export const ActivityCollectionDocument = { - __meta__: { hash: '5ba311a19d53ac42096cb55d1830aaf08f96fccf' }, + __meta__: { hash: '3e04fac286fc4b6bc6c6d91483ca2b248b25bad6' }, kind: 'Document', definitions: [ { @@ -1833,7 +1844,72 @@ export const ActivityCollectionDocument = { }, { kind: 'Field', - name: { kind: 'Name', value: 'image_url' } + name: { + kind: 'Name', + value: 'activity_uploaded_filesCollection' + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'edges' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { + kind: 'Name', + value: '__typename' + } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'node' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { + kind: 'Name', + value: '__typename' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'id' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'file_name' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'file_url' + } + } + ] + } + } + ] + } + } + ] + } } ] } diff --git a/graphql-codegen/generated/persisted-documents.json b/graphql-codegen/generated/persisted-documents.json index 26cadb0..ceef4bc 100644 --- a/graphql-codegen/generated/persisted-documents.json +++ b/graphql-codegen/generated/persisted-documents.json @@ -1,6 +1,6 @@ { "00c528b2b1c851c06119aedb1fba6b5c885713cf": "query getUser($id: UUID!) { __typename usersCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename email id name profile_picture_url } } } }", - "5ba311a19d53ac42096cb55d1830aaf08f96fccf": "query activityCollection($id: UUID!) { __typename activityCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename address cost cost_unit id image_url memo time_from time_to title trip_id url } } } }", + "3e04fac286fc4b6bc6c6d91483ca2b248b25bad6": "query activityCollection($id: UUID!) { __typename activityCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename activity_uploaded_filesCollection { __typename edges { __typename node { __typename file_name file_url id } } } address cost cost_unit id memo time_from time_to title trip_id url } } } }", "22e6378e9a0ec7a9c28a6430981dcfebf7820216": "mutation createActivity($address: String, $cost: BigFloat, $cost_unit: String, $image_url: String, $memo: String, $time_from: Datetime, $time_to: Datetime, $title: String!, $trip_id: UUID!, $url: String) { __typename insertIntoactivityCollection( objects: [{trip_id: $trip_id, title: $title, time_from: $time_from, time_to: $time_to, address: $address, url: $url, memo: $memo, cost: $cost, cost_unit: $cost_unit, image_url: $image_url}] ) { __typename records { __typename id title } } }", "9ecd6081f83febe06337ebd01345eef596b81cf9": "mutation createTag($name: String!, $userId: UUID!) { __typename insertIntotagsCollection(objects: [{name: $name, user_id: $userId}]) { __typename records { __typename id name } } }", "68140ac8465fd7c6f5bab39db9eec02dab9501ef": "mutation createTrip($object: tripsInsertInput!) { __typename insertIntotripsCollection(objects: [$object]) { __typename records { __typename id title } } }", From 6ab649991e3908ae41c3ed51592a3679a719a85f Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 16 Feb 2024 11:43:22 -0800 Subject: [PATCH 02/17] Add GraphQL mutations and queries for activity creation, update, and file upload --- .../graphql/mutation/createActivity.graphql | 35 -- .../graphql/mutation/createActivity.graphql | 9 + .../createActivityUploadedFiles.graphql | 13 + .../graphql/mutation/updateActivity.graphql | 8 + .../graphql/query/activityCollection.graphql | 0 graphql-codegen/generated/api.ts | 365 +++++++---- graphql-codegen/generated/gql.ts | 28 +- graphql-codegen/generated/graphql.ts | 592 ++++++++++-------- .../generated/persisted-documents.json | 4 +- 9 files changed, 625 insertions(+), 429 deletions(-) delete mode 100644 app/activity/create/graphql/mutation/createActivity.graphql create mode 100644 app/activity/graphql/mutation/createActivity.graphql create mode 100644 app/activity/graphql/mutation/createActivityUploadedFiles.graphql create mode 100644 app/activity/graphql/mutation/updateActivity.graphql rename app/activity/{[id] => }/graphql/query/activityCollection.graphql (100%) diff --git a/app/activity/create/graphql/mutation/createActivity.graphql b/app/activity/create/graphql/mutation/createActivity.graphql deleted file mode 100644 index d77e189..0000000 --- a/app/activity/create/graphql/mutation/createActivity.graphql +++ /dev/null @@ -1,35 +0,0 @@ -mutation createActivity( - $trip_id: UUID! - $title: String! - $time_from: Datetime - $time_to: Datetime - $address: String - $url: String - $memo: String - $cost: BigFloat - $cost_unit: String - $image_url: String -) { - insertIntoactivityCollection( - objects: [ - { - trip_id: $trip_id - title: $title - time_from: $time_from - time_to: $time_to - address: $address - url: $url - memo: $memo - cost: $cost - cost_unit: $cost_unit - image_url: $image_url - } - ] - ) { - records { - __typename - id - title - } - } -} diff --git a/app/activity/graphql/mutation/createActivity.graphql b/app/activity/graphql/mutation/createActivity.graphql new file mode 100644 index 0000000..5164526 --- /dev/null +++ b/app/activity/graphql/mutation/createActivity.graphql @@ -0,0 +1,9 @@ +mutation createActivity($object: activityInsertInput!) { + insertIntoactivityCollection(objects: [$object]) { + records { + __typename + id + title + } + } +} diff --git a/app/activity/graphql/mutation/createActivityUploadedFiles.graphql b/app/activity/graphql/mutation/createActivityUploadedFiles.graphql new file mode 100644 index 0000000..87ad0af --- /dev/null +++ b/app/activity/graphql/mutation/createActivityUploadedFiles.graphql @@ -0,0 +1,13 @@ +mutation createActivityUploadedFiles( + $objects: [activity_uploaded_filesInsertInput!]! +) { + insertIntoactivity_uploaded_filesCollection(objects: $objects) { + records { + __typename + id + activity_id + file_name + file_url + } + } +} diff --git a/app/activity/graphql/mutation/updateActivity.graphql b/app/activity/graphql/mutation/updateActivity.graphql new file mode 100644 index 0000000..2b94a5c --- /dev/null +++ b/app/activity/graphql/mutation/updateActivity.graphql @@ -0,0 +1,8 @@ +mutation updateActivity($id: UUID!, $set: activityUpdateInput!) { + updateactivityCollection(set: $set, filter: { id: { eq: $id } }) { + records { + id + title + } + } +} diff --git a/app/activity/[id]/graphql/query/activityCollection.graphql b/app/activity/graphql/query/activityCollection.graphql similarity index 100% rename from app/activity/[id]/graphql/query/activityCollection.graphql rename to app/activity/graphql/query/activityCollection.graphql diff --git a/graphql-codegen/generated/api.ts b/graphql-codegen/generated/api.ts index 510e0d8..2fef735 100644 --- a/graphql-codegen/generated/api.ts +++ b/graphql-codegen/generated/api.ts @@ -515,8 +515,8 @@ export type Activity = Node & { time_from: Scalars['Datetime']['output'] time_to?: Maybe title: Scalars['String']['output'] - trip_id?: Maybe - trips?: Maybe + trip_id: Scalars['UUID']['output'] + trips: Trips url?: Maybe } @@ -628,9 +628,9 @@ export type ActivityUpdateResponse = { export type Activity_Uploaded_Files = Node & { __typename?: 'activity_uploaded_files' - activity?: Maybe - activity_id?: Maybe - content_type: Scalars['String']['output'] + activity: Activity + activity_id: Scalars['UUID']['output'] + content_type?: Maybe created_at: Scalars['Datetime']['output'] file_data?: Maybe file_name: Scalars['String']['output'] @@ -974,10 +974,10 @@ export type Trip_Tags = Node & { id: Scalars['UUID']['output'] /** Globally Unique Record Identifier */ nodeId: Scalars['ID']['output'] - tag_id?: Maybe - tags?: Maybe - trip_id?: Maybe - trips?: Maybe + tag_id: Scalars['UUID']['output'] + tags: Tags + trip_id: Scalars['UUID']['output'] + trips: Trips } export type Trip_TagsConnection = { @@ -1057,8 +1057,8 @@ export type Trips = Node & { nodeId: Scalars['ID']['output'] title: Scalars['String']['output'] trip_tagsCollection?: Maybe - user_id?: Maybe - users?: Maybe + user_id: Scalars['UUID']['output'] + users: Users } export type TripsActivityCollectionArgs = { @@ -1303,6 +1303,51 @@ export type GetUserQuery = { } | null } +export type CreateActivityMutationVariables = Exact<{ + object: ActivityInsertInput +}> + +export type CreateActivityMutation = { + __typename: 'Mutation' + insertIntoactivityCollection?: { + __typename: 'activityInsertResponse' + records: Array<{ __typename: 'activity'; id: string; title: string }> + } | null +} + +export type CreateActivityUploadedFilesMutationVariables = Exact<{ + objects: + | Array + | Activity_Uploaded_FilesInsertInput +}> + +export type CreateActivityUploadedFilesMutation = { + __typename: 'Mutation' + insertIntoactivity_uploaded_filesCollection?: { + __typename: 'activity_uploaded_filesInsertResponse' + records: Array<{ + __typename: 'activity_uploaded_files' + id: string + activity_id: string + file_name: string + file_url: string + }> + } | null +} + +export type UpdateActivityMutationVariables = Exact<{ + id: Scalars['UUID']['input'] + set: ActivityUpdateInput +}> + +export type UpdateActivityMutation = { + __typename: 'Mutation' + updateactivityCollection: { + __typename: 'activityUpdateResponse' + records: Array<{ __typename: 'activity'; id: string; title: string }> + } +} + export type ActivityCollectionQueryVariables = Exact<{ id: Scalars['UUID']['input'] }> @@ -1316,7 +1361,7 @@ export type ActivityCollectionQuery = { node: { __typename: 'activity' id: string - trip_id?: string | null + trip_id: string title: string time_from: string time_to?: string | null @@ -1342,27 +1387,6 @@ export type ActivityCollectionQuery = { } | null } -export type CreateActivityMutationVariables = Exact<{ - trip_id: Scalars['UUID']['input'] - title: Scalars['String']['input'] - time_from?: InputMaybe - time_to?: InputMaybe - address?: InputMaybe - url?: InputMaybe - memo?: InputMaybe - cost?: InputMaybe - cost_unit?: InputMaybe - image_url?: InputMaybe -}> - -export type CreateActivityMutation = { - __typename: 'Mutation' - insertIntoactivityCollection?: { - __typename: 'activityInsertResponse' - records: Array<{ __typename: 'activity'; id: string; title: string }> - } | null -} - export type CreateTagMutationVariables = Exact<{ name: Scalars['String']['input'] userId: Scalars['UUID']['input'] @@ -1400,8 +1424,8 @@ export type CreateTripTagMutation = { records: Array<{ __typename: 'trip_tags' id: string - tag_id?: string | null - trip_id?: string | null + tag_id: string + trip_id: string }> } | null } @@ -1524,7 +1548,7 @@ export type TripDetailsQuery = { __typename: 'trip_tagsEdge' node: { __typename: 'trip_tags' - tags?: { __typename: 'tags'; id: string; name: string } | null + tags: { __typename: 'tags'; id: string; name: string } } }> } | null @@ -1546,8 +1570,8 @@ export type TripTagsCollectionQuery = { node: { __typename: 'trip_tags' id: string - trip_id?: string | null - tag_id?: string | null + trip_id: string + tag_id: string } }> } | null @@ -1684,6 +1708,180 @@ export type GetUserQueryResult = Apollo.QueryResult< export function refetchGetUserQuery(variables: GetUserQueryVariables) { return { query: GetUserDocument, variables: variables } } +export const CreateActivityDocument = gql` + mutation createActivity($object: activityInsertInput!) { + __typename + insertIntoactivityCollection(objects: [$object]) { + __typename + records { + __typename + id + title + } + } + } +` +export type CreateActivityMutationFn = Apollo.MutationFunction< + CreateActivityMutation, + CreateActivityMutationVariables +> + +/** + * __useCreateActivityMutation__ + * + * To run a mutation, you first call `useCreateActivityMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateActivityMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createActivityMutation, { data, loading, error }] = useCreateActivityMutation({ + * variables: { + * object: // value for 'object' + * }, + * }); + */ +export function useCreateActivityMutation( + baseOptions?: Apollo.MutationHookOptions< + CreateActivityMutation, + CreateActivityMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + CreateActivityMutation, + CreateActivityMutationVariables + >(CreateActivityDocument, options) +} +export type CreateActivityMutationHookResult = ReturnType< + typeof useCreateActivityMutation +> +export type CreateActivityMutationResult = + Apollo.MutationResult +export type CreateActivityMutationOptions = Apollo.BaseMutationOptions< + CreateActivityMutation, + CreateActivityMutationVariables +> +export const CreateActivityUploadedFilesDocument = gql` + mutation createActivityUploadedFiles( + $objects: [activity_uploaded_filesInsertInput!]! + ) { + __typename + insertIntoactivity_uploaded_filesCollection(objects: $objects) { + __typename + records { + __typename + id + activity_id + file_name + file_url + } + } + } +` +export type CreateActivityUploadedFilesMutationFn = Apollo.MutationFunction< + CreateActivityUploadedFilesMutation, + CreateActivityUploadedFilesMutationVariables +> + +/** + * __useCreateActivityUploadedFilesMutation__ + * + * To run a mutation, you first call `useCreateActivityUploadedFilesMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateActivityUploadedFilesMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createActivityUploadedFilesMutation, { data, loading, error }] = useCreateActivityUploadedFilesMutation({ + * variables: { + * objects: // value for 'objects' + * }, + * }); + */ +export function useCreateActivityUploadedFilesMutation( + baseOptions?: Apollo.MutationHookOptions< + CreateActivityUploadedFilesMutation, + CreateActivityUploadedFilesMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + CreateActivityUploadedFilesMutation, + CreateActivityUploadedFilesMutationVariables + >(CreateActivityUploadedFilesDocument, options) +} +export type CreateActivityUploadedFilesMutationHookResult = ReturnType< + typeof useCreateActivityUploadedFilesMutation +> +export type CreateActivityUploadedFilesMutationResult = + Apollo.MutationResult +export type CreateActivityUploadedFilesMutationOptions = + Apollo.BaseMutationOptions< + CreateActivityUploadedFilesMutation, + CreateActivityUploadedFilesMutationVariables + > +export const UpdateActivityDocument = gql` + mutation updateActivity($id: UUID!, $set: activityUpdateInput!) { + __typename + updateactivityCollection(set: $set, filter: { id: { eq: $id } }) { + __typename + records { + __typename + id + title + } + } + } +` +export type UpdateActivityMutationFn = Apollo.MutationFunction< + UpdateActivityMutation, + UpdateActivityMutationVariables +> + +/** + * __useUpdateActivityMutation__ + * + * To run a mutation, you first call `useUpdateActivityMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateActivityMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateActivityMutation, { data, loading, error }] = useUpdateActivityMutation({ + * variables: { + * id: // value for 'id' + * set: // value for 'set' + * }, + * }); + */ +export function useUpdateActivityMutation( + baseOptions?: Apollo.MutationHookOptions< + UpdateActivityMutation, + UpdateActivityMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + UpdateActivityMutation, + UpdateActivityMutationVariables + >(UpdateActivityDocument, options) +} +export type UpdateActivityMutationHookResult = ReturnType< + typeof useUpdateActivityMutation +> +export type UpdateActivityMutationResult = + Apollo.MutationResult +export type UpdateActivityMutationOptions = Apollo.BaseMutationOptions< + UpdateActivityMutation, + UpdateActivityMutationVariables +> export const ActivityCollectionDocument = gql` query activityCollection($id: UUID!) { __typename @@ -1791,97 +1989,6 @@ export function refetchActivityCollectionQuery( ) { return { query: ActivityCollectionDocument, variables: variables } } -export const CreateActivityDocument = gql` - mutation createActivity( - $trip_id: UUID! - $title: String! - $time_from: Datetime - $time_to: Datetime - $address: String - $url: String - $memo: String - $cost: BigFloat - $cost_unit: String - $image_url: String - ) { - __typename - insertIntoactivityCollection( - objects: [ - { - trip_id: $trip_id - title: $title - time_from: $time_from - time_to: $time_to - address: $address - url: $url - memo: $memo - cost: $cost - cost_unit: $cost_unit - image_url: $image_url - } - ] - ) { - __typename - records { - __typename - id - title - } - } - } -` -export type CreateActivityMutationFn = Apollo.MutationFunction< - CreateActivityMutation, - CreateActivityMutationVariables -> - -/** - * __useCreateActivityMutation__ - * - * To run a mutation, you first call `useCreateActivityMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useCreateActivityMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [createActivityMutation, { data, loading, error }] = useCreateActivityMutation({ - * variables: { - * trip_id: // value for 'trip_id' - * title: // value for 'title' - * time_from: // value for 'time_from' - * time_to: // value for 'time_to' - * address: // value for 'address' - * url: // value for 'url' - * memo: // value for 'memo' - * cost: // value for 'cost' - * cost_unit: // value for 'cost_unit' - * image_url: // value for 'image_url' - * }, - * }); - */ -export function useCreateActivityMutation( - baseOptions?: Apollo.MutationHookOptions< - CreateActivityMutation, - CreateActivityMutationVariables - > -) { - const options = { ...defaultOptions, ...baseOptions } - return Apollo.useMutation< - CreateActivityMutation, - CreateActivityMutationVariables - >(CreateActivityDocument, options) -} -export type CreateActivityMutationHookResult = ReturnType< - typeof useCreateActivityMutation -> -export type CreateActivityMutationResult = - Apollo.MutationResult -export type CreateActivityMutationOptions = Apollo.BaseMutationOptions< - CreateActivityMutation, - CreateActivityMutationVariables -> export const CreateTagDocument = gql` mutation createTag($name: String!, $userId: UUID!) { __typename diff --git a/graphql-codegen/generated/gql.ts b/graphql-codegen/generated/gql.ts index a45d40b..4708a5e 100644 --- a/graphql-codegen/generated/gql.ts +++ b/graphql-codegen/generated/gql.ts @@ -15,10 +15,14 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- const documents = { 'query getUser($id: UUID!) {\n usersCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n email\n name\n profile_picture_url\n }\n }\n }\n}': types.GetUserDocument, + 'mutation createActivity($object: activityInsertInput!) {\n insertIntoactivityCollection(objects: [$object]) {\n records {\n __typename\n id\n title\n }\n }\n}': + types.CreateActivityDocument, + 'mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) {\n insertIntoactivity_uploaded_filesCollection(objects: $objects) {\n records {\n __typename\n id\n activity_id\n file_name\n file_url\n }\n }\n}': + types.CreateActivityUploadedFilesDocument, + 'mutation updateActivity($id: UUID!, $set: activityUpdateInput!) {\n updateactivityCollection(set: $set, filter: {id: {eq: $id}}) {\n records {\n id\n title\n }\n }\n}': + types.UpdateActivityDocument, 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}': types.ActivityCollectionDocument, - 'mutation createActivity($trip_id: UUID!, $title: String!, $time_from: Datetime, $time_to: Datetime, $address: String, $url: String, $memo: String, $cost: BigFloat, $cost_unit: String, $image_url: String) {\n insertIntoactivityCollection(\n objects: [{trip_id: $trip_id, title: $title, time_from: $time_from, time_to: $time_to, address: $address, url: $url, memo: $memo, cost: $cost, cost_unit: $cost_unit, image_url: $image_url}]\n ) {\n records {\n __typename\n id\n title\n }\n }\n}': - types.CreateActivityDocument, 'mutation createTag($name: String!, $userId: UUID!) {\n insertIntotagsCollection(objects: [{name: $name, user_id: $userId}]) {\n records {\n __typename\n id\n name\n }\n }\n}': types.CreateTagDocument, 'mutation createTrip($object: tripsInsertInput!) {\n insertIntotripsCollection(objects: [$object]) {\n records {\n __typename\n id\n title\n }\n }\n}': @@ -67,14 +71,26 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}' -): (typeof documents)['query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}'] + source: 'mutation createActivity($object: activityInsertInput!) {\n insertIntoactivityCollection(objects: [$object]) {\n records {\n __typename\n id\n title\n }\n }\n}' +): (typeof documents)['mutation createActivity($object: activityInsertInput!) {\n insertIntoactivityCollection(objects: [$object]) {\n records {\n __typename\n id\n title\n }\n }\n}'] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: 'mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) {\n insertIntoactivity_uploaded_filesCollection(objects: $objects) {\n records {\n __typename\n id\n activity_id\n file_name\n file_url\n }\n }\n}' +): (typeof documents)['mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) {\n insertIntoactivity_uploaded_filesCollection(objects: $objects) {\n records {\n __typename\n id\n activity_id\n file_name\n file_url\n }\n }\n}'] /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: 'mutation createActivity($trip_id: UUID!, $title: String!, $time_from: Datetime, $time_to: Datetime, $address: String, $url: String, $memo: String, $cost: BigFloat, $cost_unit: String, $image_url: String) {\n insertIntoactivityCollection(\n objects: [{trip_id: $trip_id, title: $title, time_from: $time_from, time_to: $time_to, address: $address, url: $url, memo: $memo, cost: $cost, cost_unit: $cost_unit, image_url: $image_url}]\n ) {\n records {\n __typename\n id\n title\n }\n }\n}' -): (typeof documents)['mutation createActivity($trip_id: UUID!, $title: String!, $time_from: Datetime, $time_to: Datetime, $address: String, $url: String, $memo: String, $cost: BigFloat, $cost_unit: String, $image_url: String) {\n insertIntoactivityCollection(\n objects: [{trip_id: $trip_id, title: $title, time_from: $time_from, time_to: $time_to, address: $address, url: $url, memo: $memo, cost: $cost, cost_unit: $cost_unit, image_url: $image_url}]\n ) {\n records {\n __typename\n id\n title\n }\n }\n}'] + source: 'mutation updateActivity($id: UUID!, $set: activityUpdateInput!) {\n updateactivityCollection(set: $set, filter: {id: {eq: $id}}) {\n records {\n id\n title\n }\n }\n}' +): (typeof documents)['mutation updateActivity($id: UUID!, $set: activityUpdateInput!) {\n updateactivityCollection(set: $set, filter: {id: {eq: $id}}) {\n records {\n id\n title\n }\n }\n}'] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}' +): (typeof documents)['query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}'] /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/graphql-codegen/generated/graphql.ts b/graphql-codegen/generated/graphql.ts index 07c7abb..6ab794c 100644 --- a/graphql-codegen/generated/graphql.ts +++ b/graphql-codegen/generated/graphql.ts @@ -523,8 +523,8 @@ export type Activity = Node & { time_from: Scalars['Datetime']['output'] time_to?: Maybe title: Scalars['String']['output'] - trip_id?: Maybe - trips?: Maybe + trip_id: Scalars['UUID']['output'] + trips: Trips url?: Maybe } @@ -636,9 +636,9 @@ export type ActivityUpdateResponse = { export type Activity_Uploaded_Files = Node & { __typename?: 'activity_uploaded_files' - activity?: Maybe - activity_id?: Maybe - content_type: Scalars['String']['output'] + activity: Activity + activity_id: Scalars['UUID']['output'] + content_type?: Maybe created_at: Scalars['Datetime']['output'] file_data?: Maybe file_name: Scalars['String']['output'] @@ -982,10 +982,10 @@ export type Trip_Tags = Node & { id: Scalars['UUID']['output'] /** Globally Unique Record Identifier */ nodeId: Scalars['ID']['output'] - tag_id?: Maybe - tags?: Maybe - trip_id?: Maybe - trips?: Maybe + tag_id: Scalars['UUID']['output'] + tags: Tags + trip_id: Scalars['UUID']['output'] + trips: Trips } export type Trip_TagsConnection = { @@ -1065,8 +1065,8 @@ export type Trips = Node & { nodeId: Scalars['ID']['output'] title: Scalars['String']['output'] trip_tagsCollection?: Maybe - user_id?: Maybe - users?: Maybe + user_id: Scalars['UUID']['output'] + users: Users } export type TripsActivityCollectionArgs = { @@ -1311,6 +1311,51 @@ export type GetUserQuery = { } | null } +export type CreateActivityMutationVariables = Exact<{ + object: ActivityInsertInput +}> + +export type CreateActivityMutation = { + __typename: 'Mutation' + insertIntoactivityCollection?: { + __typename: 'activityInsertResponse' + records: Array<{ __typename: 'activity'; id: string; title: string }> + } | null +} + +export type CreateActivityUploadedFilesMutationVariables = Exact<{ + objects: + | Array + | Activity_Uploaded_FilesInsertInput +}> + +export type CreateActivityUploadedFilesMutation = { + __typename: 'Mutation' + insertIntoactivity_uploaded_filesCollection?: { + __typename: 'activity_uploaded_filesInsertResponse' + records: Array<{ + __typename: 'activity_uploaded_files' + id: string + activity_id: string + file_name: string + file_url: string + }> + } | null +} + +export type UpdateActivityMutationVariables = Exact<{ + id: Scalars['UUID']['input'] + set: ActivityUpdateInput +}> + +export type UpdateActivityMutation = { + __typename: 'Mutation' + updateactivityCollection: { + __typename: 'activityUpdateResponse' + records: Array<{ __typename: 'activity'; id: string; title: string }> + } +} + export type ActivityCollectionQueryVariables = Exact<{ id: Scalars['UUID']['input'] }> @@ -1324,7 +1369,7 @@ export type ActivityCollectionQuery = { node: { __typename: 'activity' id: string - trip_id?: string | null + trip_id: string title: string time_from: string time_to?: string | null @@ -1350,27 +1395,6 @@ export type ActivityCollectionQuery = { } | null } -export type CreateActivityMutationVariables = Exact<{ - trip_id: Scalars['UUID']['input'] - title: Scalars['String']['input'] - time_from?: InputMaybe - time_to?: InputMaybe - address?: InputMaybe - url?: InputMaybe - memo?: InputMaybe - cost?: InputMaybe - cost_unit?: InputMaybe - image_url?: InputMaybe -}> - -export type CreateActivityMutation = { - __typename: 'Mutation' - insertIntoactivityCollection?: { - __typename: 'activityInsertResponse' - records: Array<{ __typename: 'activity'; id: string; title: string }> - } | null -} - export type CreateTagMutationVariables = Exact<{ name: Scalars['String']['input'] userId: Scalars['UUID']['input'] @@ -1408,8 +1432,8 @@ export type CreateTripTagMutation = { records: Array<{ __typename: 'trip_tags' id: string - tag_id?: string | null - trip_id?: string | null + tag_id: string + trip_id: string }> } | null } @@ -1532,7 +1556,7 @@ export type TripDetailsQuery = { __typename: 'trip_tagsEdge' node: { __typename: 'trip_tags' - tags?: { __typename: 'tags'; id: string; name: string } | null + tags: { __typename: 'tags'; id: string; name: string } } }> } | null @@ -1554,8 +1578,8 @@ export type TripTagsCollectionQuery = { node: { __typename: 'trip_tags' id: string - trip_id?: string | null - tag_id?: string | null + trip_id: string + tag_id: string } }> } | null @@ -1725,6 +1749,278 @@ export const GetUserDocument = { } ] } as unknown as DocumentNode +export const CreateActivityDocument = { + __meta__: { hash: '374de4fe4f2de1741f44619ae25c008fee655937' }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'createActivity' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { + kind: 'Variable', + name: { kind: 'Name', value: 'object' } + }, + type: { + kind: 'NonNullType', + type: { + kind: 'NamedType', + name: { kind: 'Name', value: 'activityInsertInput' } + } + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'insertIntoactivityCollection' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'objects' }, + value: { + kind: 'ListValue', + values: [ + { + kind: 'Variable', + name: { kind: 'Name', value: 'object' } + } + ] + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'records' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } } + ] + } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode< + CreateActivityMutation, + CreateActivityMutationVariables +> +export const CreateActivityUploadedFilesDocument = { + __meta__: { hash: '9b12c83c4bb7cae7ead1221919c3842b1d446e35' }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'createActivityUploadedFiles' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { + kind: 'Variable', + name: { kind: 'Name', value: 'objects' } + }, + type: { + kind: 'NonNullType', + type: { + kind: 'ListType', + type: { + kind: 'NonNullType', + type: { + kind: 'NamedType', + name: { + kind: 'Name', + value: 'activity_uploaded_filesInsertInput' + } + } + } + } + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'insertIntoactivity_uploaded_filesCollection' + }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'objects' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'objects' } + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'records' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'activity_id' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'file_name' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'file_url' } + } + ] + } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode< + CreateActivityUploadedFilesMutation, + CreateActivityUploadedFilesMutationVariables +> +export const UpdateActivityDocument = { + __meta__: { hash: 'd6d6be943e738534556ff6a112b8dea268daf1f6' }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'updateActivity' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'UUID' } } + } + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'set' } }, + type: { + kind: 'NonNullType', + type: { + kind: 'NamedType', + name: { kind: 'Name', value: 'activityUpdateInput' } + } + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'updateactivityCollection' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'set' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'set' } + } + }, + { + kind: 'Argument', + name: { kind: 'Name', value: 'filter' }, + value: { + kind: 'ObjectValue', + fields: [ + { + kind: 'ObjectField', + name: { kind: 'Name', value: 'id' }, + value: { + kind: 'ObjectValue', + fields: [ + { + kind: 'ObjectField', + name: { kind: 'Name', value: 'eq' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'id' } + } + } + ] + } + } + ] + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'records' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } } + ] + } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode< + UpdateActivityMutation, + UpdateActivityMutationVariables +> export const ActivityCollectionDocument = { __meta__: { hash: '3e04fac286fc4b6bc6c6d91483ca2b248b25bad6' }, kind: 'Document', @@ -1928,226 +2224,6 @@ export const ActivityCollectionDocument = { ActivityCollectionQuery, ActivityCollectionQueryVariables > -export const CreateActivityDocument = { - __meta__: { hash: '22e6378e9a0ec7a9c28a6430981dcfebf7820216' }, - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'mutation', - name: { kind: 'Name', value: 'createActivity' }, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'trip_id' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'UUID' } } - } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'title' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'time_from' } - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'Datetime' } } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'time_to' } - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'Datetime' } } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'address' } - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - }, - { - kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'url' } }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - }, - { - kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'memo' } }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - }, - { - kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'cost' } }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'BigFloat' } } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'cost_unit' } - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'image_url' } - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, - { - kind: 'Field', - name: { kind: 'Name', value: 'insertIntoactivityCollection' }, - arguments: [ - { - kind: 'Argument', - name: { kind: 'Name', value: 'objects' }, - value: { - kind: 'ListValue', - values: [ - { - kind: 'ObjectValue', - fields: [ - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'trip_id' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'trip_id' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'title' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'title' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'time_from' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'time_from' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'time_to' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'time_to' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'address' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'address' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'url' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'url' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'memo' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'memo' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'cost' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'cost' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'cost_unit' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'cost_unit' } - } - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'image_url' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'image_url' } - } - } - ] - } - ] - } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, - { - kind: 'Field', - name: { kind: 'Name', value: 'records' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: '__typename' } - }, - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, - { kind: 'Field', name: { kind: 'Name', value: 'title' } } - ] - } - } - ] - } - } - ] - } - } - ] -} as unknown as DocumentNode< - CreateActivityMutation, - CreateActivityMutationVariables -> export const CreateTagDocument = { __meta__: { hash: '9ecd6081f83febe06337ebd01345eef596b81cf9' }, kind: 'Document', diff --git a/graphql-codegen/generated/persisted-documents.json b/graphql-codegen/generated/persisted-documents.json index ceef4bc..505eb54 100644 --- a/graphql-codegen/generated/persisted-documents.json +++ b/graphql-codegen/generated/persisted-documents.json @@ -1,7 +1,9 @@ { "00c528b2b1c851c06119aedb1fba6b5c885713cf": "query getUser($id: UUID!) { __typename usersCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename email id name profile_picture_url } } } }", + "374de4fe4f2de1741f44619ae25c008fee655937": "mutation createActivity($object: activityInsertInput!) { __typename insertIntoactivityCollection(objects: [$object]) { __typename records { __typename id title } } }", + "9b12c83c4bb7cae7ead1221919c3842b1d446e35": "mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) { __typename insertIntoactivity_uploaded_filesCollection(objects: $objects) { __typename records { __typename activity_id file_name file_url id } } }", + "d6d6be943e738534556ff6a112b8dea268daf1f6": "mutation updateActivity($id: UUID!, $set: activityUpdateInput!) { __typename updateactivityCollection(set: $set, filter: {id: {eq: $id}}) { __typename records { __typename id title } } }", "3e04fac286fc4b6bc6c6d91483ca2b248b25bad6": "query activityCollection($id: UUID!) { __typename activityCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename activity_uploaded_filesCollection { __typename edges { __typename node { __typename file_name file_url id } } } address cost cost_unit id memo time_from time_to title trip_id url } } } }", - "22e6378e9a0ec7a9c28a6430981dcfebf7820216": "mutation createActivity($address: String, $cost: BigFloat, $cost_unit: String, $image_url: String, $memo: String, $time_from: Datetime, $time_to: Datetime, $title: String!, $trip_id: UUID!, $url: String) { __typename insertIntoactivityCollection( objects: [{trip_id: $trip_id, title: $title, time_from: $time_from, time_to: $time_to, address: $address, url: $url, memo: $memo, cost: $cost, cost_unit: $cost_unit, image_url: $image_url}] ) { __typename records { __typename id title } } }", "9ecd6081f83febe06337ebd01345eef596b81cf9": "mutation createTag($name: String!, $userId: UUID!) { __typename insertIntotagsCollection(objects: [{name: $name, user_id: $userId}]) { __typename records { __typename id name } } }", "68140ac8465fd7c6f5bab39db9eec02dab9501ef": "mutation createTrip($object: tripsInsertInput!) { __typename insertIntotripsCollection(objects: [$object]) { __typename records { __typename id title } } }", "b86b0c68b8ee2697ff1b539e5df92575cedb30f0": "mutation createTripTag($tagId: UUID!, $tripId: UUID!) { __typename insertIntotrip_tagsCollection(objects: [{trip_id: $tripId, tag_id: $tagId}]) { __typename records { __typename id tag_id trip_id } } }", From 255795eca69352a424fa4226960f7193545f1640 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 16 Feb 2024 11:44:35 -0800 Subject: [PATCH 03/17] Add activity hooks for creating and updating activities --- app/activity/hooks/index.ts | 3 ++ app/activity/hooks/useActivityCreate.ts | 66 +++++++++++++++++++++++++ app/activity/hooks/useActivityUpdate.ts | 66 +++++++++++++++++++++++++ app/activity/hooks/useUploadFiles.ts | 35 +++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 app/activity/hooks/index.ts create mode 100644 app/activity/hooks/useActivityCreate.ts create mode 100644 app/activity/hooks/useActivityUpdate.ts create mode 100644 app/activity/hooks/useUploadFiles.ts diff --git a/app/activity/hooks/index.ts b/app/activity/hooks/index.ts new file mode 100644 index 0000000..6529f95 --- /dev/null +++ b/app/activity/hooks/index.ts @@ -0,0 +1,3 @@ +export { useActivityCreate } from './useActivityCreate' +export { useActivityUpdate } from './useActivityUpdate' +export { useUploadFiles } from './useUploadFiles' diff --git a/app/activity/hooks/useActivityCreate.ts b/app/activity/hooks/useActivityCreate.ts new file mode 100644 index 0000000..5c34fa6 --- /dev/null +++ b/app/activity/hooks/useActivityCreate.ts @@ -0,0 +1,66 @@ +import { useToast } from '@chakra-ui/react' +import { useRouter } from 'next/navigation' +import { formatToISODate } from '@/libs/utils' +import { ActivitySchema } from '../schema' +import { useUploadFiles } from './useUploadFiles' +import { useCreateActivityMutation } from '@generated/api' + +export const useActivityCreate = (tripId: string) => { + const toast = useToast() + const router = useRouter() + const [createActivityMutation, { loading: isActivityCreating }] = + useCreateActivityMutation() + const { uploadFiles } = useUploadFiles() + + const createActivity = async (activityData: ActivitySchema) => { + try { + const { data, errors } = await createActivityMutation({ + variables: { + object: { + trip_id: tripId, + title: activityData.title, + time_from: formatToISODate(activityData.timeFrom), + time_to: activityData.timeTo ? formatToISODate(activityData.timeTo) : null, + address: activityData.address, + url: activityData.url, + memo: activityData.memo, + cost: activityData.cost, + cost_unit: activityData.costUnit + } + }, + refetchQueries: ['activityCollection'] + }) + + if (errors) throw new Error(errors[0].message) + + const createdActivityId = data?.insertIntoactivityCollection?.records[0]?.id + + if (!createdActivityId) throw new Error('Failed to create an activity') + + if (createdActivityId && activityData.newFiles && activityData.newFiles.length > 0) { + await uploadFiles(activityData.newFiles, { id: createdActivityId, tripId }) + } + + router.push(`/activity/${createdActivityId}`) + toast({ + title: 'Successfully created!', + status: 'success', + duration: 2000, + isClosable: true, + position: 'top' + }) + } catch (error) { + toast({ + title: 'Failed to create an activity', + status: 'error', + duration: 3000, + isClosable: true + }) + } + } + + return { + createActivity, + isActivityCreating, + } +} diff --git a/app/activity/hooks/useActivityUpdate.ts b/app/activity/hooks/useActivityUpdate.ts new file mode 100644 index 0000000..e007784 --- /dev/null +++ b/app/activity/hooks/useActivityUpdate.ts @@ -0,0 +1,66 @@ +import { useToast } from '@chakra-ui/react' +import { useRouter } from 'next/navigation' +import { formatToISODate } from '@/libs/utils' +import { ActivitySchema } from '../schema' +import { useUploadFiles } from './useUploadFiles' +import { useUpdateActivityMutation } from '@generated/api' + +export const useActivityUpdate = (tripId: string) => { + const toast = useToast() + const router = useRouter() + const [updateActivityMutation, { loading: isActivityUpdating }] = + useUpdateActivityMutation() + const { uploadFiles } = useUploadFiles() + + const updateActivity = async (activityId: string, activityData: ActivitySchema) => { + try { + const { data, errors } = await updateActivityMutation({ + variables: { + id: activityId, + set: { + title: activityData.title, + time_from: formatToISODate(activityData.timeFrom), + time_to: activityData.timeTo ? formatToISODate(activityData.timeTo) : null, + address: activityData.address, + url: activityData.url, + memo: activityData.memo, + cost: activityData.cost, + cost_unit: activityData.costUnit + } + }, + refetchQueries: ['activityCollection'] + }) + + if (errors) throw new Error(errors[0].message) + + const updatedActivityId = data?.updateactivityCollection?.records[0]?.id + + if (!updatedActivityId) throw new Error('Failed to create an activity') + + if (updatedActivityId && activityData.newFiles && activityData.newFiles.length > 0) { + await uploadFiles(activityData.newFiles, { id: activityId, tripId }) + } + + router.push(`/activity/${activityId}`) + toast({ + title: 'Successfully updated!', + status: 'success', + duration: 2000, + isClosable: true, + position: 'top' + }) + } catch (error) { + toast({ + title: 'Failed to create an activity', + status: 'error', + duration: 3000, + isClosable: true + }) + } + } + + return { + updateActivity, + isActivityUpdating, + } +} diff --git a/app/activity/hooks/useUploadFiles.ts b/app/activity/hooks/useUploadFiles.ts new file mode 100644 index 0000000..5e573b7 --- /dev/null +++ b/app/activity/hooks/useUploadFiles.ts @@ -0,0 +1,35 @@ +import { createClient } from '@supabase/supabase-js' +// import { useCreateActivityUploadedFilesMutation } from '@generated/api' + +export const useUploadFiles = () => { + const uploadFiles = async (files: File[], activityDetails: { id: string; tripId: string; }) => { + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_API_KEY! + ) + + const uploadPromises = files.map(async (file) => { + const { data, error } = await supabase.storage + .from('tabi-memo-uploads') + .upload(`trips/${activityDetails.tripId}/activity/${activityDetails.id}/${file.name}`, file, { upsert: true }) + + return { data, fileName: file.name, error } + }) + + const uploadResults = await Promise.all(uploadPromises) + + const uploadErrors = uploadResults.filter((result) => result.error) + + if (uploadErrors.length) { + throw new Error(uploadErrors[0].error!.message) + } + + // create a record in the uploaded_files table + // const uploadedFiles = uploadResults.map((result) => ({ + // file_name: result.fileName, + // file_url: result.data?.path + // })) + } + + return { uploadFiles } +} From cb551567581ee7ea9933a230210a32ea9586d9d6 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 16 Feb 2024 11:45:02 -0800 Subject: [PATCH 04/17] Refactor activity creation and editing This commit refactors the activity creation and editing functionality. note: It includes changes to the `CreateActivityPage` and `ActivityEditPage` components, as well as updates to the `activitySchema` and `ActivitySchema` types. The changes allow for the inclusion of a `tripId` parameter and support for uploading files. --- app/activity/[id]/edit/page.tsx | 9 +- app/activity/[id]/page.tsx | 2 +- app/activity/components/activity-form.tsx | 129 +++++++++++------ app/activity/create/components/form.tsx | 165 ---------------------- app/activity/create/components/index.ts | 1 - app/activity/create/page.tsx | 4 +- app/activity/schema.ts | 33 +++-- app/trip/[id]/page.tsx | 4 +- 8 files changed, 119 insertions(+), 228 deletions(-) delete mode 100644 app/activity/create/components/form.tsx delete mode 100644 app/activity/create/components/index.ts diff --git a/app/activity/[id]/edit/page.tsx b/app/activity/[id]/edit/page.tsx index 8858c2f..b8ee3b7 100644 --- a/app/activity/[id]/edit/page.tsx +++ b/app/activity/[id]/edit/page.tsx @@ -57,17 +57,18 @@ export default function ActivityEditPage({ ) : ( - + Got back to Trip Details diff --git a/app/activity/components/activity-form.tsx b/app/activity/components/activity-form.tsx index 902b882..80e2fd3 100644 --- a/app/activity/components/activity-form.tsx +++ b/app/activity/components/activity-form.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react' import { useDropzone } from 'react-dropzone' -import { useForm } from 'react-hook-form' +import { Controller, useForm } from 'react-hook-form' import { Box, FormControl, @@ -10,39 +10,40 @@ import { Image, SimpleGrid, Text, - Flex + Flex, + VStack } from '@chakra-ui/react' import { PrimaryButton } from '@/components/button' import { CustomDateTimePicker } from '@/components/customDateTimePicker' import { InputForm, TextareaForm } from '@/components/input' -import { extractTimeFromDate } from '@/libs/utils' -import { activitySchema, activityResolver } from '../schema' - -type ValuePiece = Date | null -type Value = ValuePiece | [ValuePiece, ValuePiece] +import { getDateObj } from '@/libs/utils' +import { useActivityCreate, useActivityUpdate } from '../hooks' +import { ActivitySchema, activityResolver } from '../schema' export type ActivityDetails = { id: string title: string - time_from: string | null - time_to?: string | null + timeFrom?: string | null + timeTo?: string | null address?: string | null url?: string | null memo?: string | null cost?: string | null - cost_unit?: string | null - image_urls?: string[] + costUnit?: string | null + uploadedFileUrls?: string[] | null refetch: () => void refetchLoading: boolean } type ActivityFormProps = { + tripId: string activityDetails?: ActivityDetails } -export const ActivityForm = ({ activityDetails }: ActivityFormProps) => { - const [timeTo, setDateTo] = useState(null) - const [timeFrom, setDateFrom] = useState(null) +export const ActivityForm = ({ + tripId, + activityDetails +}: ActivityFormProps) => { const [selectedImages, setSelectedImages] = useState([]) const onDrop = useCallback((acceptedFiles: File[]) => { setSelectedImages((prevImages) => [...prevImages, ...acceptedFiles]) @@ -58,34 +59,42 @@ export const ActivityForm = ({ activityDetails }: ActivityFormProps) => { const { register, + control, handleSubmit, formState: { errors } - } = useForm({ + } = useForm({ defaultValues: { title: activityDetails?.title, - timeFrom: activityDetails?.time_from - ? extractTimeFromDate(activityDetails?.time_from) + timeFrom: activityDetails?.timeFrom + ? getDateObj(activityDetails?.timeFrom) : undefined, - timeTo: activityDetails?.time_to - ? extractTimeFromDate(activityDetails?.time_to) + timeTo: activityDetails?.timeTo + ? getDateObj(activityDetails?.timeTo) : undefined, address: activityDetails?.address || undefined, url: activityDetails?.url || undefined, memo: activityDetails?.memo || undefined, cost: activityDetails?.cost || undefined, - costUnit: activityDetails?.cost_unit || undefined, - imageUrls: activityDetails?.image_urls || undefined + costUnit: activityDetails?.costUnit || undefined, + uploadedFileUrls: activityDetails?.uploadedFileUrls || undefined, + newFiles: selectedImages }, resolver: activityResolver }) - const createHandler = handleSubmit(async (data: activitySchema) => { - console.log(data) - // TODO append 'images', 'date from', 'date to' to formData and send to backend - }) + const { createActivity, isActivityCreating } = useActivityCreate(tripId) + const { updateActivity, isActivityUpdating } = useActivityUpdate(tripId) + const mutateFun = activityDetails + ? (data: ActivitySchema) => updateActivity(activityDetails.id, data) + : createActivity + const isMutating = isActivityCreating || isActivityUpdating return ( - + Title { )} - + Time From - + ( + + )} + /> + {errors.timeFrom && ( + {errors.timeFrom.message} + )} Time To - + ( + + )} + /> { Image - {activityDetails?.image_urls?.map((url, index) => ( + {activityDetails?.uploadedFileUrls?.map((url, index) => ( { mt={{ base: 0, md: - (activityDetails?.image_urls?.length ?? 0) > 0 || + (activityDetails?.uploadedFileUrls?.length ?? 0) > 0 || selectedImages.length !== 0 ? '40px' : '0' @@ -198,19 +226,40 @@ export const ActivityForm = ({ activityDetails }: ActivityFormProps) => { )} - - Cost - - {errors.cost && ( - {errors.cost.message} - )} - + + + Cost + + {errors?.cost?.message} + + + + Unit + + {errors?.costUnit?.message} + + - - + ) } diff --git a/app/activity/create/components/form.tsx b/app/activity/create/components/form.tsx deleted file mode 100644 index 8d8e7e9..0000000 --- a/app/activity/create/components/form.tsx +++ /dev/null @@ -1,165 +0,0 @@ -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(null) - const [dateFrom, setDateFrom] = useState(null) - const [selectedImages, setSelectedImages] = useState([]) - 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({ - 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 ( - - - Title - - {errors.title && ( - {errors.title.message} - )} - - - - Time From - - - - - Time To - - - - - Address - - {errors.address && ( - {errors.address.message} - )} - - - - URL - - {errors.url && ( - {errors.url.message} - )} - - - - Image - - - {selectedImages.map((image, index) => ( - {`Selected removeImage(index)} - /> - ))} - - - - - - Add Image - - - - - Memo - - {errors.memo && ( - {errors.memo.message} - )} - - - - Cost - - {errors.cost && ( - {errors.cost.message} - )} - - - - - - - ) -} diff --git a/app/activity/create/components/index.ts b/app/activity/create/components/index.ts deleted file mode 100644 index 424fdad..0000000 --- a/app/activity/create/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FormActivity } from './form' diff --git a/app/activity/create/page.tsx b/app/activity/create/page.tsx index 6202d81..679eea1 100644 --- a/app/activity/create/page.tsx +++ b/app/activity/create/page.tsx @@ -1,11 +1,13 @@ 'use client' import { Box, Container, Heading, useColorModeValue } from '@chakra-ui/react' +import { useSearchParams } from 'next/navigation' import { ActivityForm } from '../components' export default function CreateActivityPage() { const bg = useColorModeValue('white', 'gray.800') const color = useColorModeValue('black', 'gray.300') + const tripId = useSearchParams().get('tripId') || '' return ( @@ -16,7 +18,7 @@ export default function CreateActivityPage() { Create Activity - + ) diff --git a/app/activity/schema.ts b/app/activity/schema.ts index 4cc2011..5117688 100644 --- a/app/activity/schema.ts +++ b/app/activity/schema.ts @@ -2,24 +2,27 @@ import { zodResolver } from '@hookform/resolvers/zod' import * as z from 'zod' const activitySchema = 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(), + title: z.string().min(1, { message: 'Please enter a title' }), + timeFrom: z.date({ + required_error: 'Please select a date from', + invalid_type_error: "That's not a date" + }), + timeTo: z.date().nullable(), + address: z.string().min(0).max(50).nullable(), 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(), - costUnit: z.string().min(0).max(15).optional(), - imageUrls: z.array(z.string()).optional() + memo: z.string().min(0).max(300).nullable(), + cost: z.string().nullable(), + costUnit: z.string().nullable(), + uploadedFileUrls: z.array(z.string()).optional(), + newFiles: z.array(z.instanceof(File)).optional() }) -export type activitySchema = z.infer +export type ActivitySchema = z.infer export const activityResolver = zodResolver(activitySchema) -const imagesSchema = z.object({ - urls: z.array(z.string()), - uploadedFiles: z.array(z.instanceof(File)) -}) +// const filesSchema = z.object({ +// uploadedFiles: z.array(z.instanceof(File)) +// }) -export type ImagesSchema = z.infer -export const imagesResolver = zodResolver(imagesSchema) +// export type FilesSchema = z.infer +// export const filesResolver = zodResolver(filesSchema) diff --git a/app/trip/[id]/page.tsx b/app/trip/[id]/page.tsx index f6d2785..f308d69 100644 --- a/app/trip/[id]/page.tsx +++ b/app/trip/[id]/page.tsx @@ -76,7 +76,9 @@ export default function TripDetailsPage({ router.push('/activity/create')} + onClick={() => + router.push(`/activity/create?tripId=${params.id}`) + } > Add Activity From fc2c941105bd0ac8d7062099827dbb0df6301ab8 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 16 Feb 2024 12:54:13 -0800 Subject: [PATCH 05/17] Add deleteActivity mutation and useActivityDelete hook --- .../[id]/components/activity-header.tsx | 4 + .../graphql/mutation/deleteActivity.graphql | 10 ++ app/activity/hooks/useActivityDelete.ts | 47 ++++++++ app/trip/[id]/components/activity-card.tsx | 9 +- graphql-codegen/generated/api.ts | 74 +++++++++++++ graphql-codegen/generated/gql.ts | 8 ++ graphql-codegen/generated/graphql.ts | 104 ++++++++++++++++++ .../generated/persisted-documents.json | 1 + 8 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 app/activity/graphql/mutation/deleteActivity.graphql create mode 100644 app/activity/hooks/useActivityDelete.ts diff --git a/app/activity/[id]/components/activity-header.tsx b/app/activity/[id]/components/activity-header.tsx index ae4e327..938bfa4 100644 --- a/app/activity/[id]/components/activity-header.tsx +++ b/app/activity/[id]/components/activity-header.tsx @@ -3,6 +3,7 @@ import { Heading, Box, Flex, Spacer, Text, IconButton } from '@chakra-ui/react' import { useRouter } from 'next/navigation' import { FiClock, FiMapPin, FiLink2, FiEdit3, FiTrash2 } from 'react-icons/fi' +import { useActivityDelete } from '@/activity/hooks/useActivityDelete' import { Link } from '@/components/link' import { formatToDateTime } from '@/libs/utils' import { customColors } from '@/theme/color' @@ -25,6 +26,7 @@ export const ActivityHeader = ({ url }: ActivityHeaderProps) => { const router = useRouter() + const { deleteActivity, isActivityDeleting } = useActivityDelete() return ( <> @@ -45,6 +47,8 @@ export const ActivityHeader = ({ aria-label="Delete this trip" variant="roundIcon" p={{ base: '6px', md: '10px' }} + onClick={() => deleteActivity(id)} + isLoading={isActivityDeleting} > diff --git a/app/activity/graphql/mutation/deleteActivity.graphql b/app/activity/graphql/mutation/deleteActivity.graphql new file mode 100644 index 0000000..a53e92e --- /dev/null +++ b/app/activity/graphql/mutation/deleteActivity.graphql @@ -0,0 +1,10 @@ +mutation DeleteActivity($id: UUID!) { + deleteFromactivityCollection(filter: { id: { eq: $id } }) { + records { + __typename + id + trip_id + title + } + } +} diff --git a/app/activity/hooks/useActivityDelete.ts b/app/activity/hooks/useActivityDelete.ts new file mode 100644 index 0000000..bc0fe96 --- /dev/null +++ b/app/activity/hooks/useActivityDelete.ts @@ -0,0 +1,47 @@ +import { useToast } from '@chakra-ui/react' +import { useRouter } from 'next/navigation' +import { useRefetchTrips } from './useRefetchTrips' +import { useDeleteActivityMutation } from '@generated/api' + +export const useActivityDelete = () => { + const toast = useToast() + const router = useRouter() + const [deleteActivityMutation, { loading: isActivityDeleting }] = + useDeleteActivityMutation() + const { refetchTrips } = useRefetchTrips() + + const deleteActivity = async (activityId: string) => { + try { + const { data, errors } = await deleteActivityMutation({ + variables: { + id: activityId + }, + refetchQueries: ['activityCollection'], + }) + + if (errors) throw new Error(errors[0].message) + if (!data) throw new Error('Failed to delete an activity') + + const tripId = data.deleteFromactivityCollection.records[0].trip_id + + refetchTrips(tripId) + router.push(`/trip/${tripId}`) + toast({ + title: 'Successfully deleted!', + status: 'success', + duration: 2000, + isClosable: true, + position: 'top' + }) + } catch (error) { + toast({ + title: 'Failed to delete an activity', + status: 'error', + duration: 3000, + isClosable: true + }) + } + } + + return { deleteActivity, isActivityDeleting } +} diff --git a/app/trip/[id]/components/activity-card.tsx b/app/trip/[id]/components/activity-card.tsx index 5061025..d1e68c7 100644 --- a/app/trip/[id]/components/activity-card.tsx +++ b/app/trip/[id]/components/activity-card.tsx @@ -12,6 +12,7 @@ import { } from '@chakra-ui/react' import { useRouter } from 'next/navigation' import { FiTrash2, FiMapPin, FiMoreHorizontal, FiEdit3 } from 'react-icons/fi' +import { useActivityDelete } from '@/activity/hooks/useActivityDelete' import { Link } from '@/components/link' import { ConfirmModal } from '@/components/modal' import { @@ -39,6 +40,8 @@ export const ActivityCard = ({ activity, selectedDate }: ActivityCardProps) => { onClose: onDeleteModalClose } = useDisclosure() + const { deleteActivity } = useActivityDelete() + const differentDate = (displayPlace: 'timeFrom' | 'timeTo') => { if (!activity.timeTo) return null @@ -166,7 +169,6 @@ export const ActivityCard = ({ activity, selectedDate }: ActivityCardProps) => { - {/* TODO Delete activity */} { Are you sure you want to delete this activity? } - onClick={() => {}} + onClick={async () => { + await deleteActivity(activity.id) + onDeleteModalClose() + }} submitLabel="Delete" /> diff --git a/graphql-codegen/generated/api.ts b/graphql-codegen/generated/api.ts index 2fef735..cc52575 100644 --- a/graphql-codegen/generated/api.ts +++ b/graphql-codegen/generated/api.ts @@ -1335,6 +1335,23 @@ export type CreateActivityUploadedFilesMutation = { } | null } +export type DeleteActivityMutationVariables = Exact<{ + id: Scalars['UUID']['input'] +}> + +export type DeleteActivityMutation = { + __typename: 'Mutation' + deleteFromactivityCollection: { + __typename: 'activityDeleteResponse' + records: Array<{ + __typename: 'activity' + id: string + trip_id: string + title: string + }> + } +} + export type UpdateActivityMutationVariables = Exact<{ id: Scalars['UUID']['input'] set: ActivityUpdateInput @@ -1825,6 +1842,63 @@ export type CreateActivityUploadedFilesMutationOptions = CreateActivityUploadedFilesMutation, CreateActivityUploadedFilesMutationVariables > +export const DeleteActivityDocument = gql` + mutation DeleteActivity($id: UUID!) { + __typename + deleteFromactivityCollection(filter: { id: { eq: $id } }) { + __typename + records { + __typename + id + trip_id + title + } + } + } +` +export type DeleteActivityMutationFn = Apollo.MutationFunction< + DeleteActivityMutation, + DeleteActivityMutationVariables +> + +/** + * __useDeleteActivityMutation__ + * + * To run a mutation, you first call `useDeleteActivityMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteActivityMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteActivityMutation, { data, loading, error }] = useDeleteActivityMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useDeleteActivityMutation( + baseOptions?: Apollo.MutationHookOptions< + DeleteActivityMutation, + DeleteActivityMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + DeleteActivityMutation, + DeleteActivityMutationVariables + >(DeleteActivityDocument, options) +} +export type DeleteActivityMutationHookResult = ReturnType< + typeof useDeleteActivityMutation +> +export type DeleteActivityMutationResult = + Apollo.MutationResult +export type DeleteActivityMutationOptions = Apollo.BaseMutationOptions< + DeleteActivityMutation, + DeleteActivityMutationVariables +> export const UpdateActivityDocument = gql` mutation updateActivity($id: UUID!, $set: activityUpdateInput!) { __typename diff --git a/graphql-codegen/generated/gql.ts b/graphql-codegen/generated/gql.ts index 4708a5e..b0a5c83 100644 --- a/graphql-codegen/generated/gql.ts +++ b/graphql-codegen/generated/gql.ts @@ -19,6 +19,8 @@ const documents = { types.CreateActivityDocument, 'mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) {\n insertIntoactivity_uploaded_filesCollection(objects: $objects) {\n records {\n __typename\n id\n activity_id\n file_name\n file_url\n }\n }\n}': types.CreateActivityUploadedFilesDocument, + 'mutation DeleteActivity($id: UUID!) {\n deleteFromactivityCollection(filter: {id: {eq: $id}}) {\n records {\n __typename\n id\n trip_id\n title\n }\n }\n}': + types.DeleteActivityDocument, 'mutation updateActivity($id: UUID!, $set: activityUpdateInput!) {\n updateactivityCollection(set: $set, filter: {id: {eq: $id}}) {\n records {\n id\n title\n }\n }\n}': types.UpdateActivityDocument, 'query activityCollection($id: UUID!) {\n activityCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n trip_id\n title\n time_from\n time_to\n address\n url\n memo\n cost\n cost_unit\n activity_uploaded_filesCollection {\n edges {\n node {\n id\n file_name\n file_url\n }\n }\n }\n }\n }\n }\n}': @@ -79,6 +81,12 @@ export function graphql( export function graphql( source: 'mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) {\n insertIntoactivity_uploaded_filesCollection(objects: $objects) {\n records {\n __typename\n id\n activity_id\n file_name\n file_url\n }\n }\n}' ): (typeof documents)['mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) {\n insertIntoactivity_uploaded_filesCollection(objects: $objects) {\n records {\n __typename\n id\n activity_id\n file_name\n file_url\n }\n }\n}'] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: 'mutation DeleteActivity($id: UUID!) {\n deleteFromactivityCollection(filter: {id: {eq: $id}}) {\n records {\n __typename\n id\n trip_id\n title\n }\n }\n}' +): (typeof documents)['mutation DeleteActivity($id: UUID!) {\n deleteFromactivityCollection(filter: {id: {eq: $id}}) {\n records {\n __typename\n id\n trip_id\n title\n }\n }\n}'] /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/graphql-codegen/generated/graphql.ts b/graphql-codegen/generated/graphql.ts index 6ab794c..d9c4788 100644 --- a/graphql-codegen/generated/graphql.ts +++ b/graphql-codegen/generated/graphql.ts @@ -1343,6 +1343,23 @@ export type CreateActivityUploadedFilesMutation = { } | null } +export type DeleteActivityMutationVariables = Exact<{ + id: Scalars['UUID']['input'] +}> + +export type DeleteActivityMutation = { + __typename: 'Mutation' + deleteFromactivityCollection: { + __typename: 'activityDeleteResponse' + records: Array<{ + __typename: 'activity' + id: string + trip_id: string + title: string + }> + } +} + export type UpdateActivityMutationVariables = Exact<{ id: Scalars['UUID']['input'] set: ActivityUpdateInput @@ -1919,6 +1936,93 @@ export const CreateActivityUploadedFilesDocument = { CreateActivityUploadedFilesMutation, CreateActivityUploadedFilesMutationVariables > +export const DeleteActivityDocument = { + __meta__: { hash: 'f86bef447c736e2937994ec37ca883ee109fa6c5' }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'DeleteActivity' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'UUID' } } + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'deleteFromactivityCollection' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'filter' }, + value: { + kind: 'ObjectValue', + fields: [ + { + kind: 'ObjectField', + name: { kind: 'Name', value: 'id' }, + value: { + kind: 'ObjectValue', + fields: [ + { + kind: 'ObjectField', + name: { kind: 'Name', value: 'eq' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'id' } + } + } + ] + } + } + ] + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'records' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'trip_id' } + }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } } + ] + } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode< + DeleteActivityMutation, + DeleteActivityMutationVariables +> export const UpdateActivityDocument = { __meta__: { hash: 'd6d6be943e738534556ff6a112b8dea268daf1f6' }, kind: 'Document', diff --git a/graphql-codegen/generated/persisted-documents.json b/graphql-codegen/generated/persisted-documents.json index 505eb54..cfae749 100644 --- a/graphql-codegen/generated/persisted-documents.json +++ b/graphql-codegen/generated/persisted-documents.json @@ -2,6 +2,7 @@ "00c528b2b1c851c06119aedb1fba6b5c885713cf": "query getUser($id: UUID!) { __typename usersCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename email id name profile_picture_url } } } }", "374de4fe4f2de1741f44619ae25c008fee655937": "mutation createActivity($object: activityInsertInput!) { __typename insertIntoactivityCollection(objects: [$object]) { __typename records { __typename id title } } }", "9b12c83c4bb7cae7ead1221919c3842b1d446e35": "mutation createActivityUploadedFiles($objects: [activity_uploaded_filesInsertInput!]!) { __typename insertIntoactivity_uploaded_filesCollection(objects: $objects) { __typename records { __typename activity_id file_name file_url id } } }", + "f86bef447c736e2937994ec37ca883ee109fa6c5": "mutation DeleteActivity($id: UUID!) { __typename deleteFromactivityCollection(filter: {id: {eq: $id}}) { __typename records { __typename id title trip_id } } }", "d6d6be943e738534556ff6a112b8dea268daf1f6": "mutation updateActivity($id: UUID!, $set: activityUpdateInput!) { __typename updateactivityCollection(set: $set, filter: {id: {eq: $id}}) { __typename records { __typename id title } } }", "3e04fac286fc4b6bc6c6d91483ca2b248b25bad6": "query activityCollection($id: UUID!) { __typename activityCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename activity_uploaded_filesCollection { __typename edges { __typename node { __typename file_name file_url id } } } address cost cost_unit id memo time_from time_to title trip_id url } } } }", "9ecd6081f83febe06337ebd01345eef596b81cf9": "mutation createTag($name: String!, $userId: UUID!) { __typename insertIntotagsCollection(objects: [{name: $name, user_id: $userId}]) { __typename records { __typename id name } } }", From e6dd60dd7dcf06278cc0b13eb36f892276b2deb7 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 16 Feb 2024 12:54:39 -0800 Subject: [PATCH 06/17] Add useRefetchTrips hook for activity create, update, and delete --- app/activity/hooks/useActivityCreate.ts | 3 +++ app/activity/hooks/useActivityUpdate.ts | 3 +++ app/activity/hooks/useRefetchTrips.ts | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 app/activity/hooks/useRefetchTrips.ts diff --git a/app/activity/hooks/useActivityCreate.ts b/app/activity/hooks/useActivityCreate.ts index 5c34fa6..c096b0e 100644 --- a/app/activity/hooks/useActivityCreate.ts +++ b/app/activity/hooks/useActivityCreate.ts @@ -2,6 +2,7 @@ import { useToast } from '@chakra-ui/react' import { useRouter } from 'next/navigation' import { formatToISODate } from '@/libs/utils' import { ActivitySchema } from '../schema' +import { useRefetchTrips } from './useRefetchTrips' import { useUploadFiles } from './useUploadFiles' import { useCreateActivityMutation } from '@generated/api' @@ -11,6 +12,7 @@ export const useActivityCreate = (tripId: string) => { const [createActivityMutation, { loading: isActivityCreating }] = useCreateActivityMutation() const { uploadFiles } = useUploadFiles() + const { refetchTrips } = useRefetchTrips() const createActivity = async (activityData: ActivitySchema) => { try { @@ -41,6 +43,7 @@ export const useActivityCreate = (tripId: string) => { await uploadFiles(activityData.newFiles, { id: createdActivityId, tripId }) } + refetchTrips(tripId) router.push(`/activity/${createdActivityId}`) toast({ title: 'Successfully created!', diff --git a/app/activity/hooks/useActivityUpdate.ts b/app/activity/hooks/useActivityUpdate.ts index e007784..493c4a1 100644 --- a/app/activity/hooks/useActivityUpdate.ts +++ b/app/activity/hooks/useActivityUpdate.ts @@ -2,6 +2,7 @@ import { useToast } from '@chakra-ui/react' import { useRouter } from 'next/navigation' import { formatToISODate } from '@/libs/utils' import { ActivitySchema } from '../schema' +import { useRefetchTrips } from './useRefetchTrips' import { useUploadFiles } from './useUploadFiles' import { useUpdateActivityMutation } from '@generated/api' @@ -11,6 +12,7 @@ export const useActivityUpdate = (tripId: string) => { const [updateActivityMutation, { loading: isActivityUpdating }] = useUpdateActivityMutation() const { uploadFiles } = useUploadFiles() + const { refetchTrips } = useRefetchTrips() const updateActivity = async (activityId: string, activityData: ActivitySchema) => { try { @@ -41,6 +43,7 @@ export const useActivityUpdate = (tripId: string) => { await uploadFiles(activityData.newFiles, { id: activityId, tripId }) } + refetchTrips(tripId) router.push(`/activity/${activityId}`) toast({ title: 'Successfully updated!', diff --git a/app/activity/hooks/useRefetchTrips.ts b/app/activity/hooks/useRefetchTrips.ts new file mode 100644 index 0000000..789ffc6 --- /dev/null +++ b/app/activity/hooks/useRefetchTrips.ts @@ -0,0 +1,18 @@ +import { useTripsCollectionQuery } from "@generated/api" + +// This hook is used to refetch trips after an activity is created, updated, or deleted. +export const useRefetchTrips = () => { + const { refetch } = useTripsCollectionQuery({}) + + const refetchTrips = (tripId: string) => { + refetch({ + filter: { + id: { eq: tripId } + }, + first: 12, + after: null + }) + } + + return { refetchTrips } +} From ab5cde9abf57b93991d5bc7dc5b16f625b824b52 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 16 Feb 2024 12:55:52 -0800 Subject: [PATCH 07/17] fix for lint format --- app/activity/hooks/useActivityCreate.ts | 20 +++++++++++++++----- app/activity/hooks/useActivityDelete.ts | 2 +- app/activity/hooks/useActivityUpdate.ts | 17 +++++++++++++---- app/activity/hooks/useRefetchTrips.ts | 2 +- app/activity/hooks/useUploadFiles.ts | 11 +++++++++-- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/app/activity/hooks/useActivityCreate.ts b/app/activity/hooks/useActivityCreate.ts index c096b0e..fef0530 100644 --- a/app/activity/hooks/useActivityCreate.ts +++ b/app/activity/hooks/useActivityCreate.ts @@ -22,7 +22,9 @@ export const useActivityCreate = (tripId: string) => { trip_id: tripId, title: activityData.title, time_from: formatToISODate(activityData.timeFrom), - time_to: activityData.timeTo ? formatToISODate(activityData.timeTo) : null, + time_to: activityData.timeTo + ? formatToISODate(activityData.timeTo) + : null, address: activityData.address, url: activityData.url, memo: activityData.memo, @@ -35,12 +37,20 @@ export const useActivityCreate = (tripId: string) => { if (errors) throw new Error(errors[0].message) - const createdActivityId = data?.insertIntoactivityCollection?.records[0]?.id + const createdActivityId = + data?.insertIntoactivityCollection?.records[0]?.id if (!createdActivityId) throw new Error('Failed to create an activity') - if (createdActivityId && activityData.newFiles && activityData.newFiles.length > 0) { - await uploadFiles(activityData.newFiles, { id: createdActivityId, tripId }) + if ( + createdActivityId && + activityData.newFiles && + activityData.newFiles.length > 0 + ) { + await uploadFiles(activityData.newFiles, { + id: createdActivityId, + tripId + }) } refetchTrips(tripId) @@ -64,6 +74,6 @@ export const useActivityCreate = (tripId: string) => { return { createActivity, - isActivityCreating, + isActivityCreating } } diff --git a/app/activity/hooks/useActivityDelete.ts b/app/activity/hooks/useActivityDelete.ts index bc0fe96..bae3ed5 100644 --- a/app/activity/hooks/useActivityDelete.ts +++ b/app/activity/hooks/useActivityDelete.ts @@ -16,7 +16,7 @@ export const useActivityDelete = () => { variables: { id: activityId }, - refetchQueries: ['activityCollection'], + refetchQueries: ['activityCollection'] }) if (errors) throw new Error(errors[0].message) diff --git a/app/activity/hooks/useActivityUpdate.ts b/app/activity/hooks/useActivityUpdate.ts index 493c4a1..caed4de 100644 --- a/app/activity/hooks/useActivityUpdate.ts +++ b/app/activity/hooks/useActivityUpdate.ts @@ -14,7 +14,10 @@ export const useActivityUpdate = (tripId: string) => { const { uploadFiles } = useUploadFiles() const { refetchTrips } = useRefetchTrips() - const updateActivity = async (activityId: string, activityData: ActivitySchema) => { + const updateActivity = async ( + activityId: string, + activityData: ActivitySchema + ) => { try { const { data, errors } = await updateActivityMutation({ variables: { @@ -22,7 +25,9 @@ export const useActivityUpdate = (tripId: string) => { set: { title: activityData.title, time_from: formatToISODate(activityData.timeFrom), - time_to: activityData.timeTo ? formatToISODate(activityData.timeTo) : null, + time_to: activityData.timeTo + ? formatToISODate(activityData.timeTo) + : null, address: activityData.address, url: activityData.url, memo: activityData.memo, @@ -39,7 +44,11 @@ export const useActivityUpdate = (tripId: string) => { if (!updatedActivityId) throw new Error('Failed to create an activity') - if (updatedActivityId && activityData.newFiles && activityData.newFiles.length > 0) { + if ( + updatedActivityId && + activityData.newFiles && + activityData.newFiles.length > 0 + ) { await uploadFiles(activityData.newFiles, { id: activityId, tripId }) } @@ -64,6 +73,6 @@ export const useActivityUpdate = (tripId: string) => { return { updateActivity, - isActivityUpdating, + isActivityUpdating } } diff --git a/app/activity/hooks/useRefetchTrips.ts b/app/activity/hooks/useRefetchTrips.ts index 789ffc6..ccb6aa6 100644 --- a/app/activity/hooks/useRefetchTrips.ts +++ b/app/activity/hooks/useRefetchTrips.ts @@ -1,4 +1,4 @@ -import { useTripsCollectionQuery } from "@generated/api" +import { useTripsCollectionQuery } from '@generated/api' // This hook is used to refetch trips after an activity is created, updated, or deleted. export const useRefetchTrips = () => { diff --git a/app/activity/hooks/useUploadFiles.ts b/app/activity/hooks/useUploadFiles.ts index 5e573b7..8e8c245 100644 --- a/app/activity/hooks/useUploadFiles.ts +++ b/app/activity/hooks/useUploadFiles.ts @@ -2,7 +2,10 @@ import { createClient } from '@supabase/supabase-js' // import { useCreateActivityUploadedFilesMutation } from '@generated/api' export const useUploadFiles = () => { - const uploadFiles = async (files: File[], activityDetails: { id: string; tripId: string; }) => { + const uploadFiles = async ( + files: File[], + activityDetails: { id: string; tripId: string } + ) => { const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_API_KEY! @@ -11,7 +14,11 @@ export const useUploadFiles = () => { const uploadPromises = files.map(async (file) => { const { data, error } = await supabase.storage .from('tabi-memo-uploads') - .upload(`trips/${activityDetails.tripId}/activity/${activityDetails.id}/${file.name}`, file, { upsert: true }) + .upload( + `trips/${activityDetails.tripId}/activity/${activityDetails.id}/${file.name}`, + file, + { upsert: true } + ) return { data, fileName: file.name, error } }) From 1c9e4632f8f3949f9d106fac97677be373b87550 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 17:06:10 -0800 Subject: [PATCH 08/17] create AcitivityUploadedFile record when upload activity image. --- app/activity/[id]/edit/page.tsx | 11 ++--- app/activity/[id]/page.tsx | 15 ++----- app/activity/components/activity-form.tsx | 51 ++++++++++++++++------- app/activity/hooks/useUploadFiles.ts | 33 +++++++++++---- app/providers/session-provider.tsx | 1 - package.json | 1 - pnpm-lock.yaml | 27 ------------ 7 files changed, 69 insertions(+), 70 deletions(-) diff --git a/app/activity/[id]/edit/page.tsx b/app/activity/[id]/edit/page.tsx index b8ee3b7..87dbd5c 100644 --- a/app/activity/[id]/edit/page.tsx +++ b/app/activity/[id]/edit/page.tsx @@ -27,12 +27,6 @@ export default function ActivityEditPage({ notifyOnNetworkStatusChange: true }) - const dummyUrls: string[] = [ - 'https://images.unsplash.com/photo-1612852098516-55d01c75769a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDR8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60', - 'https://images.unsplash.com/photo-1627875764093-315831ac12f7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60', - 'https://images.unsplash.com/photo-1571432248690-7fd6980a1ae2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDl8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60' - ] - const activityDataCollection = activityData?.activityCollection?.edges[0]?.node const activityDetailsRefetch = () => { @@ -68,7 +62,10 @@ export default function ActivityEditPage({ memo: activityDataCollection?.memo, cost: activityDataCollection?.cost, costUnit: activityDataCollection?.cost_unit, - uploadedFileUrls: dummyUrls, + uploadedFileUrls: + activityDataCollection?.activity_uploaded_filesCollection?.edges.map( + (edge) => edge?.node?.file_url + ), refetch: activityDetailsRefetch, refetchLoading: activityDetailsRefetchLoading }} diff --git a/app/activity/[id]/page.tsx b/app/activity/[id]/page.tsx index 4102db6..c0ef88e 100644 --- a/app/activity/[id]/page.tsx +++ b/app/activity/[id]/page.tsx @@ -21,18 +21,12 @@ export default function ActivityDetails({ } }) - const dummyUrls: string[] = [ - 'https://images.unsplash.com/photo-1612852098516-55d01c75769a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDR8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60', - 'https://images.unsplash.com/photo-1627875764093-315831ac12f7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60', - 'https://images.unsplash.com/photo-1571432248690-7fd6980a1ae2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDl8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=900&q=60' - ] - if (!loading && !data) throw new Error('No activity data found') const activityData = data?.activityCollection?.edges[0]?.node const uploadedImageUrls = activityData?.activity_uploaded_filesCollection ? activityData?.activity_uploaded_filesCollection?.edges.map( - (edge) => edge?.node?.file_url || '' + (edge) => edge?.node?.file_url ) : [] @@ -56,10 +50,9 @@ export default function ActivityDetails({ address={activityData?.address} url={activityData?.url} /> - {/* TODO: Fetch images from the database once available. */} - 0 ? uploadedImageUrls : dummyUrls} - /> + {uploadedImageUrls.length > 0 && ( + + )} diff --git a/app/activity/components/activity-form.tsx b/app/activity/components/activity-form.tsx index 80e2fd3..7ac3807 100644 --- a/app/activity/components/activity-form.tsx +++ b/app/activity/components/activity-form.tsx @@ -1,6 +1,5 @@ -import React, { useCallback, useState } from 'react' -import { useDropzone } from 'react-dropzone' -import { Controller, useForm } from 'react-hook-form' +import React, { useState } from 'react' +import { Controller, useForm, useWatch } from 'react-hook-form' import { Box, FormControl, @@ -11,7 +10,8 @@ import { SimpleGrid, Text, Flex, - VStack + VStack, + Input } from '@chakra-ui/react' import { PrimaryButton } from '@/components/button' import { CustomDateTimePicker } from '@/components/customDateTimePicker' @@ -45,11 +45,7 @@ export const ActivityForm = ({ activityDetails }: ActivityFormProps) => { const [selectedImages, setSelectedImages] = useState([]) - const onDrop = useCallback((acceptedFiles: File[]) => { - setSelectedImages((prevImages) => [...prevImages, ...acceptedFiles]) - }, []) - const { getRootProps, getInputProps } = useDropzone({ onDrop }) - function removeImage(index: number) { + const removeImage = (index: number) => { setSelectedImages((prevImages) => { const newImages = [...prevImages] newImages.splice(index, 1) @@ -77,10 +73,11 @@ export const ActivityForm = ({ cost: activityDetails?.cost || undefined, costUnit: activityDetails?.costUnit || undefined, uploadedFileUrls: activityDetails?.uploadedFileUrls || undefined, - newFiles: selectedImages + newFiles: [] }, resolver: activityResolver }) + const newFiles = useWatch({ control, name: 'newFiles' }) const { createActivity, isActivityCreating } = useActivityCreate(tripId) const { updateActivity, isActivityUpdating } = useActivityUpdate(tripId) @@ -180,6 +177,7 @@ export const ActivityForm = ({ width={{ base: '160px', md: '180px' }} height={{ base: '106px', md: '120px' }} margin="auto" + _hover={{ cursor: 'pointer' }} /> ))} {selectedImages.map((image, index) => ( @@ -191,13 +189,13 @@ export const ActivityForm = ({ width={{ base: '160px', md: '180px' }} height={{ base: '106px', md: '120px' }} margin="auto" + _hover={{ cursor: 'pointer' }} onClick={() => removeImage(index)} /> ))} - - - Add Image - + ( + + Add Image + { + const files = event.target.files + if (files && newFiles) { + const newFileList = Array.from(files) + setSelectedImages((prevImages) => [ + ...prevImages, + ...newFileList + ]) + newFiles.push(...newFileList) + onChange(newFiles) + } + }} + /> + + )} + /> diff --git a/app/activity/hooks/useUploadFiles.ts b/app/activity/hooks/useUploadFiles.ts index 8e8c245..c3f277e 100644 --- a/app/activity/hooks/useUploadFiles.ts +++ b/app/activity/hooks/useUploadFiles.ts @@ -1,7 +1,9 @@ import { createClient } from '@supabase/supabase-js' -// import { useCreateActivityUploadedFilesMutation } from '@generated/api' +import { useCreateActivityUploadedFilesMutation } from '@generated/api' export const useUploadFiles = () => { + const [createActivityUploadedFilesMutation] = useCreateActivityUploadedFilesMutation() + const uploadFiles = async ( files: File[], activityDetails: { id: string; tripId: string } @@ -13,7 +15,7 @@ export const useUploadFiles = () => { const uploadPromises = files.map(async (file) => { const { data, error } = await supabase.storage - .from('tabi-memo-uploads') + .from(process.env.NEXT_PUBLIC_BUCKET_NAME!) .upload( `trips/${activityDetails.tripId}/activity/${activityDetails.id}/${file.name}`, file, @@ -24,18 +26,33 @@ export const useUploadFiles = () => { }) const uploadResults = await Promise.all(uploadPromises) - const uploadErrors = uploadResults.filter((result) => result.error) - if (uploadErrors.length) { + if (uploadErrors.length > 0) { throw new Error(uploadErrors[0].error!.message) } + // get the public URL of the uploaded files + const results = uploadResults.map((result) => result) + const dataWithPublicUrls = await Promise.all( + results.map((result) => { + const { data: { publicUrl } } = supabase.storage.from(process.env.NEXT_PUBLIC_BUCKET_NAME!).getPublicUrl(result.data!.path!) + + return { publicUrl, fileName: result.fileName } + }) + ) + // create a record in the uploaded_files table - // const uploadedFiles = uploadResults.map((result) => ({ - // file_name: result.fileName, - // file_url: result.data?.path - // })) + await createActivityUploadedFilesMutation({ + variables: { + objects: dataWithPublicUrls.map((data) => ({ + activity_id: activityDetails.id, + file_name: data.fileName, + file_url: data.publicUrl, + })) + }, + refetchQueries: ['activityCollection'] + }) } return { uploadFiles } diff --git a/app/providers/session-provider.tsx b/app/providers/session-provider.tsx index ccc26d2..17a93d0 100644 --- a/app/providers/session-provider.tsx +++ b/app/providers/session-provider.tsx @@ -21,7 +21,6 @@ export function SessionProvider({ userId: string }) { const pathname = usePathname() - console.log(pathname) const AUTH_PATH_NAMES = ['/signin', '/signup'] return AUTH_PATH_NAMES.includes(pathname) ? ( {children} diff --git a/package.json b/package.json index 24a5dfe..643ecb1 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "react-datepicker": "^5.0.0", "react-datetime-picker": "^5.6.0", "react-dom": "^18", - "react-dropzone": "^14.2.3", "react-slick": "^0.29.0", "slick-carousel": "^1.8.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6dca88..e643e88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,6 @@ dependencies: react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) - react-dropzone: - specifier: ^14.2.3 - version: 14.2.3(react@18.2.0) react-slick: specifier: ^0.29.0 version: 0.29.0(react-dom@18.2.0)(react@18.2.0) @@ -8436,11 +8433,6 @@ packages: hasBin: true dev: true - /attr-accept@2.2.2: - resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} - engines: {node: '>=4'} - dev: false - /auto-bind@4.0.0: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} @@ -11140,13 +11132,6 @@ packages: flat-cache: 3.1.1 dev: true - /file-selector@0.6.0: - resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} - engines: {node: '>= 12'} - dependencies: - tslib: 2.6.2 - dev: false - /file-system-cache@1.1.0: resolution: {integrity: sha512-IzF5MBq+5CR0jXx5RxPe4BICl/oEhBSXKaL9fLhAXrIfIUS77Hr4vzrYyqYMHN6uTt+BOqi3fDCTjjEBCjERKw==} dependencies: @@ -15243,18 +15228,6 @@ packages: react: 18.2.0 scheduler: 0.23.0 - /react-dropzone@14.2.3(react@18.2.0): - resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} - engines: {node: '>= 10.13'} - peerDependencies: - react: '>= 16.8 || 18.0.0' - dependencies: - attr-accept: 2.2.2 - file-selector: 0.6.0 - prop-types: 15.8.1 - react: 18.2.0 - dev: false - /react-element-to-jsx-string@15.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==} peerDependencies: From 9808c046b12a888793f65eaa5a5e84c716275ff7 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 17:18:09 -0800 Subject: [PATCH 09/17] Fix for eslint --- app/activity/hooks/useUploadFiles.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/activity/hooks/useUploadFiles.ts b/app/activity/hooks/useUploadFiles.ts index c3f277e..48e7406 100644 --- a/app/activity/hooks/useUploadFiles.ts +++ b/app/activity/hooks/useUploadFiles.ts @@ -2,7 +2,8 @@ import { createClient } from '@supabase/supabase-js' import { useCreateActivityUploadedFilesMutation } from '@generated/api' export const useUploadFiles = () => { - const [createActivityUploadedFilesMutation] = useCreateActivityUploadedFilesMutation() + const [createActivityUploadedFilesMutation] = + useCreateActivityUploadedFilesMutation() const uploadFiles = async ( files: File[], @@ -36,7 +37,11 @@ export const useUploadFiles = () => { const results = uploadResults.map((result) => result) const dataWithPublicUrls = await Promise.all( results.map((result) => { - const { data: { publicUrl } } = supabase.storage.from(process.env.NEXT_PUBLIC_BUCKET_NAME!).getPublicUrl(result.data!.path!) + const { + data: { publicUrl } + } = supabase.storage + .from(process.env.NEXT_PUBLIC_BUCKET_NAME!) + .getPublicUrl(result.data!.path!) return { publicUrl, fileName: result.fileName } }) @@ -48,7 +53,7 @@ export const useUploadFiles = () => { objects: dataWithPublicUrls.map((data) => ({ activity_id: activityDetails.id, file_name: data.fileName, - file_url: data.publicUrl, + file_url: data.publicUrl })) }, refetchQueries: ['activityCollection'] From 943ca776a9cfaeaf338f8470c25bb82fcba5b5de Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 23 Feb 2024 17:20:32 -0800 Subject: [PATCH 10/17] fix: add validation timeTo must be later than timeFrom. --- app/activity/schema.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/activity/schema.ts b/app/activity/schema.ts index 5117688..484ce9c 100644 --- a/app/activity/schema.ts +++ b/app/activity/schema.ts @@ -16,13 +16,18 @@ const activitySchema = z.object({ uploadedFileUrls: z.array(z.string()).optional(), newFiles: z.array(z.instanceof(File)).optional() }) + .refine( + args => { + if (!args.timeTo) return true + + const { timeFrom, timeTo } = args + return timeFrom < timeTo + }, + { + message: 'Time to must be later than time from', + path: ['timeTo'] + } + ) export type ActivitySchema = z.infer export const activityResolver = zodResolver(activitySchema) - -// const filesSchema = z.object({ -// uploadedFiles: z.array(z.instanceof(File)) -// }) - -// export type FilesSchema = z.infer -// export const filesResolver = zodResolver(filesSchema) From 696293f06fdd85a7dec0066b19042e71dc26fcb5 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 23 Feb 2024 17:22:22 -0800 Subject: [PATCH 11/17] Replace CustomDateTimePicker & remove CustomDatePicker using react-datepicker. --- app/activity/components/activity-form.tsx | 24 +++- .../dateTimePickerWrapper.tsx | 117 ------------------ app/components/customDateTimePicker/index.ts | 1 - .../stories/dateTimePicker.stories.tsx | 18 --- app/components/date/custom-date-picker.tsx | 110 ++++++++++++++++ app/components/date/index.ts | 2 +- .../stories/custom-date-picker.stories.tsx | 16 ++- 7 files changed, 145 insertions(+), 143 deletions(-) delete mode 100644 app/components/customDateTimePicker/dateTimePickerWrapper.tsx delete mode 100644 app/components/customDateTimePicker/index.ts delete mode 100644 app/components/customDateTimePicker/stories/dateTimePicker.stories.tsx diff --git a/app/activity/components/activity-form.tsx b/app/activity/components/activity-form.tsx index 7ac3807..c7c7628 100644 --- a/app/activity/components/activity-form.tsx +++ b/app/activity/components/activity-form.tsx @@ -14,7 +14,7 @@ import { Input } from '@chakra-ui/react' import { PrimaryButton } from '@/components/button' -import { CustomDateTimePicker } from '@/components/customDateTimePicker' +import { CustomDateTimePicker } from '@/components/date' import { InputForm, TextareaForm } from '@/components/input' import { getDateObj } from '@/libs/utils' import { useActivityCreate, useActivityUpdate } from '../hooks' @@ -114,7 +114,12 @@ export const ActivityForm = ({ name="timeFrom" control={control} render={({ field: { onChange, value } }) => ( - + )} /> {errors.timeFrom && ( @@ -122,15 +127,26 @@ export const ActivityForm = ({ )} - + Time To ( - + )} /> + {errors.timeTo && ( + {errors.timeTo.message} + )} void - value: Value -}) => { - const { colorMode } = useColorMode() - - return ( - <> - - -
- } - clearIcon={} - format={'y-MM-dd h:mm a'} - /> -
- - ) -} diff --git a/app/components/customDateTimePicker/index.ts b/app/components/customDateTimePicker/index.ts deleted file mode 100644 index 7137b21..0000000 --- a/app/components/customDateTimePicker/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomDateTimePicker } from './dateTimePickerWrapper' diff --git a/app/components/customDateTimePicker/stories/dateTimePicker.stories.tsx b/app/components/customDateTimePicker/stories/dateTimePicker.stories.tsx deleted file mode 100644 index a75c5d8..0000000 --- a/app/components/customDateTimePicker/stories/dateTimePicker.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { StoryObj, Meta } from '@storybook/react' -import { CustomDateTimePicker } from '../index' // Adjust the import path accordingly - -const meta: Meta = { - title: 'CustomDateTimePicker', - component: CustomDateTimePicker -} - -export default meta - -type Story = StoryObj - -export const Default: Story = { - args: { - onChange: (value) => console.log(value), - value: new Date() - } -} diff --git a/app/components/date/custom-date-picker.tsx b/app/components/date/custom-date-picker.tsx index 8cbde6d..fc84bda 100644 --- a/app/components/date/custom-date-picker.tsx +++ b/app/components/date/custom-date-picker.tsx @@ -104,3 +104,113 @@ export const CustomDatePicker = ({
) } + +export const CustomDateTimePicker = ({ + selectedDate, + onChange, + placeholderText, + dateFormat +}: DatePickerProps) => { + const { colorMode } = useColorMode() + + const dateTimePickerStyles = { + '.react-datepicker-wrapper': { + width: '100%' + }, + '.react-datepicker-popper': { + right: { base: 0, md: 'unset' } + }, + '.react-datepicker ': { + width: '100%', + border: 'none', + boxShadow: '0px 2px 16px 0px rgba(0,0,0,.25);', + fontSize: 'sm' + }, + '.react-datepicker__month-container': { + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', + width: { base: '100%', md: '400px' } + }, + '.react-datepicker__triangle': { + display: 'none' + }, + '.react-datepicker__header': { + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', + borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300' + }, + '.react-datepicker__current-month': { + color: colorMode === 'dark' ? 'primary.700' : 'primary.800', + paddingY: '8px', + fontSize: 'lg' + }, + '.react-datepicker__navigation': { + top: '11px' + }, + '.react-datepicker__navigation--previous': { + left: '32px' + }, + '.react-datepicker__navigation--next': { + right: '32px' + }, + '.react-datepicker__navigation-icon': { + '&::before': { + borderColor: colorMode === 'dark' ? 'gray.500' : 'gray.400' + } + }, + '.react-datepicker__month': { + margin: '1rem' + }, + '.react-datepicker__day-names': { + paddingY: '4px' + }, + '.react-datepicker__day-name': { + width: { base: '2.4rem', md: '2.8rem' }, + lineHeight: { base: '2.4rem', md: '2.8rem' }, + color: colorMode === 'dark' ? 'gray.300' : 'black' + }, + '.react-datepicker__day': { + width: { base: '2.4rem', md: '2.8rem' }, + lineHeight: { base: '2.2rem', md: '2.4rem' }, + ':hover': { + backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.100' + }, + color: colorMode === 'dark' ? 'gray.300' : 'black' + }, + '.react-datepicker__day--selected': { + fontWeight: 'bold', + color: 'white', + backgroundColor: colorMode === 'dark' ? 'primary.700' : 'primary.500' + }, + '.react-datepicker__day--keyboard-selected': { + background: 'none' + }, + '.react-datepicker__time-container': { + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white' + }, + '.react-datepicker-time__header': { + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', + borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300', + color: colorMode === 'dark' ? 'gray.300' : 'black' + }, + '.react-datepicker__time-box': { + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', + color: colorMode === 'dark' ? 'gray.300' : 'black' + } + } + + return ( + + } + /> + + ) +} diff --git a/app/components/date/index.ts b/app/components/date/index.ts index 6ec423b..60a3b33 100644 --- a/app/components/date/index.ts +++ b/app/components/date/index.ts @@ -1 +1 @@ -export { CustomDatePicker } from './custom-date-picker' +export { CustomDatePicker, CustomDateTimePicker } from './custom-date-picker' diff --git a/app/components/date/stories/custom-date-picker.stories.tsx b/app/components/date/stories/custom-date-picker.stories.tsx index b5c1ebb..e23650a 100644 --- a/app/components/date/stories/custom-date-picker.stories.tsx +++ b/app/components/date/stories/custom-date-picker.stories.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' -import { Meta } from '@storybook/react' -import { CustomDatePicker } from '@/components/date' +import { Meta, StoryFn } from '@storybook/react' +import { CustomDatePicker, CustomDateTimePicker } from '@/components/date' const meta: Meta = { title: 'CustomDatePicker', @@ -18,3 +18,15 @@ export const Default = () => { /> ) } + +export const DateTimePicker: StoryFn = () => { + const [selectedDate, setSelectedDate] = useState(new Date()) + return ( + setSelectedDate(date)} + placeholderText="Select Date" + dateFormat="yyyy/MM/dd HH:mm" + /> + ) +} From ab9868bfea193f8021ae7e78f3a30d0fa74c8852 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 23 Feb 2024 17:23:00 -0800 Subject: [PATCH 12/17] fix for lint format. --- app/activity/schema.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/app/activity/schema.ts b/app/activity/schema.ts index 484ce9c..6d6633b 100644 --- a/app/activity/schema.ts +++ b/app/activity/schema.ts @@ -1,23 +1,24 @@ import { zodResolver } from '@hookform/resolvers/zod' import * as z from 'zod' -const activitySchema = z.object({ - title: z.string().min(1, { message: 'Please enter a title' }), - timeFrom: z.date({ - required_error: 'Please select a date from', - invalid_type_error: "That's not a date" - }), - timeTo: z.date().nullable(), - address: z.string().min(0).max(50).nullable(), - url: z.union([z.string().url().nullish(), z.literal('')]), - memo: z.string().min(0).max(300).nullable(), - cost: z.string().nullable(), - costUnit: z.string().nullable(), - uploadedFileUrls: z.array(z.string()).optional(), - newFiles: z.array(z.instanceof(File)).optional() -}) +const activitySchema = z + .object({ + title: z.string().min(1, { message: 'Please enter a title' }), + timeFrom: z.date({ + required_error: 'Please select a date from', + invalid_type_error: "That's not a date" + }), + timeTo: z.date().nullable(), + address: z.string().min(0).max(50).nullable(), + url: z.union([z.string().url().nullish(), z.literal('')]), + memo: z.string().min(0).max(300).nullable(), + cost: z.string().nullable(), + costUnit: z.string().nullable(), + uploadedFileUrls: z.array(z.string()).optional(), + newFiles: z.array(z.instanceof(File)).optional() + }) .refine( - args => { + (args) => { if (!args.timeTo) return true const { timeFrom, timeTo } = args From 68cf4448f23f9e9513671ccc1ad2b21ab25aa697 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 23 Feb 2024 17:44:59 -0800 Subject: [PATCH 13/17] add comment to explain why after param is null. --- app/activity/hooks/useRefetchTrips.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/activity/hooks/useRefetchTrips.ts b/app/activity/hooks/useRefetchTrips.ts index ccb6aa6..a0fde7e 100644 --- a/app/activity/hooks/useRefetchTrips.ts +++ b/app/activity/hooks/useRefetchTrips.ts @@ -10,6 +10,7 @@ export const useRefetchTrips = () => { id: { eq: tripId } }, first: 12, + // NOTE: This refetch hook function is designed to re-fetch only the single trip associated with an activity, so pagination considerations are unnecessary. after: null }) } From 0829cc9423b863ff6eaff7e3c7e82f2f83257ee7 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 1 Mar 2024 13:40:34 -0800 Subject: [PATCH 14/17] Fix to be able to create/update trip and activity when cost value is empty. --- app/activity/hooks/useActivityCreate.ts | 2 +- app/activity/hooks/useActivityUpdate.ts | 2 +- app/trip/hooks/useTripCreate.ts | 2 +- app/trip/hooks/useTripUpdate.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/activity/hooks/useActivityCreate.ts b/app/activity/hooks/useActivityCreate.ts index fef0530..756ed41 100644 --- a/app/activity/hooks/useActivityCreate.ts +++ b/app/activity/hooks/useActivityCreate.ts @@ -28,7 +28,7 @@ export const useActivityCreate = (tripId: string) => { address: activityData.address, url: activityData.url, memo: activityData.memo, - cost: activityData.cost, + cost: activityData.cost || null, cost_unit: activityData.costUnit } }, diff --git a/app/activity/hooks/useActivityUpdate.ts b/app/activity/hooks/useActivityUpdate.ts index caed4de..967ac04 100644 --- a/app/activity/hooks/useActivityUpdate.ts +++ b/app/activity/hooks/useActivityUpdate.ts @@ -31,7 +31,7 @@ export const useActivityUpdate = (tripId: string) => { address: activityData.address, url: activityData.url, memo: activityData.memo, - cost: activityData.cost, + cost: activityData.cost || null, cost_unit: activityData.costUnit } }, diff --git a/app/trip/hooks/useTripCreate.ts b/app/trip/hooks/useTripCreate.ts index 0f6b81e..033eea1 100644 --- a/app/trip/hooks/useTripCreate.ts +++ b/app/trip/hooks/useTripCreate.ts @@ -29,7 +29,7 @@ export const useTripCreate = () => { date_from: formatToISODate(data.date_from), date_to: data.date_to ? formatToISODate(data.date_to) : null, image_url: data.image_url, - cost: data.cost, + cost: data.cost || null, cost_unit: data.cost_unit, user_id: userId } diff --git a/app/trip/hooks/useTripUpdate.ts b/app/trip/hooks/useTripUpdate.ts index ad91b02..81d0d90 100644 --- a/app/trip/hooks/useTripUpdate.ts +++ b/app/trip/hooks/useTripUpdate.ts @@ -41,7 +41,7 @@ export const useTripUpdate = ( date_from: formatToISODate(data.date_from), date_to: data.date_to ? formatToISODate(data.date_to) : null, image_url: data.image_url, - cost: data.cost, + cost: data.cost || null, cost_unit: data.cost_unit } } From 686850ea26956625e25c0b9ddf1169a40d16592d Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 1 Mar 2024 14:06:41 -0800 Subject: [PATCH 15/17] Fix timeTo nullability in ActivityForm --- app/activity/components/activity-form.tsx | 9 ++++----- app/activity/schema.ts | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/activity/components/activity-form.tsx b/app/activity/components/activity-form.tsx index c7c7628..21f2761 100644 --- a/app/activity/components/activity-form.tsx +++ b/app/activity/components/activity-form.tsx @@ -24,7 +24,7 @@ export type ActivityDetails = { id: string title: string timeFrom?: string | null - timeTo?: string | null + timeTo?: string | null | undefined address?: string | null url?: string | null memo?: string | null @@ -66,7 +66,7 @@ export const ActivityForm = ({ : undefined, timeTo: activityDetails?.timeTo ? getDateObj(activityDetails?.timeTo) - : undefined, + : null, address: activityDetails?.address || undefined, url: activityDetails?.url || undefined, memo: activityDetails?.memo || undefined, @@ -92,7 +92,7 @@ export const ActivityForm = ({ onSubmit={handleSubmit(mutateFun)} pt={{ base: '40px', md: '40px' }} > - + Title @@ -138,7 +137,7 @@ export const ActivityForm = ({ render={({ field: { onChange, value } }) => ( diff --git a/app/activity/schema.ts b/app/activity/schema.ts index 6d6633b..8cbbf97 100644 --- a/app/activity/schema.ts +++ b/app/activity/schema.ts @@ -8,7 +8,7 @@ const activitySchema = z required_error: 'Please select a date from', invalid_type_error: "That's not a date" }), - timeTo: z.date().nullable(), + timeTo: z.date().nullable().optional(), address: z.string().min(0).max(50).nullable(), url: z.union([z.string().url().nullish(), z.literal('')]), memo: z.string().min(0).max(300).nullable(), @@ -19,7 +19,7 @@ const activitySchema = z }) .refine( (args) => { - if (!args.timeTo) return true + if (args.timeTo === null || args.timeTo === undefined) return true const { timeFrom, timeTo } = args return timeFrom < timeTo From df7c7e6c6829b7aa96e859a7137cd5bd01e444d6 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 1 Mar 2024 15:42:42 -0800 Subject: [PATCH 16/17] Update CustomDatePicker styles --- app/components/date/custom-date-picker.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/components/date/custom-date-picker.tsx b/app/components/date/custom-date-picker.tsx index fc84bda..658c284 100644 --- a/app/components/date/custom-date-picker.tsx +++ b/app/components/date/custom-date-picker.tsx @@ -128,7 +128,12 @@ export const CustomDateTimePicker = ({ }, '.react-datepicker__month-container': { backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', - width: { base: '100%', md: '400px' } + width: { base: '80%', md: '400px' }, // In order to display month and time side by side, width is set to 80% and 20% respectively + height: { base: '360px', md: '386px' } // Due to .react-datepicker__time-list height is set to static px value by inline style, this is to prevent the time list from overflowing. + }, + '.react-datepicker__time-container': { + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', + width: { base: '20%', md: '85px' } // In order to display month and time side by side, width is set to 20% and 80% respectively }, '.react-datepicker__triangle': { display: 'none' @@ -137,6 +142,11 @@ export const CustomDateTimePicker = ({ backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300' }, + '.react-datepicker__header--time': { + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', + borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300', + width: { base: '85px' } + }, '.react-datepicker__current-month': { color: colorMode === 'dark' ? 'primary.700' : 'primary.800', paddingY: '8px', @@ -163,12 +173,12 @@ export const CustomDateTimePicker = ({ paddingY: '4px' }, '.react-datepicker__day-name': { - width: { base: '2.4rem', md: '2.8rem' }, + width: { base: '2rem', md: '2.8rem' }, lineHeight: { base: '2.4rem', md: '2.8rem' }, color: colorMode === 'dark' ? 'gray.300' : 'black' }, '.react-datepicker__day': { - width: { base: '2.4rem', md: '2.8rem' }, + width: { base: '2rem', md: '2.8rem' }, lineHeight: { base: '2.2rem', md: '2.4rem' }, ':hover': { backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.100' @@ -183,9 +193,6 @@ export const CustomDateTimePicker = ({ '.react-datepicker__day--keyboard-selected': { background: 'none' }, - '.react-datepicker__time-container': { - backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white' - }, '.react-datepicker-time__header': { backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300', From f6a19f997ba86c7a036fa1a75a146a7ba1207c98 Mon Sep 17 00:00:00 2001 From: Kana Taguchi Date: Sat, 2 Mar 2024 11:58:22 -0800 Subject: [PATCH 17/17] Fix calendaer time --- app/components/date/custom-date-picker.tsx | 28 +++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/app/components/date/custom-date-picker.tsx b/app/components/date/custom-date-picker.tsx index 658c284..ef44e4d 100644 --- a/app/components/date/custom-date-picker.tsx +++ b/app/components/date/custom-date-picker.tsx @@ -124,7 +124,8 @@ export const CustomDateTimePicker = ({ width: '100%', border: 'none', boxShadow: '0px 2px 16px 0px rgba(0,0,0,.25);', - fontSize: 'sm' + fontSize: 'sm', + backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white' }, '.react-datepicker__month-container': { backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', @@ -144,8 +145,7 @@ export const CustomDateTimePicker = ({ }, '.react-datepicker__header--time': { backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', - borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300', - width: { base: '85px' } + borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300' }, '.react-datepicker__current-month': { color: colorMode === 'dark' ? 'primary.700' : 'primary.800', @@ -156,11 +156,12 @@ export const CustomDateTimePicker = ({ top: '11px' }, '.react-datepicker__navigation--previous': { - left: '32px' - }, - '.react-datepicker__navigation--next': { - right: '32px' + left: { base: '16px', md: '32px' } }, + '.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button)': + { + right: '24%' + }, '.react-datepicker__navigation-icon': { '&::before': { borderColor: colorMode === 'dark' ? 'gray.500' : 'gray.400' @@ -198,10 +199,21 @@ export const CustomDateTimePicker = ({ borderBottomColor: colorMode === 'dark' ? 'gray.500' : 'gray.300', color: colorMode === 'dark' ? 'gray.300' : 'black' }, + '.react-datepicker__time-box': { backgroundColor: colorMode === 'dark' ? 'gray.700' : 'white', color: colorMode === 'dark' ? 'gray.300' : 'black' - } + }, + + '.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box': + { + width: { base: '100%', md: '85px' } + }, + '.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected': + { + color: 'white', + backgroundColor: colorMode === 'dark' ? 'primary.700' : 'primary.500' + } } return (