From af3891721e795dced2d8d706446100d45baf73df Mon Sep 17 00:00:00 2001 From: bombies Date: Sat, 28 Oct 2023 13:57:56 -0500 Subject: [PATCH] Implement dream edits --- .../components/dreams/card/DreamCard.tsx | 8 +- .../components/dreams/card/DreamModal.tsx | 19 +- .../components/dreams/card/DreamView.tsx | 70 ----- .../card/view/DreamEditableAddButton.tsx | 56 ++++ .../dreams/card/view/DreamEditableChip.tsx | 38 +++ .../components/dreams/card/view/DreamView.tsx | 250 ++++++++++++++++++ .../containers/CurrentDreamsContainer.tsx | 6 +- .../dreams/containers/PastDreamItem.tsx | 16 +- .../dreams/containers/PastDreamsContainer.tsx | 8 +- .../forms/characters/AddCharacterForm.tsx | 2 +- .../dreams/forms/log/DreamCharacterSelect.tsx | 2 +- .../dreams/forms/log/DreamTagSelect.tsx | 2 +- .../dreams/forms/log/LogDreamForm.tsx | 4 +- .../dreams/forms/tags/AddTagForm.tsx | 2 +- .../dreams/hooks/dream-api-utils.ts | 13 + .../dreams/hooks/useDreamCharacters.tsx | 21 +- .../components/dreams/hooks/useDreamTags.tsx | 19 +- src/app/(site)/components/Dropdown.tsx | 14 + src/app/(site)/components/Modal.tsx | 5 +- src/app/(site)/components/UserProfile.tsx | 7 +- .../(site)/components/{ => inputs}/Input.tsx | 12 +- .../(site)/components/{ => inputs}/Select.tsx | 0 .../components/{ => inputs}/TextArea.tsx | 0 .../{ => inputs/editable}/EditableInput.tsx | 10 +- .../inputs/editable/EditableTextArea.tsx | 110 ++++++++ .../(site)/signin/components/LogInForm.tsx | 2 +- .../(site)/signin/components/RegisterForm.tsx | 2 +- src/app/api/me/dreams/dreams.dto.ts | 6 +- src/utils/client/client-utils.tsx | 1 + 29 files changed, 586 insertions(+), 119 deletions(-) delete mode 100644 src/app/(site)/(internal)/dashboard/components/dreams/card/DreamView.tsx create mode 100644 src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableAddButton.tsx create mode 100644 src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableChip.tsx create mode 100644 src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamView.tsx create mode 100644 src/app/(site)/(internal)/dashboard/components/dreams/hooks/dream-api-utils.ts create mode 100644 src/app/(site)/components/Dropdown.tsx rename src/app/(site)/components/{ => inputs}/Input.tsx (85%) rename src/app/(site)/components/{ => inputs}/Select.tsx (100%) rename src/app/(site)/components/{ => inputs}/TextArea.tsx (100%) rename src/app/(site)/components/{ => inputs/editable}/EditableInput.tsx (92%) create mode 100644 src/app/(site)/components/inputs/editable/EditableTextArea.tsx diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard.tsx index 68a090a..fcda902 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamCard.tsx @@ -1,7 +1,7 @@ "use client" import {FC, Fragment, useCallback, useState} from "react"; -import {Dream} from "@prisma/client"; +import {Dream, DreamCharacter, DreamTag} 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"; @@ -12,6 +12,8 @@ import {deleteMutator, handleAxiosError} from "@/utils/client/client-utils"; type Props = { dream: Dream, + allCharacters: DreamCharacter[], + allTags: DreamTag[], optimisticRemove?: OptimisticWorker, } @@ -19,7 +21,7 @@ const DeleteDream = (dreamId: string) => { return useSWRMutation(`/api/me/dreams/${dreamId}`, deleteMutator()) } -const DreamCard: FC = ({dream, optimisticRemove}) => { +const DreamCard: FC = ({dream, allTags, allCharacters, optimisticRemove}) => { const [modalOpen, setModalOpen] = useState(false) const {trigger: deleteDream} = DeleteDream(dream.id) @@ -33,6 +35,8 @@ const DreamCard: FC = ({dream, optimisticRemove}) => { setModalOpen(false)} onDelete={() => { diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal.tsx index ec28124..cb69d09 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamModal.tsx @@ -1,20 +1,18 @@ "use client" -import {FC, Fragment, useEffect, useMemo, useState} from "react"; +import {FC, Fragment, useState} 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"; +import {Dream, DreamCharacter, DreamTag} from "@prisma/client"; import {Divider} from "@nextui-org/divider"; import {Button} from "@nextui-org/react"; import ConfirmationModal from "@/app/(site)/components/ConfirmationModal"; import TrashIcon from "@/app/(site)/components/icons/TrashIcon"; -import DreamView from "@/app/(site)/(internal)/dashboard/components/dreams/card/DreamView"; +import DreamView from "@/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamView"; type Props = { dream: Dream, + allCharacters: DreamCharacter[], + allTags: DreamTag[], isOpen?: boolean, onClose?: () => void, onDelete?: () => void, @@ -22,7 +20,7 @@ type Props = { -const DreamModal: FC = ({dream, isOpen, onClose, onDelete}) => { +const DreamModal: FC = ({dream, allTags, allCharacters, isOpen, onClose, onDelete}) => { const [deleteModalOpen, setDeleteModalOpen] = useState(false) return ( @@ -43,7 +41,12 @@ const DreamModal: FC = ({dream, isOpen, onClose, onDelete}) => { > { + console.log(dto) + }} />
diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamView.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamView.tsx deleted file mode 100644 index e2fb7cc..0000000 --- a/src/app/(site)/(internal)/dashboard/components/dreams/card/DreamView.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client" - -import {FC, Fragment, useEffect, useMemo, useState} from "react"; -import {Dream} from "@prisma/client"; -import {DreamWithRelations, PatchDreamDto} from "@/app/api/me/dreams/dreams.dto"; -import {calcEstimatedReadingTime, fetcher} from "@/utils/client/client-utils"; -import useSWR from "swr"; -import {Chip} from "@nextui-org/chip"; -import EditableInput from "@/app/(site)/components/EditableInput"; -import {Button} from "@nextui-org/button"; - -type Props = { - dream: Dream, - fetchDream?: boolean, - onEdit?: (dto: PatchDreamDto) => void -} - -const FetchFullDream = (dream: Dream, doFetch: boolean) => { - return useSWR(doFetch && `/api/me/dreams/${dream.id}?tags=true&characters=true`, fetcher, {refreshInterval: 0}) -} - -const DreamView: FC = ({dream, fetchDream, onEdit}) => { - const {data: fullDream, error: fullDreamError} = FetchFullDream(dream, fetchDream ?? true) - const [editMode, setEditMode] = useState(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 - - )} -
- )} - { - console.log(value) - }} - > -

{dream.title}

-
-

{dream.comments}

-

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

-
{dream.description}
-
- ) -} - -export default DreamView \ No newline at end of file diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableAddButton.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableAddButton.tsx new file mode 100644 index 0000000..70ca322 --- /dev/null +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableAddButton.tsx @@ -0,0 +1,56 @@ +import {DropdownMenu, DropdownTrigger} from "@nextui-org/react"; +import {Button} from "@nextui-org/button"; +import PlusIcon from "@/app/(site)/components/icons/PlusIcon"; +import Dropdown from "@/app/(site)/components/Dropdown"; +import {CollectionElement} from "@react-types/shared"; + +type Props = { + items: T[] + idArray: string[], + editCallback: (dto: { + arr: string[], + newArr: string[] + }) => void, + renderItem: (item: T) => CollectionElement +} + +export default function DreamEditableAddButton({ + items, + idArray, + editCallback, + renderItem + }: Props) { + return ( + + + + + { + editCallback({ + newArr: [...idArray, key as string], + arr: idArray + }) + }} + > + {(item) => renderItem(item as T)} + + + ) +} \ No newline at end of file diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableChip.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableChip.tsx new file mode 100644 index 0000000..6c33e29 --- /dev/null +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableChip.tsx @@ -0,0 +1,38 @@ +"use client" + +import {FC} from "react"; +import {Chip} from "@nextui-org/chip"; + +type Props = { + editMode: boolean, + id: string, + value: string, + idArray: string[], + editCallback: (dto: { + arr: string[], + newArr: string[] + }) => void +} + +const DreamEditableChip: FC = ({editMode, id, value, idArray, editCallback}) => { + return ( + { + editCallback({ + arr: idArray ?? [], + newArr: idArray.filter(itemId => itemId !== id) + }) + } : undefined} + classNames={{ + base: "gap-2" + }} + size="sm" + > + {value} + + ) +} + +export default DreamEditableChip \ No newline at end of file diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamView.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamView.tsx new file mode 100644 index 0000000..6787e6e --- /dev/null +++ b/src/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamView.tsx @@ -0,0 +1,250 @@ +"use client" + +import {FC, Fragment, useCallback, useEffect, useMemo, useState} from "react"; +import {Dream, DreamCharacter, DreamTag} from "@prisma/client"; +import {PatchDreamDto} from "@/app/api/me/dreams/dreams.dto"; +import {calcEstimatedReadingTime, handleAxiosError,} from "@/utils/client/client-utils"; +import {Chip} from "@nextui-org/chip"; +import EditableInput from "@/app/(site)/components/inputs/editable/EditableInput"; +import {Button} from "@nextui-org/button"; +import {EditIcon} from "@nextui-org/shared-icons"; +import toast from "react-hot-toast"; +import EditableTextArea from "@/app/(site)/components/inputs/editable/EditableTextArea"; +import DreamEditableChip from "@/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableChip"; +import {DropdownItem, Spacer} from "@nextui-org/react"; +import DreamEditableAddButton + from "@/app/(site)/(internal)/dashboard/components/dreams/card/view/DreamEditableAddButton"; +import {useDreamsData} from "@/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider"; +import {FetchFullDream, UpdateDream} from "../../hooks/dream-api-utils"; + +type Props = { + dream: Dream, + allCharacters: DreamCharacter[], + allTags: DreamTag[] + fetchDream?: boolean, +} + +const DreamView: FC = ({dream, allTags, allCharacters, fetchDream}) => { + const {data: fullDream, error: fullDreamError, mutate: mutateFullDream} = FetchFullDream(dream, fetchDream ?? true) + const {trigger: updateDream} = UpdateDream(dream.id) + const {dreams: {optimisticData: {editOptimisticData}}} = useDreamsData() + + const [editMode, setEditMode] = useState(false) + const dreamTagIds = useMemo(() => fullDream?.tags?.map(tag => tag.id) ?? [], [fullDream?.tags]) + const dreamCharacterIds = useMemo(() => fullDream?.characters?.map(character => character.id) ?? [], [fullDream?.characters]) + + const doUpdate = useCallback((body: PatchDreamDto) => ( + updateDream({body}) + .then(res => res.data) + .catch(handleAxiosError) + ), [updateDream]) + + const update = useCallback(async (body: PatchDreamDto) => { + const {newTags, tags, newCharacters, characters, ...restBody} = body + const newDream = {...dream} + + for (let key in restBody) + // TypeScript sucks sometimes... + if (restBody[key as keyof Pick] !== undefined) + newDream[key as "title" | "description" | "comments"] = restBody[key as keyof Pick] as string + + if (editOptimisticData) + editOptimisticData(() => doUpdate(body), newDream) + + if (newCharacters && fullDream) + await mutateFullDream({ + ...fullDream, + characters: allCharacters.filter(char => newCharacters.some(newCharId => newCharId === char.id)) + }) + + if (newTags && fullDream) + await mutateFullDream({ + ...fullDream, + tags: allTags.filter(tag => newTags.some(newTagId => newTagId === tag.id)) + }) + }, [allCharacters, allTags, doUpdate, dream, editOptimisticData, fullDream, mutateFullDream]) + + const tagChips = useMemo(() => fullDream?.tags?.map(tag => ( + { + await update({ + newTags: dto.newArr, + tags: dto.arr + }) + }} + /> + )), [dreamTagIds, editMode, fullDream?.tags, update]) + + const characterChips = useMemo(() => fullDream?.characters?.map(character => ( + { + await update({ + newCharacters: dto.newArr, + characters: dto.arr + }) + }} + /> + )), [dreamCharacterIds, editMode, fullDream?.characters, update]) + + useEffect(() => { + if (fullDreamError) + console.error(fullDreamError) + }, [fullDreamError]) + + return ( + +
+ {(tagChips || fullDreamError) && ( +
+ {editMode && ( +
+

Tags

+ !fullDream?.tags?.some(addedTag => addedTag.id === tag.id))} + idArray={dreamTagIds} + editCallback={async ({newArr, arr}) => { + await update({ + newTags: newArr, + tags: arr, + }) + }} + renderItem={(item) => ( + + {item.tag} + + )} + /> +
+ )} +
+ {tagChips ?? (fullDreamError && + + Error Loading Tags + + )} +
+
+ )} +
+ +
+
+ { + if (value == undefined) + return; + + await update({title: value}) + }} + > +

