From 2c4bfad6631373b34f2a1bec87732c0c2c5c262b Mon Sep 17 00:00:00 2001 From: Sayan De Date: Sat, 30 Nov 2024 00:10:48 +0530 Subject: [PATCH] add --- apps/web/app/(portals)/[profile]/page.tsx | 285 +++++++++-------- apps/web/app/(portals)/post/page.tsx | 8 +- apps/web/app/(portals)/provider.tsx | 19 ++ .../app/api/v1/posts/find-referrer/route.ts | 22 +- apps/web/app/api/v1/posts/referral/route.ts | 1 + apps/web/app/api/v1/posts/route.ts | 60 ++++ .../post-card/post-card-skeleton.tsx | 6 +- apps/web/components/dialog/share-dialog.tsx | 2 +- apps/web/components/ui/post-forms/drafts.tsx | 171 +++++++---- .../ui/post-forms/find-referrer.tsx | 260 +++++++++++----- .../components/ui/post-forms/normal-post.tsx | 286 +++++++++--------- .../ui/post-forms/referral-post.tsx | 144 ++++++--- .../ui/post-ui/post-type-dialog.tsx | 85 +++--- .../lib/validators/find-referrer-validator.ts | 20 +- .../lib/validators/normal-post-validator.ts | 14 +- .../lib/validators/referral-post-validator.ts | 34 ++- apps/web/package.json | 2 + apps/web/store/store.ts | 12 + apps/web/types/types.d.ts | 11 +- packages/ui/components/dialog/dialog.tsx | 15 +- yarn.lock | 10 + 21 files changed, 947 insertions(+), 520 deletions(-) diff --git a/apps/web/app/(portals)/[profile]/page.tsx b/apps/web/app/(portals)/[profile]/page.tsx index cb85f764..bd659687 100644 --- a/apps/web/app/(portals)/[profile]/page.tsx +++ b/apps/web/app/(portals)/[profile]/page.tsx @@ -1,37 +1,63 @@ -import { Metadata } from "next"; +"use client"; + import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/navigation"; +import Loading from "@/app/loading"; +import { expired, fromNow } from "@refhiredcom/utils"; +import { useQuery } from "@tanstack/react-query"; +import { Calendar, Mail, MapPin } from "lucide-react"; import { ArrowRight } from "lucide-react"; +import { useSession } from "next-auth/react"; import { Button, Separator } from "@referrer/ui"; -import { ProfilePage } from "@/components/profile/profile"; +import { PostCard } from "@/components/custom-components"; +import { + ApplyStatus, + BookmarkButton, + MultipleButtons, + ShareButton, + StarButton, +} from "@/components/custom-components/post-card/post-buttons"; +import Navigate from "@/components/navigate"; +import { ApplyDialog } from "@/components/ui"; -import { auth } from "@/lib/auth"; import { request } from "@/lib/axios"; -import { TProfile } from "@/types/types"; +import { cn } from "@/utils"; -export const metadata: Metadata = { - title: "Profile", - description: "Get job referrals to the top best companies of the world", -}; +import { TProfile } from "@/types/types"; type paramsProps = { params: { profile: string }; }; -async function getProfile(profile) { - const response = await request.get(`/username/${profile}`); - - return response.data; -} - -const Profile = async ({ params }: paramsProps) => { +export default function ProfilePage({ params }: paramsProps) { const { profile } = params; - - const session = await auth(); + const { data: session } = useSession(); + const router = useRouter(); + + const { + data: profileData, + error, + isStale, + isLoading, + isFetching, + } = useQuery({ + queryKey: ["username", profile], + queryFn: () => { + return request.get(`/username/${profile}`); + }, + // refetchInterval: 5000, + // staleTime: 200000, + // gcTime: Infinity, + }); + + const data = profileData?.data?.data; + + console.log(isLoading, isFetching, isStale); if (profile === "profile") return ( @@ -55,7 +81,7 @@ const Profile = async ({ params }: paramsProps) => { ); - const { data } = await getProfile(profile); + if (isLoading) return ; if (!data) return ( @@ -77,113 +103,118 @@ const Profile = async ({ params }: paramsProps) => { ); return ( - - // <> - //
- // img - //

