Skip to content

Commit

Permalink
Refining bookings (#58)
Browse files Browse the repository at this point in the history
* Jovells workflow auto sync after pr merge (#4)

* Update main.yml

* Update main.yml

* added comments to custom metamask config

* refined bookings

* fixed error in buying expts
  • Loading branch information
Jovells authored Dec 28, 2023
1 parent eb9e819 commit e34b9a2
Show file tree
Hide file tree
Showing 12 changed files with 560 additions and 286 deletions.
139 changes: 94 additions & 45 deletions frontend/nextjs/emt.config.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,118 @@

import {connectorsForWallets} from "@rainbow-me/rainbowkit";
import { connectorsForWallets } from "@rainbow-me/rainbowkit";
import {
injectedWallet,
metaMaskWallet,
walletConnectWallet,
} from '@rainbow-me/rainbowkit/wallets';
import { configureChains, createConfig } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';
} from "@rainbow-me/rainbowkit/wallets";
import { configureChains, createConfig } from "wagmi";
import { publicProvider } from "wagmi/providers/public";

import { collection } from "firebase/firestore";
import { firestore } from "@/lib/firebase";

import {chain, envChains} from './contracts'
import { chain, envChains } from "./contracts";
import { HOME_PAGE } from "@/app/(with wallet)/_components/page-links";

const { chains, publicClient } = configureChains(
envChains,
[
publicProvider()
]
);
const { chains, publicClient } = configureChains(envChains, [publicProvider()]);

const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID as string
const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID as string;

const customMetamaskWallet = metaMaskWallet({ projectId, chains })
customMetamaskWallet.createConnector = () => {
const old = metaMaskWallet({ projectId, chains }).createConnector()
const oldMobileGetUri = old.mobile!.getUri;
old.mobile!.getUri = async () => {
if(window?.ethereum){
return oldMobileGetUri as any
}
return 'https://metamask.app.link/dapp/'+location.origin+HOME_PAGE
/**
* Creates a custom metamask connector that opens the app in the metamask app if the user is on mobile.
*
*/
const customMetamaskWallet = metaMaskWallet({ projectId, chains });
/**
* modify the createConnector function to return a custom connector
*/
customMetamaskWallet.createConnector = () => {
const newConnector = metaMaskWallet({ projectId, chains }).createConnector();
const oldMobileGetUri = newConnector.mobile!.getUri;
/**
* modify the getUri function to return a custom uri
*/
newConnector.mobile!.getUri = async () => {
/**
* if the user is not in the matamask browser, return the default uri
*/
if (window?.ethereum) {
return oldMobileGetUri as any;
}
return old
}

const connectors = connectorsForWallets([
/**
* if the user is in the metamask browser, return a custom uri
*/
return "https://metamask.app.link/dapp/" + location.origin + HOME_PAGE;
};
return newConnector;
};

const connectors = connectorsForWallets([
{
groupName: 'Recommended',
groupName: "Recommended",
wallets: [
injectedWallet({ chains }),
customMetamaskWallet,
walletConnectWallet({ projectId, chains }),
],
},
]);

const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient
})

export const emtChains = chains
export const emtWagmiConfig = wagmiConfig
export {chain}
const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient,
});

export const emtChains = chains;
export const emtWagmiConfig = wagmiConfig;
export { chain };

export const USERS_COLLECTION = collection(firestore, "users");
export const ADMIN_COLLECTION = collection(firestore, "admin");
export const NOTIFICATIONS_COLLECTION =
process.env.NODE_ENV === "production"
? collection(firestore, "notifications")
: collection(
firestore,
"dev",
String(process.env.NEXT_PUBLIC_DEV!) + chain.id,
"notifications"
);
export const CLAIM_HISTORY_COLLECTION =
process.env.NODE_ENV === "production"
? collection(firestore, "claimHistory")
: collection(
firestore,
"dev",
String(process.env.NEXT_PUBLIC_DEV!) + chain.id,
"claimHistory"
);
export const CONTENTS_COLLECTION =
process.env.NODE_ENV === "production"
? collection(firestore, "contents")
: collection(
firestore,
"dev",
String(process.env.NEXT_PUBLIC_DEV!) + chain.id,
"contents"
);
export const EXPT_LISTINGS_COLLECTION =
process.env.NODE_ENV === "production"
? collection(firestore, "exptListings")
: collection(
firestore,
"dev",
String(process.env.NEXT_PUBLIC_DEV!) + chain.id,
"exptListings"
);
export const BOOKINGS_COLLECTION =
process.env.NODE_ENV === "production"
? collection(firestore, "bookings")
: collection(
firestore,
"dev",
String(process.env.NEXT_PUBLIC_DEV!) + chain.id,
"bookings"
);

