Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions app/api/comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"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.

.

try {
const response = await instance.get(`/comments?postId=${postId}`);

return response.data;
} catch (error) {
return { message: "Failed to fetch comments" };
}
}
68 changes: 68 additions & 0 deletions app/api/posts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"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 fetchFilteredPosts(userId, currentPage, query) {
try {
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
const response = await 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

);

return response.data;
} catch (error) {
return { message: "Failed to fetch posts" };
}
}

export async function fetchPostById(id) {
try {
const response = await instance.get(`/posts/${id}`);
return response?.data;
} catch (error) {
return { message: "Failed to fetch post" };
}
}

export async function createPost(data, userId) {
try {
await instance.post("/posts", { ...data, userId });

Choose a reason for hiding this comment

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

Pass the payload from the component which is calling this function

} catch (error) {
return { message: "Failed to create post" };
}
revalidatePath(`/users/posts/${userId}`);
redirect(`/users/posts/${userId}`);
}

export async function editPost(id, data, userId) {
try {
await instance.put(`/posts/${id}`, { ...data, userId });
} catch (error) {
return { message: "Failed to edit post" };
}
revalidatePath(`/users/posts/${userId}`);

Choose a reason for hiding this comment

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

this logic should be in the case of API success

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

export async function deletePost(id, userId) {
try {
await instance.delete(`/posts/${id}`);
revalidatePath(`/users/posts/${userId}`);

return { message: "Deleted post." };
} catch (error) {
return { message: "Failed to delete post" };
}
}

export async function fetchPostsPages(userId, query) {
try {
const response = await instance.get(`/posts?userId=${userId}&q=${query}`);

Choose a reason for hiding this comment

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

Try to make the number of pages in the same API call when fetching posts for users


return Math.ceil(response.data.length / ITEMS_PER_PAGE);
} catch (error) {
return { message: "Failed to fetch post pages" };
}
}
63 changes: 63 additions & 0 deletions app/api/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"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";
const ITEMS_PER_PAGE = 10;

export async function fetchUsers(query) {
try {
const response = await instance.get(`/users?q=${query}`);
return response.data;
} catch (error) {
return { message: "Failed to fetch users" };
}
}

export async function fetchUsersPages() {
try {
const response = await instance.get("/users");
return Math.ceil(response.data.length / ITEMS_PER_PAGE);
} catch (error) {
return { message: "Failed to fetch users pages" };
}
}

export async function createUser(data) {
try {
await instance.post("/users", data);
} catch (error) {
return { message: "Failed to create user" };
}
revalidatePath("/users");
redirect("/users");
}

export async function fetchUserById(id) {
try {
const response = await instance.get(`/users/${id}`);
return response?.data;
} catch (error) {
return { message: "Failed to fetch user" };
}
}

export async function editUser(id, data) {
try {
await instance.put(`/users/${id}`, data);
} catch (error) {
return { message: "Failed to edit user" };
}
revalidatePath("/users");
redirect("/users");
}

export async function deleteUser(id) {
try {
await instance.delete(`/users/${id}`);

revalidatePath("/users");
return { message: "Deleted User." };
} catch (error) {
return { message: "Failed to delete user" };
}
}
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>
);
}
36 changes: 36 additions & 0 deletions app/components/navLinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

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

const links = [{ name: "Users", href: "/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>
);
})}
</>
);
}
26 changes: 26 additions & 0 deletions app/components/shared/Breadcrumbs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { clsx } from "clsx";
import Link from "next/link";
import { lusitana } from "../fonts";

export default 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>
);
}
108 changes: 108 additions & 0 deletions app/components/shared/Pagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"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 { 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) => {
let position = "first" | "last" | "single" | "middle";

Choose a reason for hiding this comment

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

remove the computation from here


if (index === 0) position = "first";
if (index === allPages.length - 1) position = "last";
if (allPages.length === 1) position = "single";
if (page === "...") position = "middle";

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";

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

const handleSearch = useDebouncedCallback((term) => {
const params = new URLSearchParams(searchParams);
params.set("page", "1");

Choose a reason for hiding this comment

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

make the constants for strings

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={(e) => {
handleSearch(e.target.value);

Choose a reason for hiding this comment

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

move this logic to a separate function

}}
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>
);
}
Loading