Skip to content

Commit

Permalink
Merge branch 'main' of github.com:Javan-Odhiambo/ShareHub_frontend
Browse files Browse the repository at this point in the history
Karume-lab committed Jun 7, 2024
2 parents e9fc495 + bbf602a commit 7da9914
Showing 19 changed files with 385 additions and 180 deletions.
70 changes: 70 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -27,18 +27,21 @@
"@radix-ui/themes": "^3.0.3",
"@reduxjs/toolkit": "^2.2.3",
"@superset-ui/embedded-sdk": "^0.1.0-alpha.11",
"@tanstack/react-query": "^5.40.1",
"async-mutex": "^0.5.0",
"axios": "^1.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"intersection-observer": "^0.12.2",
"lucide-react": "^0.377.0",
"next": "14.2.2",
"next-themes": "^0.3.0",
"radix": "^0.0.0",
"react": "^18.3.1",
"react-dom": "^18",
"react-hook-form": "^7.51.3",
"react-intersection-observer": "^9.10.3",
"react-redux": "^9.1.2",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
1 change: 1 addition & 0 deletions src/app/auth/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -79,6 +79,7 @@ const SignUpPage = () => {
toast({
title: "Registered successfully",
description: "Check your email to activate your account",
duration:10000,
});
})
.catch((error) => {
2 changes: 2 additions & 0 deletions src/app/dashboard/bookmarks/page.tsx
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ const BookmarkPage = () => {
author_avator_image_url={
user.profile_picture || "/profile-svgrepo-com.svg"
}
author_email={user.email}
author_first_name={user.first_name}
author_last_name={user.last_name}
project_title={innovation.title}
@@ -30,6 +31,7 @@ const BookmarkPage = () => {
comments_count={innovation.comments_number}
is_liked={innovation.is_liked}
is_bookmarked={innovation.is_bookmarked}
status={innovation.status}
/>
);
})
2 changes: 1 addition & 1 deletion src/app/dashboard/innovation/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -92,7 +92,7 @@ const InnovationDetailPage = ({ params }: InnovationDetailPageProps) => {
<div className="flex items-center justify-between mt-5 px-4 ">
<div className="flex items-center gap-3">

<CustomAvatar image_url={innovation?.author.profile_picture} first_name={innovation?.author.first_name} last_name={innovation?.author.last_name}></CustomAvatar>
<CustomAvatar image_url={innovation?.author.profile_picture} first_name={innovation?.author.first_name} last_name={innovation?.author.last_name} email={innovation?.author.email}></CustomAvatar>
<div>
<p>{`${innovation?.author.first_name} ${innovation?.author.last_name}`}</p>
<p className="text-sm">{innovation?.author.email}</p>
52 changes: 2 additions & 50 deletions src/app/dashboard/innovation/drafts/page.tsx
Original file line number Diff line number Diff line change
@@ -33,55 +33,6 @@ const DraftsPage = () => {
}, []);
const totalPages = Math.ceil(draftsList?.count / ITEMS_PER_PAGE);

// author
// :
// { id: 2, email: 'javan@mail.com', first_name: '', last_name: '', username: '', … }
// comments_number
// :
// 0
// created_at
// :
// "2024-05-31T08:21:49.783012Z"
// dashboard_definitions
// :
// "http://localhost:8000/media/dashboard_definitions/Useful_links.png"
// dashboard_id
// :
// "undefined"
// dashboard_image
// :
// "http://localhost:8000/media/dashboards/IEEE_voles.png"
// dashboard_link
// :
// "https://blog.logrocket.com/react-hook-form-complete-guide/"
// dashboard_type
// :
// "M"
// description
// :
// "Helloddfdfadf"
// is_bookmarked
// :
// false
// is_liked
// :
// false
// likes_number
// :
// 0
// status
// :
// "D"
// title
// :
// "Another test"
// updated_at
// :
// "2024-05-31T08:21:49.783063Z"
// url
// :
// "http://localhost:8000/api/innovations/2/"

return (
<main>
<section className="flex flex-wrap mx-auto gap-4 p-4">
@@ -99,9 +50,10 @@ const DraftsPage = () => {
}
author_first_name={innovation.author.first_name}
author_last_name={innovation.author.last_name}
author_email={innovation.author.email}
project_title={innovation.title}
project_description={innovation.description}
dashboard_banner_image_url={innovation.banner_image}
dashboard_image_url={innovation.dashboard_image}
likes_count={innovation.likes_number}
comments_count={innovation.comments_number}
is_liked={innovation.is_liked}
2 changes: 2 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -69,13 +69,15 @@ const Home = () => {
author_avator_image_url={innovation.author.profile_picture}
author_first_name={innovation.author.first_name}
author_last_name={innovation.author.last_name}
author_email={innovation.author.email}
project_title={innovation.title}
project_description={innovation.description}
dashboard_image_url={innovation.dashboard_image}
likes_count={innovation.likes_number}
comments_count={innovation.comments_number}
is_liked={innovation.is_liked}
is_bookmarked={innovation.is_bookmarked}
status={innovation.status}
/>
);
})}
109 changes: 109 additions & 0 deletions src/app/dashboard/scroll-test/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";
import React, { useEffect, useState } from "react";
import { useInnovationsFetchManyQuery } from "@/redux/features/innovations/innovationsApiSlice";
import ProjectCard from "@/components/ui/projectcard";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useInView } from "react-intersection-observer";
import { LoaderCircleIcon } from "lucide-react";
// * items per page in a response to calculate the total number of paginations needed at the bottom
const ITEMS_PER_PAGE = 5; // Update this to match the number of items per page in your API