export const USERS_COLLECTION = collection(firestore, 'users');
export const ADMIN_COLLECTION = collection(firestore, 'admin');
export const NOTIFICATIONS_COLLECTION = process.env.NODE_ENV === "production"? collection(firestore, 'notifications') : collection(firestore, 'dev', String(process.env.NEXT_PUBLIC_DEV!) + chain.id , 'notifications');
export const CLAIM_HISTORY_COLLECTION = process.env.NODE_ENV === "production"? collection(firestore, 'claimHistory') : collection(firestore, 'dev', String(process.env.NEXT_PUBLIC_DEV!) + chain.id , 'claimHistory');
export const CONTENTS_COLLECTION = process.env.NODE_ENV === "production"? collection(firestore, 'contents') : collection(firestore, 'dev', String(process.env.NEXT_PUBLIC_DEV!) + chain.id, 'contents');
export const EXPT_LISTINGS_COLLECTION = process.env.NODE_ENV === "production"? collection(firestore, 'exptListings') : collection(firestore, 'dev', String(process.env.NEXT_PUBLIC_DEV!) + chain.id, 'exptListings');
export const BOOKINGS_COLLECTION = process.env.NODE_ENV === "production"? collection(firestore, 'bookings') : collection(firestore, 'dev', String(process.env.NEXT_PUBLIC_DEV!) + chain.id, 'bookings');

export const exptLevelKeys = [1, 2, 3]
export const exptLevelKeys = [1, 2, 3];
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,38 @@ import useBackend from '@/lib/hooks/useBackend'
import { BookingCalendarForm } from './book-expert'
import { ExptListing, ExptListingWithAuthorProfile } from '@/lib/types'

