Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/api/comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use server";
import instance from "../utils/axios";

export async function fetchCommentsByPostId(postId) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just return the promise from this function and use the try catch block in the component making the API call

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

const response = await instance.get(`/comments?postId=${postId}`);
return response.data;
}
47 changes: 47 additions & 0 deletions app/api/posts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use server";
import instance from "../utils/axios";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
const ITEMS_PER_PAGE = 4;

export async function fetchFilteredPostsAndTotalCount(
userId,
currentPage,
query
) {
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
const [response, allData] = await Promise.all([

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

paginatedPosts, allPosts

instance.get(
`/posts?userId=${userId}&_start=${offset}&_limit=${ITEMS_PER_PAGE}&q=${query}`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use axios query params

),
instance.get(`/posts?userId=${userId}&q=${query}`),
]);

return {
data: response?.data || [],
totalPages: Math.ceil(allData?.data?.length / ITEMS_PER_PAGE),
};
}

export async function fetchPostById(id) {
const response = await instance.get(`/posts/${id}`);
return response?.data;
}

export async function createPost(data) {
await instance.post("/posts", data);
revalidatePath(`/users/posts/${data?.userId}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the redirects from here

redirect(`/users/posts/${data?.userId}`);
}

export async function editPost(id, data) {
await instance.put(`/posts/${id}`, data);

revalidatePath(`/users/posts/${data?.userId}`);
redirect(`/users/posts/${data?.userId}`);
}

export async function deletePost(id, userId) {
await instance.delete(`/posts/${id}`);
revalidatePath(`/users/posts/${userId}`);
}
31 changes: 31 additions & 0 deletions app/api/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use server";
Copy link

@usmannafzal usmannafzal Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this directive

import instance from "../utils/axios";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function fetchUsers(query) {
const response = await instance.get(`/users?q=${query}`);
return response.data;
}

export async function createUser(data) {
await instance.post("/users", data);
revalidatePath("/users");
redirect("/users");
}

export async function fetchUserById(id) {
const response = await instance.get(`/users/${id}`);
return response?.data;
}

export async function editUser(id, data) {
await instance.put(`/users/${id}`, data);
revalidatePath("/users");
redirect("/users");
}

export async function deleteUser(id) {
await instance.delete(`/users/${id}`);
revalidatePath("/users");
}
8 changes: 8 additions & 0 deletions app/components/fonts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Inter } from "next/font/google";
import { Lusitana } from "next/font/google";

export const inter = Inter({ subsets: ["latin"] });
export const lusitana = Lusitana({
subsets: ["latin"],
weight: ["700", "400"],
});
13 changes: 13 additions & 0 deletions app/components/logo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { GlobeAltIcon } from "@heroicons/react/24/outline";
import { lusitana } from "./fonts";

export default function AcmeLogo() {
return (
<div
className={`${lusitana.className} flex flex-row items-center leading-none text-white`}
>
<GlobeAltIcon className="h-12 w-12 rotate-[15deg]" />
<p className="text-[44px]">Demo</p>
</div>
);
}
37 changes: 37 additions & 0 deletions app/components/navLinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { UserGroupIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { usePathname } from "next/navigation";
import clsx from "clsx";
import { urls } from "../utils/constants";

const links = [{ name: "Users", href: urls.USERS, icon: UserGroupIcon }];

export default function NavLinks() {
const pathname = usePathname();

return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className={clsx(
"flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3",
{
"bg-sky-100 text-blue-600":
`/${pathname.split("/")?.[1]}` === link.href,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compute this expression above

}
)}
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}
28 changes: 28 additions & 0 deletions app/components/shared/Breadcrumbs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use server";

import { clsx } from "clsx";
import Link from "next/link";
import { lusitana } from "../fonts";

export default async function Breadcrumbs({ breadcrumbs }) {
return (
<nav aria-label="Breadcrumb" className="mb-6 block">
<ol className={clsx(lusitana.className, "flex text-xl md:text-2xl")}>
{breadcrumbs.map((breadcrumb, index) => (
<li
key={breadcrumb.href}
aria-current={breadcrumb.active}
className={clsx(
breadcrumb.active ? "text-gray-900" : "text-gray-500"
)}
>
<Link href={breadcrumb.href}>{breadcrumb.label}</Link>
{index < breadcrumbs.length - 1 ? (
<span className="mx-3 inline-block">/</span>
) : null}
</li>
))}
</ol>
</nav>
);
}
15 changes: 15 additions & 0 deletions app/components/shared/Button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import clsx from "clsx";

export function Button({ children, className, ...rest }) {
return (
<button
{...rest}
className={clsx(
"flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:bg-blue-600 aria-disabled:cursor-not-allowed aria-disabled:opacity-50",
className
)}
>
{children}
</button>
);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
"use client";

import { useEffect } from "react";

export default function Error({ error, reset }) {
import { SOMETHING_WENT_WRONG, TRY_AGAIN } from "../../utils/constants";
export default function CustomError({ error, reset }) {
useEffect(() => {
console.error(error);
}, [error]);

return (
<main className="flex h-full flex-col items-center justify-center">
<h2 className="text-center">Something went wrong!</h2>
<h2 className="text-center">{SOMETHING_WENT_WRONG}</h2>
<button
className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
onClick={() => reset()}
onClick={reset}
>
Try again
{TRY_AGAIN}
</button>
</main>
);
Expand Down
103 changes: 103 additions & 0 deletions app/components/shared/Pagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";

import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { calculatePosition, generatePagination } from "../../utils";

export default function Pagination({ totalPages }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get("page")) || 1;

const createPageURL = (pageNumber) => {
const params = new URLSearchParams(searchParams);
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
};

const allPages = generatePagination(currentPage, totalPages);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compute the position here

return (
<>
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>

<div className="flex -space-x-px">
{allPages.map((page, index) => {
const position = calculatePosition(index, allPages.length, page);

return (
<PaginationNumber
key={page}
href={createPageURL(page)}
page={page}
position={position}
isActive={currentPage === page}
/>
);
})}
</div>

<PaginationArrow
direction="right"
href={createPageURL(currentPage + 1)}
isDisabled={currentPage >= totalPages}
/>
</div>
</>
);
}

function PaginationNumber({ page, href, isActive, position }) {
const className = clsx(
"flex h-10 w-10 items-center justify-center text-sm border",
{
"rounded-l-md": position === "first" || position === "single",
"rounded-r-md": position === "last" || position === "single",
"z-10 bg-blue-600 border-blue-600 text-white": isActive,
"hover:bg-gray-100": !isActive && position !== "middle",
"text-gray-300": position === "middle",
}
);

return isActive || position === "middle" ? (
<div className={className}>{page}</div>
) : (
<Link href={href} className={className}>
{page}
</Link>
);
}

function PaginationArrow({ href, direction, isDisabled }) {
const className = clsx(
"flex h-10 w-10 items-center justify-center rounded-md border",
{
"pointer-events-none text-gray-300": isDisabled,
"hover:bg-gray-100": !isDisabled,
"mr-2 md:mr-4": direction === "left",
"ml-2 md:ml-4": direction === "right",
}
);

const icon =
direction === "left" ? (
<ArrowLeftIcon className="w-4" />
) : (
<ArrowRightIcon className="w-4" />
);

return isDisabled ? (
<div className={className}>{icon}</div>
) : (
<Link className={className} href={href}>
{icon}
</Link>
);
}
39 changes: 39 additions & 0 deletions app/components/shared/Search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useDebouncedCallback } from "use-debounce";
import { PAGE, QUERY, SEARCH } from "../../utils/constants";

export default function Search({ placeholder }) {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();

const handleSearch = useDebouncedCallback((e) => {
const term = e.target.value;
const params = new URLSearchParams(searchParams);
params.set(PAGE, "1");
if (term) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (term) params.set(QUERY, term);
else params.delete(QUERY);

params.set(QUERY, term);
} else {
params.delete(QUERY);
}
replace(`${pathname}?${params.toString()}`);
}, 300);

return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
{SEARCH}
</label>
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={handleSearch}
defaultValue={searchParams.get("query")?.toString()}
/>
<MagnifyingGlassIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
);
}
33 changes: 33 additions & 0 deletions app/components/shared/SideBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Link from "next/link";
import { PowerIcon } from "@heroicons/react/24/outline";
import AcmeLogo from "../logo";
import NavLinks from "../navLinks";

export default function SideBar() {
const handleSignOut = async () => {
"use server";
};

return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
<Link
className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
href="/"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a constant here, HOME maybe

>
<div className="w-32 text-white md:w-40">
<AcmeLogo />
</div>
</Link>
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<form action={handleSignOut}>
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<PowerIcon className="w-6" />
<div className="hidden md:block">Sign Out</div>
</button>
</form>
</div>
</div>
);
}
Loading