const Home = () => {
const [hasSearchResults, setHasSearchResults] = useState(false);

const { ref, inView } = useInView();

// * fetching all innovations
async function fetchInnovations({ pageParam }: { pageParam: number }) {
const res = await fetch(
`http://localhost:8000/api/innovations/?page=${pageParam}`
);
return res.json();
}

const {
data: innovationDataFetch,
status: innovationStatus,
error: innovationError,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
} = useInfiniteQuery({
queryKey: ["innovations"],
queryFn: fetchInnovations,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.next) {
return allPages.length + 1;
}
},
});

const content = innovationDataFetch?.pages?.flatMap((page: any) => {
return page.results.map((innovation: any, index: number) => {
if (page.results.length == index + 1) {
return (
<ProjectCard
key={innovation.url}
innovation_url={innovation.url}
author_avator_image_url={innovation.author.profile_picture}
author_first_name={innovation.author.first_name}
author_last_name={innovation.author.last_name}
project_title={innovation.title}
project_description={innovation.description}
dashboard_image_url={innovation.dashboard_image}
likes_count={innovation.likes_number}
comments_count={innovation.comments_number}
is_liked={innovation.is_liked}
is_bookmarked={innovation.is_bookmarked}
innerRef={ref}
/>
);
} else {
return (
<ProjectCard
key={innovation.url}
innovation_url={innovation.url}
author_avator_image_url={innovation.author.profile_picture}
author_first_name={innovation.author.first_name}
author_last_name={innovation.author.last_name}
project_title={innovation.title}
project_description={innovation.description}
dashboard_image_url={innovation.dashboard_image}
likes_count={innovation.likes_number}
comments_count={innovation.comments_number}
is_liked={innovation.is_liked}
is_bookmarked={innovation.is_bookmarked}
/>
);
}
});
});

useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);

if (innovationStatus === "pending") {
return <p>Loading...</p>;
}
if (innovationStatus === "error") {
return <p>Error: {innovationError?.message}</p>;
}
if (!hasSearchResults) {
return (
<>
<section className="flex flex-wrap mx-auto gap-4 p-4">
{content}
</section>
{isFetchingNextPage && (
<LoaderCircleIcon size={28} className="animate-spin ml-10" />
)}
</>
);
}
};