export default function BookExpertDialogue({data}: {data: ExptListingWithAuthorProfile}){
const {fetchProfile} = useBackend()

const {data: author, isLoading}= useQuery({
queryKey: ["author", data.author],
queryFn: ()=>fetchProfile(data.author)

});

if(!author && isLoading) return <DataLoading/>
if(!author) return <NoData message='No bookings'/>

export default function BookExpertDialogue({data}: {data: {listing: ExptListingWithAuthorProfile, id: string, remainingSessions: number}}){
const {listing, id, remainingSessions} = data;
return <Dialog >
<DialogTrigger>
<ExpertHubCard data={data} type="modal" />
<ExpertHubCard data={listing} type="modal" />
</DialogTrigger>

<DialogContent className='w-full py-0 max-h-[90vh] overflow-hidden'>
<div className="grid grid-cols-[35%_60%]">
<ScrollArea className="h-[90vh]">
<div className="border-r pr-6 py-6">
<DialogHeader className='mb-6'>
<DialogTitle>Book a Session with {author.displayName}</DialogTitle>
<DialogTitle>Book a Session with {listing.authorProfile.displayName}</DialogTitle>
</DialogHeader>
<ExpertHubCard data={data} disableLink={true} />
<ExpertHubCard data={listing} disableLink={true} />
<div className="my-5">
<div className="text-sm mb-2">Token Id</div>
<div className="text-xs text-muted">{id}</div>
</div>
<div className="my-5">
<div className="text-sm mb-2">Session Duration</div>
<div className="text-xs text-muted">{data.sessionCount} session(s) x {data.sessionDuration} minutes</div>
<div className="text-xs text-muted">{listing.sessionDuration} session(s) x {listing.sessionCount} minutes</div>
</div>

<div className="">
<div className="text-sm mb-2">Description</div>
<div className="text-xs text-muted">{data.description}</div>
<div className="text-xs text-muted">{listing.description}</div>
</div>
</div>
</ScrollArea>
<ScrollArea className='h-[90vh] p-6'>
<BookingCalendarForm />
<BookingCalendarForm remainingSessions={remainingSessions} exptTokenId={id} exptListing={listing}/>
</ScrollArea>
</div>
</DialogContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import React from "react";
import { ExpertTicket, ExptListing } from "@/lib/types";
import { Booking, ExpertTicket, ExptListing } from "@/lib/types";
import { ScrollArea } from "@/components/ui/scroll-area";
import { zodResolver } from "@hookform/resolvers/zod";
import { CalendarIcon } from "@radix-ui/react-icons";
Expand Down Expand Up @@ -28,28 +28,47 @@ import {
import { toast } from "@/components/ui/use-toast";
import { Globe2Icon } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import DataLoading from "@/components/ui/data-loading";
import NoData from "@/components/ui/no-data";
import BookExpertDialogue from "./book-expert-dialogue";
import useBackend from "@/lib/hooks/useBackend";
import InfiniteScroll from "@/components/ui/infinite-scroller";
import { useUser } from "@/lib/hooks/user";
import { Timestamp } from "firebase/firestore";
import { Input } from "@/components/ui/input";

const FormSchema = z.object({
availableDate: z.date({
required_error: "A date is required.",
}),
message: z.string().optional(),
});

export function BookingCalendarForm() {
export function BookingCalendarForm({exptListing, exptTokenId, remainingSessions}:{exptListing: ExptListing, exptTokenId: string, remainingSessions: number}) {
const { bookExpert } = useBackend();
const queryClient = useQueryClient();
const FormSchema = z.object({
sessionTimestamp: z.date({
required_error: "A date is required.",
}),
message: z.string().optional(),
sessionCount: z.coerce.number().max(remainingSessions, {
message: `You can only book ${exptListing.sessionCount} sessions`,
})
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});

const currentTimezone = "UTC Time (10:00)";

const { data, mutateAsync} = useMutation({
mutationFn: bookExpert,
onSuccess: (data) => {
queryClient.setQueryData(['bookings'], (oldData: Booking[])=>{
return [
...oldData,
data
]
})
},
})

function onSubmit(data: z.infer<typeof FormSchema>) {
toast({
title: "You submitted the following values:",
Expand All @@ -59,14 +78,17 @@ export function BookingCalendarForm() {
// {/* </pre> */}
),
});

mutateAsync({...data, exptListingId: exptListing.id, sessionTimestamp: Timestamp.fromDate(data.sessionTimestamp), mentor: exptListing.author, exptTokenId})

}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="availableDate"
name="sessionTimestamp"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Select Date &amp; Time</FormLabel>
Expand Down Expand Up @@ -131,6 +153,24 @@ export function BookingCalendarForm() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="sessionCount"
render={({ field }) => (
<FormItem>
<FormLabel>Number of Sessions ({remainingSessions} remaining)</FormLabel>
<FormControl>
<Input
type="number"
max={remainingSessions}
placeholder="select number of sessions"
{...field}
/>
</FormControl>
<FormMessage className="text-xs text-muted font-normal" />
</FormItem>
)}
/>

<div className="">
<div className="text-sm mb-2">Time Zone</div>
Expand All @@ -155,18 +195,18 @@ export function BookingCalendarForm() {


const BookExpert = () => {
const { fetchExptListings } = useBackend();
const { fetchExpts } = useBackend();
const {user} = useUser();

return (
<InfiniteScroll
ItemComponent={BookExpertDialogue}
getNextPageParam={(lastPage) => {
return lastPage[lastPage.length - 1]?.timestamp;
return lastPage[lastPage.length - 1];
}}
queryKey={["ownedExpt"]}
filters={{mentee: user?.uid, key: 'djsj'}}
fetcher={fetchExptListings}
queryKey={["menteeExpts"]}
filters={{mentee: user?.uid}}
fetcher={fetchExpts}
className="w-full flex flex-wrap gap-4 flex-grow"
/>
);
Expand Down
4 changes: 2 additions & 2 deletions frontend/nextjs/src/app/(with wallet)/dapp/bookings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
'use client';
import React from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Booking, BookingFilters, Content, ExpertTicket, ExptListing, ReviewItem as ReviewItemProps, UserProfile } from '@/lib/types';
import { Booking, BookingFilters, Content, ExpertTicket, ExptFilters, ExptListing, ReviewItem as ReviewItemProps, UserProfile } from '@/lib/types';
import ExpertHubCard from '@/components/ui/expert-hub-card';
import SessionReviewForm from './_components/session-review-form';
import BookExpert from './_components/book-expert';
Expand Down Expand Up @@ -31,7 +31,7 @@ const {user} = useUser();
<TabsContent value="history" className='pt-2'>
<InfiniteScroll className="grid grid-cols-[1fr] md:grid-cols-[280px_1fr] gap-8 mt-4"
fetcher={fetchBookings}
filters={{mentor: user?.uid} as BookingFilters}
filters={{mentee: user?.uid} as ExptFilters}
queryKey={["bookings"]}
noDataMessage="No bookings found. Please try later"
ItemComponent ={({data}: {data: Booking})=><><div className="opacity-[0.4]">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ const ExpertHub = () => {
fetcher={fetchExptListings}
size={3}
ItemComponent={ExpertHubCard}
getNextPageParam={(lastPage) => {
return lastPage[lastPage.length - 1]?.timestamp;
}}
queryKey={["exptListings"]}
noDataMessage="No EXPT listings found. Please try later"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ const Notifications = () => {
<InfiniteScroll
queryKey={['notifications', user]}
noDataMessage="No notifications found. Please try later"
fetcher={(lastDocTimeStamp: Timestamp | undefined) => {
return fetchNotifications(lastDocTimeStamp);
fetcher={(lastDoc) => {
return fetchNotifications(lastDoc?.timestamp);
}}
className='space-y-5'
enabled={!!user}
Expand Down
Loading

0 comments on commit e34b9a2

Please sign in to comment.