diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/CurrentDreamsContainer.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/CurrentDreamsContainer.tsx index ad3618f..6a995dc 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/CurrentDreamsContainer.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/CurrentDreamsContainer.tsx @@ -1,8 +1,10 @@ -import {FC, useMemo} from "react"; +import {FC, Fragment, useMemo} from "react"; import {useDreamsData} from "@/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider"; -import DreamCard from "@/app/(site)/(internal)/dashboard/components/dreams/DreamCard"; +import DreamCard from "@/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard"; import LogDreamCard from "@/app/(site)/(internal)/dashboard/components/dreams/LogDreamCard"; import useTodayTimeRange from "@/app/(site)/hooks/useTodayTimeRange"; +import {Spinner} from "@nextui-org/react"; +import DreamCardSkeleton from "@/app/(site)/(internal)/dashboard/components/dreams/card/DreamCardSkeleton"; const CurrentDreamsContainer: FC = () => { const [startOfToday, endOfToday] = useTodayTimeRange() @@ -18,13 +20,21 @@ const CurrentDreamsContainer: FC = () => { return (
-

Today - {startOfToday.toLocaleDateString("en-US", { - dateStyle: "medium" - })} +

Today + - {startOfToday.toLocaleDateString("en-US", { + dateStyle: "medium" + })}

-
+
- {dreamCards} + {dreams.loading ? ( + + + + + + ) : dreamCards}
diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider.tsx index f2923d1..bc04ae0 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider.tsx @@ -1,19 +1,20 @@ "use client" -import {createContext, FC, PropsWithChildren, useContext} from "react"; +import {FC, PropsWithChildren} from "react"; import useDreams, {DreamsState} from "@/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreams"; import useDreamCharacters, { DreamCharactersState } from "@/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters"; import useDreamTags, {DreamTagsState} from "@/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamTags"; +import {createDataContext, DataContextProps} from "@/utils/client/client-data-utils"; -type DreamsContextProps = { +interface DreamsContextProps extends DataContextProps { dreams: DreamsState, characters: DreamCharactersState, tags: DreamTagsState, } -const DreamsContext = createContext(undefined) +const [DreamsContext, useHook] = createDataContext("useDreamsData must be used in a DreamsProvider!") const DreamsProvider: FC = ({children}) => { const dreams = useDreams() @@ -28,10 +29,4 @@ const DreamsProvider: FC = ({children}) => { } export default DreamsProvider - -export const useDreamsData = () => { - const dreams = useContext(DreamsContext) - if (!dreams) - throw new Error("useDreams can only be used in a DreamProvider!") - return dreams; -} \ No newline at end of file +export const useDreamsData = useHook \ No newline at end of file diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/DreamCard.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard.tsx similarity index 63% rename from src/app/(site)/(internal)/dashboard/components/dreams/DreamCard.tsx rename to src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard.tsx index c709e7d..4a37542 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/DreamCard.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard.tsx @@ -1,20 +1,33 @@ -import {FC, Fragment} from "react"; +"use client" + +import {FC, Fragment, useState} from "react"; import {Dream} from "@prisma/client"; import {CardBody, CardHeader} from "@nextui-org/card"; import Card from "@/app/(site)/components/Card"; +import DreamModal from "@/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal"; type Props = { dream: Dream } const DreamCard: FC = ({dream}) => { + const [modalOpen, setModalOpen] = useState(false) + return ( - + setModalOpen(false)} + /> + setModalOpen(true)} + classNames={{ + header: "bg-[#0C0015] pt-6 px-8 pb-0", + body: "bg-[#0C0015] px-8 pt-4", + footer: "bg-[#0C0015] px-8", + }}>

{dream.title}

diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCardSkeleton.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCardSkeleton.tsx new file mode 100644 index 0000000..75836d8 --- /dev/null +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCardSkeleton.tsx @@ -0,0 +1,27 @@ +"use client" + +import {FC} from "react"; +import {CardBody, CardHeader} from "@nextui-org/card"; +import Card from "@/app/(site)/components/Card"; +import {Skeleton} from "@nextui-org/react"; + +const DreamCardSkeleton: FC = () => { + return ( + + + + + + + + + + ) +} + +export default DreamCardSkeleton \ No newline at end of file diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal.tsx new file mode 100644 index 0000000..3bf51a9 --- /dev/null +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal.tsx @@ -0,0 +1,63 @@ +"use client" + +import {FC, Fragment, useEffect, useMemo} from "react"; +import Modal from "@/app/(site)/components/Modal"; +import {Dream} from "@prisma/client"; +import useSWR from "swr"; +import {calcEstimatedReadingTime, fetcher} from "@/utils/client/client-utils"; +import {DreamWithRelations} from "@/app/api/me/dreams/dreams.dto"; +import {Chip} from "@nextui-org/chip"; + +type Props = { + dream: Dream, + isOpen?: boolean, + onClose?: () => void +} + +const FetchFullDream = (dream: Dream, modalOpen: boolean) => { + return useSWR(modalOpen && `/api/me/dreams/${dream.id}?tags=true&characters=true`, fetcher, {refreshInterval: 0}) +} + +const DreamModal: FC = ({dream, isOpen, onClose}) => { + const {data: fullDream, error: fullDreamError} = FetchFullDream(dream, isOpen ?? false) + + const tagChips = useMemo(() => fullDream?.tags?.map(tag => ( + + {tag.tag} + + )), [fullDream?.tags]) + + useEffect(() => { + if (fullDreamError) + console.error(fullDreamError) + }, [fullDreamError]) + + return ( + + {(tagChips || fullDreamError) && ( +

+ {tagChips ?? (fullDreamError && + + Error Loading Tags + + )} +
+ )} +

{dream.title}

+

{dream.comments}

+

~{calcEstimatedReadingTime(dream.description)} min. read

+
+ } + isOpen={isOpen} + onClose={onClose} + > +
{dream.description}
+ + ) +} + +export default DreamModal \ No newline at end of file diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/forms/characters/AddCharacterForm.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/forms/characters/AddCharacterForm.tsx index 6b077c6..516b3f1 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/forms/characters/AddCharacterForm.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/forms/characters/AddCharacterForm.tsx @@ -7,7 +7,7 @@ import axios from "axios"; import useSWRMutation from "swr/mutation"; import {useDreamsData} from "@/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider"; import {DreamCharacter} from "@prisma/client"; -import {handleAxiosError} from "@/utils/client-utils"; +import {handleAxiosError} from "@/utils/client/client-utils"; import {useSession} from "next-auth/react"; import toast from "react-hot-toast"; import Input from "@/app/(site)/components/Input"; diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/LogDreamForm.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/LogDreamForm.tsx index 4517f45..9bd6fc7 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/LogDreamForm.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/LogDreamForm.tsx @@ -15,7 +15,7 @@ import CloseIcon from "@/app/(site)/components/icons/CloseIcon"; import axios from "axios"; import useSWRMutation from "swr/mutation"; import {Dream} from "@prisma/client"; -import {handleAxiosError} from "@/utils/client-utils"; +import {handleAxiosError} from "@/utils/client/client-utils"; import {useSession} from "next-auth/react"; import toast from "react-hot-toast"; import AddTagModal from "@/app/(site)/(internal)/dashboard/components/dreams/forms/tags/AddTagModal"; diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/forms/tags/AddTagForm.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/forms/tags/AddTagForm.tsx index d1035a2..f2a40bd 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/forms/tags/AddTagForm.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/forms/tags/AddTagForm.tsx @@ -7,7 +7,7 @@ import axios from "axios"; import useSWRMutation from "swr/mutation"; import {useDreamsData} from "@/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider"; import {DreamTag} from "@prisma/client"; -import {handleAxiosError} from "@/utils/client-utils"; +import {handleAxiosError} from "@/utils/client/client-utils"; import {useSession} from "next-auth/react"; import toast from "react-hot-toast"; import Input from "@/app/(site)/components/Input"; diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters.tsx index 44c2e75..7d37797 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters.tsx @@ -1,10 +1,10 @@ import useSWR, {KeyedMutator} from "swr"; -import {fetcher} from "@/utils/client-utils"; +import {fetcher} from "@/utils/client/client-utils"; import {DreamCharacter} from "@prisma/client"; import {useCallback} from "react"; -import {DreamContextState} from "@/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreams"; +import {DataContextState} from "@/utils/client/client-data-utils"; -export type DreamCharactersState = DreamContextState +export type DreamCharactersState = DataContextState const useDreamCharacters = (): DreamCharactersState => { const {data: characters, isLoading: charactersLoading, mutate: mutateCharacters} = useSWR('/api/me/dreams/characters', fetcher) diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamTags.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamTags.tsx index 4573cd4..6e633f5 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamTags.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamTags.tsx @@ -1,10 +1,10 @@ import useSWR, {KeyedMutator} from "swr"; -import {fetcher} from "@/utils/client-utils"; +import {fetcher} from "@/utils/client/client-utils"; import {DreamTag} from "@prisma/client"; import {useCallback} from "react"; -import {DreamContextState} from "@/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreams"; +import {DataContextState} from "@/utils/client/client-data-utils"; -export type DreamTagsState = DreamContextState +export type DreamTagsState = DataContextState const useDreamTags = (): DreamTagsState => { const {data: tags, isLoading: tagsLoading, mutate: mutateTags} = useSWR('/api/me/dreams/tags', fetcher) diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreams.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreams.tsx index a3a6115..82db4b2 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreams.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreams.tsx @@ -1,19 +1,10 @@ import useSWR, {KeyedMutator} from "swr"; -import {fetcher} from "@/utils/client-utils"; +import {fetcher} from "@/utils/client/client-utils"; import {Dream} from "@prisma/client"; import {useCallback} from "react"; +import {DataContextState} from "@/utils/client/client-data-utils"; -export type DreamsState = DreamContextState - -export type DreamContextState = { - loading: boolean, - data: T, - mutateData?: KeyedMutator, - optimisticData: { - addOptimisticData: (work: () => Promise, optimisticData: O) => Promise, - removeOptimisticData: (work: () => Promise, removedData: O) => Promise, - } -} +export type DreamsState = DataContextState const useDreams = (): DreamsState => { const {data: dreams, isLoading: dreamsLoading, mutate: mutateDreams} = useSWR('/api/me/dreams', fetcher) diff --git a/src/app/(site)/components/Modal.tsx b/src/app/(site)/components/Modal.tsx index c832f14..b28474f 100644 --- a/src/app/(site)/components/Modal.tsx +++ b/src/app/(site)/components/Modal.tsx @@ -3,10 +3,11 @@ import {Modal as NextModal, ModalBody, ModalContent, ModalFooter, ModalHeader, M type Props = { subtitle?: string, + header?: ReactElement | ReactElement[] footer?: ReactElement | ReactElement[] } & ModalProps -const Modal: FC = ({footer, title, subtitle, children, ...props}) => { +const Modal: FC = ({header, footer, title, subtitle, children, ...props}) => { return ( = ({footer, title, subtitle, children, ...props}) => { {...props} > - {(title || subtitle) && ( + {(title || subtitle || header) && (
{title &&

{title}

} {subtitle &&

{subtitle}

} + {header}
)} diff --git a/src/app/(site)/hooks/useMemberInfo.tsx b/src/app/(site)/hooks/useMemberInfo.tsx index 7636c8d..6f8bb4f 100644 --- a/src/app/(site)/hooks/useMemberInfo.tsx +++ b/src/app/(site)/hooks/useMemberInfo.tsx @@ -1,6 +1,6 @@ import {useSession} from "next-auth/react"; import useSWR from "swr"; -import {fetcher} from "@/utils/client-utils"; +import {fetcher} from "@/utils/client/client-utils"; import {Member} from "@prisma/client"; diff --git a/src/app/api/me/dreams/[id]/route.ts b/src/app/api/me/dreams/[id]/route.ts new file mode 100644 index 0000000..bb30d31 --- /dev/null +++ b/src/app/api/me/dreams/[id]/route.ts @@ -0,0 +1,19 @@ +import {authenticated} from "@/app/api/utils/api-utils"; +import dreamsService from "@/app/api/me/dreams/dreams.service"; + +export type DreamRouteContext = { + params: { + id: string + } +} + +const TAGS_SEARCH_PARAM = "tags" +const CHARACTERS_SEARCH_PARAM = "characters" + +export const GET = async (request: Request, {params}: DreamRouteContext) => + authenticated((session) => { + const searchParams = new URL(request.url).searchParams + const withTags = searchParams.get(TAGS_SEARCH_PARAM)?.toLowerCase() === "true" + const withCharacters = searchParams.get(CHARACTERS_SEARCH_PARAM)?.toLowerCase() === "true" + return dreamsService.fetchDream(session, params.id, {withTags, withCharacters}) + }) \ No newline at end of file diff --git a/src/app/api/me/dreams/dreams.dto.ts b/src/app/api/me/dreams/dreams.dto.ts index b8efaf5..dd37475 100644 --- a/src/app/api/me/dreams/dreams.dto.ts +++ b/src/app/api/me/dreams/dreams.dto.ts @@ -1,4 +1,5 @@ import {z} from "zod"; +import {Dream, DreamCharacter, DreamTag, Member} from "@prisma/client"; export type PostDreamDto = { title: string, @@ -55,4 +56,10 @@ export const PostDreamCharacterSchema = z.object({ name: z.string() .min(DREAM_CHARACTER_MIN, `The name can't be less than ${DREAM_CHARACTER_MIN} character!`) .max(DREAM_CHARACTER_MAX, `The name can't be more than ${DREAM_CHARACTER_MAX} characters!`) -}).strict() \ No newline at end of file +}).strict() + +export type DreamWithRelations = Dream & { + tags?: DreamTag[] + characters?: DreamCharacter[] + user?: Member[] +} \ No newline at end of file diff --git a/src/app/api/me/dreams/dreams.service.ts b/src/app/api/me/dreams/dreams.service.ts index d146b0a..acfc7ab 100644 --- a/src/app/api/me/dreams/dreams.service.ts +++ b/src/app/api/me/dreams/dreams.service.ts @@ -4,6 +4,7 @@ import prisma from "@/libs/prisma"; import {NextResponse} from "next/server"; import {Session} from "next-auth"; import { + DreamWithRelations, PostDreamCharacterDto, PostDreamCharacterSchema, PostDreamDto, @@ -25,6 +26,33 @@ class DreamsService { }) } + public async fetchDream(session: Session, dreamId: string, options?: { + withTags?: boolean, + withCharacters?: boolean + }): Promise> { + const member = session.user + const dream = await prisma.dream.findFirst({ + where: { + id: dreamId, + userId: member.id + }, + include: { + tags: options?.withTags, + characters: options?.withCharacters + } + }) + + if (!dream) + return buildResponse({ + status: 404, + message: `Couldn't find a dream for you with ID: ${dreamId}` + }) + + return buildResponse({ + data: dream + }) + } + public async createDream(session: Session, dto: PostDreamDto): Promise> { const dtoValidated = PostDreamSchema.safeParse(dto) if (!dtoValidated.success) diff --git a/src/utils/client/client-data-utils.tsx b/src/utils/client/client-data-utils.tsx new file mode 100644 index 0000000..353698b --- /dev/null +++ b/src/utils/client/client-data-utils.tsx @@ -0,0 +1,30 @@ +"use client" +import {KeyedMutator} from "swr"; +import {Context, createContext, useContext} from "react"; + +export type DataContextState = { + loading: boolean, + data: T, + mutateData?: KeyedMutator, + optimisticData: { + addOptimisticData: (work: () => Promise, optimisticData: O) => Promise, + removeOptimisticData: (work: () => Promise, removedData: O) => Promise, + } +} + +export interface DataContextProps { + [K: string]: DataContextState +} + +export function createDataContext(hookErr?: string): [Context, () => T] { + const context = createContext(undefined) + const useHook = () => useGenericContextHook(context, hookErr) + return [context, useHook] +} + +export function useGenericContextHook(context: Context, hookErr?: string): T { + const data = useContext(context) + if (!data) + throw new Error(hookErr ?? "This hook cannot be used here!") + return data +} \ No newline at end of file diff --git a/src/utils/client-utils.tsx b/src/utils/client/client-utils.tsx similarity index 57% rename from src/utils/client-utils.tsx rename to src/utils/client/client-utils.tsx index 33b673f..5e43309 100644 --- a/src/utils/client-utils.tsx +++ b/src/utils/client/client-utils.tsx @@ -20,4 +20,16 @@ export function handleAxiosError(error: any): undefined { toast.error(error.response?.statusText ?? "Something went wrong!") return undefined +} + +/** + * Calculated the estimated reading time for a string + * @param text The text to analyze + * @return number The number of estimated minutes it would take to read + * the text + */ +export function calcEstimatedReadingTime(text: string): number { + const WORDS_PER_MINUTE = 200 // Based on research of the average case + const numOfWords = text.split(" ").length + return Math.ceil(numOfWords / WORDS_PER_MINUTE) } \ No newline at end of file