export default Home;
35 changes: 18 additions & 17 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "@/styles/css/globals.css";
import { ThemeProvider } from 'next-themes'
import { ThemeProvider } from "next-themes";
import { ReduxPovider } from "@/redux/ReduxProvider";
import { Toaster } from "@/components/ui/toaster";
import TenstackProvider from "@/redux/TenstackProvider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Sharehub",
description: "A sharehub nextjs app",
title: "Sharehub",
description: "A sharehub nextjs app",
};

export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ReduxPovider>
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
</ReduxPovider>
<Toaster/>
</body>
</html>
);
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ReduxPovider>
<TenstackProvider>
<ThemeProvider attribute="class">{children}</ThemeProvider>
</TenstackProvider>
</ReduxPovider>
<Toaster />
</body>
</html>
);
}
2 changes: 2 additions & 0 deletions src/components/SearchComponent.tsx
Original file line number Diff line number Diff line change
@@ -90,13 +90,15 @@ function SearchComponent({
author_avator_image_url={innovation.author.profile_picture}
author_first_name={innovation.author.first_name}
author_last_name={innovation.author.last_name}
author_email={innovation.author.email}
project_title={innovation.title}
project_description={innovation.description}
dashboard_image_url={innovation.dashboard_image}
likes_count={innovation.likes_number}
comments_count={innovation.comments_number}
is_liked={innovation.is_liked}
is_bookmarked={innovation.is_bookmarked}
status={innovation.status}
/>
)
)}
230 changes: 134 additions & 96 deletions src/components/commentsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,106 +1,144 @@
import React, { useState } from 'react'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { useInnovationsCommentsListQuery } from '@/redux/features/innovations/innovationsApiSlice'
import React from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useInView } from "react-intersection-observer";
import {
EllipsisVertical,
Trash2,
} from "lucide-react";
import { PaginationDemo } from "@/components/Pagination";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { EllipsisVertical, Trash2 } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import CustomAvatar from '@/components/ui/custom-avatar';

import CustomAvatar from "@/components/ui/custom-avatar";

type CommentsContainerProps = {
innovationID: string
}
const CommentsContainer = ({ innovationID}: CommentsContainerProps) => {
const commentsPerPage = 5;
const [currentPage, setCurrentPage] = useState(1);

innovationID: string;
};

const {
data: commentData,
isLoading: isGettingComments,
error: errorGettingComments,
} = useInnovationsCommentsListQuery({ id: innovationID, page: currentPage });
console.log(commentData);
const CommentsContainer = ({ innovationID }: CommentsContainerProps) => {
const fetchComments = async ({ pageParam }: { pageParam: number }) => {
const response = await fetch(
`http://localhost:8000/api/innovations/${innovationID}/comments/?page=${pageParam}`
);
if (!response.ok) {
throw new Error("Problem fetching comments");
}
return response.json();
};

// extracting comments from the request
const {
results: comments,
next: nextCommentPage,
previous: previousCommentPage,
count: totalComments,
} = commentData || {};
const {
data: commentData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: isGettingComments,
error: errorGettingComments,
} = useInfiniteQuery({
queryKey: ["comments", innovationID],
queryFn: ({ pageParam = 1 }) => fetchComments({ pageParam }),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.next) {
return allPages.length + 1;
}
},
});

const totalPages =
totalComments && Math.ceil(totalComments / commentsPerPage);

// * handling pagination
const handlePrevious = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const { ref, inView } = useInView();

