From fc3146524b9585be0205a445e3347526eb51193f Mon Sep 17 00:00:00 2001 From: samuraikun Date: Fri, 9 Feb 2024 18:13:40 -0800 Subject: [PATCH 01/31] preview uploaded file in Trip Create/Edit page --- app/trip/components/trip-form.tsx | 39 ++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/app/trip/components/trip-form.tsx b/app/trip/components/trip-form.tsx index a2f406a..b16d739 100644 --- a/app/trip/components/trip-form.tsx +++ b/app/trip/components/trip-form.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { useForm, Controller } from 'react-hook-form' import { FormControl, @@ -11,7 +12,8 @@ import { Flex, Checkbox, Box, - useDisclosure + useDisclosure, + Input } from '@chakra-ui/react' import { PrimaryButton, SecondaryButton } from '@/components/button' import { CustomDatePicker } from '@/components/date' @@ -91,6 +93,9 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { const mutateFun = tripDetails ? updateTrip : createTrip const isMutating = isTripCreating || isTripUpdating + // Image Upload + const [selectedImage, setSelectedImage] = useState(null) + return ( <> { Image - - Select Image + {selectedImage ? ( + Selected Image + ) : ( + Default Image + )} + + Select Image + { + const file = event.target.files?.[0] + if (file) { + setSelectedImage(file) + } + }} + hidden + /> + {errors?.image_url?.message} From c4aaa899ea911afaf004e0dc3a9b6d8b1a3f6ae0 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 10 Feb 2024 14:53:19 -0800 Subject: [PATCH 02/31] Upload image in Trip Create/Edit page --- app/trip/[id]/page.tsx | 49 ++----------------- .../action/update-image-metadata.ts | 7 --- app/trip/components/trip-form.tsx | 42 +++++++++------- app/trip/hooks/useTripCreate.ts | 6 +++ app/trip/hooks/useTripUpdate.ts | 6 +++ app/trip/hooks/useUploadFile.ts | 24 +++++++++ app/trip/schema.ts | 1 + 7 files changed, 64 insertions(+), 71 deletions(-) rename app/trip/{[id] => }/action/update-image-metadata.ts (82%) create mode 100644 app/trip/hooks/useUploadFile.ts diff --git a/app/trip/[id]/page.tsx b/app/trip/[id]/page.tsx index b3a24d7..783e4a2 100644 --- a/app/trip/[id]/page.tsx +++ b/app/trip/[id]/page.tsx @@ -1,12 +1,9 @@ 'use client' -import { useState } from 'react' -import { Box, Container, useColorModeValue, Input } from '@chakra-ui/react' -import { createClient } from '@supabase/supabase-js' +import { Box, Container, useColorModeValue } from '@chakra-ui/react' import { useRouter } from 'next/navigation' import { PrimaryButton } from '@/components/button' import { Loading } from '@/components/loading' -import { updateImageMetadataAction } from './action/update-image-metadata' import { TripDetailsHeader, TripDetailsTabs } from './components' import { useTripDetailsQuery } from '@generated/api' @@ -20,11 +17,7 @@ export default function TripDetailsPage({ const router = useRouter() - const { - data: tripData, - loading: tripLoading, - refetch: refetchTrip - } = useTripDetailsQuery({ + const { data: tripData, loading: tripLoading } = useTripDetailsQuery({ variables: { id: params.id } @@ -33,32 +26,6 @@ export default function TripDetailsPage({ if (!tripData && !tripLoading) throw new Error('No trip data found') const tripDataCollection = tripData?.tripsCollection - const [selectedImage, setSelectedImage] = useState(null) - - const uploadImage = async (id: string, file: File) => { - try { - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_API_KEY! - ) - - // TODO: Authenticated user can upload images with policy - const { data: uploadData, error: uploadError } = await supabase.storage - .from('tabi-memo-uploads') - .upload(`trips/${id}/${file.name}`, file, { upsert: true }) - - if (uploadError) throw new Error(uploadError.message) - - // NOTE: Server action doen't return result in Client Component. I don't know why. - await updateImageMetadataAction(id, uploadData.path) - - setSelectedImage(file) - await refetchTrip() - } catch (error) { - console.error({ error }) - throw error - } - } return ( @@ -73,11 +40,7 @@ export default function TripDetailsPage({ <> Add Activity - uploadImage(params.id, e.target.files![0])} - /> )} diff --git a/app/trip/[id]/action/update-image-metadata.ts b/app/trip/action/update-image-metadata.ts similarity index 82% rename from app/trip/[id]/action/update-image-metadata.ts rename to app/trip/action/update-image-metadata.ts index 924d636..c9ebc08 100644 --- a/app/trip/[id]/action/update-image-metadata.ts +++ b/app/trip/action/update-image-metadata.ts @@ -15,13 +15,6 @@ export const updateImageMetadataAction = async ( // https://supabase.com/docs/guides/auth/auth-helpers/nextjs?language=ts#server-actions const supabase = createServerActionClient({ cookies: () => cookieStore }) - // Supabase Storage API is not available in the server action. - // const { data: uploadData, error: uploadError } = await supabase.storage - // .from('tabi-memo-uploads') - // .upload(`trips/${id}/${file.name}`, file, { upsert: true }) - - // if (uploadError) throw uploadError - const { data: { publicUrl } } = await supabase.storage diff --git a/app/trip/components/trip-form.tsx b/app/trip/components/trip-form.tsx index b16d739..2eda183 100644 --- a/app/trip/components/trip-form.tsx +++ b/app/trip/components/trip-form.tsx @@ -59,6 +59,7 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { '/images/no_image_light.jpg', '/images/no_image_dark.jpg' ) + const [selectedImage, setSelectedImage] = useState(null) const { isOpen, onOpen, onClose } = useDisclosure() const { createTrip, isTripCreating } = useTripCreate() @@ -83,6 +84,7 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { : undefined, date_to: tripDetails?.dateTo ? getDateObj(tripDetails.dateTo) : null, image_url: tripDetails?.image || null, + uploaded_image_file: null, selectedTags: tripTags ? tripTags.data.map((tag) => tag.tag_id) : [], cost: tripDetails?.cost ? tripDetails.cost.toString() : null, cost_unit: tripDetails?.costUnit @@ -93,9 +95,6 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { const mutateFun = tripDetails ? updateTrip : createTrip const isMutating = isTripCreating || isTripUpdating - // Image Upload - const [selectedImage, setSelectedImage] = useState(null) - return ( <> { objectFit="cover" /> )} - - Select Image - { - const file = event.target.files?.[0] - if (file) { - setSelectedImage(file) - } - }} - hidden - /> - + ( + + Select Image + { + const file = event.target.files?.[0] + if (file) { + setSelectedImage(file) + onChange(file) + } + }} + hidden + /> + + )} + /> {errors?.image_url?.message} diff --git a/app/trip/hooks/useTripCreate.ts b/app/trip/hooks/useTripCreate.ts index 1efd38e..4231c69 100644 --- a/app/trip/hooks/useTripCreate.ts +++ b/app/trip/hooks/useTripCreate.ts @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation' import { formatToISODate } from '@/libs/utils' import { useUserId } from '@/providers/session-provider' import { TripSchema } from '../schema' +import { useUploadFile } from './useUploadFile' import { useTripsGet } from '.' import { useCreateTripMutation, useCreateTripTagMutation } from '@generated/api' @@ -17,6 +18,7 @@ export const useTripCreate = () => { useCreateTripTagMutation() const { tripsRefetch } = useTripsGet() + const { uploadFile } = useUploadFile() const createTrip = async (data: TripSchema) => { try { @@ -50,6 +52,10 @@ export const useTripCreate = () => { await Promise.all([...createPromises]) + if (data.uploaded_image_file && createdTripId) { + uploadFile(data.uploaded_image_file, createdTripId) + } + tripsRefetch() router.push('/') diff --git a/app/trip/hooks/useTripUpdate.ts b/app/trip/hooks/useTripUpdate.ts index ca53209..e3b3d8b 100644 --- a/app/trip/hooks/useTripUpdate.ts +++ b/app/trip/hooks/useTripUpdate.ts @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation' import { formatToISODate } from '@/libs/utils' import { TripDetailsArgs, TripTagsArgs } from '../components/trip-form' import { TripSchema } from '../schema' +import { useUploadFile } from './useUploadFile' import { useTripsGet } from '.' import { useUpdateTripMutation, @@ -25,6 +26,7 @@ export const useTripUpdate = ( useDeleteTripTagMutation() const { tripsRefetch } = useTripsGet() + const { uploadFile } = useUploadFile() const updateTrip = async (data: TripSchema) => { if (!tripDetails) throw new Error('Trip details is not found') @@ -71,6 +73,10 @@ export const useTripUpdate = ( await Promise.all([...deletePromises, ...createPromises]) + if (data.uploaded_image_file && tripDetails) { + uploadFile(data.uploaded_image_file, tripDetails.id) + } + tripDetails.refetch() tripTags?.refetch() tripsRefetch() diff --git a/app/trip/hooks/useUploadFile.ts b/app/trip/hooks/useUploadFile.ts new file mode 100644 index 0000000..1d81d47 --- /dev/null +++ b/app/trip/hooks/useUploadFile.ts @@ -0,0 +1,24 @@ +import { createClient } from '@supabase/supabase-js' +import { updateImageMetadataAction } from '../action/update-image-metadata' + +export const useUploadFile = () => { + // Upload image to Supabase Storage & Update image_url by server action + const uploadFile = async (file: File, tripId: string) => { + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_API_KEY! + ) + + // TODO: Authenticated user can upload images with policy + const { data: uploadData, error: uploadError } = await supabase.storage + .from('tabi-memo-uploads') + .upload(`trips/${tripId}/${file.name}`, file, { upsert: true }) + + if (uploadError) throw new Error(uploadError.message) + + // NOTE: Server action doen't return result in Client Component. I don't know why. + await updateImageMetadataAction(tripId, uploadData.path) + } + + return { uploadFile } +} diff --git a/app/trip/schema.ts b/app/trip/schema.ts index 6b73ee7..30fbdca 100644 --- a/app/trip/schema.ts +++ b/app/trip/schema.ts @@ -9,6 +9,7 @@ const tripSchema = z.object({ }), date_to: z.date().nullable(), image_url: z.string().nullable(), + uploaded_image_file: z.instanceof(File).nullable(), selectedTags: z.array(z.string()), cost: z.string().nullable(), cost_unit: z.string().nullable() From 5735c99e291e5e399be7b2967301350fea6f6b91 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 10 Feb 2024 15:17:28 -0800 Subject: [PATCH 03/31] fix: wait for file upload to complete --- app/trip/hooks/useTripCreate.ts | 2 +- app/trip/hooks/useTripUpdate.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/trip/hooks/useTripCreate.ts b/app/trip/hooks/useTripCreate.ts index 4231c69..0f6b81e 100644 --- a/app/trip/hooks/useTripCreate.ts +++ b/app/trip/hooks/useTripCreate.ts @@ -53,7 +53,7 @@ export const useTripCreate = () => { await Promise.all([...createPromises]) if (data.uploaded_image_file && createdTripId) { - uploadFile(data.uploaded_image_file, createdTripId) + await uploadFile(data.uploaded_image_file, createdTripId) } tripsRefetch() diff --git a/app/trip/hooks/useTripUpdate.ts b/app/trip/hooks/useTripUpdate.ts index e3b3d8b..2780457 100644 --- a/app/trip/hooks/useTripUpdate.ts +++ b/app/trip/hooks/useTripUpdate.ts @@ -74,7 +74,7 @@ export const useTripUpdate = ( await Promise.all([...deletePromises, ...createPromises]) if (data.uploaded_image_file && tripDetails) { - uploadFile(data.uploaded_image_file, tripDetails.id) + await uploadFile(data.uploaded_image_file, tripDetails.id) } tripDetails.refetch() From a10ed09d85a6b4ef80db8aa3d4eac759733865de Mon Sep 17 00:00:00 2001 From: Kana Taguchi Date: Sat, 17 Feb 2024 17:32:37 -0800 Subject: [PATCH 04/31] Add UI of manage group --- .vscode/settings.json | 4 +- app/const/index.ts | 7 + .../[id]/components/trip-details-header.tsx | 3 +- .../[id]/manage-group/components/index.ts | 2 + .../components/invited-user-card.tsx | 89 ++++++ .../manage-group/components/owner-card.tsx | 46 +++ .../graphql/query/trip-shared-users.graphql | 30 ++ app/trip/[id]/manage-group/page.tsx | 75 +++++ graphql-codegen/generated/api.ts | 156 ++++++++++ graphql-codegen/generated/gql.ts | 8 + graphql-codegen/generated/graphql.ts | 291 ++++++++++++++++++ .../generated/persisted-documents.json | 1 + 12 files changed, 708 insertions(+), 4 deletions(-) create mode 100644 app/const/index.ts create mode 100644 app/trip/[id]/manage-group/components/index.ts create mode 100644 app/trip/[id]/manage-group/components/invited-user-card.tsx create mode 100644 app/trip/[id]/manage-group/components/owner-card.tsx create mode 100644 app/trip/[id]/manage-group/graphql/query/trip-shared-users.graphql create mode 100644 app/trip/[id]/manage-group/page.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 66243f6..ca831ba 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,8 +5,8 @@ "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, - "node_modules": true, - ".next": true + "node_modules": false, + ".next": false }, "eslint.lintTask.enable": true, "typescript.tsdk": "node_modules/typescript/lib", diff --git a/app/const/index.ts b/app/const/index.ts new file mode 100644 index 0000000..1204abd --- /dev/null +++ b/app/const/index.ts @@ -0,0 +1,7 @@ +export const INVITATION_PERMISSION_LEVEL = [ + { value: 'editable', label: 'Can Edit' }, + { value: 'view_only', label: 'View Only' } +] as const + +export type InvitationPermissionLevelType = + (typeof INVITATION_PERMISSION_LEVEL)[number]['value'] diff --git a/app/trip/[id]/components/trip-details-header.tsx b/app/trip/[id]/components/trip-details-header.tsx index ecff77f..ce104a4 100644 --- a/app/trip/[id]/components/trip-details-header.tsx +++ b/app/trip/[id]/components/trip-details-header.tsx @@ -175,9 +175,8 @@ export const TripDetailsHeader = ({ ))} )} - {/* TODO Change URL */} { + const borderColor = useColorModeValue('gray.300', 'gray.600') + + return ( + + + + + + {name} + + {email} + + + + + + + {INVITATION_PERMISSION_LEVEL.map((level) => ( + + {level.label} + + ))} + + + + {}} + variant="unstyled" + color={'gray.400'} + _hover={{ + color: 'gray.500' + }} + minW={{ base: '18px', md: '24px' }} + h={{ base: '18px', md: '24px' }} + > + + + + + ) +} diff --git a/app/trip/[id]/manage-group/components/owner-card.tsx b/app/trip/[id]/manage-group/components/owner-card.tsx new file mode 100644 index 0000000..e5ee600 --- /dev/null +++ b/app/trip/[id]/manage-group/components/owner-card.tsx @@ -0,0 +1,46 @@ +import { + Box, + useColorModeValue, + Heading, + Avatar, + Flex, + Text +} from '@chakra-ui/react' + +type OwnerCardProps = { + image: string | null | undefined + name: string + email: string +} + +export const OwnerCard = ({ image, name, email }: OwnerCardProps) => { + const borderColor = useColorModeValue('gray.300', 'gray.600') + + return ( + + + + + + {name} + + {email} + + + + + Owner + + + ) +} diff --git a/app/trip/[id]/manage-group/graphql/query/trip-shared-users.graphql b/app/trip/[id]/manage-group/graphql/query/trip-shared-users.graphql new file mode 100644 index 0000000..e072c7b --- /dev/null +++ b/app/trip/[id]/manage-group/graphql/query/trip-shared-users.graphql @@ -0,0 +1,30 @@ +query tripSharedUsers($tripId: UUID!) { + tripsCollection(filter: { id: { eq: $tripId } }) { + edges { + node { + id + title + users { + id + name + profile_picture_url + email + } + invitationsCollection { + edges { + node { + id + permission_level + users { + id + name + email + profile_picture_url + } + } + } + } + } + } + } +} diff --git a/app/trip/[id]/manage-group/page.tsx b/app/trip/[id]/manage-group/page.tsx new file mode 100644 index 0000000..4ba405f --- /dev/null +++ b/app/trip/[id]/manage-group/page.tsx @@ -0,0 +1,75 @@ +'use client' + +import { + Box, + Container, + useColorModeValue, + Heading, + VStack +} from '@chakra-ui/react' +import { Loading } from '@/components/loading' +import { OwnerCard, InvitedUserCard } from './components' +import { useTripSharedUsersQuery } from '@generated/api' + +export default function ManageGroup({ params }: { params: { id: string } }) { + const bg = useColorModeValue('white', 'gray.800') + const color = useColorModeValue('black', 'gray.300') + + const { data, loading } = useTripSharedUsersQuery({ + variables: { + tripId: params.id + } + }) + + const tripSharedUsers = data?.tripsCollection?.edges[0].node + + return ( + + + + Manage Group + + + {!tripSharedUsers || loading ? ( + + ) : ( + + + + {tripSharedUsers.invitationsCollection?.edges.map( + (invitedUser) => { + return ( + + ) + } + )} + + )} + + + + ) +} diff --git a/graphql-codegen/generated/api.ts b/graphql-codegen/generated/api.ts index 8fb224a..f4ac75c 100644 --- a/graphql-codegen/generated/api.ts +++ b/graphql-codegen/generated/api.ts @@ -1352,6 +1352,50 @@ export type CreateActivityMutation = { } | null } +export type TripSharedUsersQueryVariables = Exact<{ + tripId: Scalars['UUID']['input'] +}> + +export type TripSharedUsersQuery = { + __typename: 'Query' + tripsCollection?: { + __typename: 'tripsConnection' + edges: Array<{ + __typename: 'tripsEdge' + node: { + __typename: 'trips' + id: string + title: string + users?: { + __typename: 'users' + id: string + name: string + profile_picture_url?: string | null + email: string + } | null + invitationsCollection?: { + __typename: 'invitationsConnection' + edges: Array<{ + __typename: 'invitationsEdge' + node: { + __typename: 'invitations' + id: string + permission_level: Permission_Level_Enum + users?: { + __typename: 'users' + id: string + name: string + email: string + profile_picture_url?: string | null + } | null + } + }> + } | null + } + }> + } | null +} + export type CreateTagMutationVariables = Exact<{ name: Scalars['String']['input'] userId: Scalars['UUID']['input'] @@ -1860,6 +1904,118 @@ export type CreateActivityMutationOptions = Apollo.BaseMutationOptions< CreateActivityMutation, CreateActivityMutationVariables > +export const TripSharedUsersDocument = gql` + query tripSharedUsers($tripId: UUID!) { + __typename + tripsCollection(filter: { id: { eq: $tripId } }) { + __typename + edges { + __typename + node { + __typename + id + title + users { + __typename + id + name + profile_picture_url + email + } + invitationsCollection { + __typename + edges { + __typename + node { + __typename + id + permission_level + users { + __typename + id + name + email + profile_picture_url + } + } + } + } + } + } + } + } +` + +/** + * __useTripSharedUsersQuery__ + * + * To run a query within a React component, call `useTripSharedUsersQuery` and pass it any options that fit your needs. + * When your component renders, `useTripSharedUsersQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useTripSharedUsersQuery({ + * variables: { + * tripId: // value for 'tripId' + * }, + * }); + */ +export function useTripSharedUsersQuery( + baseOptions: Apollo.QueryHookOptions< + TripSharedUsersQuery, + TripSharedUsersQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useQuery( + TripSharedUsersDocument, + options + ) +} +export function useTripSharedUsersLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + TripSharedUsersQuery, + TripSharedUsersQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useLazyQuery< + TripSharedUsersQuery, + TripSharedUsersQueryVariables + >(TripSharedUsersDocument, options) +} +export function useTripSharedUsersSuspenseQuery( + baseOptions?: Apollo.SuspenseQueryHookOptions< + TripSharedUsersQuery, + TripSharedUsersQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useSuspenseQuery< + TripSharedUsersQuery, + TripSharedUsersQueryVariables + >(TripSharedUsersDocument, options) +} +export type TripSharedUsersQueryHookResult = ReturnType< + typeof useTripSharedUsersQuery +> +export type TripSharedUsersLazyQueryHookResult = ReturnType< + typeof useTripSharedUsersLazyQuery +> +export type TripSharedUsersSuspenseQueryHookResult = ReturnType< + typeof useTripSharedUsersSuspenseQuery +> +export type TripSharedUsersQueryResult = Apollo.QueryResult< + TripSharedUsersQuery, + TripSharedUsersQueryVariables +> +export function refetchTripSharedUsersQuery( + variables: TripSharedUsersQueryVariables +) { + return { query: TripSharedUsersDocument, variables: variables } +} 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 5136b13..c5d468c 100644 --- a/graphql-codegen/generated/gql.ts +++ b/graphql-codegen/generated/gql.ts @@ -19,6 +19,8 @@ const documents = { 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, + 'query tripSharedUsers($tripId: UUID!) {\n tripsCollection(filter: {id: {eq: $tripId}}) {\n edges {\n node {\n id\n title\n users {\n id\n name\n profile_picture_url\n email\n }\n invitationsCollection {\n edges {\n node {\n id\n permission_level\n users {\n id\n name\n email\n profile_picture_url\n }\n }\n }\n }\n }\n }\n }\n}': + types.TripSharedUsersDocument, '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}': @@ -75,6 +77,12 @@ export function graphql( 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}'] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: 'query tripSharedUsers($tripId: UUID!) {\n tripsCollection(filter: {id: {eq: $tripId}}) {\n edges {\n node {\n id\n title\n users {\n id\n name\n profile_picture_url\n email\n }\n invitationsCollection {\n edges {\n node {\n id\n permission_level\n users {\n id\n name\n email\n profile_picture_url\n }\n }\n }\n }\n }\n }\n }\n}' +): (typeof documents)['query tripSharedUsers($tripId: UUID!) {\n tripsCollection(filter: {id: {eq: $tripId}}) {\n edges {\n node {\n id\n title\n users {\n id\n name\n profile_picture_url\n email\n }\n invitationsCollection {\n edges {\n node {\n id\n permission_level\n users {\n id\n name\n email\n profile_picture_url\n }\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..899a446 100644 --- a/graphql-codegen/generated/graphql.ts +++ b/graphql-codegen/generated/graphql.ts @@ -1360,6 +1360,50 @@ export type CreateActivityMutation = { } | null } +export type TripSharedUsersQueryVariables = Exact<{ + tripId: Scalars['UUID']['input'] +}> + +export type TripSharedUsersQuery = { + __typename: 'Query' + tripsCollection?: { + __typename: 'tripsConnection' + edges: Array<{ + __typename: 'tripsEdge' + node: { + __typename: 'trips' + id: string + title: string + users?: { + __typename: 'users' + id: string + name: string + profile_picture_url?: string | null + email: string + } | null + invitationsCollection?: { + __typename: 'invitationsConnection' + edges: Array<{ + __typename: 'invitationsEdge' + node: { + __typename: 'invitations' + id: string + permission_level: Permission_Level_Enum + users?: { + __typename: 'users' + id: string + name: string + email: string + profile_picture_url?: string | null + } | null + } + }> + } | null + } + }> + } | null +} + export type CreateTagMutationVariables = Exact<{ name: Scalars['String']['input'] userId: Scalars['UUID']['input'] @@ -2072,6 +2116,253 @@ export const CreateActivityDocument = { CreateActivityMutation, CreateActivityMutationVariables > +export const TripSharedUsersDocument = { + __meta__: { hash: '16ece6918ea030b3bf8bcf763c1a296e90d6dcf9' }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'tripSharedUsers' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { + kind: 'Variable', + name: { kind: 'Name', value: 'tripId' } + }, + 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: 'tripsCollection' }, + 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: 'tripId' } + } + } + ] + } + } + ] + } + } + ], + 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: 'title' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'users' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'id' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'name' } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'profile_picture_url' + } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'email' } + } + ] + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'invitationsCollection' + }, + 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: 'permission_level' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'users' + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { + kind: 'Name', + value: '__typename' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'id' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'name' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'email' + } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: + 'profile_picture_url' + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode< + TripSharedUsersQuery, + TripSharedUsersQueryVariables +> 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 26cadb0..d9b4860 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 } } } }", "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 } } } }", "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 } } }", + "16ece6918ea030b3bf8bcf763c1a296e90d6dcf9": "query tripSharedUsers($tripId: UUID!) { __typename tripsCollection(filter: {id: {eq: $tripId}}) { __typename edges { __typename node { __typename id invitationsCollection { __typename edges { __typename node { __typename id permission_level users { __typename email id name profile_picture_url } } } } title users { __typename email id name profile_picture_url } } } } }", "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 3d3237cae33ee00f1163411a9cd512d4da6e3490 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 18:06:02 -0800 Subject: [PATCH 05/31] Add image upload form validation & cursor pointer. --- app/trip/components/trip-form.tsx | 9 +++++---- app/trip/schema.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/trip/components/trip-form.tsx b/app/trip/components/trip-form.tsx index 916f630..5482c7e 100644 --- a/app/trip/components/trip-form.tsx +++ b/app/trip/components/trip-form.tsx @@ -148,7 +148,7 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { {/* TODO Image Upload to storage & Send the URL string to DB */} - + Image {selectedImage ? ( @@ -170,12 +170,11 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { name="uploaded_image_file" control={control} render={({ field: { onChange } }) => ( - + Select Image { const file = event.target.files?.[0] if (file) { @@ -189,7 +188,9 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { )} /> - {errors?.image_url?.message} + + {errors?.uploaded_image_file?.message} + diff --git a/app/trip/schema.ts b/app/trip/schema.ts index 30fbdca..b7f24b5 100644 --- a/app/trip/schema.ts +++ b/app/trip/schema.ts @@ -9,7 +9,15 @@ const tripSchema = z.object({ }), date_to: z.date().nullable(), image_url: z.string().nullable(), - uploaded_image_file: z.instanceof(File).nullable(), + uploaded_image_file: z + .instanceof(File) + .nullable() + .refine((file) => file === null || file.size <= 10_000_000, { + message: "File size must be less than 10MB", + }) + .refine((file) => file === null || file.type.startsWith('image/'), { + message: "Only image files are allowed", + }), selectedTags: z.array(z.string()), cost: z.string().nullable(), cost_unit: z.string().nullable() From 401a13824181eb71381d3ed316d4f8852bb29cdb Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 18:21:43 -0800 Subject: [PATCH 06/31] fix for lint --- app/trip/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/trip/schema.ts b/app/trip/schema.ts index b7f24b5..d4de9e7 100644 --- a/app/trip/schema.ts +++ b/app/trip/schema.ts @@ -13,10 +13,10 @@ const tripSchema = z.object({ .instanceof(File) .nullable() .refine((file) => file === null || file.size <= 10_000_000, { - message: "File size must be less than 10MB", + message: 'File size must be less than 10MB' }) .refine((file) => file === null || file.type.startsWith('image/'), { - message: "Only image files are allowed", + message: 'Only image files are allowed' }), selectedTags: z.array(z.string()), cost: z.string().nullable(), From 7277186607c4f2e1b4fcd539db728fe1b9f83d88 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 21:56:25 -0800 Subject: [PATCH 07/31] Refactor trip update and image upload logic Remove server action --- app/trip/action/update-image-metadata.ts | 46 ------------------------ app/trip/hooks/useTripUpdate.ts | 12 ++++--- app/trip/hooks/useUploadFile.ts | 25 ++++++++++--- 3 files changed, 29 insertions(+), 54 deletions(-) delete mode 100644 app/trip/action/update-image-metadata.ts diff --git a/app/trip/action/update-image-metadata.ts b/app/trip/action/update-image-metadata.ts deleted file mode 100644 index c9ebc08..0000000 --- a/app/trip/action/update-image-metadata.ts +++ /dev/null @@ -1,46 +0,0 @@ -'use server' -import { createServerActionClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' - -export const updateImageMetadataAction = async ( - id: string, - filePath: string -): Promise< - | { status: 'success'; data: { publicUrl: string } } - | { status: 'error'; data: { publicUrl: string }; error: { message: string } } -> => { - try { - const cookieStore = cookies() - // NOTE: Use createServerActionClient instead of createClient in with-cookie.ts - // https://supabase.com/docs/guides/auth/auth-helpers/nextjs?language=ts#server-actions - const supabase = createServerActionClient({ cookies: () => cookieStore }) - - const { - data: { publicUrl } - } = await supabase.storage - .from(process.env.NEXT_PUBLIC_BUCKET_NAME!) - .getPublicUrl(filePath) - - const updateResponse = await supabase - .from('trips') - .update({ image_url: publicUrl }) - .match({ id }) - - if (updateResponse.error) { - return { - status: 'error', - data: { publicUrl }, - error: { message: updateResponse.error.message } - } - } - - return { status: 'success', data: { publicUrl } } - } catch (error) { - console.error({ error }) - return { - status: 'error', - data: { publicUrl: '' }, - error: { message: 'Failed to upload image' } - } - } -} diff --git a/app/trip/hooks/useTripUpdate.ts b/app/trip/hooks/useTripUpdate.ts index 2780457..8cc2548 100644 --- a/app/trip/hooks/useTripUpdate.ts +++ b/app/trip/hooks/useTripUpdate.ts @@ -33,7 +33,7 @@ export const useTripUpdate = ( try { // Trip Update - await updateTripMutation({ + const res = await updateTripMutation({ variables: { id: tripDetails.id, set: { @@ -47,6 +47,10 @@ export const useTripUpdate = ( } }) + const updatedTripId = res.data?.updatetripsCollection?.records[0]?.id + + if (!updatedTripId) throw new Error('Failed to update a trip') + // TripTags Update const selectedTags = data.selectedTags const tripTagsArray = tripTags?.data || [] @@ -73,14 +77,14 @@ export const useTripUpdate = ( await Promise.all([...deletePromises, ...createPromises]) - if (data.uploaded_image_file && tripDetails) { - await uploadFile(data.uploaded_image_file, tripDetails.id) + if (data.uploaded_image_file && updatedTripId) { + await uploadFile(data.uploaded_image_file, updatedTripId) } tripDetails.refetch() tripTags?.refetch() tripsRefetch() - router.push(`/trip/${tripDetails.id}`) + router.push(`/trip/${updatedTripId}`) toast({ title: 'Successfully updated!', diff --git a/app/trip/hooks/useUploadFile.ts b/app/trip/hooks/useUploadFile.ts index 1d81d47..76b523a 100644 --- a/app/trip/hooks/useUploadFile.ts +++ b/app/trip/hooks/useUploadFile.ts @@ -1,7 +1,11 @@ import { createClient } from '@supabase/supabase-js' -import { updateImageMetadataAction } from '../action/update-image-metadata' +import { useUpdateTripMutation } from '@generated/api' +// import { updateImageMetadataAction } from '../action/update-image-metadata' export const useUploadFile = () => { + const [updateTripMutation, { loading: isTripUpdating }] = + useUpdateTripMutation() + // Upload image to Supabase Storage & Update image_url by server action const uploadFile = async (file: File, tripId: string) => { const supabase = createClient( @@ -16,9 +20,22 @@ export const useUploadFile = () => { if (uploadError) throw new Error(uploadError.message) - // NOTE: Server action doen't return result in Client Component. I don't know why. - await updateImageMetadataAction(tripId, uploadData.path) + const { + data: { publicUrl } + } = await supabase.storage + .from(process.env.NEXT_PUBLIC_BUCKET_NAME!) + .getPublicUrl(uploadData.path) + + // Update image_url by mutation + await updateTripMutation({ + variables: { + id: tripId, + set: { + image_url: publicUrl + } + } + }) } - return { uploadFile } + return { uploadFile, isTripUpdating } } From 0955042f639fd2c3c80b72688a6e0ac2080febdf Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 22:07:34 -0800 Subject: [PATCH 08/31] add loading for useUploadFile --- app/trip/hooks/useTripUpdate.ts | 5 +++-- app/trip/hooks/useUploadFile.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/trip/hooks/useTripUpdate.ts b/app/trip/hooks/useTripUpdate.ts index 8cc2548..ad91b02 100644 --- a/app/trip/hooks/useTripUpdate.ts +++ b/app/trip/hooks/useTripUpdate.ts @@ -26,7 +26,7 @@ export const useTripUpdate = ( useDeleteTripTagMutation() const { tripsRefetch } = useTripsGet() - const { uploadFile } = useUploadFile() + const { uploadFile, isMetadataUpdating } = useUploadFile() const updateTrip = async (data: TripSchema) => { if (!tripDetails) throw new Error('Trip details is not found') @@ -114,6 +114,7 @@ export const useTripUpdate = ( return { updateTrip, isTripUpdating, - isTripTagsUpdating: isTripTagCreating || isTripTagDeleting + isTripTagsUpdating: + isTripTagCreating || isTripTagDeleting || isMetadataUpdating } } diff --git a/app/trip/hooks/useUploadFile.ts b/app/trip/hooks/useUploadFile.ts index 76b523a..28101ce 100644 --- a/app/trip/hooks/useUploadFile.ts +++ b/app/trip/hooks/useUploadFile.ts @@ -3,7 +3,7 @@ import { useUpdateTripMutation } from '@generated/api' // import { updateImageMetadataAction } from '../action/update-image-metadata' export const useUploadFile = () => { - const [updateTripMutation, { loading: isTripUpdating }] = + const [updateTripMutation, { loading: isMetadataUpdating }] = useUpdateTripMutation() // Upload image to Supabase Storage & Update image_url by server action @@ -37,5 +37,5 @@ export const useUploadFile = () => { }) } - return { uploadFile, isTripUpdating } + return { uploadFile, isMetadataUpdating } } From 70696d63732fb3f1816f23c5338bbca267e91247 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 22:48:55 -0800 Subject: [PATCH 09/31] remove unnecessary await syntax --- app/trip/hooks/useUploadFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/trip/hooks/useUploadFile.ts b/app/trip/hooks/useUploadFile.ts index 28101ce..1daec01 100644 --- a/app/trip/hooks/useUploadFile.ts +++ b/app/trip/hooks/useUploadFile.ts @@ -22,7 +22,7 @@ export const useUploadFile = () => { const { data: { publicUrl } - } = await supabase.storage + } = supabase.storage .from(process.env.NEXT_PUBLIC_BUCKET_NAME!) .getPublicUrl(uploadData.path) From 1c967fa86bc5bf1134833dd3bc922297571626c9 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 22:51:13 -0800 Subject: [PATCH 10/31] Remove unused import in useUploadFile hook --- app/trip/hooks/useUploadFile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/trip/hooks/useUploadFile.ts b/app/trip/hooks/useUploadFile.ts index 1daec01..c81ac81 100644 --- a/app/trip/hooks/useUploadFile.ts +++ b/app/trip/hooks/useUploadFile.ts @@ -1,6 +1,5 @@ import { createClient } from '@supabase/supabase-js' import { useUpdateTripMutation } from '@generated/api' -// import { updateImageMetadataAction } from '../action/update-image-metadata' export const useUploadFile = () => { const [updateTripMutation, { loading: isMetadataUpdating }] = From e6d0e73c226bb45c576e65585555c0f2dfbfddea Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 17 Feb 2024 23:07:38 -0800 Subject: [PATCH 11/31] Remove TODO for image upload --- app/trip/components/trip-form.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/trip/components/trip-form.tsx b/app/trip/components/trip-form.tsx index 5482c7e..413dd6a 100644 --- a/app/trip/components/trip-form.tsx +++ b/app/trip/components/trip-form.tsx @@ -147,7 +147,6 @@ export const TripForm = ({ tripDetails, tags, tripTags }: TripFormProps) => { {errors?.date_to?.message} - {/* TODO Image Upload to storage & Send the URL string to DB */} Image From c12337932ae2b26250e7ce702d4a11e30a4b03ac Mon Sep 17 00:00:00 2001 From: Kana Taguchi Date: Sat, 17 Feb 2024 23:08:02 -0800 Subject: [PATCH 12/31] Add mutation of update and delete --- app/const/index.ts | 7 - .../components/invited-user-card.tsx | 39 ++- .../mutation/delete-invitation.graphql | 10 + .../mutation/update-invitation.graphql | 9 + app/trip/[id]/manage-group/hooks/index.ts | 3 + .../hooks/use-delete-invitation.ts | 37 +++ .../hooks/use-get-trip-shared-users.ts | 15 ++ .../hooks/use-update-invitation.ts | 47 ++++ app/trip/[id]/manage-group/page.tsx | 12 +- graphql-codegen/generated/api.ts | 150 ++++++++++++ graphql-codegen/generated/gql.ts | 16 ++ graphql-codegen/generated/graphql.ts | 228 ++++++++++++++++++ .../generated/persisted-documents.json | 2 + 13 files changed, 549 insertions(+), 26 deletions(-) delete mode 100644 app/const/index.ts create mode 100644 app/trip/[id]/manage-group/graphql/mutation/delete-invitation.graphql create mode 100644 app/trip/[id]/manage-group/graphql/mutation/update-invitation.graphql create mode 100644 app/trip/[id]/manage-group/hooks/index.ts create mode 100644 app/trip/[id]/manage-group/hooks/use-delete-invitation.ts create mode 100644 app/trip/[id]/manage-group/hooks/use-get-trip-shared-users.ts create mode 100644 app/trip/[id]/manage-group/hooks/use-update-invitation.ts diff --git a/app/const/index.ts b/app/const/index.ts deleted file mode 100644 index 1204abd..0000000 --- a/app/const/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const INVITATION_PERMISSION_LEVEL = [ - { value: 'editable', label: 'Can Edit' }, - { value: 'view_only', label: 'View Only' } -] as const - -export type InvitationPermissionLevelType = - (typeof INVITATION_PERMISSION_LEVEL)[number]['value'] diff --git a/app/trip/[id]/manage-group/components/invited-user-card.tsx b/app/trip/[id]/manage-group/components/invited-user-card.tsx index 0a0f83c..a4610fa 100644 --- a/app/trip/[id]/manage-group/components/invited-user-card.tsx +++ b/app/trip/[id]/manage-group/components/invited-user-card.tsx @@ -11,19 +11,21 @@ import { Text } from '@chakra-ui/react' import { FiTrash2 } from 'react-icons/fi' -import { - INVITATION_PERMISSION_LEVEL, - InvitationPermissionLevelType -} from '@/const' +import { useUpdateInvitation, useDeleteInvitation } from '../hooks' +import { Permission_Level_Enum } from '@generated/api' type InvitedUserCardProps = { + id: string + tripId: string image: string | null | undefined name: string email: string - permissionLevel: InvitationPermissionLevelType + permissionLevel: Permission_Level_Enum } export const InvitedUserCard = ({ + id, + tripId, image, name, email, @@ -31,6 +33,16 @@ export const InvitedUserCard = ({ }: InvitedUserCardProps) => { const borderColor = useColorModeValue('gray.300', 'gray.600') + const labelMap = { + [Permission_Level_Enum.Editable]: 'Can Edit', + [Permission_Level_Enum.ViewOnly]: 'View Only' + } + + const { updatePermissionLevel, isInvitationUpdating } = + useUpdateInvitation(tripId) + + const { deleteInvitation, isInvitationDeleting } = useDeleteInvitation(tripId) + return ( - + - {INVITATION_PERMISSION_LEVEL.map((level) => ( + {Object.entries(Permission_Level_Enum).map(([key, value]) => ( updatePermissionLevel(id, value)} > - {level.label} + {labelMap[value]} ))} @@ -72,7 +88,6 @@ export const InvitedUserCard = ({ {}} variant="unstyled" color={'gray.400'} _hover={{ @@ -80,6 +95,8 @@ export const InvitedUserCard = ({ }} minW={{ base: '18px', md: '24px' }} h={{ base: '18px', md: '24px' }} + isLoading={isInvitationDeleting} + onClick={() => deleteInvitation(id)} > diff --git a/app/trip/[id]/manage-group/graphql/mutation/delete-invitation.graphql b/app/trip/[id]/manage-group/graphql/mutation/delete-invitation.graphql new file mode 100644 index 0000000..62fcfc5 --- /dev/null +++ b/app/trip/[id]/manage-group/graphql/mutation/delete-invitation.graphql @@ -0,0 +1,10 @@ +mutation deleteInvitation($id: UUID!) { + deleteFrominvitationsCollection(filter: { id: { eq: $id } }) { + records { + __typename + id + email + permission_level + } + } +} diff --git a/app/trip/[id]/manage-group/graphql/mutation/update-invitation.graphql b/app/trip/[id]/manage-group/graphql/mutation/update-invitation.graphql new file mode 100644 index 0000000..db8a3c1 --- /dev/null +++ b/app/trip/[id]/manage-group/graphql/mutation/update-invitation.graphql @@ -0,0 +1,9 @@ +mutation updateInvitation($id: UUID!, $set: invitationsUpdateInput!) { + updateinvitationsCollection(set: $set, filter: { id: { eq: $id } }) { + records { + id + email + permission_level + } + } +} diff --git a/app/trip/[id]/manage-group/hooks/index.ts b/app/trip/[id]/manage-group/hooks/index.ts new file mode 100644 index 0000000..10d6006 --- /dev/null +++ b/app/trip/[id]/manage-group/hooks/index.ts @@ -0,0 +1,3 @@ +export { useDeleteInvitation } from './use-delete-invitation' +export { useGetTripSharedUsers } from './use-get-trip-shared-users' +export { useUpdateInvitation } from './use-update-invitation' diff --git a/app/trip/[id]/manage-group/hooks/use-delete-invitation.ts b/app/trip/[id]/manage-group/hooks/use-delete-invitation.ts new file mode 100644 index 0000000..5262e22 --- /dev/null +++ b/app/trip/[id]/manage-group/hooks/use-delete-invitation.ts @@ -0,0 +1,37 @@ +import { useToast } from '@chakra-ui/react' +import { useGetTripSharedUsers } from '../hooks' +import { useDeleteInvitationMutation } from '@generated/api' + +export const useDeleteInvitation = (tripId: string) => { + const toast = useToast() + + const [deleteInvitationMutation, { loading: isInvitationDeleting }] = + useDeleteInvitationMutation() + + const { refetch } = useGetTripSharedUsers(tripId) + + const deleteInvitation = async (id: string) => { + try { + await deleteInvitationMutation({ + variables: { + id + } + }) + refetch() + } catch (error) { + toast({ + title: "We're sorry, but you failed to delete the shared user", + description: + error instanceof Error ? error.message : 'Please try again later.', + status: 'error', + duration: 5000, + isClosable: true, + position: 'top' + }) + } + } + return { + deleteInvitation, + isInvitationDeleting + } +} diff --git a/app/trip/[id]/manage-group/hooks/use-get-trip-shared-users.ts b/app/trip/[id]/manage-group/hooks/use-get-trip-shared-users.ts new file mode 100644 index 0000000..0538ee9 --- /dev/null +++ b/app/trip/[id]/manage-group/hooks/use-get-trip-shared-users.ts @@ -0,0 +1,15 @@ +import { useTripSharedUsersQuery } from '@generated/api' + +export const useGetTripSharedUsers = (tripId: string) => { + const { data, loading, refetch } = useTripSharedUsersQuery({ + variables: { + tripId + } + }) + + return { + tripSharedUsers: data?.tripsCollection?.edges[0].node, + loading, + refetch + } +} diff --git a/app/trip/[id]/manage-group/hooks/use-update-invitation.ts b/app/trip/[id]/manage-group/hooks/use-update-invitation.ts new file mode 100644 index 0000000..40ee458 --- /dev/null +++ b/app/trip/[id]/manage-group/hooks/use-update-invitation.ts @@ -0,0 +1,47 @@ +import { useToast } from '@chakra-ui/react' +import { useGetTripSharedUsers } from '../hooks' +import { + useUpdateInvitationMutation, + Permission_Level_Enum +} from '@generated/api' + +export const useUpdateInvitation = (tripId: string) => { + const toast = useToast() + + const [updateInvitationMutation, { loading: isInvitationUpdating }] = + useUpdateInvitationMutation() + + const { refetch } = useGetTripSharedUsers(tripId) + + const updatePermissionLevel = async ( + id: string, + permissionLevel: Permission_Level_Enum + ) => { + try { + await updateInvitationMutation({ + variables: { + id, + set: { + permission_level: permissionLevel + } + } + }) + refetch() + } catch (error) { + toast({ + title: "We're sorry, but you failed to update the option", + description: + error instanceof Error ? error.message : 'Please try again later.', + status: 'error', + duration: 5000, + isClosable: true, + position: 'top' + }) + } + } + + return { + updatePermissionLevel, + isInvitationUpdating + } +} diff --git a/app/trip/[id]/manage-group/page.tsx b/app/trip/[id]/manage-group/page.tsx index 4ba405f..668d406 100644 --- a/app/trip/[id]/manage-group/page.tsx +++ b/app/trip/[id]/manage-group/page.tsx @@ -9,19 +9,13 @@ import { } from '@chakra-ui/react' import { Loading } from '@/components/loading' import { OwnerCard, InvitedUserCard } from './components' -import { useTripSharedUsersQuery } from '@generated/api' +import { useGetTripSharedUsers } from './hooks' export default function ManageGroup({ params }: { params: { id: string } }) { const bg = useColorModeValue('white', 'gray.800') const color = useColorModeValue('black', 'gray.300') - const { data, loading } = useTripSharedUsersQuery({ - variables: { - tripId: params.id - } - }) - - const tripSharedUsers = data?.tripsCollection?.edges[0].node + const { tripSharedUsers, loading } = useGetTripSharedUsers(params.id) return ( + +export type DeleteInvitationMutation = { + __typename: 'Mutation' + deleteFrominvitationsCollection: { + __typename: 'invitationsDeleteResponse' + records: Array<{ + __typename: 'invitations' + id: string + email: string + permission_level: Permission_Level_Enum + }> + } +} + +export type UpdateInvitationMutationVariables = Exact<{ + id: Scalars['UUID']['input'] + set: InvitationsUpdateInput +}> + +export type UpdateInvitationMutation = { + __typename: 'Mutation' + updateinvitationsCollection: { + __typename: 'invitationsUpdateResponse' + records: Array<{ + __typename: 'invitations' + id: string + email: string + permission_level: Permission_Level_Enum + }> + } +} + export type TripSharedUsersQueryVariables = Exact<{ tripId: Scalars['UUID']['input'] }> @@ -1904,6 +1939,121 @@ export type CreateActivityMutationOptions = Apollo.BaseMutationOptions< CreateActivityMutation, CreateActivityMutationVariables > +export const DeleteInvitationDocument = gql` + mutation deleteInvitation($id: UUID!) { + __typename + deleteFrominvitationsCollection(filter: { id: { eq: $id } }) { + __typename + records { + __typename + id + email + permission_level + } + } + } +` +export type DeleteInvitationMutationFn = Apollo.MutationFunction< + DeleteInvitationMutation, + DeleteInvitationMutationVariables +> + +/** + * __useDeleteInvitationMutation__ + * + * To run a mutation, you first call `useDeleteInvitationMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteInvitationMutation` 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 [deleteInvitationMutation, { data, loading, error }] = useDeleteInvitationMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useDeleteInvitationMutation( + baseOptions?: Apollo.MutationHookOptions< + DeleteInvitationMutation, + DeleteInvitationMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + DeleteInvitationMutation, + DeleteInvitationMutationVariables + >(DeleteInvitationDocument, options) +} +export type DeleteInvitationMutationHookResult = ReturnType< + typeof useDeleteInvitationMutation +> +export type DeleteInvitationMutationResult = + Apollo.MutationResult +export type DeleteInvitationMutationOptions = Apollo.BaseMutationOptions< + DeleteInvitationMutation, + DeleteInvitationMutationVariables +> +export const UpdateInvitationDocument = gql` + mutation updateInvitation($id: UUID!, $set: invitationsUpdateInput!) { + __typename + updateinvitationsCollection(set: $set, filter: { id: { eq: $id } }) { + __typename + records { + __typename + id + email + permission_level + } + } + } +` +export type UpdateInvitationMutationFn = Apollo.MutationFunction< + UpdateInvitationMutation, + UpdateInvitationMutationVariables +> + +/** + * __useUpdateInvitationMutation__ + * + * To run a mutation, you first call `useUpdateInvitationMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateInvitationMutation` 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 [updateInvitationMutation, { data, loading, error }] = useUpdateInvitationMutation({ + * variables: { + * id: // value for 'id' + * set: // value for 'set' + * }, + * }); + */ +export function useUpdateInvitationMutation( + baseOptions?: Apollo.MutationHookOptions< + UpdateInvitationMutation, + UpdateInvitationMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + UpdateInvitationMutation, + UpdateInvitationMutationVariables + >(UpdateInvitationDocument, options) +} +export type UpdateInvitationMutationHookResult = ReturnType< + typeof useUpdateInvitationMutation +> +export type UpdateInvitationMutationResult = + Apollo.MutationResult +export type UpdateInvitationMutationOptions = Apollo.BaseMutationOptions< + UpdateInvitationMutation, + UpdateInvitationMutationVariables +> export const TripSharedUsersDocument = gql` query tripSharedUsers($tripId: UUID!) { __typename diff --git a/graphql-codegen/generated/gql.ts b/graphql-codegen/generated/gql.ts index c5d468c..3e004a2 100644 --- a/graphql-codegen/generated/gql.ts +++ b/graphql-codegen/generated/gql.ts @@ -19,6 +19,10 @@ const documents = { 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 deleteInvitation($id: UUID!) {\n deleteFrominvitationsCollection(filter: {id: {eq: $id}}) {\n records {\n __typename\n id\n email\n permission_level\n }\n }\n}': + types.DeleteInvitationDocument, + 'mutation updateInvitation($id: UUID!, $set: invitationsUpdateInput!) {\n updateinvitationsCollection(set: $set, filter: {id: {eq: $id}}) {\n records {\n id\n email\n permission_level\n }\n }\n}': + types.UpdateInvitationDocument, 'query tripSharedUsers($tripId: UUID!) {\n tripsCollection(filter: {id: {eq: $tripId}}) {\n edges {\n node {\n id\n title\n users {\n id\n name\n profile_picture_url\n email\n }\n invitationsCollection {\n edges {\n node {\n id\n permission_level\n users {\n id\n name\n email\n profile_picture_url\n }\n }\n }\n }\n }\n }\n }\n}': types.TripSharedUsersDocument, '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}': @@ -77,6 +81,18 @@ export function graphql( 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}'] +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: 'mutation deleteInvitation($id: UUID!) {\n deleteFrominvitationsCollection(filter: {id: {eq: $id}}) {\n records {\n __typename\n id\n email\n permission_level\n }\n }\n}' +): (typeof documents)['mutation deleteInvitation($id: UUID!) {\n deleteFrominvitationsCollection(filter: {id: {eq: $id}}) {\n records {\n __typename\n id\n email\n permission_level\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 updateInvitation($id: UUID!, $set: invitationsUpdateInput!) {\n updateinvitationsCollection(set: $set, filter: {id: {eq: $id}}) {\n records {\n id\n email\n permission_level\n }\n }\n}' +): (typeof documents)['mutation updateInvitation($id: UUID!, $set: invitationsUpdateInput!) {\n updateinvitationsCollection(set: $set, filter: {id: {eq: $id}}) {\n records {\n id\n email\n permission_level\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 899a446..2807cde 100644 --- a/graphql-codegen/generated/graphql.ts +++ b/graphql-codegen/generated/graphql.ts @@ -1360,6 +1360,41 @@ export type CreateActivityMutation = { } | null } +export type DeleteInvitationMutationVariables = Exact<{ + id: Scalars['UUID']['input'] +}> + +export type DeleteInvitationMutation = { + __typename: 'Mutation' + deleteFrominvitationsCollection: { + __typename: 'invitationsDeleteResponse' + records: Array<{ + __typename: 'invitations' + id: string + email: string + permission_level: Permission_Level_Enum + }> + } +} + +export type UpdateInvitationMutationVariables = Exact<{ + id: Scalars['UUID']['input'] + set: InvitationsUpdateInput +}> + +export type UpdateInvitationMutation = { + __typename: 'Mutation' + updateinvitationsCollection: { + __typename: 'invitationsUpdateResponse' + records: Array<{ + __typename: 'invitations' + id: string + email: string + permission_level: Permission_Level_Enum + }> + } +} + export type TripSharedUsersQueryVariables = Exact<{ tripId: Scalars['UUID']['input'] }> @@ -2116,6 +2151,199 @@ export const CreateActivityDocument = { CreateActivityMutation, CreateActivityMutationVariables > +export const DeleteInvitationDocument = { + __meta__: { hash: 'f83929f3ffa8a8ea910e5e66156925ab379aa04d' }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'deleteInvitation' }, + 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: 'deleteFrominvitationsCollection' }, + 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: 'email' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'permission_level' } + } + ] + } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode< + DeleteInvitationMutation, + DeleteInvitationMutationVariables +> +export const UpdateInvitationDocument = { + __meta__: { hash: '173d22f901d3e0b557eb5eb4ce5ab0adededf9e7' }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'updateInvitation' }, + 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: 'invitationsUpdateInput' } + } + } + } + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'updateinvitationsCollection' }, + 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: 'email' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'permission_level' } + } + ] + } + } + ] + } + } + ] + } + } + ] +} as unknown as DocumentNode< + UpdateInvitationMutation, + UpdateInvitationMutationVariables +> export const TripSharedUsersDocument = { __meta__: { hash: '16ece6918ea030b3bf8bcf763c1a296e90d6dcf9' }, kind: 'Document', diff --git a/graphql-codegen/generated/persisted-documents.json b/graphql-codegen/generated/persisted-documents.json index d9b4860..f2508ca 100644 --- a/graphql-codegen/generated/persisted-documents.json +++ b/graphql-codegen/generated/persisted-documents.json @@ -2,6 +2,8 @@ "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 } } } }", "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 } } }", + "f83929f3ffa8a8ea910e5e66156925ab379aa04d": "mutation deleteInvitation($id: UUID!) { __typename deleteFrominvitationsCollection(filter: {id: {eq: $id}}) { __typename records { __typename email id permission_level } } }", + "173d22f901d3e0b557eb5eb4ce5ab0adededf9e7": "mutation updateInvitation($id: UUID!, $set: invitationsUpdateInput!) { __typename updateinvitationsCollection(set: $set, filter: {id: {eq: $id}}) { __typename records { __typename email id permission_level } } }", "16ece6918ea030b3bf8bcf763c1a296e90d6dcf9": "query tripSharedUsers($tripId: UUID!) { __typename tripsCollection(filter: {id: {eq: $tripId}}) { __typename edges { __typename node { __typename id invitationsCollection { __typename edges { __typename node { __typename id permission_level users { __typename email id name profile_picture_url } } } } title users { __typename email id name profile_picture_url } } } } }", "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 7b5f2b8633dcda0ae2a2c6c81b08eaebd4e57950 Mon Sep 17 00:00:00 2001 From: samuraikun Date: Sat, 10 Feb 2024 22:26:02 -0800 Subject: [PATCH 13/31] 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 14/31] 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 15/31] 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 16/31] 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 17/31] 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 18/31] 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 19/31] 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 20/31] 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 21/31] 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 0058aa508b552ed470281f67d304af2eef7d2b4b Mon Sep 17 00:00:00 2001 From: Kana Taguchi Date: Sun, 18 Feb 2024 19:30:57 -0800 Subject: [PATCH 22/31] Show owner in trip card and trip details --- .../components/invited-user-card.tsx | 43 ++++--- .../manage-group/components/owner-card.tsx | 2 +- app/trip/[id]/manage-group/page.tsx | 1 + app/trip/[id]/page.tsx | 20 ++-- app/trip/components/trip-card.tsx | 64 +++++++---- app/trip/graphql/query/tripDetails.graphql | 4 + .../graphql/query/tripsCollection.graphql | 5 + app/trip/hooks/useTripsGet.ts | 1 + graphql-codegen/generated/api.ts | 60 ++++++---- graphql-codegen/generated/gql.ts | 12 +- graphql-codegen/generated/graphql.ts | 105 ++++++++++++++---- .../generated/persisted-documents.json | 4 +- 12 files changed, 225 insertions(+), 96 deletions(-) diff --git a/app/trip/[id]/manage-group/components/invited-user-card.tsx b/app/trip/[id]/manage-group/components/invited-user-card.tsx index a4610fa..4858b3e 100644 --- a/app/trip/[id]/manage-group/components/invited-user-card.tsx +++ b/app/trip/[id]/manage-group/components/invited-user-card.tsx @@ -11,6 +11,7 @@ import { Text } from '@chakra-ui/react' import { FiTrash2 } from 'react-icons/fi' +import { useUserId } from '@/providers/session-provider' import { useUpdateInvitation, useDeleteInvitation } from '../hooks' import { Permission_Level_Enum } from '@generated/api' @@ -21,6 +22,7 @@ type InvitedUserCardProps = { name: string email: string permissionLevel: Permission_Level_Enum + ownerId: string } export const InvitedUserCard = ({ @@ -29,7 +31,8 @@ export const InvitedUserCard = ({ image, name, email, - permissionLevel + permissionLevel, + ownerId }: InvitedUserCardProps) => { const borderColor = useColorModeValue('gray.300', 'gray.600') @@ -38,11 +41,15 @@ export const InvitedUserCard = ({ [Permission_Level_Enum.ViewOnly]: 'View Only' } + const userId = useUserId() + const { updatePermissionLevel, isInvitationUpdating } = useUpdateInvitation(tripId) const { deleteInvitation, isInvitationDeleting } = useDeleteInvitation(tripId) + const currentUserIsOwner = userId === ownerId + return ( + {/* TODO If not owner, readme only */} updatePermissionLevel(id, value)} > @@ -86,20 +95,22 @@ export const InvitedUserCard = ({ - deleteInvitation(id)} - > - - + {currentUserIsOwner && ( + deleteInvitation(id)} + > + + + )} ) diff --git a/app/trip/[id]/manage-group/components/owner-card.tsx b/app/trip/[id]/manage-group/components/owner-card.tsx index e5ee600..9c36121 100644 --- a/app/trip/[id]/manage-group/components/owner-card.tsx +++ b/app/trip/[id]/manage-group/components/owner-card.tsx @@ -36,7 +36,7 @@ export const OwnerCard = ({ image, name, email }: OwnerCardProps) => { Owner diff --git a/app/trip/[id]/manage-group/page.tsx b/app/trip/[id]/manage-group/page.tsx index 668d406..c77b4bc 100644 --- a/app/trip/[id]/manage-group/page.tsx +++ b/app/trip/[id]/manage-group/page.tsx @@ -58,6 +58,7 @@ export default function ManageGroup({ params }: { params: { id: string } }) { name={invitedUser.node.users?.name || ''} email={invitedUser.node.users?.email || ''} permissionLevel={invitedUser.node.permission_level} + ownerId={tripSharedUsers.users?.id || ''} /> ) } diff --git a/app/trip/[id]/page.tsx b/app/trip/[id]/page.tsx index 9bc0edb..bec468c 100644 --- a/app/trip/[id]/page.tsx +++ b/app/trip/[id]/page.tsx @@ -53,6 +53,17 @@ export default function TripDetailsPage({ } } + const owner = { + id: tripData?.edges[0].node.users.id, + image: tripData?.edges[0].node.users.profile_picture_url + } + + const sharedUsers = + tripData?.edges[0].node.invitationsCollection?.edges.map((invitation) => ({ + id: invitation.node.users?.id, + image: invitation.node.users?.profile_picture_url + })) || [] + return ( ({ - id: invitation.node.users?.id, - image: invitation.node.users?.profile_picture_url - }) - ) || [] - } + users={[owner, ...sharedUsers]} tags={ tripData.edges[0].node.trip_tagsCollection?.edges.map( (tag) => ({ diff --git a/app/trip/components/trip-card.tsx b/app/trip/components/trip-card.tsx index df16931..8f1b9a7 100644 --- a/app/trip/components/trip-card.tsx +++ b/app/trip/components/trip-card.tsx @@ -83,29 +83,47 @@ export const TripCard = ({ data }: TripCardProps) => { > {data.node.title} - {data.node.invitationsCollection?.edges && - data.node.invitationsCollection.edges.length > 0 && ( - - {data.node.invitationsCollection?.edges.map((invitation) => ( - - {invitation.node.users?.profile_picture_url ? ( - - ) : ( - - )} - - ))} - - )} + + + {data.node.users.profile_picture_url ? ( + + ) : ( + + )} + + + {data.node.invitationsCollection?.edges && + data.node.invitationsCollection.edges.length > 0 && ( + <> + {data.node.invitationsCollection?.edges.map((invitation) => ( + + {invitation.node.users?.profile_picture_url ? ( + + ) : ( + + )} + + ))} + + )} + {formatDateToSlash(data.node.date_from, 'dayMonthYear')} -{' '} {formatDateToSlash(data.node.date_to, 'dayMonthYear')} diff --git a/app/trip/graphql/query/tripDetails.graphql b/app/trip/graphql/query/tripDetails.graphql index 1c7d132..6c3c575 100644 --- a/app/trip/graphql/query/tripDetails.graphql +++ b/app/trip/graphql/query/tripDetails.graphql @@ -9,6 +9,10 @@ query tripDetails($id: UUID) { image_url cost cost_unit + users { + id + profile_picture_url + } invitationsCollection { edges { node { diff --git a/app/trip/graphql/query/tripsCollection.graphql b/app/trip/graphql/query/tripsCollection.graphql index b446d46..00718ef 100644 --- a/app/trip/graphql/query/tripsCollection.graphql +++ b/app/trip/graphql/query/tripsCollection.graphql @@ -19,6 +19,11 @@ query tripsCollection( date_to image_url created_at + users { + id + name + profile_picture_url + } invitationsCollection { edges { node { diff --git a/app/trip/hooks/useTripsGet.ts b/app/trip/hooks/useTripsGet.ts index 13203a3..1a767d1 100644 --- a/app/trip/hooks/useTripsGet.ts +++ b/app/trip/hooks/useTripsGet.ts @@ -5,6 +5,7 @@ import { useTripsCollectionQuery, TripsOrderBy } from '@generated/api' export const useTripsGet = (searchWord?: string) => { const userId = useUserId() + // TODO - need to change as currently just showing all trips user created. We also need to show invited trips const { data, loading, error, fetchMore, refetch, networkStatus } = useTripsCollectionQuery({ variables: { diff --git a/graphql-codegen/generated/api.ts b/graphql-codegen/generated/api.ts index 54f1b6f..47b7912 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 = { @@ -1316,7 +1316,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 @@ -1401,13 +1401,13 @@ export type TripSharedUsersQuery = { __typename: 'trips' id: string title: string - users?: { + users: { __typename: 'users' id: string name: string profile_picture_url?: string | null email: string - } | null + } invitationsCollection?: { __typename: 'invitationsConnection' edges: Array<{ @@ -1468,8 +1468,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 } @@ -1558,6 +1558,11 @@ export type TripDetailsQuery = { image_url?: string | null cost?: string | null cost_unit?: string | null + users: { + __typename: 'users' + id: string + profile_picture_url?: string | null + } invitationsCollection?: { __typename: 'invitationsConnection' edges: Array<{ @@ -1592,7 +1597,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 @@ -1614,8 +1619,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 @@ -1642,6 +1647,12 @@ export type TripsCollectionQuery = { date_to?: string | null image_url?: string | null created_at: string + users: { + __typename: 'users' + id: string + name: string + profile_picture_url?: string | null + } invitationsCollection?: { __typename: 'invitationsConnection' edges: Array<{ @@ -2667,6 +2678,11 @@ export const TripDetailsDocument = gql` image_url cost cost_unit + users { + __typename + id + profile_picture_url + } invitationsCollection { __typename edges { @@ -2895,6 +2911,12 @@ export const TripsCollectionDocument = gql` date_to image_url created_at + users { + __typename + id + name + profile_picture_url + } invitationsCollection { __typename edges { diff --git a/graphql-codegen/generated/gql.ts b/graphql-codegen/generated/gql.ts index 3e004a2..2080bc3 100644 --- a/graphql-codegen/generated/gql.ts +++ b/graphql-codegen/generated/gql.ts @@ -41,11 +41,11 @@ const documents = { types.UpdateTripDocument, 'query tagsCollection($userId: UUID!) {\n tagsCollection(\n filter: {user_id: {eq: $userId}}\n orderBy: {created_at: AscNullsLast}\n ) {\n edges {\n node {\n id\n name\n }\n }\n }\n}': types.TagsCollectionDocument, - 'query tripDetails($id: UUID) {\n tripsCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n title\n date_from\n date_to\n image_url\n cost\n cost_unit\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n title\n time_from\n time_to\n address\n }\n }\n }\n trip_tagsCollection {\n edges {\n node {\n tags {\n id\n name\n }\n }\n }\n }\n }\n }\n }\n}': + 'query tripDetails($id: UUID) {\n tripsCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n title\n date_from\n date_to\n image_url\n cost\n cost_unit\n users {\n id\n profile_picture_url\n }\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n title\n time_from\n time_to\n address\n }\n }\n }\n trip_tagsCollection {\n edges {\n node {\n tags {\n id\n name\n }\n }\n }\n }\n }\n }\n }\n}': types.TripDetailsDocument, 'query tripTagsCollection($filter: trip_tagsFilter) {\n trip_tagsCollection(filter: $filter) {\n edges {\n node {\n id\n trip_id\n tag_id\n }\n }\n }\n}': types.TripTagsCollectionDocument, - 'query tripsCollection($filter: tripsFilter, $orderBy: [tripsOrderBy!], $first: Int!, $after: Cursor) {\n tripsCollection(\n filter: $filter\n first: $first\n after: $after\n orderBy: $orderBy\n ) {\n edges {\n node {\n id\n id\n title\n date_from\n date_to\n image_url\n created_at\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n }\n }\n }\n }\n }\n pageInfo {\n startCursor\n endCursor\n hasPreviousPage\n hasNextPage\n }\n }\n}': + 'query tripsCollection($filter: tripsFilter, $orderBy: [tripsOrderBy!], $first: Int!, $after: Cursor) {\n tripsCollection(\n filter: $filter\n first: $first\n after: $after\n orderBy: $orderBy\n ) {\n edges {\n node {\n id\n id\n title\n date_from\n date_to\n image_url\n created_at\n users {\n id\n name\n profile_picture_url\n }\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n }\n }\n }\n }\n }\n pageInfo {\n startCursor\n endCursor\n hasPreviousPage\n hasNextPage\n }\n }\n}': types.TripsCollectionDocument } @@ -151,8 +151,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 tripDetails($id: UUID) {\n tripsCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n title\n date_from\n date_to\n image_url\n cost\n cost_unit\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n title\n time_from\n time_to\n address\n }\n }\n }\n trip_tagsCollection {\n edges {\n node {\n tags {\n id\n name\n }\n }\n }\n }\n }\n }\n }\n}' -): (typeof documents)['query tripDetails($id: UUID) {\n tripsCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n title\n date_from\n date_to\n image_url\n cost\n cost_unit\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n title\n time_from\n time_to\n address\n }\n }\n }\n trip_tagsCollection {\n edges {\n node {\n tags {\n id\n name\n }\n }\n }\n }\n }\n }\n }\n}'] + source: 'query tripDetails($id: UUID) {\n tripsCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n title\n date_from\n date_to\n image_url\n cost\n cost_unit\n users {\n id\n profile_picture_url\n }\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n title\n time_from\n time_to\n address\n }\n }\n }\n trip_tagsCollection {\n edges {\n node {\n tags {\n id\n name\n }\n }\n }\n }\n }\n }\n }\n}' +): (typeof documents)['query tripDetails($id: UUID) {\n tripsCollection(filter: {id: {eq: $id}}) {\n edges {\n node {\n id\n title\n date_from\n date_to\n image_url\n cost\n cost_unit\n users {\n id\n profile_picture_url\n }\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n title\n time_from\n time_to\n address\n }\n }\n }\n trip_tagsCollection {\n edges {\n node {\n tags {\n id\n name\n }\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. */ @@ -163,8 +163,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 tripsCollection($filter: tripsFilter, $orderBy: [tripsOrderBy!], $first: Int!, $after: Cursor) {\n tripsCollection(\n filter: $filter\n first: $first\n after: $after\n orderBy: $orderBy\n ) {\n edges {\n node {\n id\n id\n title\n date_from\n date_to\n image_url\n created_at\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n }\n }\n }\n }\n }\n pageInfo {\n startCursor\n endCursor\n hasPreviousPage\n hasNextPage\n }\n }\n}' -): (typeof documents)['query tripsCollection($filter: tripsFilter, $orderBy: [tripsOrderBy!], $first: Int!, $after: Cursor) {\n tripsCollection(\n filter: $filter\n first: $first\n after: $after\n orderBy: $orderBy\n ) {\n edges {\n node {\n id\n id\n title\n date_from\n date_to\n image_url\n created_at\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n }\n }\n }\n }\n }\n pageInfo {\n startCursor\n endCursor\n hasPreviousPage\n hasNextPage\n }\n }\n}'] + source: 'query tripsCollection($filter: tripsFilter, $orderBy: [tripsOrderBy!], $first: Int!, $after: Cursor) {\n tripsCollection(\n filter: $filter\n first: $first\n after: $after\n orderBy: $orderBy\n ) {\n edges {\n node {\n id\n id\n title\n date_from\n date_to\n image_url\n created_at\n users {\n id\n name\n profile_picture_url\n }\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n }\n }\n }\n }\n }\n pageInfo {\n startCursor\n endCursor\n hasPreviousPage\n hasNextPage\n }\n }\n}' +): (typeof documents)['query tripsCollection($filter: tripsFilter, $orderBy: [tripsOrderBy!], $first: Int!, $after: Cursor) {\n tripsCollection(\n filter: $filter\n first: $first\n after: $after\n orderBy: $orderBy\n ) {\n edges {\n node {\n id\n id\n title\n date_from\n date_to\n image_url\n created_at\n users {\n id\n name\n profile_picture_url\n }\n invitationsCollection {\n edges {\n node {\n users {\n id\n profile_picture_url\n }\n }\n }\n }\n activityCollection {\n edges {\n node {\n id\n }\n }\n }\n }\n }\n pageInfo {\n startCursor\n endCursor\n hasPreviousPage\n hasNextPage\n }\n }\n}'] export function graphql(source: string) { return (documents as any)[source] ?? {} diff --git a/graphql-codegen/generated/graphql.ts b/graphql-codegen/generated/graphql.ts index 2807cde..50c5b52 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 = { @@ -1324,7 +1324,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 @@ -1409,13 +1409,13 @@ export type TripSharedUsersQuery = { __typename: 'trips' id: string title: string - users?: { + users: { __typename: 'users' id: string name: string profile_picture_url?: string | null email: string - } | null + } invitationsCollection?: { __typename: 'invitationsConnection' edges: Array<{ @@ -1476,8 +1476,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 } @@ -1566,6 +1566,11 @@ export type TripDetailsQuery = { image_url?: string | null cost?: string | null cost_unit?: string | null + users: { + __typename: 'users' + id: string + profile_picture_url?: string | null + } invitationsCollection?: { __typename: 'invitationsConnection' edges: Array<{ @@ -1600,7 +1605,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 @@ -1622,8 +1627,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 @@ -1650,6 +1655,12 @@ export type TripsCollectionQuery = { date_to?: string | null image_url?: string | null created_at: string + users: { + __typename: 'users' + id: string + name: string + profile_picture_url?: string | null + } invitationsCollection?: { __typename: 'invitationsConnection' edges: Array<{ @@ -3353,7 +3364,7 @@ export const TagsCollectionDocument = { ] } as unknown as DocumentNode export const TripDetailsDocument = { - __meta__: { hash: '1bc05beaca139a887f8c922c4de4dc6c86706670' }, + __meta__: { hash: 'e40d7c2a09852d29b44908c4c24cbcfa2dbce660' }, kind: 'Document', definitions: [ { @@ -3454,6 +3465,30 @@ export const TripDetailsDocument = { kind: 'Field', name: { kind: 'Name', value: 'cost_unit' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'users' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'id' } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'profile_picture_url' + } + } + ] + } + }, { kind: 'Field', name: { @@ -3806,7 +3841,7 @@ export const TripTagsCollectionDocument = { TripTagsCollectionQueryVariables > export const TripsCollectionDocument = { - __meta__: { hash: '3256c7ff43134c13502c53f475e55fa91f55cabb' }, + __meta__: { hash: '848b1879d86d9282d2bb3a3d1016f42dd7e5cc56' }, kind: 'Document', definitions: [ { @@ -3955,6 +3990,34 @@ export const TripsCollectionDocument = { kind: 'Field', name: { kind: 'Name', value: 'created_at' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'users' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'id' } + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'name' } + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'profile_picture_url' + } + } + ] + } + }, { kind: 'Field', name: { diff --git a/graphql-codegen/generated/persisted-documents.json b/graphql-codegen/generated/persisted-documents.json index f2508ca..4f09715 100644 --- a/graphql-codegen/generated/persisted-documents.json +++ b/graphql-codegen/generated/persisted-documents.json @@ -13,7 +13,7 @@ "1a4d048db574dedfebd859dc737e16a42021f3ff": "mutation deleteTripTag($id: UUID!) { __typename deleteFromtrip_tagsCollection(filter: {id: {eq: $id}}) { __typename records { __typename id } } }", "7b9783d44e5c70098239e977664434de3eae6204": "mutation updateTrip($id: UUID!, $set: tripsUpdateInput!) { __typename updatetripsCollection(set: $set, filter: {id: {eq: $id}}) { __typename records { __typename id title } } }", "994a2e7e0694c279cbfeff6c8696cbbe6a0c687c": "query tagsCollection($userId: UUID!) { __typename tagsCollection( filter: {user_id: {eq: $userId}} orderBy: {created_at: AscNullsLast} ) { __typename edges { __typename node { __typename id name } } } }", - "1bc05beaca139a887f8c922c4de4dc6c86706670": "query tripDetails($id: UUID) { __typename tripsCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename activityCollection { __typename edges { __typename node { __typename address id time_from time_to title } } } cost cost_unit date_from date_to id image_url invitationsCollection { __typename edges { __typename node { __typename users { __typename id profile_picture_url } } } } title trip_tagsCollection { __typename edges { __typename node { __typename tags { __typename id name } } } } } } } }", + "e40d7c2a09852d29b44908c4c24cbcfa2dbce660": "query tripDetails($id: UUID) { __typename tripsCollection(filter: {id: {eq: $id}}) { __typename edges { __typename node { __typename activityCollection { __typename edges { __typename node { __typename address id time_from time_to title } } } cost cost_unit date_from date_to id image_url invitationsCollection { __typename edges { __typename node { __typename users { __typename id profile_picture_url } } } } title trip_tagsCollection { __typename edges { __typename node { __typename tags { __typename id name } } } } users { __typename id profile_picture_url } } } } }", "f4142b2a2f4ab96ce0e69488851fdff976486652": "query tripTagsCollection($filter: trip_tagsFilter) { __typename trip_tagsCollection(filter: $filter) { __typename edges { __typename node { __typename id tag_id trip_id } } } }", - "3256c7ff43134c13502c53f475e55fa91f55cabb": "query tripsCollection($after: Cursor, $filter: tripsFilter, $first: Int!, $orderBy: [tripsOrderBy!]) { __typename tripsCollection( filter: $filter first: $first after: $after orderBy: $orderBy ) { __typename edges { __typename node { __typename activityCollection { __typename edges { __typename node { __typename id } } } created_at date_from date_to id id image_url invitationsCollection { __typename edges { __typename node { __typename users { __typename id profile_picture_url } } } } title } } pageInfo { __typename endCursor hasNextPage hasPreviousPage startCursor } } }" + "848b1879d86d9282d2bb3a3d1016f42dd7e5cc56": "query tripsCollection($after: Cursor, $filter: tripsFilter, $first: Int!, $orderBy: [tripsOrderBy!]) { __typename tripsCollection( filter: $filter first: $first after: $after orderBy: $orderBy ) { __typename edges { __typename node { __typename activityCollection { __typename edges { __typename node { __typename id } } } created_at date_from date_to id id image_url invitationsCollection { __typename edges { __typename node { __typename users { __typename id profile_picture_url } } } } title users { __typename id name profile_picture_url } } } pageInfo { __typename endCursor hasNextPage hasPreviousPage startCursor } } }" } From dfac6a23b87e14bdbc5e5fa26a083a15cd46f02e Mon Sep 17 00:00:00 2001 From: Kana Taguchi Date: Sun, 18 Feb 2024 20:11:50 -0800 Subject: [PATCH 23/31] Delete unused comment --- app/trip/[id]/manage-group/components/invited-user-card.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/trip/[id]/manage-group/components/invited-user-card.tsx b/app/trip/[id]/manage-group/components/invited-user-card.tsx index 4858b3e..0ae9a8a 100644 --- a/app/trip/[id]/manage-group/components/invited-user-card.tsx +++ b/app/trip/[id]/manage-group/components/invited-user-card.tsx @@ -68,7 +68,6 @@ export const InvitedUserCard = ({ - {/* TODO If not owner, readme only */} Date: Fri, 23 Feb 2024 17:20:32 -0800 Subject: [PATCH 24/31] 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 25/31] 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 26/31] 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 27/31] 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 28/31] 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 29/31] 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 30/31] 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 31/31] 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 (