{dream.title}

+
+ {dream.comments && ( + { + update({comments: value ?? null}) + }} + > +

{dream.comments}

+
+ )} +

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

+ + { + if (value == undefined) + return; + + await update({description: value}) + }} + > +
+ {dream.description} +
+
+ {(editMode || characterChips) && ( + + +

Characters

+
+ )} +
+ {(characterChips || fullDreamError) && ( + +
+ {characterChips ?? (fullDreamError && + + Error Loading Tags + + )} +
+
+ )} + {editMode && ( + !fullDream?.characters?.some(addedCharacter => addedCharacter.id === character.id))} + idArray={dreamCharacterIds} + editCallback={async ({newArr, arr}) => { + await update({ + newCharacters: newArr, + characters: arr, + }) + }} + renderItem={(item) => ( + + {item.name} + + )} + /> + )} +
+
+ ) +} + +export default DreamView \ No newline at end of file diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/containers/CurrentDreamsContainer.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/containers/CurrentDreamsContainer.tsx index 4cee2f8..e52c909 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/containers/CurrentDreamsContainer.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/containers/CurrentDreamsContainer.tsx @@ -8,7 +8,7 @@ import DreamContainer from "@/app/(site)/(internal)/dashboard/components/dreams/ const CurrentDreamsContainer: FC = () => { const [startOfToday, endOfToday] = useTodayTimeRange() - const {dreams} = useDreamsData() + const {dreams, tags, characters} = useDreamsData() const dreamCards = useMemo(() => dreams.data .filter(dream => { const creationDate = new Date(dream.createdAt.toString()); @@ -19,9 +19,11 @@ const CurrentDreamsContainer: FC = () => { - )), [dreams.data, dreams.optimisticData.removeOptimisticData, endOfToday, startOfToday]) + )), [characters.data, dreams.data, dreams.optimisticData.removeOptimisticData, endOfToday, startOfToday, tags.data]) return ( = ({dream}) => { +const PastDreamItem: FC = ({dream, allTags, allCharacters}) => { const cards = useMemo(() => dream.dreams.map(pastDream => ( - + )), [dream.dreams]) - + return (

