diff --git a/package.json b/package.json index 83a293b..96fb6c1 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/typography": "^0.5.10", "@types/node": "20.10.4", "@types/react": "18.2.45", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff8a965..051bab7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,9 @@ dependencies: version: 3.22.4 devDependencies: + '@tailwindcss/line-clamp': + specifier: ^0.4.4 + version: 0.4.4(tailwindcss@3.3.6) '@tailwindcss/typography': specifier: ^0.5.10 version: 0.5.10(tailwindcss@3.3.6) @@ -3935,6 +3938,14 @@ packages: dependencies: tslib: 2.6.2 + /@tailwindcss/line-clamp@0.4.4(tailwindcss@3.3.6): + resolution: {integrity: sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==} + peerDependencies: + tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' + dependencies: + tailwindcss: 3.3.6 + dev: true + /@tailwindcss/typography@0.5.10(tailwindcss@3.3.6): resolution: {integrity: sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==} peerDependencies: diff --git a/src/app/api/auth/login/github/callback/route.ts b/src/app/api/auth/login/github/callback/route.ts index da9a4a7..cec76f4 100644 --- a/src/app/api/auth/login/github/callback/route.ts +++ b/src/app/api/auth/login/github/callback/route.ts @@ -1,9 +1,8 @@ import { OAuthRequestError } from "@lucia-auth/oauth"; import { cookies, headers } from "next/headers"; -import { auth, githubAuth } from "~/lib/auth"; - import type { NextRequest } from "next/server"; -import { sendMail } from "~/server/actions"; +import { auth, githubAuth } from "~/lib/auth"; +import { sendMail } from "~/lib/resend"; export const GET = async (request: NextRequest) => { const storedState = cookies().get("github_oauth_state")?.value; @@ -30,20 +29,16 @@ export const GET = async (request: NextRequest) => { picture: githubUser.avatar_url, }, }); - return user; - }; - - const user = await getUser(); - - const existingUser = await getExistingUser(); - if (!existingUser) { sendMail({ toMail: user.email, data: { name: user.name, }, }); - } + return user; + }; + + const user = await getUser(); const session = await auth.createSession({ userId: user.userId, diff --git a/src/app/dashboard/billing/page.tsx b/src/app/dashboard/billing/page.tsx index 6e6ec76..44367ea 100644 --- a/src/app/dashboard/billing/page.tsx +++ b/src/app/dashboard/billing/page.tsx @@ -1,18 +1,19 @@ import { AlertTriangleIcon } from "lucide-react"; import { BillingForm } from "~/components/billing-form"; import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; +import { getPageSession } from "~/lib/auth"; import { stripe } from "~/lib/stripe"; import { getUserSubscriptionPlan } from "~/lib/subscription"; -import { getUser } from "~/server/user"; -import { type CurrentUser } from "~/types"; export const revalidate = 0; export const dynamic = "force-dynamic"; export default async function Billing() { - const user = (await getUser()) as CurrentUser; + const session = await getPageSession(); - const subscriptionPlan = await getUserSubscriptionPlan(user.id); + const subscriptionPlan = await getUserSubscriptionPlan( + session?.user?.userId as string + ); // If user has a pro plan, check cancel status on Stripe. let isCanceled = false; diff --git a/src/app/dashboard/projects/action.ts b/src/app/dashboard/projects/action.ts index 3483e66..ab0d845 100644 --- a/src/app/dashboard/projects/action.ts +++ b/src/app/dashboard/projects/action.ts @@ -3,9 +3,9 @@ import { type Project } from "@prisma/client"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; +import { getPageSession } from "~/lib/auth"; import db from "~/lib/db"; import { getUserSubscriptionPlan } from "~/lib/subscription"; -import { getUser } from "~/server/user"; interface Payload { name: string; @@ -13,14 +13,14 @@ interface Payload { } export async function createProject(payload: Payload) { - const user = await getUser(); + const session = await getPageSession(); await db.project.create({ data: { ...payload, user: { connect: { - id: user?.id, + id: session?.user?.userId, }, }, }, @@ -30,8 +30,10 @@ export async function createProject(payload: Payload) { } export async function checkIfFreePlanLimitReached() { - const user = await getUser(); - const subscriptionPlan = await getUserSubscriptionPlan(user?.id as string); + const session = await getPageSession(); + const subscriptionPlan = await getUserSubscriptionPlan( + session?.user?.userId as string + ); // If user is on a free plan. // Check if user has reached limit of 3 projects. @@ -39,7 +41,7 @@ export async function checkIfFreePlanLimitReached() { const count = await db.project.count({ where: { - userId: user?.id, + userId: session?.user?.userId, }, }); @@ -47,10 +49,10 @@ export async function checkIfFreePlanLimitReached() { } export async function getProjects() { - const user = await getUser(); + const session = await getPageSession(); const projects = await db.project.findMany({ where: { - userId: user?.id, + userId: session?.user?.userId, }, orderBy: { createdAt: "desc", @@ -60,22 +62,22 @@ export async function getProjects() { } export async function getProjectById(id: string) { - const user = await getUser(); + const session = await getPageSession(); const project = await db.project.findFirst({ where: { id, - userId: user?.id, + userId: session?.user?.userId, }, }); return project as Project; } export async function updateProjectById(id: string, payload: Payload) { - const user = await getUser(); + const session = await getPageSession(); await db.project.update({ where: { id, - userId: user?.id, + userId: session?.user?.userId, }, data: payload, }); @@ -83,11 +85,11 @@ export async function updateProjectById(id: string, payload: Payload) { } export async function deleteProjectById(id: string) { - const user = await getUser(); + const session = await getPageSession(); await db.project.delete({ where: { id, - userId: user?.id, + userId: session?.user?.userId, }, }); revalidatePath(`/dashboard/projects`); diff --git a/src/app/dashboard/projects/create-project-modal.tsx b/src/app/dashboard/projects/create-project-modal.tsx index 642dac3..65a7ea9 100644 --- a/src/app/dashboard/projects/create-project-modal.tsx +++ b/src/app/dashboard/projects/create-project-modal.tsx @@ -25,7 +25,7 @@ import { } from "~/components/ui/form"; import { Input } from "~/components/ui/input"; import { toast } from "~/components/ui/use-toast"; -import { FreePlanLimitError } from "~/server/utils"; +import { FreePlanLimitError } from "~/lib/utils"; import { checkIfFreePlanLimitReached, createProject } from "./action"; export const projectSchema = z.object({ diff --git a/src/app/dashboard/settings/actions.ts b/src/app/dashboard/settings/actions.ts new file mode 100644 index 0000000..b043bc3 --- /dev/null +++ b/src/app/dashboard/settings/actions.ts @@ -0,0 +1,50 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import db from "~/lib/db"; +import { getImageKeyFromUrl, isOurCdnUrl } from "~/lib/utils"; +import { utapi } from "~/server/uploadthing"; +import { type payload } from "~/types"; + +export const updateUser = async (id: string, payload: payload) => { + await db.user.update({ + where: { id }, + data: { ...payload }, + }); + + revalidatePath("/dashboard/settings"); +}; + +export async function removeUserOldImageFromCDN( + id: string, + newImageUrl: string +) { + const user = await db.user.findFirst({ + where: { id }, + select: { picture: true }, + }); + + const currentImageUrl = user?.picture; + + if (!currentImageUrl) throw new Error("User Picture Missing"); + + try { + if (isOurCdnUrl(currentImageUrl)) { + const currentImageFileKey = getImageKeyFromUrl(currentImageUrl); + + await utapi.deleteFiles(currentImageFileKey as string); + revalidatePath("/dashboard/settings"); + } + } catch (e) { + if (e instanceof Error) { + const newImageFileKey = getImageKeyFromUrl(newImageUrl); + await utapi.deleteFiles(newImageFileKey as string); + console.error(e.message); + } + } +} + +export async function removeNewImageFromCDN(image: string) { + const imageFileKey = getImageKeyFromUrl(image); + await utapi.deleteFiles(imageFileKey as string); +} diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index ebdd9c4..142bf12 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -1,6 +1,6 @@ +import { type User } from "lucia"; import { type Metadata } from "next"; -import { getUser } from "~/server/user"; -import { type CurrentUser } from "~/types"; +import { getPageSession } from "~/lib/auth"; import SettingsForm from "./settings-form"; export const metadata: Metadata = { @@ -8,6 +8,6 @@ export const metadata: Metadata = { }; export default async function Settings() { - const currentUser = (await getUser()) as CurrentUser; - return ; + const session = await getPageSession(); + return ; } diff --git a/src/app/dashboard/settings/settings-form.tsx b/src/app/dashboard/settings/settings-form.tsx index 7e85372..abef414 100644 --- a/src/app/dashboard/settings/settings-form.tsx +++ b/src/app/dashboard/settings/settings-form.tsx @@ -1,10 +1,16 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { type User } from "lucia"; import { Loader2 } from "lucide-react"; import dynamic from "next/dynamic"; import { useEffect, useRef, useState, useTransition } from "react"; import { useForm } from "react-hook-form"; +import { + removeNewImageFromCDN, + removeUserOldImageFromCDN, + updateUser, +} from "~/app/dashboard/settings/actions"; import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; import { Button } from "~/components/ui/button"; import { @@ -18,12 +24,7 @@ import { } from "~/components/ui/form"; import { Input } from "~/components/ui/input"; import { toast } from "~/components/ui/use-toast"; -import { - saRemoveNewImageFromCDN, - saRemoveUserOldImageFromCDN, - saUpdateUserInDb, -} from "~/server/actions"; -import { settingsSchema, type CurrentUser, type SettingsValues } from "~/types"; +import { settingsSchema, type SettingsValues } from "~/types"; const ImageUploadModal = dynamic( () => import("~/components/layout/image-upload-modal") @@ -33,11 +34,7 @@ const CancelConfirmModal = dynamic( () => import("~/components/layout/cancel-confirm-modal") ); -export default function SettingsForm({ - currentUser, -}: { - currentUser: CurrentUser; -}) { +export default function SettingsForm({ currentUser }: { currentUser: User }) { const oldImage = useRef(""); const [pending, startTransition] = useTransition(); @@ -65,43 +62,31 @@ export default function SettingsForm({ function onSubmit(data: SettingsValues) { if (!formState.isDirty) return; - if (isImageChanged) { - startTransition(() => - saRemoveUserOldImageFromCDN(currentUser.id, data.picture) - .then(() => saUpdateUserInDb(currentUser.id, data)) - .then(() => { - toast({ - title: "Updated successfully!", - }); - }) - .catch(() => { - toast({ - title: "Something went wrong.", - variant: "destructive", - }); - }) - ); - } else { - startTransition(() => - saUpdateUserInDb(currentUser.id, data) - .then(() => { - toast({ - title: "Updated successfully!", - }); - }) - .catch(() => { - toast({ - title: "Something went wrong.", - variant: "destructive", - }); - }) - ); - } + startTransition(() => { + const updatePromise = isImageChanged + ? removeUserOldImageFromCDN(currentUser.userId, data.picture).then(() => + updateUser(currentUser.userId, data) + ) + : updateUser(currentUser.userId, data); + + return updatePromise + .then(() => { + toast({ + title: "Updated successfully!", + }); + }) + .catch(() => { + toast({ + title: "Something went wrong.", + variant: "destructive", + }); + }); + }); } function handleReset() { if (isImageChanged) { - saRemoveNewImageFromCDN(form.getValues().picture) + removeNewImageFromCDN(form.getValues().picture) .then(() => form.reset()) .catch((error) => console.error(error)); } else { diff --git a/src/components/layout/auth-form.tsx b/src/components/layout/auth-form.tsx index 486383a..abc8aff 100644 --- a/src/components/layout/auth-form.tsx +++ b/src/components/layout/auth-form.tsx @@ -1,127 +1,28 @@ "use client"; import Link from "next/link"; -import { buttonVariants } from "~/components/ui/button"; +import { useState } from "react"; +import { Button, buttonVariants } from "~/components/ui/button"; import { cn } from "~/lib/utils"; import Icons from "../shared/icons"; -// const userAuthSchema = z.object({ -// email: z.string().email("Please enter a valid email address."), -// }); - -// type FormData = z.infer; - export default function AuthForm() { - // const [isLoading, setIsLoading] = useState(false); - // const [isGithubLoading, setIsGithubLoading] = useState(false); - - // const { - // register, - // handleSubmit, - // reset, - // formState: { errors }, - // } = useForm({ - // resolver: zodResolver(userAuthSchema), - // }); - - // async function onSubmit(data: FormData) { - // setIsLoading(true); - - // saCheckEmailExists(data.email.toLowerCase()) - // .then(async () => { - // try { - // const res = await signIn("email", { - // email: data.email.toLowerCase(), - // redirect: false, - // }); - - // if (!res?.ok) { - // throw new Error("Something went wrong."); - // } - - // toast({ - // title: "Check your email", - // description: - // "We sent you a sign in link. Be sure to check your spam too.", - // }); - // reset(); - // } catch (err) { - // toast({ - // title: "Something went wrong.", - // description: "Your signin request failed. Please try again.", - // variant: "destructive", - // }); - // } finally { - // setIsLoading(false); - // } - // }) - // .catch(() => { - // toast({ - // title: "Account not found.", - // description: - // "You have to use the email already linked to your account.", - // variant: "destructive", - // }); - // setIsLoading(false); - // }); - // } + const [isLoading, setIsLoading] = useState(false); return (
- {/*
-
-
- - - {errors?.email && ( -

- {errors.email.message} -

- )} -
- -
-
-
-
- -
-
- Or -
-
*/} - { - // setIsGithubLoading(true); - // signIn("github"); - // }} - // disabled={isLoading || isGithubLoading} - > - {/* {isGithubLoading ? ( + {isLoading ? ( + + ) : ( + setIsLoading(true)} + > + Continue with + + )}
); } diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index 00a5c20..bc60e22 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -1,13 +1,13 @@ -import { getUser } from "~/server/user"; -import { type CurrentUser } from "~/types"; +import { type User } from "lucia"; +import { getPageSession } from "~/lib/auth"; import Navbar from "./navbar"; export default async function Header() { - const currentUser = (await getUser()) as CurrentUser; + const session = await getPageSession(); return (
- +
); diff --git a/src/components/layout/header/navbar.tsx b/src/components/layout/header/navbar.tsx index cb590fa..c517741 100644 --- a/src/components/layout/header/navbar.tsx +++ b/src/components/layout/header/navbar.tsx @@ -1,18 +1,14 @@ "use client"; +import { type User } from "lucia"; import { MenuIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; import { buttonVariants } from "~/components/ui/button"; import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet"; -import { type CurrentUser } from "~/types"; import UserNav from "../user-nav"; -export default function Navbar({ - loggedInUser, -}: { - loggedInUser: CurrentUser; -}) { +export default function Navbar({ loggedInUser }: { loggedInUser: User }) { const [isModalOpen, setIsModalOpen] = useState(false); return (