From de11c0dc96325bd4ae7181ed979c38251cd69271 Mon Sep 17 00:00:00 2001 From: Timo Clasen Date: Sat, 7 Oct 2023 20:51:39 +0200 Subject: [PATCH] Add Collections Backend (#243) * Add Display Collections PoC * Add createCollection Function * Add Collections to Cache * Dialog PoC * Add More Collection Dialogs * Fix Build * Add Delete Collection Button * Update PoC * Update PoC * Update Next.js Redirect Handling * Update Create Server Action Handling Add server action clients that can have different middleware. * Fix Error Handling in Client Hook * Update Types * Add Util Functions * Remove Public References * Update Dependencies * Use Planetscale Serverless --- apps/resources/app/(home)/page.tsx | 16 +- apps/resources/app/collections/[id]/page.tsx | 76 + apps/resources/app/collections/page.tsx | 46 + apps/resources/app/imprint/page.tsx | 29 +- apps/resources/app/privacy/page.tsx | 28 +- .../app/profile/DeleteAccountButton.tsx | 2 +- apps/resources/app/profile/actions.ts | 8 +- apps/resources/app/profile/page.tsx | 58 +- .../app/resources/Resources/Resources.tsx | 24 +- .../ResourcesList/ResourcesList.tsx | 2 +- .../ResourcesTable/ResourcesTable.tsx | 2 +- .../resources/Suggestion/SuggestionForm.tsx | 2 +- .../app/resources/Suggestion/actions.ts | 2 +- apps/resources/app/resources/[slug]/page.tsx | 9 +- apps/resources/app/resources/page.tsx | 29 +- .../app/sign-in/[[...sign-in]]/page.tsx | 66 +- .../app/sign-up/[[...sign-up]]/page.tsx | 82 +- apps/resources/components/Card/Card.tsx | 11 + .../components/Card/CollectionButton.tsx | 36 + .../Card/LikesButton/LikesButtonClient.tsx | 2 +- apps/resources/components/Card/actions.ts | 13 +- .../Collections/AddCollectionButton.tsx | 18 + .../AddCollectionDialog.tsx | 22 + .../AddCollectionDialog/AddCollectionForm.tsx | 99 ++ .../AddCollectionDialog/actions.ts | 24 + .../AddCollectionDialog/schemas.ts | 21 + .../Collections/AddToCollectionButton.tsx | 27 + .../AddToCollectionDialog.tsx | 70 + .../AddToCollectionItem.tsx | 91 + .../AddToCollectionDialog/actions.ts | 66 + .../Collections/AddToThisCollectionButton.tsx | 34 + .../DeleteCollectionButton.tsx | 31 + .../DeleteCollectionButton/actions.ts | 32 + .../Collections/UpdateBollectionButton.tsx | 24 + .../components/Comments/AddCommentForm.tsx | 4 +- .../Comments/DeleteCommentButton.tsx | 2 +- apps/resources/components/Comments/actions.ts | 14 +- .../Header/Navigation/Navigation.tsx | 3 + .../components/Newsletter/NewsletterForm.tsx | 2 +- .../components/Newsletter/actions.ts | 4 +- apps/resources/components/Page/Page.tsx | 17 + .../RefreshOnPolling/RefreshOnPolling.tsx | 22 + .../components/ServerDialog/ServerDialog.tsx | 38 + .../ServerDialog/ServerDialogRoot.tsx | 37 + .../ServerDialog/useServerDialog.ts | 53 + .../UpdateCollectionDialog.tsx | 40 + .../UpdateCollectionForm.tsx | 123 ++ .../UpdateCollectionDialog/actions.ts | 40 + apps/resources/components/utils.tsx | 14 +- apps/resources/lib/actions/createAction.ts | 80 - apps/resources/lib/cache.ts | 42 + apps/resources/lib/collections.ts | 137 ++ apps/resources/lib/prisma.ts | 13 +- apps/resources/lib/resources.ts | 4 +- .../useAction.ts => serverActions/client.ts} | 29 +- apps/resources/lib/serverActions/create.ts | 18 + apps/resources/lib/serverActions/server.ts | 90 + apps/resources/lib/types.ts | 1 + apps/resources/lib/users.ts | 31 +- apps/resources/lib/utils.ts | 6 + apps/resources/package.json | 38 +- package.json | 6 +- packages/database/package.json | 7 +- packages/database/prisma/schema.prisma | 69 +- packages/design-system/package.json | 14 +- packages/eslint-config-custom/package.json | 4 +- packages/tailwind-config/package.json | 4 +- pnpm-lock.yaml | 1507 ++++++++++------- 68 files changed, 2636 insertions(+), 979 deletions(-) create mode 100644 apps/resources/app/collections/[id]/page.tsx create mode 100644 apps/resources/app/collections/page.tsx create mode 100644 apps/resources/components/Card/CollectionButton.tsx create mode 100644 apps/resources/components/Collections/AddCollectionButton.tsx create mode 100644 apps/resources/components/Collections/AddCollectionDialog/AddCollectionDialog.tsx create mode 100644 apps/resources/components/Collections/AddCollectionDialog/AddCollectionForm.tsx create mode 100644 apps/resources/components/Collections/AddCollectionDialog/actions.ts create mode 100644 apps/resources/components/Collections/AddCollectionDialog/schemas.ts create mode 100644 apps/resources/components/Collections/AddToCollectionButton.tsx create mode 100644 apps/resources/components/Collections/AddToCollectionDialog/AddToCollectionDialog.tsx create mode 100644 apps/resources/components/Collections/AddToCollectionDialog/AddToCollectionItem.tsx create mode 100644 apps/resources/components/Collections/AddToCollectionDialog/actions.ts create mode 100644 apps/resources/components/Collections/AddToThisCollectionButton.tsx create mode 100644 apps/resources/components/Collections/DeleteCollectionButton/DeleteCollectionButton.tsx create mode 100644 apps/resources/components/Collections/DeleteCollectionButton/actions.ts create mode 100644 apps/resources/components/Collections/UpdateBollectionButton.tsx create mode 100644 apps/resources/components/Page/Page.tsx create mode 100644 apps/resources/components/RefreshOnPolling/RefreshOnPolling.tsx create mode 100644 apps/resources/components/ServerDialog/ServerDialog.tsx create mode 100644 apps/resources/components/ServerDialog/ServerDialogRoot.tsx create mode 100644 apps/resources/components/ServerDialog/useServerDialog.ts create mode 100644 apps/resources/components/UpdateCollectionDialog/UpdateCollectionDialog.tsx create mode 100644 apps/resources/components/UpdateCollectionDialog/UpdateCollectionForm.tsx create mode 100644 apps/resources/components/UpdateCollectionDialog/actions.ts delete mode 100644 apps/resources/lib/actions/createAction.ts create mode 100644 apps/resources/lib/collections.ts rename apps/resources/lib/{actions/useAction.ts => serverActions/client.ts} (77%) create mode 100644 apps/resources/lib/serverActions/create.ts create mode 100644 apps/resources/lib/serverActions/server.ts create mode 100644 apps/resources/lib/types.ts diff --git a/apps/resources/app/(home)/page.tsx b/apps/resources/app/(home)/page.tsx index eb52664e..e7f60129 100644 --- a/apps/resources/app/(home)/page.tsx +++ b/apps/resources/app/(home)/page.tsx @@ -1,16 +1,22 @@ -import { About } from './About/About'; -import { Header } from './Header/Header'; +import { Page } from 'components/Page/Page'; +import { SearchParams } from 'lib/types'; import { NewResources } from '../../components/NewResources/NewResources'; import { Newsletter } from '../../components/Newsletter/Newsletter'; +import { About } from './About/About'; +import { Header } from './Header/Header'; + +interface Props { + searchParams: SearchParams; +} -const Home = () => { +const Home = ({ searchParams }: Props) => { return ( - <> +
- + ); }; diff --git a/apps/resources/app/collections/[id]/page.tsx b/apps/resources/app/collections/[id]/page.tsx new file mode 100644 index 00000000..2ea7d057 --- /dev/null +++ b/apps/resources/app/collections/[id]/page.tsx @@ -0,0 +1,76 @@ +import { auth } from '@clerk/nextjs'; +import { Resources } from 'app/resources/Resources/Resources'; +import { Await } from 'components/Await/Await'; +import { Page } from 'components/Page/Page'; +import { Heading, Text } from 'design-system'; +import { getCollectionCached } from 'lib/cache'; +import { SearchParams } from 'lib/types'; +import { DeleteCollectionButton } from '../../../components/Collections/DeleteCollectionButton/DeleteCollectionButton'; +import { UpdateBollectionButton } from '../../../components/Collections/UpdateBollectionButton'; + +interface Props { + params: { + id: string; + }; + searchParams: SearchParams; +} + +const CollectionPage = async ({ params, searchParams }: Props) => { + const { id } = params; + const promise = getCollectionCached(Number(id)); + const { userId } = auth(); + + return ( + +
+ Collection Page + {userId && ( +
+ + +
+ )} +
+ + {(collection) => { + if (!collection) return
No collection found
; + const isOwnCollection = collection.user?.id === userId; + + return ( +
+
+ Title: {collection.title} + Description: {collection.description} + Username: {collection.user?.username ?? 'anonymos'} +
+ {collection.resources.length === 0 ? ( +
No resources in collection
+ ) : ( +
+ Resources: +
    + {collection.resources.map((resource) => { + return ( + <> +
  • + {'title' in resource + ? resource.title + : resource.name} +
  • +
    ----------------------------------
    + + ); + })} +
+
+ )} + {isOwnCollection && } +
+ ); + }} +
+
+ ); +}; + +export default CollectionPage; diff --git a/apps/resources/app/collections/page.tsx b/apps/resources/app/collections/page.tsx new file mode 100644 index 00000000..e0d66b44 --- /dev/null +++ b/apps/resources/app/collections/page.tsx @@ -0,0 +1,46 @@ +import { auth } from '@clerk/nextjs'; +import { Await } from 'components/Await/Await'; +import { Page } from 'components/Page/Page'; +import { Heading, Text } from 'design-system'; +import { getCollectionsCached } from 'lib/cache'; +import { SearchParams } from 'lib/types'; +import Link from 'next/link'; +import { AddCollectionButton } from '../../components/Collections/AddCollectionButton'; + +interface Props { + searchParams: SearchParams; +} + +const CollectionsPage = async ({ searchParams }: Props) => { + const promise = getCollectionsCached(); + const { userId } = auth(); + return ( + + Collections + {userId && } + + {(collections) => { + return ( +
    + {collections.map(({ id, title, description, user }) => ( +
  • + + Title: {title} + Description: {description} + Username: {user?.username ?? 'anonymos'} + +
    ----------------------------------
    +
  • + ))} +
+ ); + }} +
+
+ ); +}; + +export default CollectionsPage; diff --git a/apps/resources/app/imprint/page.tsx b/apps/resources/app/imprint/page.tsx index 400ccfcd..ea7e1bc1 100644 --- a/apps/resources/app/imprint/page.tsx +++ b/apps/resources/app/imprint/page.tsx @@ -1,23 +1,32 @@ +import { Page } from 'components/Page/Page'; import { allPages } from 'contentlayer/generated'; import { Heading } from 'design-system'; +import { SearchParams } from 'lib/types'; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Imprint', }; -const ImprintPage = () => { +interface Props { + searchParams: SearchParams; +} + +const ImprintPage = ({ searchParams }: Props) => { const content = allPages.find((page) => page.title === 'Imprint'); return ( -
- - {content?.title} - -
-
+ +
+ + {content?.title} + +
+
+
); }; + export default ImprintPage; diff --git a/apps/resources/app/privacy/page.tsx b/apps/resources/app/privacy/page.tsx index 64e8f2ad..1f79f9bf 100644 --- a/apps/resources/app/privacy/page.tsx +++ b/apps/resources/app/privacy/page.tsx @@ -1,23 +1,31 @@ +import { Page } from 'components/Page/Page'; import { allPages } from 'contentlayer/generated'; import { Heading } from 'design-system'; +import { SearchParams } from 'lib/types'; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Privacy Policy', }; -const PrivacyPage = () => { +interface Props { + searchParams: SearchParams; +} + +const PrivacyPage = ({ searchParams }: Props) => { const content = allPages.find((page) => page.title === 'Privacy'); return ( -
- - {content?.title} - -
-
+ +
+ + {content?.title} + +
+
+
); }; diff --git a/apps/resources/app/profile/DeleteAccountButton.tsx b/apps/resources/app/profile/DeleteAccountButton.tsx index a2732266..c9a5748c 100644 --- a/apps/resources/app/profile/DeleteAccountButton.tsx +++ b/apps/resources/app/profile/DeleteAccountButton.tsx @@ -3,7 +3,7 @@ import { useAuth } from '@clerk/nextjs'; import { Alert, Button, InfoBox } from 'design-system'; import { AlertTriangle, CheckCircle2, Loader2, XCircle } from 'lucide-react'; -import { useAction } from '../../lib/actions/useAction'; +import { useAction } from '../../lib/serverActions/client'; import { deleteAccount } from './actions'; export const DeleteAccountButton = () => { diff --git a/apps/resources/app/profile/actions.ts b/apps/resources/app/profile/actions.ts index 640ec5df..06c29717 100644 --- a/apps/resources/app/profile/actions.ts +++ b/apps/resources/app/profile/actions.ts @@ -1,18 +1,14 @@ 'use server'; import { clerkClient } from '@clerk/nextjs'; +import { createProtectedAction } from 'lib/serverActions/create'; import { revalidatePath } from 'next/cache'; -import { createAction } from '../../lib/actions/createAction'; import { deleteUserData } from '../../lib/resources'; -export const deleteAccount = createAction({ +export const deleteAccount = createProtectedAction({ action: async ({ ctx }) => { const { userId } = ctx; - if (!userId) { - throw new Error('You must be logged in to delete your account.'); - } - try { await clerkClient.users.deleteUser(userId); await deleteUserData(userId); diff --git a/apps/resources/app/profile/page.tsx b/apps/resources/app/profile/page.tsx index b63df679..d14863da 100644 --- a/apps/resources/app/profile/page.tsx +++ b/apps/resources/app/profile/page.tsx @@ -4,37 +4,45 @@ import { SignedOut, UserProfile, } from '@clerk/nextjs'; +import { Page } from 'components/Page/Page'; import { Heading } from 'design-system'; +import { SearchParams } from 'lib/types'; import { DeleteAccountButton } from './DeleteAccountButton'; -const ProfilePage = () => { +interface Props { + searchParams: SearchParams; +} + +const ProfilePage = ({ searchParams }: Props) => { return ( -
- - Profile - - -
- + +
+ + Profile + +
- + +
+ +
-
-
- - - -
+ + + + + + ); }; diff --git a/apps/resources/app/resources/Resources/Resources.tsx b/apps/resources/app/resources/Resources/Resources.tsx index 1a3758e9..8ef9c927 100644 --- a/apps/resources/app/resources/Resources/Resources.tsx +++ b/apps/resources/app/resources/Resources/Resources.tsx @@ -1,13 +1,31 @@ import { Heading, Text } from 'design-system'; +import { SearchParams } from 'lib/types'; +import { z } from 'zod'; import { formateDate } from '../../../lib/utils'; -import { ReseourcesFilter } from '../page'; import { ResourcesTable } from './ResourcesTable/ResourcesTable'; +export type ReseourcesFilter = z.infer; +const reseourcesFilterSchema = z.object({ + type: z.coerce.string().optional(), + category: z.coerce.string().optional(), + topic: z.coerce.string().optional(), + sort: z.coerce.string().optional(), + search: z.coerce.string().optional(), + likes: z.coerce.boolean().optional(), + comments: z.coerce.boolean().optional(), + from: z.coerce.string().optional(), + till: z.coerce.string().optional(), + limit: z.coerce.number().optional(), + title: z.coerce.string().optional(), +}); + interface Props { - resourcesFilter: ReseourcesFilter; + searchParams: SearchParams; } -export const Resources = async ({ resourcesFilter }: Props) => { +export const Resources = async ({ searchParams }: Props) => { + const resourcesFilter = reseourcesFilterSchema.parse(searchParams); + const title = resourcesFilter.title; const from = resourcesFilter.from; const till = resourcesFilter.till; diff --git a/apps/resources/app/resources/Resources/ResourcesTable/ResourcesList/ResourcesList.tsx b/apps/resources/app/resources/Resources/ResourcesTable/ResourcesList/ResourcesList.tsx index b6474fd8..4c36b655 100644 --- a/apps/resources/app/resources/Resources/ResourcesTable/ResourcesList/ResourcesList.tsx +++ b/apps/resources/app/resources/Resources/ResourcesTable/ResourcesList/ResourcesList.tsx @@ -7,7 +7,7 @@ import { LikedResources, Resource, } from '../../../../../lib/resources'; -import { ReseourcesFilter } from '../../../page'; +import { ReseourcesFilter } from '../../Resources'; import { ClearAllButton } from './ClearAllButton'; import { DownloadButton } from './DownloadButton/DownloadButton'; import { ResourcesListTop } from './ResourcesListTop'; diff --git a/apps/resources/app/resources/Resources/ResourcesTable/ResourcesTable.tsx b/apps/resources/app/resources/Resources/ResourcesTable/ResourcesTable.tsx index c1f9519b..552c2edb 100644 --- a/apps/resources/app/resources/Resources/ResourcesTable/ResourcesTable.tsx +++ b/apps/resources/app/resources/Resources/ResourcesTable/ResourcesTable.tsx @@ -9,7 +9,7 @@ import { getResourcesCached, getTopicsCached, } from '../../../../lib/cache'; -import { ReseourcesFilter } from '../../page'; +import { ReseourcesFilter } from '../Resources'; import { ResourcesFilter } from './ResourcesFilter/ResourcesFilter'; import { ResourcesList } from './ResourcesList/ResourcesList'; import { ResourcesTableProvider } from './ResourcesTableProvider'; diff --git a/apps/resources/app/resources/Suggestion/SuggestionForm.tsx b/apps/resources/app/resources/Suggestion/SuggestionForm.tsx index 007c45fd..e05cbee7 100644 --- a/apps/resources/app/resources/Suggestion/SuggestionForm.tsx +++ b/apps/resources/app/resources/Suggestion/SuggestionForm.tsx @@ -10,7 +10,7 @@ import { inputStyles, } from '../../../components/ForrestSection/ForrestSection'; import { useZodForm } from '../../../hooks/useZodForm'; -import { useAction } from '../../../lib/actions/useAction'; +import { useAction } from '../../../lib/serverActions/client'; import { submit } from './actions'; import { SuggestionFormSchema, suggestionFormSchema } from './schemas'; diff --git a/apps/resources/app/resources/Suggestion/actions.ts b/apps/resources/app/resources/Suggestion/actions.ts index d8f9572d..c138c892 100644 --- a/apps/resources/app/resources/Suggestion/actions.ts +++ b/apps/resources/app/resources/Suggestion/actions.ts @@ -1,7 +1,7 @@ 'use server'; +import { createAction } from 'lib/serverActions/create'; import nodemailer from 'nodemailer'; -import { createAction } from '../../../lib/actions/createAction'; import { envSchema, suggestionFormSchema } from './schemas'; const { SUGGESTION_MAIL_PASSWORD } = envSchema.parse(process.env); diff --git a/apps/resources/app/resources/[slug]/page.tsx b/apps/resources/app/resources/[slug]/page.tsx index 65d60f17..3bce480e 100644 --- a/apps/resources/app/resources/[slug]/page.tsx +++ b/apps/resources/app/resources/[slug]/page.tsx @@ -1,3 +1,5 @@ +import { Page } from 'components/Page/Page'; +import { SearchParams } from 'lib/types'; import { Comments } from '../../../components/Comments/Comments'; import { NewResources } from '../../../components/NewResources/NewResources'; import { Newsletter } from '../../../components/Newsletter/Newsletter'; @@ -55,20 +57,21 @@ interface Props { params: { slug: string; }; + searchParams: SearchParams; } -const ResourcePage = async ({ params }: Props) => { +const ResourcePage = async ({ params, searchParams }: Props) => { const { slug } = params; const { resourceId, resourceType } = parseResourceSlug(slug); return ( - <> + - + ); }; diff --git a/apps/resources/app/resources/page.tsx b/apps/resources/app/resources/page.tsx index cef200fa..7d7df244 100644 --- a/apps/resources/app/resources/page.tsx +++ b/apps/resources/app/resources/page.tsx @@ -1,5 +1,6 @@ +import { Page } from 'components/Page/Page'; +import { SearchParams } from 'lib/types'; import { Metadata } from 'next'; -import { z } from 'zod'; import { Resources } from './Resources/Resources'; import { Suggestion } from './Suggestion/Suggestion'; @@ -7,34 +8,16 @@ export const metadata: Metadata = { title: 'Resources', }; -export type ReseourcesFilter = z.infer; -const reseourcesFilterSchema = z.object({ - type: z.coerce.string().optional(), - category: z.coerce.string().optional(), - topic: z.coerce.string().optional(), - sort: z.coerce.string().optional(), - search: z.coerce.string().optional(), - likes: z.coerce.boolean().optional(), - comments: z.coerce.boolean().optional(), - from: z.coerce.string().optional(), - till: z.coerce.string().optional(), - limit: z.coerce.number().optional(), - title: z.coerce.string().optional(), -}); - interface Props { - searchParams?: { - [key: string]: string | string[] | undefined; - }; + searchParams: SearchParams; } const ResourcesPage = ({ searchParams }: Props) => { - const resourcesFilter = reseourcesFilterSchema.parse(searchParams); return ( - <> - + + - + ); }; diff --git a/apps/resources/app/sign-in/[[...sign-in]]/page.tsx b/apps/resources/app/sign-in/[[...sign-in]]/page.tsx index 4947c7c6..1c220a4d 100644 --- a/apps/resources/app/sign-in/[[...sign-in]]/page.tsx +++ b/apps/resources/app/sign-in/[[...sign-in]]/page.tsx @@ -1,36 +1,44 @@ import { SignIn } from '@clerk/nextjs'; +import { Page } from 'components/Page/Page'; import { Heading } from 'design-system'; +import { SearchParams } from 'lib/types'; -const SignInPage = () => { +interface Props { + searchParams: SearchParams; +} + +const SignInPage = ({ searchParams }: Props) => { return ( -
- - Sign in - -
- -
-
+ +
+ + Sign in + +
+ +
+
+
); }; diff --git a/apps/resources/app/sign-up/[[...sign-up]]/page.tsx b/apps/resources/app/sign-up/[[...sign-up]]/page.tsx index 5a0e486d..fe2a6137 100644 --- a/apps/resources/app/sign-up/[[...sign-up]]/page.tsx +++ b/apps/resources/app/sign-up/[[...sign-up]]/page.tsx @@ -1,44 +1,52 @@ import { SignUp } from '@clerk/nextjs'; +import { Page } from 'components/Page/Page'; import { Heading, Link, Text } from 'design-system'; +import { SearchParams } from 'lib/types'; -const SignUpPage = () => { +interface Props { + searchParams: SearchParams; +} + +const SignUpPage = ({ searchParams }: Props) => { return ( -
- - Sign up - - - Sign up for an account to be able to use personalized features like - liking resources and more. - -
- -
- - With your registration you declare to have read and to agree with our{' '} - privacy policy. - -
+ +
+ + Sign up + + + Sign up for an account to be able to use personalized features like + liking resources and more. + +
+ +
+ + With your registration you declare to have read and to agree with our{' '} + privacy policy. + +
+
); }; diff --git a/apps/resources/components/Card/Card.tsx b/apps/resources/components/Card/Card.tsx index c3d389d2..a2c147f9 100644 --- a/apps/resources/components/Card/Card.tsx +++ b/apps/resources/components/Card/Card.tsx @@ -1,3 +1,4 @@ +import { auth } from '@clerk/nextjs'; import { Card as CardPrimitive, CardProps, @@ -10,6 +11,7 @@ import { ExternalLink, LucideIcon, StickyNote, Users2 } from 'lucide-react'; import { ReactNode } from 'react'; import { ContentType } from '../../lib/resources'; import { CategoryButton } from './CategoryButton'; +import { CollectionButton } from './CollectionButton'; import { CommentsButton } from './CommentsButton/CommentsButton'; import { CopyButton } from './CopyButton'; import { DetailsLink } from './DetailsLink'; @@ -56,6 +58,7 @@ export const Card = ({ note, }: Props) => { const resourceLink = tags?.at(0)?.url; + const { userId } = auth(); return ( {description} )} + + {/* Add to collection */} + {/* {userId && ( + + )} */} diff --git a/apps/resources/components/Card/CollectionButton.tsx b/apps/resources/components/Card/CollectionButton.tsx new file mode 100644 index 00000000..b4b47a5c --- /dev/null +++ b/apps/resources/components/Card/CollectionButton.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { AddToCollectionButton } from 'components/Collections/AddToCollectionButton'; +import { AddToThisCollectionButton } from 'components/Collections/AddToThisCollectionButton'; +import { ContentType } from 'lib/resources'; +import { usePathname } from 'next/navigation'; + +interface Props { + resourceId: number; + resourceType: ContentType; +} + +export const CollectionButton = ({ resourceId, resourceType }: Props) => { + const pathname = usePathname(); + + const collectionId = pathname.includes('/collections/') + ? pathname.split('/').pop() + : undefined; + + return ( +
+ {collectionId ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/apps/resources/components/Card/LikesButton/LikesButtonClient.tsx b/apps/resources/components/Card/LikesButton/LikesButtonClient.tsx index c26323f4..4dd575a6 100644 --- a/apps/resources/components/Card/LikesButton/LikesButtonClient.tsx +++ b/apps/resources/components/Card/LikesButton/LikesButtonClient.tsx @@ -3,7 +3,7 @@ import { useAuth } from '@clerk/nextjs'; import { cva } from 'class-variance-authority'; import { Tooltip } from 'design-system'; -import { useAction } from 'lib/actions/useAction'; +import { useAction } from 'lib/serverActions/client'; import { Heart } from 'lucide-react'; import { experimental_useOptimistic as useOptimistic } from 'react'; import { ContentType } from '../../../lib/resources'; diff --git a/apps/resources/components/Card/actions.ts b/apps/resources/components/Card/actions.ts index f138bbf6..c4555bba 100644 --- a/apps/resources/components/Card/actions.ts +++ b/apps/resources/components/Card/actions.ts @@ -1,8 +1,9 @@ 'use server'; +import { auth } from '@clerk/nextjs'; +import { createAction, createProtectedAction } from 'lib/serverActions/create'; import { revalidateTag } from 'next/cache'; import { z } from 'zod'; -import { createAction } from '../../lib/actions/createAction'; import { resourceLikesTag } from '../../lib/cache'; import { anonymousLikeResource, @@ -20,9 +21,9 @@ const inputSchema = z.object({ export const like = createAction({ input: inputSchema, - action: async ({ input, ctx }) => { + action: async ({ input }) => { const { id, type } = input; - const { userId } = ctx; + const { userId } = auth(); if (userId) { await likeResource(userId, id, type); @@ -35,16 +36,12 @@ export const like = createAction({ }, }); -export const unLike = createAction({ +export const unLike = createProtectedAction({ input: inputSchema, action: async ({ input, ctx }) => { const { id, type } = input; const { userId } = ctx; - if (!userId) { - throw new Error('Unauthorized'); - } - await unlikeResource(userId, id, type); const tag = resourceLikesTag(id, type); diff --git a/apps/resources/components/Collections/AddCollectionButton.tsx b/apps/resources/components/Collections/AddCollectionButton.tsx new file mode 100644 index 00000000..7aecb528 --- /dev/null +++ b/apps/resources/components/Collections/AddCollectionButton.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useServerDialog } from 'components/ServerDialog/useServerDialog'; +import { Button } from 'design-system'; + +export const AddCollectionButton = () => { + const { openDialog, isPending } = useServerDialog(); + return ( + + ); +}; diff --git a/apps/resources/components/Collections/AddCollectionDialog/AddCollectionDialog.tsx b/apps/resources/components/Collections/AddCollectionDialog/AddCollectionDialog.tsx new file mode 100644 index 00000000..b040ceac --- /dev/null +++ b/apps/resources/components/Collections/AddCollectionDialog/AddCollectionDialog.tsx @@ -0,0 +1,22 @@ +import { ServerDialogRoot } from 'components/ServerDialog/ServerDialogRoot'; +import { + DialogContent, + DialogOverlay, + DialogPortal, + Heading, +} from 'design-system'; +import { AddCollectionForm } from './AddCollectionForm'; + +export const AddCollectionDialog = () => { + return ( + + + + + Add Collection + + + + + ); +}; diff --git a/apps/resources/components/Collections/AddCollectionDialog/AddCollectionForm.tsx b/apps/resources/components/Collections/AddCollectionDialog/AddCollectionForm.tsx new file mode 100644 index 00000000..d31688f3 --- /dev/null +++ b/apps/resources/components/Collections/AddCollectionDialog/AddCollectionForm.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { cva } from 'class-variance-authority'; +import { Button, InfoBox } from 'design-system'; +import { useZodForm } from 'hooks/useZodForm'; +import { useAction } from 'lib/serverActions/client'; +import { AlertTriangle, Loader2, MessageCircle } from 'lucide-react'; +import { SubmitHandler } from 'react-hook-form'; +import { addCollection } from './actions'; +import { AddCollectionSchema, addCollectionSchema } from './schemas'; + +const inputStyles = cva( + 'px-8 py-4 text-base text-text-secondary bg-ghost-main-dark-bg outline-none w-full ring-inset', + { + variants: { + error: { + true: 'ring-2 ring-red-700', + false: 'focus-visible:ring-2 focus-visible:ring-ghost-contrast-text', + }, + }, + }, +); + +const errorStyles = + 'absolute left-0 bottom-0 -mb-4 text-red-700 text-sm slide-in-from-top-full duration-100 ease-in-out fade-in animate-in'; + +export const AddCollectionForm = () => { + const { error, runAction, isRunning } = useAction(addCollection); + const { + register, + handleSubmit, + formState: { errors }, + } = useZodForm({ + schema: addCollectionSchema, + }); + + const onSubmit: SubmitHandler = async ({ + title, + description, + }) => { + await runAction({ + title, + description, + }); + }; + + return ( +
+ {/* Title input */} +
+ + + {errors.title &&

{errors.title.message}

} +
+ + {/* Description input */} +
+ +