{data.name}

- //

@{data?.userName}

- // {data?.bio} - //
- //
- // - // {data?.email} - //
- // {data?.location && ( - //
- // - // {data.location} - //
- // )} - //
- // - // Joined {fromNow(data.createdAt)} - //
- // {/*
- // - // {data?.workingAt} - //
*/} - //
- // {session?.user.userName === data?.userName && ( - // - // Edit Profile - // - // )} - //
- // - // {data?.posts.map((postData) => ( - // - // - // - // - // - // - // {postData.description.substring(0, 350).concat(" ...")} - // - // - // - // - // - // {/* */} - // - // - // - // - // - // {session?.user.id === data.id ? ( - // <> - // ) : ( - // // - // - // )} - // - // - // - // ))} - // + <> +
+ img +

{data.name}

+

@{data?.userName}

+ {data?.bio} +
+
+ + {data?.email} +
+ {data?.location && ( +
+ + {data.location} +
+ )} +
+ + Joined {fromNow(data.createdAt)} +
+ {/*
+ + {data?.workingAt} +
*/} +
+ {session?.user.userName === data?.userName && ( + + )} +
+ + {data?.posts.map((postData) => ( + + + + + + + {postData.description.substring(0, 350).concat(" ...")} + + + + + + {/* */} + + + + + + {session?.user.id === data.id ? ( + postData.totalApplied > 0 && ( + + ) + ) : ( + // + + )} + + + + ))} + ); -}; - -export default Profile; +} diff --git a/apps/web/app/(portals)/post/page.tsx b/apps/web/app/(portals)/post/page.tsx index 21ecce86..7989be75 100644 --- a/apps/web/app/(portals)/post/page.tsx +++ b/apps/web/app/(portals)/post/page.tsx @@ -27,7 +27,7 @@ const DynamicDrafts = dynamic(() => import("@/components/ui/post-forms/drafts"), loading: () => , }); -type TabParams = { tab: "referral" | "find" | "normal" | "drafts" }; +type TabParams = { tab: "referral" | "find" | "post" | "drafts" }; export default function Post() { const pathName = usePathname(); @@ -63,9 +63,9 @@ export default function Post() { { - router.push(pathName + "?" + createQueryString("tab", "normal")); + router.push(pathName + "?" + createQueryString("tab", "post")); }} - value="normal"> + value="post"> Post - + diff --git a/apps/web/app/(portals)/provider.tsx b/apps/web/app/(portals)/provider.tsx index 2319e447..415599a4 100644 --- a/apps/web/app/(portals)/provider.tsx +++ b/apps/web/app/(portals)/provider.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; +import setupIndexedDB from "use-indexeddb"; import { useLocalStorage } from "usehooks-ts"; import { Walkthrough } from "@/components/custom-components"; @@ -12,6 +13,20 @@ import { PostSteps, Steps } from "@/config"; import { useStore } from "@/store/store"; +const idbConfig = { + databaseName: "refhired-db", + version: 1, + stores: [ + { + name: "posts", // Store name + id: { keyPath: "id", autoIncrement: true }, // Primary key setup + indices: [ + { name: "body", keyPath: "body", options: { unique: false } }, // Index for the "body" field + ], + }, + ], +}; + export function Provider({ children }: { children: React.ReactNode }) { const { data: session } = useSession(); @@ -30,6 +45,10 @@ export function Provider({ children }: { children: React.ReactNode }) { }, [session?.error]); useEffect(() => { + setupIndexedDB(idbConfig) + .then(() => console.log("success")) + .catch((e) => console.error("error / unsupported", e)); + const timer = setTimeout(() => { setShowComponent(true); }, 3000); diff --git a/apps/web/app/api/v1/posts/find-referrer/route.ts b/apps/web/app/api/v1/posts/find-referrer/route.ts index f0668803..f07eb47b 100644 --- a/apps/web/app/api/v1/posts/find-referrer/route.ts +++ b/apps/web/app/api/v1/posts/find-referrer/route.ts @@ -7,7 +7,22 @@ import { auth } from "@/lib/auth"; import { TFindReferralPost } from "@/types/types"; export async function POST(request: NextRequest) { + const token = request.headers.get("authorization")?.replace(/^Bearer\s+/, ""); + + if (!token) { + return NextResponse.json( + { message: "You are not authenticated" }, + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + const response: TFindReferralPost = await request.json(); + const session = await auth(); // const key = `RATE_LIMIT:POST:${info.userId}`; // const currentCount = await redis.incr(key); @@ -24,14 +39,13 @@ export async function POST(request: NextRequest) { // const rateLimitFlag = await redis.get(`RATE_LIMIT:POST:${info.userId}`); // if (rateLimitFlag) throw new RateLimitError("Please Wait"); - const session = await auth(); - const createdPost = await prisma.posts.create({ + const data = await prisma.posts.create({ data: { userId: session.user.id, description: response.description, jobCode: response.jobCode, postType: "FINDREFERRER", - companyName: response.company, + companyName: response.companyName, }, }); @@ -39,7 +53,7 @@ export async function POST(request: NextRequest) { // // await redis.del("ALL_POSTS"); return NextResponse.json( - { data: createdPost, message: "Sucessfully created Referral Post" }, + { data: data, message: "Sucessfully created the Post" }, { status: 200, headers: { diff --git a/apps/web/app/api/v1/posts/referral/route.ts b/apps/web/app/api/v1/posts/referral/route.ts index 7d57c2ad..90b4bc79 100644 --- a/apps/web/app/api/v1/posts/referral/route.ts +++ b/apps/web/app/api/v1/posts/referral/route.ts @@ -20,6 +20,7 @@ export async function POST(request: NextRequest) { } ); } + const response: TPostReferralPost = await request.json(); const session = await auth(); diff --git a/apps/web/app/api/v1/posts/route.ts b/apps/web/app/api/v1/posts/route.ts index 36ced816..22daf9bf 100644 --- a/apps/web/app/api/v1/posts/route.ts +++ b/apps/web/app/api/v1/posts/route.ts @@ -2,12 +2,17 @@ import { type NextRequest, NextResponse } from "next/server"; import prisma from "@referrer/prisma"; +import { auth } from "@/lib/auth"; + +import { TPost } from "@/types/types"; + export async function GET(request: NextRequest, context: any) { // const cachedAllPosts = await redis.get("ALL_POSTS"); // if (cachedAllPosts) return JSON.parse(cachedAllPosts); const posts = await prisma.posts.findMany({ take: 10, + skip: 0, orderBy: { createdAt: "desc", }, @@ -28,3 +33,58 @@ export async function GET(request: NextRequest, context: any) { } ); } + +export async function POST(request: NextRequest) { + const token = request.headers.get("authorization")?.replace(/^Bearer\s+/, ""); + + if (!token) { + return NextResponse.json( + { message: "You are not authenticated" }, + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + const response: TPost = await request.json(); + const session = await auth(); + + // const key = `RATE_LIMIT:POST:${info.userId}`; + // const currentCount = await redis.incr(key); + + // if (currentCount === 1) { + // // If the key was just created, set its expiration time + // await redis.expire(key, rateLimitTimeInSeconds); + // } + + // if (currentCount > rateLimitMaxActionsAllowed) { + // // If the user has exceeded the maximum allowed posts, return false + // throw new RateLimitError("Please Wait"); + // } + + // const rateLimitFlag = await redis.get(`RATE_LIMIT:POST:${info.userId}`); + // if (rateLimitFlag) throw new RateLimitError("Please Wait"); + const data = await prisma.posts.create({ + data: { + userId: session.user.id, + description: response.description, + postType: "POST", + }, + }); + + // // await redis.set(`RATE_LIMIT:POST:${info.userId}`, 1, "EX", rateLimitTimeInSeconds); + // // await redis.del("ALL_POSTS"); + + return NextResponse.json( + { data: data, message: "Sucessfully created the Post" }, + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); +} diff --git a/apps/web/components/custom-components/post-card/post-card-skeleton.tsx b/apps/web/components/custom-components/post-card/post-card-skeleton.tsx index bd75cccc..603f1231 100644 --- a/apps/web/components/custom-components/post-card/post-card-skeleton.tsx +++ b/apps/web/components/custom-components/post-card/post-card-skeleton.tsx @@ -6,9 +6,9 @@ export const PostCardSkeleton = () => { return ( <> -
+
- +
@@ -27,7 +27,7 @@ export const PostCardSkeleton = () => {
- +
diff --git a/apps/web/components/dialog/share-dialog.tsx b/apps/web/components/dialog/share-dialog.tsx index b27fee9c..7efe270e 100644 --- a/apps/web/components/dialog/share-dialog.tsx +++ b/apps/web/components/dialog/share-dialog.tsx @@ -69,7 +69,7 @@ export function ShareDialog({ return ( {children} - + Share ! Share on your Social Media sitess diff --git a/apps/web/components/ui/post-forms/drafts.tsx b/apps/web/components/ui/post-forms/drafts.tsx index 20c380c9..f4476ff7 100644 --- a/apps/web/components/ui/post-forms/drafts.tsx +++ b/apps/web/components/ui/post-forms/drafts.tsx @@ -1,71 +1,126 @@ -import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@referrer/ui"; +"use client"; -const invoices = [ - { - invoice: "INV001", - paymentStatus: "Paid", - totalAmount: "$250.00", - paymentMethod: "Credit Card", - }, - { - invoice: "INV002", - paymentStatus: "Pending", - totalAmount: "$150.00", - paymentMethod: "PayPal", - }, - { - invoice: "INV003", - paymentStatus: "Unpaid", - totalAmount: "$350.00", - paymentMethod: "Bank Transfer", - }, - { - invoice: "INV004", - paymentStatus: "Paid", - totalAmount: "$450.00", - paymentMethod: "Credit Card", - }, - { - invoice: "INV005", - paymentStatus: "Paid", - totalAmount: "$550.00", - paymentMethod: "PayPal", - }, - { - invoice: "INV006", - paymentStatus: "Pending", - totalAmount: "$200.00", - paymentMethod: "Bank Transfer", - }, - { - invoice: "INV007", - paymentStatus: "Unpaid", - totalAmount: "$300.00", - paymentMethod: "Credit Card", - }, -]; +import { useEffect, useState } from "react"; + +import { useRouter } from "next/navigation"; + +import Loading from "@/app/loading"; +import parse from "html-react-parser"; +import { Pencil, Trash2 } from "lucide-react"; +import { useIndexedDBStore } from "use-indexeddb"; + +import { + Button, + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@referrer/ui"; + +import { useStore } from "@/store/store"; + +import { Badge } from "../badge/badge"; +import { sonerToast } from "../soner-toast"; +import { TooltipDemo } from "../tooltip/tooltip"; export default function Drafts() { + const { getAll, deleteByID } = useIndexedDBStore("posts"); + + const [drafts, setDrafts] = useState([]); // State to store fetched drafts + const [loading, setLoading] = useState(true); // State to track loading + const router = useRouter(); + + const setPostFromDraft = useStore((state) => state.setPostFromDraft); + const setReferralPostFromDraft = useStore((state) => state.setReferralPostFromDraft); + const setFindReferrerPostFromDraft = useStore((state) => state.setFindReferrerPostFromDraft); + + useEffect(() => { + // Fetch drafts on component mount + getAll() + .then((fetchedDrafts) => { + setDrafts(fetchedDrafts); // Update state with fetched drafts + setLoading(false); // Stop loading + }) + .catch((error) => { + console.error("Error fetching drafts:", error); + setLoading(false); // Stop loading in case of error + }); + }, [getAll]); // Empty dependency array ensures this runs only once on mount + + if (loading) { + return ; + } + + const deleteDraft = (id) => { + deleteByID(id).then(console.log).catch(console.error); + sonerToast({ + severity: "success", + title: "Sucess !", + message: "You have sucessfully deleted all drafts", + }); + }; + + const getPostRoute = (data) => { + switch (data.body.postType) { + case "Referral Post": + setReferralPostFromDraft(data); + return "post?tab=referral"; + case "Find Referrer": + setFindReferrerPostFromDraft(data); + return "post?tab=find"; + default: + setPostFromDraft(data); + return "post?tab=post"; + } + }; + + const editPost = (data) => { + const route = getPostRoute(data); + router.push(route); + }; + return ( - A list of your recent invoices. + A list of your recent drafts. - Invoice - Status - Method - Amount + Post Type + Date + Description + Actions - {invoices.map((invoice) => ( - - {invoice.invoice} - {invoice.paymentStatus} - {invoice.paymentMethod} - {invoice.totalAmount} - - ))} + {drafts ? ( + drafts.map((data) => ( + + + {data.body.postType} + + {data.body.updatedAt} + +
{parse(data.body.description)}
+
+ + + + +
+ )) + ) : ( +

No drafts have been saved yet

+ )}
); diff --git a/apps/web/components/ui/post-forms/find-referrer.tsx b/apps/web/components/ui/post-forms/find-referrer.tsx index 2b036f7c..2113664e 100644 --- a/apps/web/components/ui/post-forms/find-referrer.tsx +++ b/apps/web/components/ui/post-forms/find-referrer.tsx @@ -1,10 +1,16 @@ "use client"; +import { useState } from "react"; + +import { useRouter } from "next/navigation"; + import { zodResolver } from "@hookform/resolvers/zod"; -import { useFieldArray, useForm } from "react-hook-form"; +import { useMutation } from "@tanstack/react-query"; +import { useSession } from "next-auth/react"; +import { useForm } from "react-hook-form"; +import { useIndexedDBStore } from "use-indexeddb"; import * as z from "zod"; -import { cn } from "@referrer/lib/utils/cn"; import { Button, Form, @@ -16,115 +22,231 @@ import { FormLabel, FormMessage, Input, - Separator, - Textarea, } from "@referrer/ui"; -import { Icons } from "@/components/icons/icons"; +import RichTextEditor from "@/components/Tiptap"; +import { request } from "@/lib/axios"; import { findReferrerValidator } from "@/lib/validators"; -import { jobTypeList } from "@/config"; +import { companyList } from "@/config"; + +import { useStore } from "@/store/store"; + +import { TFindReferralPost } from "@/types/types"; import { Required } from "../required"; +import { sonerToast } from "../soner-toast"; import { SelectComponent } from "./select"; export default function FindReferrer() { + const { data: session } = useSession(); + + const router = useRouter(); + const findReferrerPostFromDraft = useStore((state) => state.findReferrerPostFromDraft); + const { add, getByID, update } = useIndexedDBStore("posts"); + const [draftId, setDraftId] = useState(findReferrerPostFromDraft?.id ?? null); // Use state for draft ID + const form = useForm>({ resolver: zodResolver(findReferrerValidator), defaultValues: { - company: "", - jobCode: "", - jobURL: "", - description: "", - resume: "", - coverLetter: "", + companyName: findReferrerPostFromDraft?.body.companyName ?? "", + jobCode: findReferrerPostFromDraft?.body.jobCode ?? "", + jobURL: findReferrerPostFromDraft?.body.jobURL ?? "", + description: findReferrerPostFromDraft?.body.description ?? "", + // resume: "", + // coverLetter: "", }, }); - const { fields, append } = useFieldArray({ - name: "urls", - control: form.control, + // const { fields, append } = useFieldArray({ + // name: "urls", + // control: form.control, + // }); + + const { mutate, isPending } = useMutation({ + mutationKey: ["referral"], + mutationFn: ({ companyName, jobCode, jobURL, description }: TFindReferralPost) => { + return request.post( + "/posts/find-referrer", + { + companyName, + jobCode, + jobURL, + description, + }, + { + headers: { + Authorization: session?.user?.refresh_token && `Bearer ${session?.user?.refresh_token}`, + }, + } + ); + }, + onSuccess(data, variables) { + sonerToast({ + severity: "success", + title: "Sucess !", + message: data.data.message, + }); + router.push("/home"); + form.reset(); + }, + onError(error, variables, context) { + ///@ts-expect-error + setError(error?.response.data.message); + sonerToast({ + severity: "error", + title: "Error !", + ///@ts-expect-error + message: error?.response.data.message, + }); + }, }); async function onSubmit(values: z.infer) { - console.log(values); - form.reset(); + mutate({ + companyName: values.companyName, + jobCode: values.jobCode, + jobURL: values.jobURL, + description: values.description, + }); } + + const saveDraft = async () => { + const data = form.getValues(); + if (draftId) { + try { + // Fetch existing draft by ID + const existingDraft = await getByID(draftId); + + if (existingDraft) { + // Update the draft if it exists + update({ + //@ts-ignore + ...existingDraft, + body: { ...data, postType: "Find Referrer", updatedAt: new Date().toDateString() }, + }) + // .then(() => console.log("Draft updated")) + .catch(console.error); + + sonerToast({ + severity: "success", + title: "Sucess !", + message: "Your draft has been sucessfully updated", + }); + } else { + sonerToast({ + severity: "error", + title: "Sucess !", + message: "Draft not found for update!", + }); + } + } catch (error) { + sonerToast({ + severity: "error", + title: "Sucess !", + message: "Error fetching draft:", + }); + console.error("Error fetching draft:", error); + } + } else { + // Add a new draft if no ID is available + add({ body: { ...data, postType: "Find Referrer", updatedAt: new Date().toDateString() } }) + .then((id) => { + console.log("Draft added with ID:", id); + setDraftId(id); // Save the ID for future updates + }) + .catch(console.error); + + sonerToast({ + severity: "success", + title: "Sucess !", + message: "Your post has been sucessfully added to draft", + }); + } + }; + return ( -
-
-

Find referrer

-

- Enter the details for the company you would like to referred to. You can add a unique bio and - important links to ttract referrers. -

- -
-
- - {/* Company */} + + +
+
+

Find referrer

+

+ Enter the details for the company you would like to referred to. You can add a unique bio and + important links to ttract referrers. +

+
+
+ + +
+
+
+ {/* Conpany Name */} ( - Company + Conpany Name + - Select the company you like to get referral. {/* */} Required + Select the Conpany Name. )} /> - {/* Job Code */} + {/* Job URL */} ( - - Job Code - - + Job URL - + - Job codes are individualized sets of numbers assigned to different jobs in order to identify - which class a position belongs to. + Please visit the companies career site and find + the find you would like to refer to. Once you find the job you are interested in, copy the + url and paste it here. )} /> - {/* Job URL */} + {/* Job Code */} ( - Job URL + Job Code - + - Please visit the companies career site and find - the find you would like to refer to. Once you find the job you are interested in, copy the - url and paste it here. + Job codes are individualized sets of numbers assigned to different jobs in order to identify + which class a position belongs to. )} @@ -139,20 +261,29 @@ export default function FindReferrer() { Write a description -