const handleNext = () => {
if (totalPages && currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
return isGettingComments ? (
<div>Loading...</div>
) : errorGettingComments ? (
<div>Something went wrong</div>
) : (

<section className="p-7 space-y-4">
{comments?.map((comment, index) => (
<div className="border p-3 rounded-lg shadow-md" key={index}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CustomAvatar image_url={comment.author.profile_picture} first_name={comment.author.first_name} last_name={comment.author.last_name}></CustomAvatar>
<div>
<p>
{comment.author.first_name} {comment.author.last_name}
</p>
<p className="text-sm">{comment.author.email}</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="hover:bg-accent rounded-full p-1">
<EllipsisVertical />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
{/* <DropdownMenuItem>
<SquarePen className="mr-2 h-4 w-4" />
// TODO: Comment Editing
<Link href={""}>
<span>Edit</span>
</Link>
</DropdownMenuItem> */}
<DropdownMenuItem className="hover:bg-destructive active:bg-destructive focus:bg-destructive hover:text-white active:text-white focus:text-white">
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator className="my-3" />
<p>{comment.text}</p>
</div>
))}
<PaginationDemo
currentPage={currentPage}
totalPages={totalPages ?? 0}
onPrevious={handlePrevious}
onNext={handleNext}
/>
</section>
)
}
React.useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);

return isGettingComments ? (
<div>Loading...</div>
) : errorGettingComments ? (
<div>Something went wrong</div>
) : (
<section className="p-7 space-y-4" ref={ref}>
{commentData?.pages.flatMap((page) =>
page.results.map((comment: any, index: number) => {
if (page.results.length === index + 1) {
return (
<div
className="border p-3 rounded-lg shadow-md"
key={index}
ref={ref}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CustomAvatar
image_url={comment.author.profile_picture}
first_name={comment.author.first_name}
last_name={comment.author.last_name}
></CustomAvatar>
<div>
<p>
{comment.author.first_name} {comment.author.last_name}
</p>
<p className="text-sm">{comment.author.email}</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="hover:bg-accent rounded-full p-1">
<EllipsisVertical />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem className="hover:bg-destructive active:bg-destructive focus:bg-destructive hover:text-white active:text-white focus:text-white">
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator className="my-3" />
<p>{comment.text}</p>
</div>
);
} else {
return (
<div className="border p-3 rounded-lg shadow-md" key={index}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CustomAvatar
image_url={comment.author.profile_picture}
first_name={comment.author.first_name}
last_name={comment.author.last_name}
></CustomAvatar>
<div>
<p>
{comment.author.first_name} {comment.author.last_name}
</p>
<p className="text-sm">{comment.author.email}</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="hover:bg-accent rounded-full p-1">
<EllipsisVertical />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem className="hover:bg-destructive active:bg-destructive focus:bg-destructive hover:text-white active:text-white focus:text-white">
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator className="my-3" />
<p>{comment.text}</p>
</div>
);
}
})
)}
</section>
);
};

export default CommentsContainer
export default CommentsContainer;
15 changes: 8 additions & 7 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -26,12 +26,13 @@ interface headerProps {
avatarUrl?: string;
firstName?: string;
lastName?: string;
email?: string;
handleLogoutClick?: () => void;
handleEditClick?: () => void;

}