{new Date(dream.timestamp).toLocaleString("en-US", { diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/containers/PastDreamsContainer.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/containers/PastDreamsContainer.tsx index a6ef70a..30234a3 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/containers/PastDreamsContainer.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/containers/PastDreamsContainer.tsx @@ -9,10 +9,8 @@ import PastDreamItem from "@/app/(site)/(internal)/dashboard/components/dreams/c import DreamCardSkeleton from "@/app/(site)/(internal)/dashboard/components/dreams/card/DreamCardSkeleton"; import {Divider} from "@nextui-org/divider"; import Card from "@/app/(site)/components/Card"; -import Link from "next/link"; import {CardBody} from "@nextui-org/card"; import {useRouter} from "next/navigation"; -import PillowIcon from "@/app/(site)/components/icons/PillowIcon"; import CloudIcon from "@/app/(site)/components/icons/CloudIcon"; type GroupedDreams = { @@ -28,7 +26,7 @@ const NUMBER_OF_DAYS = 7 const PastDreamsContainer: FC = () => { const router = useRouter() - const {dreams} = useDreamsData() + const {dreams, tags, characters} = useDreamsData() const earliestDate = useStartOfDay({ dayOffset: -NUMBER_OF_DAYS, // A week ago @ 12:00 AM }) @@ -76,8 +74,10 @@ const PastDreamsContainer: FC = () => { - )), [pastDreams]) + )), [characters.data, pastDreams, tags.data]) return ( 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 516b3f1..bec30e9 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 @@ -10,7 +10,7 @@ import {DreamCharacter} from "@prisma/client"; 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"; +import Input from "@/app/(site)/components/inputs/Input"; import {Button} from "@nextui-org/button"; type FormProps = PostDreamCharacterDto diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamCharacterSelect.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamCharacterSelect.tsx index 7e021f3..247c18b 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamCharacterSelect.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamCharacterSelect.tsx @@ -2,7 +2,7 @@ import {FC, Fragment, useState} from "react"; import Button from "@/app/(site)/components/Button"; -import Select from "@/app/(site)/components/Select"; +import Select from "@/app/(site)/components/inputs/Select"; import {Chip} from "@nextui-org/chip"; import {SelectItem} from "@nextui-org/react"; import {DreamCharactersState} from "@/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters"; diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamTagSelect.tsx b/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamTagSelect.tsx index c96bd9a..62101cd 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamTagSelect.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamTagSelect.tsx @@ -2,7 +2,7 @@ import {FC, Fragment, useState} from "react"; import Button from "@/app/(site)/components/Button"; -import Select from "@/app/(site)/components/Select"; +import Select from "@/app/(site)/components/inputs/Select"; import {Chip} from "@nextui-org/chip"; import {SelectItem} from "@nextui-org/react"; import {UseFormRegister} from "react-hook-form"; 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 f5353f6..ad3a531 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 @@ -3,8 +3,8 @@ import {FC, Fragment, useCallback, useState} from "react"; import {SubmitHandler, useForm} from "react-hook-form"; import {PostDreamDto} from "@/app/api/me/dreams/dreams.dto"; -import Input from "@/app/(site)/components/Input"; -import TextArea from "@/app/(site)/components/TextArea"; +import Input from "@/app/(site)/components/inputs/Input"; +import TextArea from "@/app/(site)/components/inputs/TextArea"; import Button from "@/app/(site)/components/Button"; import {useDreamsData} from "@/app/(site)/(internal)/dashboard/components/dreams/DreamsProvider"; import DreamCharacterSelect from "@/app/(site)/(internal)/dashboard/components/dreams/forms/log/DreamCharacterSelect"; 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 f2a40bd..4e29c79 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 @@ -10,7 +10,7 @@ import {DreamTag} from "@prisma/client"; 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"; +import Input from "@/app/(site)/components/inputs/Input"; import {Button} from "@nextui-org/button"; type FormProps = PostDreamTagDto diff --git a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/dream-api-utils.ts b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/dream-api-utils.ts new file mode 100644 index 0000000..843119c --- /dev/null +++ b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/dream-api-utils.ts @@ -0,0 +1,13 @@ +import {Dream} from "@prisma/client"; +import useSWR from "swr"; +import {fetcher, patchMutator} from "@/utils/client/client-utils"; +import {DreamWithRelations, PatchDreamDto} from "@/app/api/me/dreams/dreams.dto"; +import useSWRMutation from "swr/mutation"; + +export const FetchFullDream = (dream: Dream, doFetch: boolean) => { + return useSWR(doFetch && `/api/me/dreams/${dream.id}?tags=true&characters=true`, fetcher, {refreshInterval: 0}) +} + +export const UpdateDream = (dreamId: string) => { + return useSWRMutation(`/api/me/dreams/${dreamId}`, patchMutator()) +} \ No newline at end of file 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 7d37797..d415718 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamCharacters.tsx @@ -1,13 +1,28 @@ import useSWR, {KeyedMutator} from "swr"; import {fetcher} from "@/utils/client/client-utils"; -import {DreamCharacter} from "@prisma/client"; +import {DreamCharacter, DreamTag} from "@prisma/client"; import {useCallback} from "react"; import {DataContextState} from "@/utils/client/client-data-utils"; +import useSWRMutation from "swr/mutation"; export type DreamCharactersState = DataContextState -const useDreamCharacters = (): DreamCharactersState => { - const {data: characters, isLoading: charactersLoading, mutate: mutateCharacters} = useSWR('/api/me/dreams/characters', fetcher) +const API_ROUTE = '/api/me/dreams/characters' + +export const FetchDreamCharacters = () => { + return useSWRMutation(API_ROUTE, fetcher) +} + +type Args = { + load?: boolean +} + +const useDreamCharacters = (args?: Args): DreamCharactersState => { + const { + data: characters, + isLoading: charactersLoading, + mutate: mutateCharacters + } = useSWR(args?.load !== false && API_ROUTE, fetcher) const addOptimisticCharacter = useCallback(async (work: () => Promise, optimisticCharacter: DreamCharacter) => { if (!characters) 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 6e633f5..f74c4b6 100644 --- a/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamTags.tsx +++ b/src/app/(site)/(internal)/dashboard/components/dreams/hooks/useDreamTags.tsx @@ -3,11 +3,26 @@ import {fetcher} from "@/utils/client/client-utils"; import {DreamTag} from "@prisma/client"; import {useCallback} from "react"; import {DataContextState} from "@/utils/client/client-data-utils"; +import useSWRMutation from "swr/mutation"; export type DreamTagsState = DataContextState -const useDreamTags = (): DreamTagsState => { - const {data: tags, isLoading: tagsLoading, mutate: mutateTags} = useSWR('/api/me/dreams/tags', fetcher) +type Args = { + load?: boolean +} + +const API_ROUTE = '/api/me/dreams/tags' + +export const FetchDreamTags = () => { + return useSWRMutation(API_ROUTE, fetcher) +} + +const useDreamTags = (args?: Args): DreamTagsState => { + const { + data: tags, + isLoading: tagsLoading, + mutate: mutateTags + } = useSWR(args?.load !== false && API_ROUTE, fetcher) const addOptimisticCharacter = useCallback(async (work: () => Promise, optimisticCharacter: DreamTag) => { if (!tags) diff --git a/src/app/(site)/components/Dropdown.tsx b/src/app/(site)/components/Dropdown.tsx new file mode 100644 index 0000000..966e2e4 --- /dev/null +++ b/src/app/(site)/components/Dropdown.tsx @@ -0,0 +1,14 @@ +import {Dropdown as NextDropdown} from "@nextui-org/dropdown"; +import {extendVariants} from "@nextui-org/react"; + +const Dropdown = extendVariants(NextDropdown, { + variants: { + color: { + gradient: { + base: "border font-semibold border-primary/30 px-4 py-6 bg-gradient-to-b from-[#8F00FF30] to-[#27007940] backdrop-blur-md" + } + } + } +}) + +export default Dropdown \ No newline at end of file diff --git a/src/app/(site)/components/Modal.tsx b/src/app/(site)/components/Modal.tsx index 5fba48d..c5c39b5 100644 --- a/src/app/(site)/components/Modal.tsx +++ b/src/app/(site)/components/Modal.tsx @@ -7,7 +7,7 @@ type Props = { footer?: ReactElement | ReactElement[] } & ModalProps -const Modal: FC = ({header, footer, title, subtitle, children, ...props}) => { +const Modal: FC = ({classNames, header, footer, title, subtitle, children, ...props}) => { return ( = ({header, footer, title, subtitle, children, ...props}) scrollBehavior="outside" classNames={{ base: "py-6 px-3 phone:px-0 bg-secondary", - closeButton: "hover:bg-primary/20" + closeButton: "hover:bg-primary/20", + ...classNames }} {...props} > diff --git a/src/app/(site)/components/UserProfile.tsx b/src/app/(site)/components/UserProfile.tsx index d7c107d..5fb3003 100644 --- a/src/app/(site)/components/UserProfile.tsx +++ b/src/app/(site)/components/UserProfile.tsx @@ -1,5 +1,5 @@ import {FC} from "react"; -import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger} from "@nextui-org/react"; +import {Avatar, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger} from "@nextui-org/react"; import {signOut} from "next-auth/react"; import DashboardIcon from "@/app/(site)/components/icons/DashboardIcon"; import SettingsIcon from "@/app/(site)/components/icons/SettingsIcon"; @@ -8,6 +8,7 @@ import {OverlayPlacement} from "@nextui-org/aria-utils"; import Link from "next/link"; import Image from "@/app/(site)/components/Image"; import HomeIcon from "@/app/(site)/components/icons/HomeIcon"; +import Dropdown from "@/app/(site)/components/Dropdown"; type Props = { placement?: OverlayPlacement @@ -19,12 +20,10 @@ const UserProfile: FC = ({placement}) => { return ( boolean, @@ -59,9 +60,16 @@ const Input: FC = ({classNames, id, register, errors, setValidationErrors color: "primary", size: "lg", classNames: { - inputWrapper: "h-fit py-6 bg-[#9E23FF1A]/10 border-1 border-[#3E0070] hover:!bg-[#9E23FF1A]/20 focus-within:!bg-[#9E23FF1A]/20", + inputWrapper: clsx( + "rounded-3xl h-fit bg-[#9E23FF1A]/10 border-1 border-[#3E0070] hover:!bg-[#9E23FF1A]/20 focus-within:!bg-[#9E23FF1A]/20", + props.size === "sm" ? "py-3" : (props.size === "md" ? "py-6" : "p-8") + ), input: "text-[#EAE0FF]", - label: "text-lg phone:text-medium text-[#EAE0FF]", + label: clsx( + "phone:text-medium text-[#EAE0FF]", + props.size === "sm" ? "text-sm" : (props.size === "md" ? "text-lg" : "text-xl") + + ) , ...classNames }, isInvalid: errMsg != undefined, diff --git a/src/app/(site)/components/Select.tsx b/src/app/(site)/components/inputs/Select.tsx similarity index 100% rename from src/app/(site)/components/Select.tsx rename to src/app/(site)/components/inputs/Select.tsx diff --git a/src/app/(site)/components/TextArea.tsx b/src/app/(site)/components/inputs/TextArea.tsx similarity index 100% rename from src/app/(site)/components/TextArea.tsx rename to src/app/(site)/components/inputs/TextArea.tsx diff --git a/src/app/(site)/components/EditableInput.tsx b/src/app/(site)/components/inputs/editable/EditableInput.tsx similarity index 92% rename from src/app/(site)/components/EditableInput.tsx rename to src/app/(site)/components/inputs/editable/EditableInput.tsx index 17a65c6..c018ca2 100644 --- a/src/app/(site)/components/EditableInput.tsx +++ b/src/app/(site)/components/inputs/editable/EditableInput.tsx @@ -3,7 +3,7 @@ import {FC, PropsWithChildren, useCallback, useEffect, useState} from "react"; import {InputProps} from "@nextui-org/react"; import {AnimatePresence, motion} from "framer-motion"; -import Input from "@/app/(site)/components/Input"; +import Input from "@/app/(site)/components/inputs/Input"; import {Button} from "@nextui-org/button"; import {SubmitHandler, useForm} from "react-hook-form"; import CheckIcon from "@/app/(site)/components/icons/CheckIcon"; @@ -16,8 +16,8 @@ type FormProps = { type Props = { isEditable?: boolean, value?: string, - onEdit?: (value: string) => void -} & Pick & PropsWithChildren + onEdit?: (value?: string) => void, +} & Pick & PropsWithChildren const EditableInput: FC = ({isEditable, value, children, onEdit, ...inputProps}) => { const {register, handleSubmit} = useForm() @@ -35,8 +35,9 @@ const EditableInput: FC = ({isEditable, value, children, onEdit, ...input return setEditToggled(false) if (onEdit) - onEdit(data.value) + onEdit(data.value.length === 0 ? undefined : data.value) setEditToggled(false) + setCurrentValue(value ?? "") }, [onEdit, value]) return ( @@ -49,7 +50,6 @@ const EditableInput: FC = ({isEditable, value, children, onEdit, ...input onSubmit={handleSubmit(onSubmit)} > void, +} & Pick & PropsWithChildren + +const EditableTextArea: FC = ({isEditable, value, children, onEdit, ...inputProps}) => { + const {register, handleSubmit} = useForm() + const [currentValue, setCurrentValue] = useState(value ?? "") + const [editToggled, setEditToggled] = useState(false) + + useEffect(() => { + if (isEditable) + setCurrentValue(value ?? "") + else setEditToggled(false) + }, [isEditable, value]) + + const onSubmit: SubmitHandler = useCallback((data) => { + if (data.value === value) + return setEditToggled(false) + + if (onEdit) + onEdit(data.value.length === 0 ? undefined : data.value) + setEditToggled(false) + setCurrentValue(value ?? "") + }, [onEdit, value]) + + return ( + + {isEditable ? (editToggled ? + +