From 349af854a4715705736843a0159d1c5f6fa52db7 Mon Sep 17 00:00:00 2001 From: jasondev01 Date: Mon, 11 Sep 2023 02:36:27 +0800 Subject: [PATCH] added all the interim functionality and features, will add more.. --- app/api/auth/token/route.ts | 10 ++ app/api/upload/route.ts | 34 ++++ app/create-project/page.tsx | 23 +++ app/edit-project/[id]/page.tsx | 29 +++ app/layout.tsx | 6 +- app/page.tsx | 73 +++++++- app/profile/[id]/page.tsx | 25 +++ app/project/[id]/page.tsx | 113 ++++++++++++ components/AuthProviders.tsx | 18 +- components/Button.tsx | 33 ++++ components/Categories.tsx | 42 +++++ components/CustomMenu.tsx | 52 ++++++ components/FormField.tsx | 40 +++++ components/LoadMore.tsx | 53 ++++++ components/Modal.tsx | 41 +++++ components/Navbar.tsx | 2 - components/ProfileMenu.tsx | 2 +- components/ProfilePage.tsx | 74 ++++++++ components/ProjectActions.tsx | 69 +++++++ components/ProjectCard.tsx | 86 +++++++++ components/ProjectForm.tsx | 166 +++++++++++++++++ components/RelatedProjects.tsx | 61 +++++++ grafbase/.env | 2 +- grafbase/grafbase.config.ts | 1 - grapql/index.ts | 168 ++++++++++++++++-- lib/actions.ts | 118 +++++++++++- lib/session.ts | 2 +- next.config.js | 12 +- .../{hearth-purple.svg => heart-purple.svg} | 0 public/{hearth-white.svg => heart-white.svg} | 0 public/{hearth.svg => heart.svg} | 0 public/{pencile.svg => pencil.svg} | 0 32 files changed, 1310 insertions(+), 45 deletions(-) create mode 100644 app/api/auth/token/route.ts create mode 100644 app/api/upload/route.ts create mode 100644 app/create-project/page.tsx create mode 100644 app/edit-project/[id]/page.tsx create mode 100644 app/profile/[id]/page.tsx create mode 100644 app/project/[id]/page.tsx create mode 100644 components/Button.tsx create mode 100644 components/Categories.tsx create mode 100644 components/CustomMenu.tsx create mode 100644 components/FormField.tsx create mode 100644 components/LoadMore.tsx create mode 100644 components/Modal.tsx create mode 100644 components/ProfilePage.tsx create mode 100644 components/ProjectActions.tsx create mode 100644 components/ProjectCard.tsx create mode 100644 components/ProjectForm.tsx create mode 100644 components/RelatedProjects.tsx rename public/{hearth-purple.svg => heart-purple.svg} (100%) rename public/{hearth-white.svg => heart-white.svg} (100%) rename public/{hearth.svg => heart.svg} (100%) rename public/{pencile.svg => pencil.svg} (100%) diff --git a/app/api/auth/token/route.ts b/app/api/auth/token/route.ts new file mode 100644 index 0000000..dca65d2 --- /dev/null +++ b/app/api/auth/token/route.ts @@ -0,0 +1,10 @@ +import { getToken } from "next-auth/jwt"; +import { NextRequest, NextResponse } from "next/server"; + +const secret = process.env.NEXTAUTH_SECRET; + +export async function GET(req: NextRequest) { + const token = await getToken({ req, secret, raw: true }); + + return NextResponse.json({ token }, { status: 200 }) +} \ No newline at end of file diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts new file mode 100644 index 0000000..375589a --- /dev/null +++ b/app/api/upload/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from "next/server"; +import { v2 as cloudinary } from 'cloudinary'; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_NAME, + api_key: process.env.CLOUDINARY_KEY, + api_secret: process.env.CLOUDINARY_SECRET, +}); + +export async function POST(req: Request) { + const { path } = await req.json(); + + if(!path) { + return NextResponse.json( + { message: 'Image path is required' }, + { status: 400 } + ) + } + + try { + const options = { + use_filename: true, + unique_filename: false, + overwrite: true, + transformation: [{ width: 1000, height: 752, crop: 'scale' }] + } + + const result = await cloudinary.uploader.upload(path, options) + + return NextResponse.json(result, { status: 200 }) + } catch (error) { + return NextResponse.json({ message: error }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/create-project/page.tsx b/app/create-project/page.tsx new file mode 100644 index 0000000..285c05a --- /dev/null +++ b/app/create-project/page.tsx @@ -0,0 +1,23 @@ +import Modal from "@/components/Modal" +import ProjectForm from "@/components/ProjectForm" +import { getCurrentUser } from "@/lib/session" +import { redirect } from "next/navigation"; + + +const CreateProject = async () => { + const session = await getCurrentUser(); + if (!session) redirect('/') + return ( + +

+ Create a New Project +

+ +
+ ) +} + +export default CreateProject diff --git a/app/edit-project/[id]/page.tsx b/app/edit-project/[id]/page.tsx new file mode 100644 index 0000000..8b0442d --- /dev/null +++ b/app/edit-project/[id]/page.tsx @@ -0,0 +1,29 @@ +import { ProjectInterface } from "@/common.types"; +import Modal from "@/components/Modal" +import ProjectForm from "@/components/ProjectForm" +import { getProjectDetails } from "@/lib/actions"; +import { getCurrentUser } from "@/lib/session" +import { redirect } from "next/navigation"; + + +const EditProject = async ({ params: { id } }: { params: { id: string } }) => { + const session = await getCurrentUser(); + if (!session) redirect('/') + + const result = await getProjectDetails(id) as { project?: ProjectInterface } + + return ( + +

+ Edit Project +

+ +
+ ) +} + +export default EditProject diff --git a/app/layout.tsx b/app/layout.tsx index c1da464..144af73 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,11 +7,7 @@ export const metadata = { description: "Showcase and discover remarkable developer projects", }; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( diff --git a/app/page.tsx b/app/page.tsx index 081c726..ac64993 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,11 +1,76 @@ +import { ProjectInterface } from "@/common.types" +import Categories from "@/components/Categories"; +import LoadMore from "@/components/LoadMore"; +import ProjectCard from "@/components/ProjectCard"; +import { fetchAllProjects } from "@/lib/actions" +type ProjectSearch = { + projectSearch: { + edges: { node: ProjectInterface}[]; + pageInfo: { + hasPreviousPage: boolean + hasNextPage: boolean + startCursor: string + endCursor: string + } + } +} + +type SearchParamms = { + category?: string + endcursor?: string +} + +type Props = { + searchParams: SearchParamms +} + +export const dynamic = 'force-dynamic'; +export const dynamicParams = true; +export const revalidate = 0; + +const Home = async ({ searchParams: { category, endcursor } }: Props) => { + const data = await fetchAllProjects(category, endcursor) as ProjectSearch + + const projectsToDisplay = data?.projectSearch?.edges || [] + + if (projectsToDisplay.length === 0) { + return ( +
+ +

+ No found, go create some first +

+
+ ) + } + + const pagination = data?.projectSearch?.pageInfo -const Home = () => { return (
-

Categories

-

Posts

-

LoadMore

+ +
+ { + projectsToDisplay?.map(({ node }: { node: ProjectInterface }) => ( + + )) + } +
+
) } diff --git a/app/profile/[id]/page.tsx b/app/profile/[id]/page.tsx new file mode 100644 index 0000000..0a27676 --- /dev/null +++ b/app/profile/[id]/page.tsx @@ -0,0 +1,25 @@ +import { UserProfile } from "@/common.types" +import ProfilePage from "@/components/ProfilePage" +import { getUserProjects } from "@/lib/actions" + +type Props = { + params: { + id: string + } +} + +const UserProfile = async ({ params }: Props) => { + const result = await getUserProjects(params.id, 100) as { user: UserProfile } + + if (!result?.user) return

Failed to Fetch User Info

+ + + return ( + + + ) +} + +export default UserProfile diff --git a/app/project/[id]/page.tsx b/app/project/[id]/page.tsx new file mode 100644 index 0000000..e839d9c --- /dev/null +++ b/app/project/[id]/page.tsx @@ -0,0 +1,113 @@ +import Image from "next/image" +import Link from "next/link" + +import { getCurrentUser } from "@/lib/session" +import { getProjectDetails } from "@/lib/actions" +import Modal from "@/components/Modal" +// import ProjectActions from "@/components/ProjectActions" +import RelatedProjects from "@/components/RelatedProjects" +import { ProjectInterface } from "@/common.types" +import ProjectActions from "@/components/ProjectActions" + +const Project = async ({ params: { id } }: { params: { id: string } }) => { + const session = await getCurrentUser() + const result = await getProjectDetails(id) as { project?: ProjectInterface} + + if (!result?.project) return ( +

Failed to fetch project info

+ ) + + const projectDetails = result?.project + + const renderLink = () => `/profile/${projectDetails?.createdBy?.id}` + + return ( + +
+
+ + profile + + +
+

+ {projectDetails?.title} +

+
+ + {projectDetails?.createdBy?.name} + + dot + + {projectDetails?.category} + +
+
+
+ + { + session?.user?.email === projectDetails?.createdBy?.email && ( +
+ +
+ ) + } +
+ +
+ poster +
+ +
+

+ {projectDetails?.description} +

+ +
+ + šŸ–„ Github + + dot + + šŸš€ Live Site + +
+
+ +
+ + + profile image + + +
+ + +
+ ) +} + +export default Project \ No newline at end of file diff --git a/components/AuthProviders.tsx b/components/AuthProviders.tsx index 26d16d6..5a81520 100644 --- a/components/AuthProviders.tsx +++ b/components/AuthProviders.tsx @@ -2,6 +2,7 @@ import { getProviders, signIn } from 'next-auth/react' import { useState, useEffect } from 'react' +import Button from './Button' type Provider = { id: string @@ -29,14 +30,15 @@ const AuthProviders = () => { if(providers) { return (
- {Object.values(providers).map((provider: Provider, i) => ( - - ))} + { + Object.values(providers).map((provider: Provider, i) => ( +
) } diff --git a/components/Button.tsx b/components/Button.tsx new file mode 100644 index 0000000..43c2139 --- /dev/null +++ b/components/Button.tsx @@ -0,0 +1,33 @@ +import { MouseEventHandler } from "react" +import Image from 'next/image' + +type Props = { + title: string + leftIcon?: string | null + rightIcon?: string | null + handleClick?: MouseEventHandler + isSubmitting?: boolean + type?: 'button' | 'submit' + bgColor?: string + textColor?: string +} + +const Button = ({ title, leftIcon, rightIcon, handleClick, isSubmitting, type, bgColor, textColor }: Props) => { + return ( + + ) +} + +export default Button diff --git a/components/Categories.tsx b/components/Categories.tsx new file mode 100644 index 0000000..4ba9830 --- /dev/null +++ b/components/Categories.tsx @@ -0,0 +1,42 @@ +'use client' + +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { categoryFilters } from '@/constants' + +const Categories = () => { + const router = useRouter(); + const pathName = usePathname(); + const searchParamms = useSearchParams(); + + const category = searchParamms.get('category') + + const handleTags = (filter: string) => { + router.push(`${pathName}?category=${filter}`) + } + + return ( +
+
    + { + categoryFilters.map((filter) => ( + + )) + } +
+
+ ) +} + +export default Categories diff --git a/components/CustomMenu.tsx b/components/CustomMenu.tsx new file mode 100644 index 0000000..9f8ab01 --- /dev/null +++ b/components/CustomMenu.tsx @@ -0,0 +1,52 @@ +import { Menu } from '@headlessui/react' +import Image from 'next/image' + +type Props = { + title: string + state: string + filters: Array + setState: (value: string) => void +} + +const CustomMenu = ({ title, state, setState, filters }: Props) => { + return ( +
+ + +
+ + {state || 'Select a category'} + Arrow down + +
+ + {filters.map((tag) => ( + + + + ))} + +
+
+ ) +} + +export default CustomMenu diff --git a/components/FormField.tsx b/components/FormField.tsx new file mode 100644 index 0000000..8d77dc1 --- /dev/null +++ b/components/FormField.tsx @@ -0,0 +1,40 @@ +type Props = { + type?: string + title: string + state: string + placeholder: string + isTextArea?: boolean + setState: (value: string) => void +} + +const FormField = ({ type, title, state, placeholder, isTextArea, setState }: Props) => { + return ( +
+ + + {isTextArea + ? +