const Header = ({ avatarUrl, firstName, lastName, handleLogoutClick, handleEditClick }: headerProps) => {
const Header = ({ avatarUrl, firstName, lastName, email, handleLogoutClick, handleEditClick }: headerProps) => {
return (
<header className="flex h-14 items-center gap-4 border-b bg-muted/40 px-4 lg:h-[60px] lg:px-6">
<Sheet>
@@ -63,12 +64,12 @@ const Header = ({ avatarUrl, firstName, lastName, handleLogoutClick, handleEditC
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="h-10 w-10">
<AvatarImage src={avatarUrl} />
<AvatarFallback className="p-2">
{get_fallback_name(firstName, lastName)}
</AvatarFallback>
</Avatar>
<Avatar className="h-10 w-10">
<AvatarImage src={avatarUrl} />
<AvatarFallback className="p-2">
{get_fallback_name(firstName, lastName, email)}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
2 changes: 1 addition & 1 deletion src/components/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ const Sidebar = () => {
<div className="hidden border-r bg-muted/40 md:block">
<div className="flex h-full max-h-screen flex-col gap-2">
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
<Link href="/" className="font-semibold">
<Link href="/dashboard" className="font-semibold">
<span className="">ShareHub</span>
</Link>
</div>
2 changes: 2 additions & 0 deletions src/components/sidebarNav.tsx
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import {
ShoppingCart,
Users,
Bookmark,
SquarePen,
Plus
} from "lucide-react"

@@ -40,6 +41,7 @@ const SidebarNav = () => {
},
{
href: "/dashboard/innovation/drafts",
icon: SquarePen,
label: "Drafts"
}
]
4 changes: 3 additions & 1 deletion src/components/ui/custom-avatar.tsx
Original file line number Diff line number Diff line change
@@ -8,18 +8,20 @@ type CustomAvatarProps = {
image_url?: string;
first_name?: string;
last_name?: string;
email?: string;
};
const CustomAvatar = ({
className,
image_url,
first_name,
last_name,
email,
}: CustomAvatarProps) => {
return (
<Avatar className={cn("h-12 w-12", className)}>
<AvatarImage src={image_url} />
<AvatarFallback className="p-2">
{get_fallback_name(first_name, last_name)}
{get_fallback_name(first_name, last_name, email)}
</AvatarFallback>
</Avatar>
);
12 changes: 8 additions & 4 deletions src/components/ui/projectcard.tsx
Original file line number Diff line number Diff line change
@@ -78,29 +78,33 @@ interface CardProps {
author_avator_image_url?: string;
author_first_name?: string;
author_last_name?: string;
author_email?: string;
project_title: string;
project_description: string;
dashboard_image_url: string;
likes_count: number;
comments_count: number;
is_liked: boolean;
is_bookmarked: boolean;
status: "D" | "P"
status?: "D" | "P",
innerRef?: React.Ref<HTMLDivElement>;
}

const ProjectCard = ({
innovation_url,
author_avator_image_url,
author_first_name,
author_last_name,
author_email,
project_title,
project_description,
dashboard_image_url,
likes_count,
comments_count,
is_liked = false,
is_bookmarked = false,
status
status,
innerRef,
}: CardProps) => {
const innovationId = extractIdFromUrl(innovation_url) as string;

@@ -187,10 +191,10 @@ const ProjectCard = ({
}

return (
<Card className="max-w-[500px]">
<Card className="max-w-[500px]" ref={innerRef}>
<div className="p-0 mx-6 flex justify-between items-center">
<div className="flex items-center gap-2 mt-2">
<CustomAvatar image_url={author_avator_image_url} first_name={author_first_name} last_name={author_last_name}></CustomAvatar>
<CustomAvatar email={author_email} image_url={author_avator_image_url} first_name={author_first_name} last_name={author_last_name}></CustomAvatar>
<Link href="/dashboard">
{author_first_name} {author_last_name}
</Link>
2 changes: 1 addition & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ interface TInnovation {
updated_at: string;
is_liked: boolean;
is_bookmarked: boolean;
status: string;
status: "D" | "P";
dashboard_type: string;
embed_id: string;
// category: string;
5 changes: 3 additions & 2 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

export function get_fallback_name(first_name: string | undefined, last_name: string | undefined) {
return `${first_name && first_name[0].toUpperCase()} ${last_name && last_name[0].toUpperCase()}`
export function get_fallback_name(first_name: string | undefined, last_name: string | undefined, email: string | undefined) {
if (!first_name && !last_name) return email && (email[0] + email[1]).toUpperCase()
return `${first_name && first_name[0].toUpperCase()}${last_name && last_name[0].toUpperCase()}`
}
15 changes: 15 additions & 0 deletions src/redux/TenstackProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client"
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import React from "react";

const queryClient = new QueryClient();
const TenstackProvider = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

export default TenstackProvider;

0 comments on commit 7da9914

Please sign in to comment.