-
Notifications
You must be signed in to change notification settings - Fork 0
initial commit for demo next js project #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
379e06e
0c19af9
f63e0d6
c93a8fa
84fb096
65d7e74
e899ae2
83ba8e5
c0e5f31
9b2c1f2
be95afe
01bd0f2
9eb7015
7721f9b
ae00036
c9bc92a
44bd253
e313a65
319a78d
9734d26
5111145
c3e1373
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
try { | ||
const response = await instance.get(`/comments?postId=${postId}`); | ||
|
||
return response.data; | ||
} catch (error) { | ||
return { message: "Failed to fetch comments" }; | ||
} | ||
} |
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}` | ||
|
||
); | ||
|
||
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 }); | ||
|
||
} 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}`); | ||
|
||
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}`); | ||
|
||
|
||
return Math.ceil(response.data.length / ITEMS_PER_PAGE); | ||
} catch (error) { | ||
return { message: "Failed to fetch post pages" }; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
"use server"; | ||
|
||
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" }; | ||
} | ||
} |
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"], | ||
}); |
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> | ||
); | ||
} |
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, | ||
|
||
} | ||
)} | ||
> | ||
<LinkIcon className="w-6" /> | ||
<p className="hidden md:block">{link.name}</p> | ||
</Link> | ||
); | ||
})} | ||
</> | ||
); | ||
} |
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> | ||
); | ||
} |
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 |
---|---|---|
@@ -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); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
|
||
|
||
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> | ||
); | ||
} |
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"); | ||
|
||
if (term) { | ||
|
||
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); | ||
|
||
}} | ||
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> | ||
); | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.