From 46ca8327631ca3529ec4576f13ac60d853a4a90d Mon Sep 17 00:00:00 2001
From: Emilio Schaedler Heinzmann
<103655828+emiliosheinz@users.noreply.github.com>
Date: Sat, 21 Jan 2023 10:30:08 -0300
Subject: [PATCH 1/8] feat: :sparkles: adds four oh four page
---
public/images/not-found-illustration.svg | 1 +
src/pages/404.tsx | 33 ++++++++++++++++++++++++
2 files changed, 34 insertions(+)
create mode 100644 public/images/not-found-illustration.svg
create mode 100644 src/pages/404.tsx
diff --git a/public/images/not-found-illustration.svg b/public/images/not-found-illustration.svg
new file mode 100644
index 0000000..ad3785c
--- /dev/null
+++ b/public/images/not-found-illustration.svg
@@ -0,0 +1 @@
+
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
new file mode 100644
index 0000000..f9e0811
--- /dev/null
+++ b/src/pages/404.tsx
@@ -0,0 +1,33 @@
+import { useRouter } from 'next/router'
+
+import { Button } from '~/components/button'
+import { Image } from '~/components/image'
+import { routes } from '~/utils/navigation'
+
+export default function FourOhFor() {
+ const router = useRouter()
+
+ return (
+
+
+
Desculpe,
+
+ não foi possível encontrar esta página.
+
+
+
+
+
+ )
+}
From 2b49bc72e40ea28c1e7e07a31d408f744c770d58 Mon Sep 17 00:00:00 2001
From: Emilio Schaedler Heinzmann
<103655828+emiliosheinz@users.noreply.github.com>
Date: Sat, 21 Jan 2023 11:42:57 -0300
Subject: [PATCH 2/8] feat: :sparkles: adds five hundred error page
---
public/images/lost-in-space.svg | 1 +
src/pages/500.tsx | 35 +++++++++++++++++++++++++++++++++
2 files changed, 36 insertions(+)
create mode 100644 public/images/lost-in-space.svg
create mode 100644 src/pages/500.tsx
diff --git a/public/images/lost-in-space.svg b/public/images/lost-in-space.svg
new file mode 100644
index 0000000..6dfc88d
--- /dev/null
+++ b/public/images/lost-in-space.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/pages/500.tsx b/src/pages/500.tsx
new file mode 100644
index 0000000..19367b7
--- /dev/null
+++ b/src/pages/500.tsx
@@ -0,0 +1,35 @@
+import { useRouter } from 'next/router'
+
+import { Button } from '~/components/button'
+import { Image } from '~/components/image'
+import { routes } from '~/utils/navigation'
+
+export default function FiveHundred() {
+ const router = useRouter()
+
+ return (
+
+
+
+ Erro inesperado!
+
+
+ Desculpe, parece que houve um erro inesperado em nosso site.
+
+
+
+
+
+ )
+}
From d3b3c3c4c6f597cca16ec8faa19ba60d405e8951 Mon Sep 17 00:00:00 2001
From: Emilio Schaedler Heinzmann
<103655828+emiliosheinz@users.noreply.github.com>
Date: Sat, 21 Jan 2023 17:41:23 -0300
Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=E2=9C=A8=20adds=20deck=20details?=
=?UTF-8?q?=20screen=20with=20deck=20edit=20and=20dellete=20options?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: :sparkles: adds deck details route
* feat: :sparkles: adds server side logic for deck details screen
* refactor: :recycle: adds not found as error
* feat: :sparkles: adds deck details screen with deck edit and delet options
---
src/components/card/card.component.tsx | 9 +-
src/components/card/card.types.ts | 3 +
.../deck-card-list.component.tsx | 2 +-
.../create-new-deck.context.tsx | 6 +-
src/pages/decks/[id].tsx | 254 ++++++++++++++++++
src/pages/decks/create/[id].tsx | 16 +-
src/server/api/routers/decks/decks.router.ts | 9 +-
src/utils/api/index.ts | 12 +-
src/utils/navigation/index.ts | 1 +
9 files changed, 288 insertions(+), 24 deletions(-)
create mode 100644 src/pages/decks/[id].tsx
diff --git a/src/components/card/card.component.tsx b/src/components/card/card.component.tsx
index f4c79f2..936a194 100644
--- a/src/components/card/card.component.tsx
+++ b/src/components/card/card.component.tsx
@@ -7,7 +7,8 @@ import { classNames } from '~/utils/css'
import type { CardProps } from './card.types'
export function _Card(props: CardProps) {
- const { children, isEditable, onDeletePress, onEditPress, onClick } = props
+ const { children, isEditable, onDeletePress, onEditPress, onClick, as } =
+ props
const renderEditButtons = () => {
if (!isEditable) return null
@@ -24,8 +25,10 @@ export function _Card(props: CardProps) {
)
}
+ const Container = as || 'div'
+
return (
-
{children}
{renderEditButtons()}
-
+
)
}
diff --git a/src/components/card/card.types.ts b/src/components/card/card.types.ts
index b10193c..18b98f1 100644
--- a/src/components/card/card.types.ts
+++ b/src/components/card/card.types.ts
@@ -1,7 +1,10 @@
+import type { ReactTag } from '@headlessui/react/dist/types'
+
export type CardProps = {
children: React.ReactNode
isEditable?: boolean
onDeletePress?: () => void
onEditPress?: () => void
onClick?: () => void
+ as?: ReactTag
}
diff --git a/src/components/deck-card-list/deck-card-list.component.tsx b/src/components/deck-card-list/deck-card-list.component.tsx
index 484073d..985a14f 100644
--- a/src/components/deck-card-list/deck-card-list.component.tsx
+++ b/src/components/deck-card-list/deck-card-list.component.tsx
@@ -12,7 +12,7 @@ export function DeckCardList({ decks }: DeckCardListProps) {
{decks.map((deck, idx) => (
diff --git a/src/contexts/create-new-deck/create-new-deck.context.tsx b/src/contexts/create-new-deck/create-new-deck.context.tsx
index 84f2a4f..ba44fec 100644
--- a/src/contexts/create-new-deck/create-new-deck.context.tsx
+++ b/src/contexts/create-new-deck/create-new-deck.context.tsx
@@ -153,7 +153,7 @@ export function CreateNewDeckContextProvider(
compress(values.image[0] as File),
])
- await createNewDeckMutation.mutateAsync({
+ const deck = await createNewDeckMutation.mutateAsync({
...values,
cards,
topics,
@@ -164,7 +164,7 @@ export function CreateNewDeckContextProvider(
await uploadImage(uploadConfig.uploadUrl, image)
notify.success('Deck criado com sucesso!')
- router.replace(routes.home())
+ router.replace(routes.deckDetails(deck.id))
} catch (error) {
handleApiClientSideError({ error })
} finally {
@@ -225,7 +225,7 @@ export function CreateNewDeckContextProvider(
}
notify.success('Deck editado com sucesso!')
- router.replace(routes.home())
+ router.back()
} catch (error) {
console.log(error)
handleApiClientSideError({ error })
diff --git a/src/pages/decks/[id].tsx b/src/pages/decks/[id].tsx
new file mode 100644
index 0000000..569a233
--- /dev/null
+++ b/src/pages/decks/[id].tsx
@@ -0,0 +1,254 @@
+import { Fragment } from 'react'
+
+import { Menu, Transition } from '@headlessui/react'
+import {
+ PencilSquareIcon,
+ TrashIcon,
+ EllipsisVerticalIcon,
+} from '@heroicons/react/24/outline'
+import { Visibility } from '@prisma/client'
+import { useSetAtom } from 'jotai'
+import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'
+import { type NextPage } from 'next'
+import { useSession } from 'next-auth/react'
+import dynamic from 'next/dynamic'
+import Head from 'next/head'
+import { useRouter } from 'next/router'
+
+import { Card } from '~/components/card'
+import { Image } from '~/components/image'
+import { getServerAuthSession } from '~/server/common/auth'
+import { prisma } from '~/server/common/db'
+import { getS3ImageUrl } from '~/server/common/s3'
+import { api, handleApiClientSideError } from '~/utils/api'
+import { fullScreenLoaderAtom } from '~/utils/atoms'
+import { classNames } from '~/utils/css'
+import { routes } from '~/utils/navigation'
+import { notify } from '~/utils/toast'
+
+const Pill = dynamic(() =>
+ import('~/components/pill').then(module => module.Pill),
+)
+
+async function getDeckFromDatabase(deckId: string) {
+ return await prisma.deck.findFirst({
+ where: { id: deckId },
+ include: {
+ cards: { select: { id: true, question: true } },
+ topics: true,
+ },
+ })
+}
+
+function buildDeckWithS3Image(deck: DeckQueryResult) {
+ return {
+ ...deck,
+ image: getS3ImageUrl(deck.image),
+ }
+}
+
+type DeckQueryResult = NonNullable<
+ Awaited
>
+>
+
+export const getServerSideProps: GetServerSideProps<{
+ deck: DeckQueryResult
+}> = async context => {
+ const deckId = context.params?.id as string
+
+ if (!deckId) return { notFound: true }
+
+ const deck = await getDeckFromDatabase(deckId)
+
+ if (!deck) return { notFound: true }
+
+ /**
+ * If deck is not private anyone can access it
+ */
+ if (deck.visibility !== Visibility.Private) {
+ return {
+ props: {
+ deck: buildDeckWithS3Image(deck),
+ },
+ }
+ }
+
+ const session = await getServerAuthSession(context)
+
+ /**
+ * Verifies if user is signed in and if is the owner of the deck
+ */
+ if (!session?.user) return { notFound: true }
+ if (deck.ownerId !== session.user.id) return { notFound: true }
+
+ return {
+ props: {
+ deck: buildDeckWithS3Image(deck),
+ },
+ }
+}
+
+function ActionsDropDown({
+ className,
+ deckId,
+}: {
+ className?: string
+ deckId: string
+}) {
+ const router = useRouter()
+ const setIsLoading = useSetAtom(fullScreenLoaderAtom)
+ const deleteDeckMutation = api.decks.deleteDeck.useMutation()
+
+ const actions = [
+ {
+ label: 'Editar Deck',
+ icon: PencilSquareIcon,
+ onClick: () => router.push(routes.editDeck(deckId)),
+ },
+ {
+ label: 'Excluir Deck',
+ icon: TrashIcon,
+ onClick: async () => {
+ try {
+ setIsLoading(true)
+
+ await deleteDeckMutation.mutateAsync({ id: deckId })
+
+ notify.success('Deck excluído com sucesso!')
+ router.back()
+ } catch (error) {
+ handleApiClientSideError({ error })
+ } finally {
+ setIsLoading(false)
+ }
+ },
+ },
+ ]
+
+ return (
+
+
+
+ )
+}
+
+const DeckDetails: NextPage<
+ InferGetServerSidePropsType
+> = props => {
+ const { deck } = props
+
+ const { data: session } = useSession()
+
+ const renderTopics = () => {
+ if (deck.topics.length === 0) return null
+
+ return (
+ <>
+ Tópicos:
+
+ {deck.topics.map(topic => (
+ -
+ {topic.title}
+
+ ))}
+
+ >
+ )
+ }
+
+ const renderActionButtons = () => {
+ const isCurrentUserDeckOwner = session?.user?.id === deck.ownerId
+
+ if (!session?.user || !isCurrentUserDeckOwner) return null
+
+ return (
+
+ )
+ }
+
+ return (
+ <>
+
+ {deck.title}
+
+
+
+
+
+
+
+
+
+
+ {deck.title}
+
+
+ {deck.description}
+
+ {renderTopics()}
+
+
+
Cards:
+
+ {deck.cards.map(card => (
+
+ {card.question}
+
+ ))}
+
+ {renderActionButtons()}
+
+ >
+ )
+}
+
+export default DeckDetails
diff --git a/src/pages/decks/create/[id].tsx b/src/pages/decks/create/[id].tsx
index dbeae88..4b31fac 100644
--- a/src/pages/decks/create/[id].tsx
+++ b/src/pages/decks/create/[id].tsx
@@ -25,7 +25,6 @@ import { getServerAuthSession } from '~/server/common/auth'
import { prisma } from '~/server/common/db'
import { getS3ImageUrl } from '~/server/common/s3'
import type { WithAuthentication } from '~/types/auth'
-import { routes } from '~/utils/navigation'
const NEW_DECK_ID = 'new'
@@ -42,12 +41,7 @@ export const getServerSideProps: GetServerSideProps<{
const deckId = context.params?.id as string
if (!deckId) {
- return {
- redirect: {
- destination: '/',
- permanent: false,
- },
- }
+ return { notFound: true }
}
const session = await getServerAuthSession(context)
@@ -183,7 +177,7 @@ const CardsSection = () => {
return (
<>
Cards
-
+
{cards.map((card, idx) => (
{
return (
+ {renderCurrentStudySessionCard()}
Cards:
{deck.cards.map(card => (
diff --git a/src/server/api/root.ts b/src/server/api/root.ts
index d8cf9b9..0c7daa7 100644
--- a/src/server/api/root.ts
+++ b/src/server/api/root.ts
@@ -1,5 +1,6 @@
import { decksRouter } from './routers/decks'
import { filesRouter } from './routers/files'
+import { studySessionRouter } from './routers/study-session'
import { createTRPCRouter } from './trpc'
/**
@@ -10,6 +11,7 @@ import { createTRPCRouter } from './trpc'
export const appRouter = createTRPCRouter({
decks: decksRouter,
files: filesRouter,
+ studySession: studySessionRouter,
})
export type AppRouter = typeof appRouter
diff --git a/src/server/api/routers/study-session/index.ts b/src/server/api/routers/study-session/index.ts
new file mode 100644
index 0000000..e149e8e
--- /dev/null
+++ b/src/server/api/routers/study-session/index.ts
@@ -0,0 +1 @@
+export { studySessionRouter } from './study-session.router'
diff --git a/src/server/api/routers/study-session/study-session.router.ts b/src/server/api/routers/study-session/study-session.router.ts
new file mode 100644
index 0000000..c5164fa
--- /dev/null
+++ b/src/server/api/routers/study-session/study-session.router.ts
@@ -0,0 +1,113 @@
+import { Visibility } from '@prisma/client'
+import { TRPCError } from '@trpc/server'
+import { z } from 'zod'
+
+import { STUDY_SESSION_BOXES } from '~/constants'
+import { createTRPCRouter, protectedProcedure } from '~/server/api/trpc'
+import { addHours } from '~/utils/date-time'
+
+export const studySessionRouter = createTRPCRouter({
+ create: protectedProcedure
+ .input(
+ z.object({
+ deckId: z.string().min(1),
+ }),
+ )
+ .mutation(async ({ input: { deckId }, ctx }) => {
+ const hasStudySession = !!(await ctx.prisma.studySession.findFirst({
+ where: { deckId, userId: ctx.session.user.id },
+ }))
+
+ if (hasStudySession) {
+ throw new TRPCError({
+ code: 'CONFLICT',
+ message: 'Você já tem uma sessão de estudos vinculada a este Deck',
+ })
+ }
+
+ const deck = await ctx.prisma.deck.findFirst({
+ where: { id: deckId },
+ include: { cards: true },
+ })
+
+ if (!deck) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Deck não foi encontrado em nossa base de dados',
+ })
+ }
+
+ const isDeckPrivate = deck.visibility === Visibility.Private
+ const isDeckOwner = deck.ownerId === ctx.session.user.id
+
+ if (isDeckPrivate && !isDeckOwner) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message:
+ 'Para iniciar uma sessão de estudos o deck não deve ser privado ou vocês deve ser o dono do Deck',
+ })
+ }
+
+ // TODO emiliosheinz: Improve endpoint performance by grouping queries
+ const studySession = await ctx.prisma.studySession.create({
+ data: {
+ deckId,
+ userId: ctx.session.user.id,
+ },
+ })
+
+ const [firstStudySessionBox] = await ctx.prisma.$transaction(
+ STUDY_SESSION_BOXES.map(({ reviewGapInHours }) =>
+ ctx.prisma.studySessionBox.create({
+ data: {
+ reviewGapInHours,
+ studySessionId: studySession.id,
+ },
+ }),
+ ),
+ )
+
+ await ctx.prisma.studySessionBoxCard.createMany({
+ data: deck.cards.map(({ id }) => ({
+ cardId: id,
+ studySessionBoxId: firstStudySessionBox!.id,
+ })),
+ })
+ }),
+ getLastAndNextReviewDates: protectedProcedure
+ .input(
+ z.object({
+ deckId: z.string().min(1),
+ }),
+ )
+ .query(async ({ input: { deckId }, ctx }) => {
+ await new Promise(r => setTimeout(r, 3000))
+ const studySession = await ctx.prisma.studySession.findFirst({
+ where: { deckId: deckId },
+ })
+
+ if (!studySession) return null
+
+ const studySessionBoxes = await ctx.prisma.studySessionBox.findMany({
+ where: { studySessionId: studySession.id },
+ orderBy: { lastReview: 'desc' },
+ })
+
+ let nextReviewDateTime
+
+ for (const box of studySessionBoxes) {
+ const currentReviewDate = addHours(box.lastReview, box.reviewGapInHours)
+
+ if (!nextReviewDateTime) {
+ nextReviewDateTime = currentReviewDate
+ } else if (currentReviewDate < nextReviewDateTime) {
+ nextReviewDateTime = currentReviewDate
+ }
+ }
+
+ return {
+ nextReviewDateTime,
+ lastReviewDateTime: studySessionBoxes[0]?.lastReview,
+ }
+ }),
+})
diff --git a/src/utils/date-time/index.ts b/src/utils/date-time/index.ts
new file mode 100644
index 0000000..cf83103
--- /dev/null
+++ b/src/utils/date-time/index.ts
@@ -0,0 +1,17 @@
+export function formatToDayMonthYearWithHourAndSeconds(dateTime?: Date) {
+ if (!dateTime) return 'Data Inválida'
+
+ return dateTime.toLocaleString('pt-BR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+}
+
+export function addHours(dateTime: Date, hours: number) {
+ const copiedDate = new Date(dateTime.getTime())
+ copiedDate.setTime(copiedDate.getTime() + hours * 60 * 60 * 1000)
+ return copiedDate
+}
From 1afa25fd34947ad479e5f872e8c14309954ae74c Mon Sep 17 00:00:00 2001
From: Emilio Schaedler Heinzmann
Date: Sun, 22 Jan 2023 19:10:07 -0300
Subject: [PATCH 6/8] refactor: :recycle: adds modules forlder with page
specific components
---
.../decks/actions-drop-down.component.tsx | 136 ++++++++++++
src/modules/decks/create/cards.component.tsx | 71 ++++++
.../decks/create/main-info.component.tsx | 41 ++++
.../decks/create/submit-buttons.component.tsx | 16 ++
src/modules/decks/create/topics.component.tsx | 38 ++++
.../decks/create/visibility.component.tsx | 16 ++
.../decks/study-session-card.component.tsx | 53 +++++
src/pages/decks/[id].tsx | 196 ++---------------
src/pages/decks/create/[id].tsx | 208 +++---------------
9 files changed, 416 insertions(+), 359 deletions(-)
create mode 100644 src/modules/decks/actions-drop-down.component.tsx
create mode 100644 src/modules/decks/create/cards.component.tsx
create mode 100644 src/modules/decks/create/main-info.component.tsx
create mode 100644 src/modules/decks/create/submit-buttons.component.tsx
create mode 100644 src/modules/decks/create/topics.component.tsx
create mode 100644 src/modules/decks/create/visibility.component.tsx
create mode 100644 src/modules/decks/study-session-card.component.tsx
diff --git a/src/modules/decks/actions-drop-down.component.tsx b/src/modules/decks/actions-drop-down.component.tsx
new file mode 100644
index 0000000..91192f2
--- /dev/null
+++ b/src/modules/decks/actions-drop-down.component.tsx
@@ -0,0 +1,136 @@
+import { Fragment } from 'react'
+
+import { Menu, Transition } from '@headlessui/react'
+import {
+ EllipsisVerticalIcon,
+ PencilSquareIcon,
+ RectangleStackIcon,
+ TrashIcon,
+} from '@heroicons/react/24/outline'
+import { useSetAtom } from 'jotai'
+import { useSession } from 'next-auth/react'
+import { useRouter } from 'next/router'
+
+import { api, handleApiClientSideError } from '~/utils/api'
+import { fullScreenLoaderAtom } from '~/utils/atoms'
+import { classNames } from '~/utils/css'
+import { routes } from '~/utils/navigation'
+import { notify } from '~/utils/toast'
+
+type ActionsDropDownProps = {
+ deckOwnerId: string
+ className?: string
+ deckId: string
+}
+
+export const ActionsDropDown = (props: ActionsDropDownProps) => {
+ const { deckOwnerId, className, deckId } = props
+
+ const router = useRouter()
+ const setIsLoading = useSetAtom(fullScreenLoaderAtom)
+
+ const deleteDeckMutation = api.decks.deleteDeck.useMutation()
+ const createStudySessionMutation = api.studySession.create.useMutation()
+
+ const { data: session } = useSession()
+ const isAuthenticated = !!session?.user
+ const isUserDeckOwner = session?.user?.id === deckOwnerId
+
+ const actions = [
+ {
+ label: 'Editar Deck',
+ icon: PencilSquareIcon,
+ isEnabled: isAuthenticated && isUserDeckOwner,
+ onClick: () => router.push(routes.editDeck(deckId)),
+ },
+ {
+ label: 'Excluir Deck',
+ icon: TrashIcon,
+ isEnabled: isAuthenticated && isUserDeckOwner,
+ onClick: async () => {
+ try {
+ setIsLoading(true)
+
+ await deleteDeckMutation.mutateAsync({ id: deckId })
+
+ notify.success('Deck excluído com sucesso!')
+ router.back()
+ } catch (error) {
+ handleApiClientSideError({ error })
+ } finally {
+ setIsLoading(false)
+ }
+ },
+ },
+ {
+ label: 'Estudar com este Deck',
+ icon: RectangleStackIcon,
+ isEnabled: isAuthenticated,
+ onClick: async () => {
+ try {
+ setIsLoading(true)
+
+ await createStudySessionMutation.mutateAsync({ deckId })
+
+ notify.success('Sessão de estudo criada com sucesso!')
+ router.back()
+ } catch (error) {
+ handleApiClientSideError({ error })
+ } finally {
+ setIsLoading(false)
+ }
+ },
+ },
+ ]
+
+ const hasEnabledActions = actions.some(({ isEnabled }) => isEnabled)
+
+ if (!hasEnabledActions) return null
+
+ return (
+
+
+
+ )
+}
diff --git a/src/modules/decks/create/cards.component.tsx b/src/modules/decks/create/cards.component.tsx
new file mode 100644
index 0000000..213f872
--- /dev/null
+++ b/src/modules/decks/create/cards.component.tsx
@@ -0,0 +1,71 @@
+import { useState } from 'react'
+
+import { PlusCircleIcon } from '@heroicons/react/24/outline'
+
+import { Card } from '~/components/card'
+import { NewCardModal } from '~/components/modal/new-card/new-card-modal.component'
+import type { CardFormInputValues } from '~/components/modal/new-card/new-card-modal.types'
+import { useCreateNewDeckContext } from '~/contexts/create-new-deck'
+
+type NewCardModalState = {
+ isOpen: boolean
+ cardIdx?: number
+}
+
+export const Cards = () => {
+ const { cards, addCard, deleteCard, editCard } = useCreateNewDeckContext()
+
+ const [newCardModalState, setNewCardModalState] = useState(
+ { isOpen: false },
+ )
+
+ const isCreatingNewCard = newCardModalState.cardIdx === undefined
+
+ const modalFieldsValues = isCreatingNewCard
+ ? { answer: '', question: '' }
+ : cards[newCardModalState.cardIdx!]
+
+ const handleNewCardFormSubmit = (values: CardFormInputValues) => {
+ if (isCreatingNewCard) {
+ addCard(values)
+ } else {
+ editCard(newCardModalState.cardIdx!, values)
+ }
+ }
+
+ const closeModal = (isOpen: boolean) => {
+ setNewCardModalState(state => ({
+ ...state,
+ isOpen,
+ }))
+ }
+
+ return (
+ <>
+ Cards
+
+ {cards.map((card, idx) => (
+
deleteCard(idx)}
+ onEditPress={() => {
+ setNewCardModalState({ isOpen: true, cardIdx: idx })
+ }}
+ >
+ {card.question}
+
+ ))}
+
setNewCardModalState({ isOpen: true })}>
+
+
+
+
+ >
+ )
+}
diff --git a/src/modules/decks/create/main-info.component.tsx b/src/modules/decks/create/main-info.component.tsx
new file mode 100644
index 0000000..c23b45c
--- /dev/null
+++ b/src/modules/decks/create/main-info.component.tsx
@@ -0,0 +1,41 @@
+import { ImageUploader } from '~/components/image-uploader'
+import { Input } from '~/components/input'
+import { TextArea } from '~/components/text-area'
+import { useCreateNewDeckContext } from '~/contexts/create-new-deck'
+
+export const MainInfo = () => {
+ const { createNewDeckForm } = useCreateNewDeckContext()
+
+ const { formState, register } = createNewDeckForm ?? {}
+
+ return (
+ <>
+ Criar Deck
+
+ >
+ )
+}
diff --git a/src/modules/decks/create/submit-buttons.component.tsx b/src/modules/decks/create/submit-buttons.component.tsx
new file mode 100644
index 0000000..e9d8a3e
--- /dev/null
+++ b/src/modules/decks/create/submit-buttons.component.tsx
@@ -0,0 +1,16 @@
+import { useRouter } from 'next/router'
+
+import { Button } from '~/components/button'
+
+export const SubmitButtons = () => {
+ const router = useRouter()
+
+ return (
+
+ )
+}
diff --git a/src/modules/decks/create/topics.component.tsx b/src/modules/decks/create/topics.component.tsx
new file mode 100644
index 0000000..7039334
--- /dev/null
+++ b/src/modules/decks/create/topics.component.tsx
@@ -0,0 +1,38 @@
+import { useState } from 'react'
+
+import { PlusCircleIcon } from '@heroicons/react/24/outline'
+
+import { NewTopicModal } from '~/components/modal/new-topic/new-topic-modal.component'
+import { Pill } from '~/components/pill'
+import { MAX_TOPICS_PER_DECK } from '~/constants'
+import { useCreateNewDeckContext } from '~/contexts/create-new-deck'
+
+export const Topics = () => {
+ const { topics, addTopic, deleteTopic } = useCreateNewDeckContext()
+
+ const [isCreatingTopic, setIsCreatingTopic] = useState(false)
+
+ return (
+ <>
+ Tópicos
+
+ {topics.map(({ title: topic }, idx) => (
+
deleteTopic(idx)}>
+ {topic}
+
+ ))}
+
= MAX_TOPICS_PER_DECK}
+ onClick={() => setIsCreatingTopic(true)}
+ >
+
+
+
+ addTopic(values.title)}
+ />
+ >
+ )
+}
diff --git a/src/modules/decks/create/visibility.component.tsx b/src/modules/decks/create/visibility.component.tsx
new file mode 100644
index 0000000..4dee6dd
--- /dev/null
+++ b/src/modules/decks/create/visibility.component.tsx
@@ -0,0 +1,16 @@
+import { RadioGroup } from '~/components/radio-group'
+import { useCreateNewDeckContext } from '~/contexts/create-new-deck'
+
+export const Visibility = () => {
+ const { setVisibility, visibility, visibilityOptions } =
+ useCreateNewDeckContext()
+
+ return (
+
+ )
+}
diff --git a/src/modules/decks/study-session-card.component.tsx b/src/modules/decks/study-session-card.component.tsx
new file mode 100644
index 0000000..6a706a2
--- /dev/null
+++ b/src/modules/decks/study-session-card.component.tsx
@@ -0,0 +1,53 @@
+import { RectangleStackIcon } from '@heroicons/react/24/outline'
+
+import { Button } from '~/components/button'
+import { api } from '~/utils/api'
+import { formatToDayMonthYearWithHourAndSeconds } from '~/utils/date-time'
+
+export const StudySessionCard = (props: { deckId: string }) => {
+ const { deckId } = props
+
+ const { data, isLoading } =
+ api.studySession.getLastAndNextReviewDates.useQuery({ deckId })
+
+ if (isLoading) {
+ return (
+
+ Loading...
+
+ )
+ }
+
+ if (!data) return null
+
+ const { lastReviewDateTime, nextReviewDateTime } = data
+
+ return (
+
+
+
+
+
Sessão de estudos atual
+
+ {`Última revisão: ${formatToDayMonthYearWithHourAndSeconds(
+ lastReviewDateTime,
+ )}`}
+
+
+ {`Próxima revisão: ${formatToDayMonthYearWithHourAndSeconds(
+ nextReviewDateTime,
+ )}`}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/pages/decks/[id].tsx b/src/pages/decks/[id].tsx
index 741b22b..41e6678 100644
--- a/src/pages/decks/[id].tsx
+++ b/src/pages/decks/[id].tsx
@@ -1,38 +1,34 @@
import { Fragment } from 'react'
-import { Menu, Transition } from '@headlessui/react'
-import {
- PencilSquareIcon,
- TrashIcon,
- EllipsisVerticalIcon,
- RectangleStackIcon,
-} from '@heroicons/react/24/outline'
import { Visibility } from '@prisma/client'
-import { useSetAtom } from 'jotai'
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'
import { type NextPage } from 'next'
import { useSession } from 'next-auth/react'
import dynamic from 'next/dynamic'
import Head from 'next/head'
-import { useRouter } from 'next/router'
-import { Button } from '~/components/button'
import { Card } from '~/components/card'
import { Image } from '~/components/image'
import { getServerAuthSession } from '~/server/common/auth'
import { prisma } from '~/server/common/db'
import { getS3ImageUrl } from '~/server/common/s3'
-import { api, handleApiClientSideError } from '~/utils/api'
-import { fullScreenLoaderAtom } from '~/utils/atoms'
-import { classNames } from '~/utils/css'
-import { formatToDayMonthYearWithHourAndSeconds } from '~/utils/date-time'
-import { routes } from '~/utils/navigation'
-import { notify } from '~/utils/toast'
const Pill = dynamic(() =>
import('~/components/pill').then(module => module.Pill),
)
+const ActionsDropDown = dynamic(() =>
+ import('~/modules/decks/actions-drop-down.component').then(
+ module => module.ActionsDropDown,
+ ),
+)
+
+const StudySessionCard = dynamic(() =>
+ import('~/modules/decks/study-session-card.component').then(
+ module => module.StudySessionCard,
+ ),
+)
+
async function getDeckFromDatabase(deckId: string) {
return await prisma.deck.findFirst({
where: { id: deckId },
@@ -91,158 +87,6 @@ export const getServerSideProps: GetServerSideProps<{
}
}
-function ActionsDropDown({
- className,
- deckId,
-}: {
- className?: string
- deckId: string
-}) {
- const router = useRouter()
- const setIsLoading = useSetAtom(fullScreenLoaderAtom)
- const deleteDeckMutation = api.decks.deleteDeck.useMutation()
- const createStudySessionMutation = api.studySession.create.useMutation()
-
- const actions = [
- {
- label: 'Editar Deck',
- icon: PencilSquareIcon,
- onClick: () => router.push(routes.editDeck(deckId)),
- },
- {
- label: 'Excluir Deck',
- icon: TrashIcon,
- onClick: async () => {
- try {
- setIsLoading(true)
-
- await deleteDeckMutation.mutateAsync({ id: deckId })
-
- notify.success('Deck excluído com sucesso!')
- router.back()
- } catch (error) {
- handleApiClientSideError({ error })
- } finally {
- setIsLoading(false)
- }
- },
- },
- {
- label: 'Estudar com este Deck',
- icon: RectangleStackIcon,
- onClick: async () => {
- try {
- setIsLoading(true)
-
- await createStudySessionMutation.mutateAsync({ deckId })
-
- notify.success('Sessão de estudo criada com sucesso!')
- router.back()
- } catch (error) {
- handleApiClientSideError({ error })
- } finally {
- setIsLoading(false)
- }
- },
- },
- ]
-
- return (
-
-
-
- )
-}
-
-const StudySessionCard = (props: { deckId: string }) => {
- const { deckId } = props
-
- const { data, isLoading } =
- api.studySession.getLastAndNextReviewDates.useQuery({ deckId })
-
- if (isLoading) {
- return (
-
- Loading...
-
- )
- }
-
- if (!data) return null
-
- const { lastReviewDateTime, nextReviewDateTime } = data
-
- return (
-
-
-
-
-
Sessão de estudos atual
-
- {`Última revisão: ${formatToDayMonthYearWithHourAndSeconds(
- lastReviewDateTime,
- )}`}
-
-
- {`Próxima revisão: ${formatToDayMonthYearWithHourAndSeconds(
- nextReviewDateTime,
- )}`}
-
-
-
-
-
-
-
-
-
-
- )
-}
-
const DeckDetails: NextPage<
InferGetServerSidePropsType
> = props => {
@@ -274,16 +118,6 @@ const DeckDetails: NextPage<
return
}
- const renderActionButtons = () => {
- const isCurrentUserDeckOwner = session?.user?.id === deck.ownerId
-
- if (!isAuthenticated || !isCurrentUserDeckOwner) return null
-
- return (
-
- )
- }
-
return (
<>
@@ -323,7 +157,11 @@ const DeckDetails: NextPage<
))}
- {renderActionButtons()}
+
>
)
diff --git a/src/pages/decks/create/[id].tsx b/src/pages/decks/create/[id].tsx
index 4b31fac..fc8572f 100644
--- a/src/pages/decks/create/[id].tsx
+++ b/src/pages/decks/create/[id].tsx
@@ -1,21 +1,8 @@
-import { useState } from 'react'
-
-import { PlusCircleIcon } from '@heroicons/react/24/outline'
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'
import { type NextPage } from 'next'
import dynamic from 'next/dynamic'
import Head from 'next/head'
-import { useRouter } from 'next/router'
-import { Button } from '~/components/button'
-import { Card } from '~/components/card'
-import { ImageUploader } from '~/components/image-uploader'
-import { Input } from '~/components/input'
-import type { CardFormInputValues } from '~/components/modal/new-card/new-card-modal.types'
-import { Pill } from '~/components/pill'
-import { RadioGroup } from '~/components/radio-group'
-import { TextArea } from '~/components/text-area'
-import { MAX_TOPICS_PER_DECK } from '~/constants'
import type { DeckWithCardsAndTopics } from '~/contexts/create-new-deck'
import {
CreateNewDeckContextProvider,
@@ -26,14 +13,32 @@ import { prisma } from '~/server/common/db'
import { getS3ImageUrl } from '~/server/common/s3'
import type { WithAuthentication } from '~/types/auth'
-const NEW_DECK_ID = 'new'
+const Cards = dynamic(() =>
+ import('~/modules/decks/create/cards.component').then(module => module.Cards),
+)
-const NewCardModal = dynamic(() =>
- import('~/components/modal').then(m => m.Modal.NewCard),
+const MainInfo = dynamic(() =>
+ import('~/modules/decks/create/main-info.component').then(
+ module => module.MainInfo,
+ ),
+)
+const SubmitButtons = dynamic(() =>
+ import('~/modules/decks/create/submit-buttons.component').then(
+ module => module.SubmitButtons,
+ ),
)
-const NewTopicModal = dynamic(() =>
- import('~/components/modal').then(m => m.Modal.NewTopic),
+const Topics = dynamic(() =>
+ import('~/modules/decks/create/topics.component').then(
+ module => module.Topics,
+ ),
)
+const Visibility = dynamic(() =>
+ import('~/modules/decks/create/visibility.component').then(
+ module => module.Visibility,
+ ),
+)
+
+const NEW_DECK_ID = 'new'
export const getServerSideProps: GetServerSideProps<{
deck?: DeckWithCardsAndTopics | null
@@ -74,163 +79,6 @@ export const getServerSideProps: GetServerSideProps<{
}
}
-const MainInfoSection = () => {
- const { createNewDeckForm } = useCreateNewDeckContext()
-
- const { formState, register } = createNewDeckForm ?? {}
-
- return (
- <>
- Criar Deck
-
- >
- )
-}
-
-const TopicsSection = () => {
- const { topics, addTopic, deleteTopic } = useCreateNewDeckContext()
-
- const [isCreatingTopic, setIsCreatingTopic] = useState(false)
-
- return (
- <>
- Tópicos
-
- {topics.map(({ title: topic }, idx) => (
-
deleteTopic(idx)}>
- {topic}
-
- ))}
-
= MAX_TOPICS_PER_DECK}
- onClick={() => setIsCreatingTopic(true)}
- >
-
-
-
- addTopic(values.title)}
- />
- >
- )
-}
-
-type NewCardModalState = {
- isOpen: boolean
- cardIdx?: number
-}
-
-const CardsSection = () => {
- const { cards, addCard, deleteCard, editCard } = useCreateNewDeckContext()
-
- const [newCardModalState, setNewCardModalState] = useState(
- { isOpen: false },
- )
-
- const isCreatingNewCard = newCardModalState.cardIdx === undefined
-
- const modalFieldsValues = isCreatingNewCard
- ? { answer: '', question: '' }
- : cards[newCardModalState.cardIdx!]
-
- const handleNewCardFormSubmit = (values: CardFormInputValues) => {
- if (isCreatingNewCard) {
- addCard(values)
- } else {
- editCard(newCardModalState.cardIdx!, values)
- }
- }
-
- const closeModal = (isOpen: boolean) => {
- setNewCardModalState(state => ({
- ...state,
- isOpen,
- }))
- }
-
- return (
- <>
- Cards
-
- {cards.map((card, idx) => (
-
deleteCard(idx)}
- onEditPress={() => {
- setNewCardModalState({ isOpen: true, cardIdx: idx })
- }}
- >
- {card.question}
-
- ))}
-
setNewCardModalState({ isOpen: true })}>
-
-
-
-
- >
- )
-}
-
-const VisibilitySection = () => {
- const { setVisibility, visibility, visibilityOptions } =
- useCreateNewDeckContext()
-
- return (
-
- )
-}
-
-const SubmitButtonsSection = () => {
- const router = useRouter()
-
- return (
-
- )
-}
-
const DecksCrudContent = () => {
const { createNewDeckForm, submitDeck } = useCreateNewDeckContext()
@@ -247,11 +95,11 @@ const DecksCrudContent = () => {
>
)
From b39c01582c13c08f9abe6f64a70b37f64409b47e Mon Sep 17 00:00:00 2001
From: Emilio Schaedler Heinzmann
Date: Sun, 22 Jan 2023 19:12:50 -0300
Subject: [PATCH 7/8] fix: :bug: removes sleep from query
---
src/server/api/routers/study-session/study-session.router.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/server/api/routers/study-session/study-session.router.ts b/src/server/api/routers/study-session/study-session.router.ts
index c5164fa..ab2a728 100644
--- a/src/server/api/routers/study-session/study-session.router.ts
+++ b/src/server/api/routers/study-session/study-session.router.ts
@@ -81,7 +81,6 @@ export const studySessionRouter = createTRPCRouter({
}),
)
.query(async ({ input: { deckId }, ctx }) => {
- await new Promise(r => setTimeout(r, 3000))
const studySession = await ctx.prisma.studySession.findFirst({
where: { deckId: deckId },
})
From c7e9a4efd860adcb27c581f69233d3f70d1e35d9 Mon Sep 17 00:00:00 2001
From: Emilio Schaedler Heinzmann
Date: Sun, 22 Jan 2023 19:23:14 -0300
Subject: [PATCH 8/8] fix: :bug: fixes user specific features
---
src/modules/decks/actions-drop-down.component.tsx | 6 ++----
.../api/routers/study-session/study-session.router.ts | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/modules/decks/actions-drop-down.component.tsx b/src/modules/decks/actions-drop-down.component.tsx
index 91192f2..4c776a0 100644
--- a/src/modules/decks/actions-drop-down.component.tsx
+++ b/src/modules/decks/actions-drop-down.component.tsx
@@ -81,11 +81,9 @@ export const ActionsDropDown = (props: ActionsDropDownProps) => {
}
},
},
- ]
+ ].filter(({ isEnabled }) => isEnabled)
- const hasEnabledActions = actions.some(({ isEnabled }) => isEnabled)
-
- if (!hasEnabledActions) return null
+ if (actions.length === 0) return null
return (
diff --git a/src/server/api/routers/study-session/study-session.router.ts b/src/server/api/routers/study-session/study-session.router.ts
index ab2a728..c5621f1 100644
--- a/src/server/api/routers/study-session/study-session.router.ts
+++ b/src/server/api/routers/study-session/study-session.router.ts
@@ -82,7 +82,7 @@ export const studySessionRouter = createTRPCRouter({
)
.query(async ({ input: { deckId }, ctx }) => {
const studySession = await ctx.prisma.studySession.findFirst({
- where: { deckId: deckId },
+ where: { deckId: deckId, deck: { ownerId: ctx.session.user.id } },
})
if (!studySession) return null