87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/components/BookCard.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import Link from "next/link";
3 | import Image from "next/image";
4 |
5 | import BookCover from "./BookCover";
6 | import BookReceipt from "./BookReceipt";
7 |
8 | export const NormalBook = ({
9 | id,
10 | title,
11 | genre,
12 | coverColor,
13 | coverUrl,
14 | }: Book) => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
{title}
22 |
{genre}
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export const BorrowedBook = (props: BorrowedBook) => {
30 | const { id, title, genre, coverColor, coverUrl, borrow } = props;
31 | const { borrowDate, dueDate, returnDate, status } = borrow;
32 |
33 | const daysLeft = dayjs(dueDate).diff(dayjs(), "day");
34 |
35 | const isReturned = status === "RETURNED";
36 | const isOverDue = daysLeft < 0 && status === "BORROWED";
37 |
38 | return (
39 |
40 | {isOverDue && (
41 |
48 | )}
49 |
50 |
51 |
57 |
62 |
63 |
64 |
65 |
{title}
66 |
{genre}
67 |
68 |
69 |
70 |
76 |
77 | Borrowed on {dayjs(borrowDate).format("MMM DD")}
78 |
79 |
80 |
81 |
82 |
83 |
95 |
96 | {isReturned
97 | ? "Returned on " + dayjs(returnDate).format("MMM DD")
98 | : isOverDue
99 | ? "Overdue by " + Math.abs(daysLeft) + " days"
100 | : `${daysLeft} days left to due`}
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | };
112 |
--------------------------------------------------------------------------------
/app/admin/books/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import Link from "next/link";
3 | import Image from "next/image";
4 | import { redirect } from "next/navigation";
5 |
6 | import BookCover from "@/components/BookCover";
7 | import BookVideo from "@/components/BookVideo";
8 | import { Button } from "@/components/ui/button";
9 | import { getBook } from "@/lib/admin/actions/book";
10 |
11 | const Page = async ({ params }: PageProps) => {
12 | const { id } = await params;
13 | const { success, data: book } = (await getBook({ id })) as {
14 | success: boolean;
15 | data: Book;
16 | };
17 |
18 | if (!success) redirect("/404");
19 |
20 | return (
21 | <>
22 |
23 | Go Back
24 |
25 |
26 |
27 |
28 |
34 |
39 |
40 |
41 |
42 |
43 |
Created At:
44 |
50 |
51 | {dayjs(book.createdAt).format("DD/MM/YYYY")}
52 |
53 |
54 |
55 |
56 | {book.title}
57 |
58 |
59 | By {book.author}
60 |
61 |
{book.genre}
62 |
63 |
{book.description}
64 |
65 |
69 |
70 |
77 | Edit Book
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | Summary
86 |
87 | {book.summary
88 | ?.split("\n")
89 | .map((line, index) =>
{line}
)}
90 |
91 |
92 |
93 |
97 |
98 |
99 | >
100 | );
101 | };
102 |
103 | export default Page;
104 |
--------------------------------------------------------------------------------
/components/admin/dialogs/ConfirmationDialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { useState, useTransition } from "react";
5 |
6 | import {
7 | Dialog,
8 | DialogContent,
9 | DialogDescription,
10 | DialogFooter,
11 | DialogHeader,
12 | DialogTitle,
13 | DialogTrigger,
14 | } from "../../ui/dialog";
15 | import { Button } from "../../ui/button";
16 |
17 | import { cn } from "@/lib/utils";
18 | import { toast } from "@/hooks/use-toast";
19 |
20 | interface ConfirmationDialogProps {
21 | variant: "approve" | "deny";
22 | title: string;
23 | description: string;
24 | triggerLabel: string;
25 | onConfirm: () => void;
26 | confirmLabel: string;
27 | iconSrc?: string;
28 | }
29 |
30 | const ConfirmationDialog = ({
31 | variant,
32 | title,
33 | description,
34 | triggerLabel,
35 | onConfirm,
36 | confirmLabel,
37 | iconSrc = "/icons/admin/info.svg",
38 | }: ConfirmationDialogProps) => {
39 | const [isOpen, setIsOpen] = useState(false);
40 | const [isPending, startTransition] = useTransition();
41 |
42 | const isApprove = variant === "approve";
43 |
44 | const handleConfirm = () => {
45 | startTransition(() => {
46 | try {
47 | onConfirm();
48 | toast({
49 | title: "Success",
50 | description: "Your request has been processed successfully.",
51 | variant: "default",
52 | });
53 | } catch (error) {
54 | toast({
55 | title: "Error",
56 | description: "An error occurred while processing your request.",
57 | variant: "destructive",
58 | });
59 | console.log(error);
60 | } finally {
61 | setIsOpen(false);
62 | }
63 | });
64 | };
65 |
66 | return (
67 | setIsOpen(open)}>
68 |
69 |
75 | {triggerLabel}
76 |
77 |
78 |
79 |
80 |
96 |
97 |
98 | {title}
99 |
100 |
101 | {description}
102 |
103 |
104 |
105 |
106 |
117 | {confirmLabel}
118 |
119 |
120 |
121 |
122 | );
123 | };
124 |
125 | export default ConfirmationDialog;
126 |
--------------------------------------------------------------------------------
/app/admin/account-requests/page.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import Link from "next/link";
3 | import Image from "next/image";
4 |
5 | import {
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableHead,
10 | TableHeader,
11 | TableRow,
12 | } from "@/components/ui/table";
13 |
14 | import config from "@/lib/config";
15 | import Pagination from "@/components/Pagination";
16 | import { getUsers } from "@/lib/admin/actions/user";
17 | import AccountConfirmation from "@/components/admin/dialogs/AccountConfirmation";
18 |
19 | const Page = async ({ searchParams }: PageProps) => {
20 | const { query, sort, page } = await searchParams;
21 |
22 | const { data: allRecords, metadata } = await getUsers({
23 | query,
24 | sort,
25 | page,
26 | });
27 |
28 | return (
29 |
30 | Account Registration Requests
31 |
32 |
33 |
34 |
35 |
36 | Name
37 | Date Joined
38 | University ID No
39 | University ID Card
40 | Status
41 | Actions
42 |
43 |
44 |
45 |
46 | {allRecords!?.length > 0 ? (
47 | allRecords!.map(({ user }) => (
48 |
52 |
53 | {user.fullname}
54 |
55 |
56 | {dayjs(user.createdAt).format("MMM DD, YYYY")}
57 |
58 |
59 | {user.universityId}
60 |
61 |
62 |
63 |
67 | View ID Card
68 |
69 |
76 |
77 |
78 | {user.status}
79 |
80 |
81 |
82 |
83 | ))
84 | ) : (
85 |
86 |
87 | No records found
88 |
89 |
90 | )}
91 |
92 |
93 |
94 |
95 |
98 |
99 | );
100 | };
101 |
102 | export default Page;
103 |
--------------------------------------------------------------------------------
/app/api/workflow/borrow-book/route.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import { and, eq } from "drizzle-orm";
3 | import { serve } from "@upstash/workflow/nextjs";
4 |
5 | import { db } from "@/database/drizzle";
6 | import { sendEmail } from "@/lib/workflow";
7 | import { borrowRecords, users, books } from "@/database/schema";
8 |
9 | type BorrowEventData = {
10 | userId: string;
11 | bookId: string;
12 | borrowDate: string;
13 | dueDate: string;
14 | };
15 |
16 | async function getBookDetails(bookId: string) {
17 | const bookDetails = await db
18 | .select()
19 | .from(books)
20 | .where(eq(books.id, bookId))
21 | .limit(1);
22 |
23 | return bookDetails[0];
24 | }
25 |
26 | async function getUserDetails(userId: string) {
27 | const userDetails = await db
28 | .select()
29 | .from(users)
30 | .where(eq(users.id, userId))
31 | .limit(1);
32 |
33 | return userDetails[0];
34 | }
35 |
36 | async function isBookReturned(userId: string, bookId: string) {
37 | const borrowRecord = await db
38 | .select()
39 | .from(borrowRecords)
40 | .where(
41 | and(eq(borrowRecords.userId, userId), eq(borrowRecords.bookId, bookId))
42 | )
43 | .limit(1);
44 |
45 | if (borrowRecord[0].status === "RETURNED") return true;
46 |
47 | return false;
48 | }
49 |
50 | export const { POST } = serve(async (context) => {
51 | const { userId, bookId, borrowDate, dueDate } = context.requestPayload;
52 |
53 | console.log("BORROWING BOOK:", userId, bookId, borrowDate, dueDate);
54 |
55 | const book = await getBookDetails(bookId);
56 | const user = await getUserDetails(userId);
57 |
58 | const { fullname, email } = user;
59 | const { title } = book;
60 |
61 | // calculate difference between borrow date and due date
62 | const diff = dayjs(dueDate).diff(dayjs(borrowDate), "day");
63 |
64 | // Send initial borrow confirmation email
65 | await context.run("send-borrowed-email", async () => {
66 | await sendEmail({
67 | email,
68 | subject: `You borrowed "${title}"!`,
69 | message: `Hi ${fullname},\n\nYou've successfully borrowed the book "${title}". Enjoy your reading! The due date is ${dueDate}.`,
70 | });
71 | });
72 |
73 | // Wait until 1 day before due date to send reminder
74 | await context.sleep("wait-for-1-day-before-due", 60 * 60 * 24 * (diff - 1));
75 |
76 | // Send 1 day before due date reminder email
77 | await context.run("send-reminder-before-due", async () => {
78 | await sendEmail({
79 | email,
80 | subject: `Reminder: "${title}" is due tomorrow!`,
81 | message: `Hi ${fullname},\n\nThis is a reminder that the book "${title}" is due tomorrow. Please return it on time to avoid late fees.`,
82 | });
83 | });
84 |
85 | // Wait until the due date to send the "last day" reminder
86 | await context.sleep("wait-for-due-date", 60 * 60 * 24 * diff);
87 |
88 | // Send final day reminder email
89 | await context.run("send-final-reminder", async () => {
90 | await sendEmail({
91 | email,
92 | subject: `Today is the last day to return "${title}"!`,
93 | message: `Hi ${fullname},\n\nThis is the final reminder that today is the last day to return the book "${title}". Please return it today.`,
94 | });
95 | });
96 |
97 | // Wait until after due date to check if the book has been returned
98 | await context.sleep("wait-for-check-if-returned", 60 * 60 * 24 * (diff + 1));
99 |
100 | // Check if the book has been returned, if not, send overdue email
101 | const isReturned = await isBookReturned(userId, bookId);
102 |
103 | if (!isReturned) {
104 | await context.run("send-overdue-email", async () => {
105 | await sendEmail({
106 | email,
107 | subject: `🚨 Overdue. Return the book "${title}" to avoid charges.`,
108 | message: `Hi ${fullname},\n\nThe book "${title}" is overdue. If you don't return it soon, you will be charged for the late return. Please return it as soon as possible.`,
109 | });
110 | });
111 | }
112 | });
113 |
--------------------------------------------------------------------------------
/app/(root)/my-profile/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { eq } from "drizzle-orm";
3 | import { redirect } from "next/navigation";
4 |
5 | import { auth, signOut } from "@/auth";
6 |
7 | import Avatar from "@/components/Avatar";
8 | import BookList from "@/components/BookList";
9 | import NotFound from "@/components/NotFound";
10 | import { Button } from "@/components/ui/button";
11 |
12 | import { db } from "@/database/drizzle";
13 | import { users } from "@/database/schema";
14 |
15 | import config from "@/lib/config";
16 | import { getBorrowedBooks } from "@/lib/actions/book";
17 |
18 | interface BorrowedBookProps {
19 | data: BorrowedBook[];
20 | success: boolean;
21 | }
22 |
23 | const Page = async () => {
24 | const session = await auth();
25 | if (!session?.user?.id) return;
26 |
27 | const [user] = await db
28 | .select()
29 | .from(users)
30 | .where(eq(users.id, session?.user?.id))
31 | .limit(1);
32 |
33 | if (!user) redirect("/404");
34 |
35 | const { data: borrowedBooks, success } = (await getBorrowedBooks(
36 | session?.user?.id
37 | )) as BorrowedBookProps;
38 |
39 | return (
40 | <>
41 |
42 |
43 |
44 |
47 |
48 |
49 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {user.fullname}
64 |
65 |
{user.email}
66 |
67 |
68 |
69 |
70 |
University
71 |
72 | JS Mastery Pro
73 |
74 |
75 |
76 |
77 |
Student ID
78 |
79 | {user.universityId}
80 |
81 |
82 |
83 |
84 |
90 |
91 |
92 |
93 |
94 | Valid for {new Date().getFullYear()}-
95 | {new Date().getFullYear() + 1} Academic Year
96 |
97 |
98 |
99 |
100 |
111 |
112 |
113 |
114 | {success &&
115 | (borrowedBooks.length > 0 ? (
116 |
121 | ) : (
122 |
126 | ))}
127 |
128 |
129 | >
130 | );
131 | };
132 |
133 | export default Page;
134 |
--------------------------------------------------------------------------------
/app/admin/users/page.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import Link from "next/link";
3 | import Image from "next/image";
4 |
5 | import {
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableHead,
10 | TableHeader,
11 | TableRow,
12 | } from "@/components/ui/table";
13 |
14 | import config from "@/lib/config";
15 | import Pagination from "@/components/Pagination";
16 | import { getUsers } from "@/lib/admin/actions/user";
17 | import Menu from "@/components/admin/Menu";
18 | import { userRoles } from "@/constants";
19 |
20 | const Page = async ({ searchParams }: PageProps) => {
21 | const { query, sort, page } = await searchParams;
22 |
23 | const { data: allRecords, metadata } = await getUsers({
24 | query,
25 | sort,
26 | page,
27 | });
28 |
29 | return (
30 |
31 | All Users
32 |
33 |
34 |
35 |
36 |
37 | Name
38 | Date Joined
39 | Role
40 | University ID No
41 | University ID Card
42 | Books Borrowed
43 | Action
44 |
45 |
46 |
47 |
48 | {allRecords!?.length > 0 ? (
49 | allRecords!.map(({ user, totalBorrowedBooks }) => (
50 |
54 |
55 | {user.fullname}
56 |
57 |
58 | {dayjs(user.createdAt).format("MMM DD, YYYY")}
59 |
60 |
61 |
66 |
67 |
68 |
69 | {user.universityId}
70 |
71 |
72 |
73 |
77 | View ID Card
78 |
79 |
86 |
87 |
88 |
89 | {totalBorrowedBooks}
90 |
91 |
92 |
99 |
100 |
101 | ))
102 | ) : (
103 |
104 |
105 | No records found
106 |
107 |
108 | )}
109 |
110 |
111 |
112 |
113 |
116 |
117 | );
118 | };
119 |
120 | export default Page;
121 |
--------------------------------------------------------------------------------
/lib/actions/book.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import dayjs from "dayjs";
4 | import {
5 | and,
6 | asc,
7 | count,
8 | desc,
9 | eq,
10 | getTableColumns,
11 | ilike,
12 | or,
13 | } from "drizzle-orm";
14 |
15 | import { db } from "@/database/drizzle";
16 | import { books, borrowRecords, users } from "@/database/schema";
17 | import { workflowClient } from "../workflow";
18 | import config from "../config";
19 |
20 | const ITEMS_PER_PAGE = 20;
21 |
22 | export async function borrowBook(params: BorrowBookParams) {
23 | const { userId, bookId } = params;
24 |
25 | try {
26 | const book = await db
27 | .select({
28 | availableCopies: books.availableCopies,
29 | })
30 | .from(books)
31 | .where(eq(books.id, bookId))
32 | .limit(1);
33 |
34 | if (!book.length || book[0].availableCopies <= 0) {
35 | return {
36 | success: false,
37 | error: "Book is not available",
38 | };
39 | }
40 |
41 | const dueDate = dayjs().add(7, "day").toDate().toDateString();
42 |
43 | const record = await db.insert(borrowRecords).values({
44 | userId,
45 | bookId,
46 | dueDate,
47 | status: "BORROWED",
48 | });
49 |
50 | await db
51 | .update(books)
52 | .set({
53 | availableCopies: book[0].availableCopies - 1,
54 | })
55 | .where(eq(books.id, bookId));
56 |
57 | await workflowClient.trigger({
58 | url: `${config.env.prodApiEndpoint}/api/workflow/borrow-book`,
59 | body: {
60 | userId,
61 | bookId,
62 | borrowDate: dayjs().toDate().toDateString(),
63 | dueDate,
64 | },
65 | });
66 |
67 | return {
68 | success: true,
69 | data: JSON.parse(JSON.stringify(record)),
70 | };
71 | } catch (error) {
72 | console.log(error);
73 | return {
74 | success: false,
75 | error: "Error borrowing book",
76 | };
77 | }
78 | }
79 |
80 | export async function getBorrowedBooks(userId: string) {
81 | try {
82 | const borrowedBooks = await db
83 | .select({
84 | ...getTableColumns(books),
85 | borrow: {
86 | ...getTableColumns(borrowRecords),
87 | },
88 | })
89 | .from(borrowRecords)
90 | .innerJoin(books, eq(borrowRecords.bookId, books.id))
91 | .innerJoin(users, eq(borrowRecords.userId, users.id))
92 | .where(eq(borrowRecords.userId, userId))
93 | .orderBy(desc(borrowRecords.borrowDate));
94 |
95 | return {
96 | success: true,
97 | data: JSON.parse(JSON.stringify(borrowedBooks)),
98 | };
99 | } catch (error) {
100 | console.log(error);
101 | return {
102 | success: false,
103 | error: "Error getting borrowed books",
104 | };
105 | }
106 | }
107 |
108 | export async function searchBooks({
109 | query,
110 | sort = "available",
111 | page = 1,
112 | }: {
113 | query?: string;
114 | sort?: string;
115 | page?: number;
116 | }) {
117 | try {
118 | const searchConditions = query
119 | ? or(
120 | ilike(books.title, `%${query}%`),
121 | ilike(books.genre, `%${query}%`),
122 | ilike(books.author, `%${query}%`)
123 | )
124 | : undefined;
125 |
126 | const sortOptions: { [key: string]: any } = {
127 | newest: desc(books.createdAt),
128 | oldest: asc(books.createdAt),
129 | highestRated: desc(books.rating),
130 | available: desc(books.totalCopies),
131 | };
132 |
133 | const sortingCondition = sortOptions[sort] || desc(books.totalCopies);
134 |
135 | const allBooks = await db
136 | .select()
137 | .from(books)
138 | .where(searchConditions)
139 | .orderBy(sortingCondition)
140 | .limit(ITEMS_PER_PAGE)
141 | .offset((page - 1) * ITEMS_PER_PAGE);
142 |
143 | const totalBooks = await db
144 | .select({
145 | count: count(),
146 | })
147 | .from(books)
148 | .where(searchConditions);
149 |
150 | const totalPage = Math.ceil(totalBooks[0].count / ITEMS_PER_PAGE);
151 | const hasNextPage = page < totalPage;
152 |
153 | return {
154 | success: true,
155 | data: JSON.parse(JSON.stringify(allBooks)),
156 | metadata: {
157 | totalPage,
158 | hasNextPage,
159 | },
160 | };
161 | } catch (error) {
162 | console.log(error);
163 | return {
164 | success: false,
165 | error: "Error searching books",
166 | };
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/app/admin/books/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 |
4 | import {
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableHeader,
10 | TableRow,
11 | } from "@/components/ui/table";
12 | import BookCover from "@/components/BookCover";
13 | import { Button } from "@/components/ui/button";
14 | import Pagination from "@/components/Pagination";
15 |
16 | import { getBooks } from "@/lib/admin/actions/book";
17 |
18 | const Page = async ({ searchParams }: PageProps) => {
19 | const { query, sort, page } = await searchParams;
20 |
21 | const { data: allBooks, metadata } = await getBooks({
22 | query,
23 | sort,
24 | page,
25 | });
26 |
27 | return (
28 |
29 |
30 |
All Books
31 |
32 | + Create a New Book
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Book Title
41 | Author
42 | Genre
43 | Date Created
44 | View
45 | Actions
46 |
47 |
48 |
49 |
50 | {allBooks!?.length > 0 ? (
51 | allBooks?.map((book) => (
52 |
53 |
54 |
55 |
60 |
{book.title}
61 |
62 |
63 |
64 | {book.author}
65 |
66 |
67 | {book.genre}
68 |
69 |
70 | Dec 19 2023
71 |
72 |
73 |
74 | View
75 |
76 |
77 |
78 |
79 |
83 |
89 |
90 |
97 |
98 |
99 |
100 | ))
101 | ) : (
102 |
103 |
104 | No records found
105 |
106 |
107 | )}
108 |
109 |
110 |
111 |
112 |
115 |
116 | );
117 | };
118 |
119 | export default Page;
120 |
--------------------------------------------------------------------------------
/components/BookCoverSvg.tsx:
--------------------------------------------------------------------------------
1 | const BookCoverSvg = ({ coverColor }: { coverColor: string }) => {
2 | return (
3 |
12 |
16 |
20 |
24 |
28 |
32 |
36 |
40 |
44 |
48 |
52 |
53 | );
54 | };
55 |
56 | export default BookCoverSvg;
57 |
--------------------------------------------------------------------------------
/app/admin/borrow-records/page.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 |
3 | import {
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableHead,
8 | TableHeader,
9 | TableRow,
10 | } from "@/components/ui/table";
11 | import Avatar from "@/components/Avatar";
12 | import BookCover from "@/components/BookCover";
13 | import Pagination from "@/components/Pagination";
14 | import BookReceipt from "@/components/BookReceipt";
15 |
16 | import Menu from "@/components/admin/Menu";
17 | import { borrowStatuses } from "@/constants";
18 | import { getBorrowRecords } from "@/lib/admin/actions/book";
19 |
20 | const Page = async ({ searchParams }: PageProps) => {
21 | const { query, sort, page } = await searchParams;
22 |
23 | const { data: allRecords, metadata } = await getBorrowRecords({
24 | query,
25 | sort,
26 | page,
27 | });
28 |
29 | return (
30 |
31 | Borrow Book Requests
32 |
33 |
34 |
35 |
36 |
37 | Book Title
38 | User Requested
39 | Borrowed Date
40 | Return Date
41 | Due Date
42 | Status
43 | Receipt
44 |
45 |
46 |
47 |
48 | {allRecords!?.length > 0 ? (
49 | allRecords!.map((record) => (
50 |
54 |
55 |
56 |
61 |
{record.title}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {record.user.fullname}
71 |
72 |
{record.user.email}
73 |
74 |
75 |
76 |
77 |
78 | {dayjs(record.borrow.borrowDate).format("MMM DD, YYYY")}
79 |
80 |
81 | {record.borrow.returnDate
82 | ? dayjs(record.borrow.returnDate).format("MMM DD, YYYY")
83 | : "---"}
84 |
85 |
86 | {dayjs(record.borrow.dueDate).format("MMM DD, YYYY")}
87 |
88 |
89 |
94 |
95 |
96 |
100 |
101 |
102 | ))
103 | ) : (
104 |
105 |
106 | No records found
107 |
108 |
109 | )}
110 |
111 |
112 |
113 |
114 |
117 |
118 | );
119 | };
120 |
121 | export default Page;
122 |
--------------------------------------------------------------------------------
/components/forms/AuthForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | DefaultValues,
5 | FieldValues,
6 | Path,
7 | SubmitHandler,
8 | useForm,
9 | UseFormReturn,
10 | } from "react-hook-form";
11 | import Link from "next/link";
12 | import { ZodType } from "zod";
13 | import { useRouter } from "next/navigation";
14 |
15 | import {
16 | Form,
17 | FormControl,
18 | FormField,
19 | FormItem,
20 | FormLabel,
21 | FormMessage,
22 | } from "@/components/ui/form";
23 | import FileUpload from "../FileUpload";
24 | import { Input } from "@/components/ui/input";
25 | import { Button } from "@/components/ui/button";
26 | import { FIELD_NAMES, FIELD_TYPES } from "@/constants";
27 |
28 | import { toast } from "@/hooks/use-toast";
29 | import { zodResolver } from "@hookform/resolvers/zod";
30 |
31 | interface Props {
32 | schema: ZodType;
33 | defaultValues: T;
34 | onSubmit: (data: T) => Promise<{ success: boolean; error?: string }>;
35 | type: "SIGN_IN" | "SIGN_UP";
36 | }
37 |
38 | const AuthForm = ({
39 | schema,
40 | defaultValues,
41 | onSubmit,
42 | type,
43 | }: Props) => {
44 | const router = useRouter();
45 |
46 | const isSignIn = type === "SIGN_IN";
47 |
48 | const form: UseFormReturn = useForm({
49 | resolver: zodResolver(schema),
50 | defaultValues: defaultValues as DefaultValues,
51 | });
52 |
53 | const handleSubmit: SubmitHandler = async (data) => {
54 | console.log("FORM DATA", data);
55 |
56 | const result = await onSubmit(data);
57 |
58 | if (result.success) {
59 | toast({
60 | title: "Success",
61 | description: isSignIn
62 | ? "You have successfully signed in."
63 | : "You have successfully signed up.",
64 | });
65 |
66 | router.push("/");
67 | } else {
68 | toast({
69 | title: `Error ${isSignIn ? "signing in" : "signing up"}`,
70 | description: result.error,
71 | variant: "destructive",
72 | });
73 | }
74 | };
75 |
76 | return (
77 |
78 |
79 | {isSignIn
80 | ? "Welcome Back to the Bookwise"
81 | : "Create Your Library Account"}
82 |
83 |
84 | {isSignIn
85 | ? "Access the vast collection of resources, and stay updated"
86 | : "Please complete all fields and upload a valid university ID to gain access to the library"}
87 |
88 |
134 |
135 |
136 |
137 | {!isSignIn ? "No account yet? " : "Have an account already? "}
138 |
139 |
143 | {!isSignIn ? "Sign In" : "Sign Up"}
144 |
145 |
146 |
147 | );
148 | };
149 |
150 | export default AuthForm;
151 |
--------------------------------------------------------------------------------
/database/seed.ts:
--------------------------------------------------------------------------------
1 | import { config } from "dotenv";
2 | import ImageKit from "imagekit";
3 | import { drizzle } from "drizzle-orm/neon-http";
4 | import { neon } from "@neondatabase/serverless";
5 |
6 | import { books } from "./schema";
7 |
8 | config({ path: ".env.local" });
9 |
10 | const sql = neon(process.env.DATABASE_URL!);
11 | export const db = drizzle({ client: sql });
12 |
13 | const dummyBooks = [
14 | {
15 | title: "Artificial Intelligence: A Modern Approach",
16 | author: "Stuart Russell and Peter Norvig",
17 | genre: "Artificial Intelligence",
18 | rating: 4,
19 | coverUrl:
20 | "https://m.media-amazon.com/images/I/61nHC3YWZlL._AC_UF1000,1000_QL80_.jpg",
21 | coverColor: "#c7cdd9",
22 | description:
23 | "A leading textbook on artificial intelligence, offering a deep dive into algorithms, machine learning, and robotics, suitable for both beginners and professionals.",
24 | totalCopies: 10,
25 | videoUrl:
26 | "https://www.shutterstock.com/shutterstock/videos/3482284603/preview/stock-footage-new-book-opening-green-screen-k-video-animation-chrome-key.webm",
27 | summary:
28 | "Artificial Intelligence: A Modern Approach is a comprehensive guide to the field of AI, combining foundational concepts with cutting-edge research. The book covers topics like search algorithms, knowledge representation, machine learning, and robotics. \n\nIts clear explanations and practical examples make it a valuable resource for students, researchers, and industry professionals. By bridging theory and application, this book serves as a cornerstone for understanding and advancing AI technologies. \n\nThe book is suitable for both beginners and professionals, offering a deep understanding of the fundamental concepts and applications of AI.",
29 | },
30 | {
31 | title: "Computer Networking: A Top-Down Approach",
32 | author: "James F. Kurose and Keith W. Ross",
33 | genre: "Networking",
34 | rating: 5,
35 | coverUrl:
36 | "https://m.media-amazon.com/images/I/91hg1HHyiWL._AC_UF1000,1000_QL80_.jpg",
37 | coverColor: "#f7a13e",
38 | description:
39 | "A comprehensive introduction to computer networking, using a top-down approach to explain protocols, architecture, and applications.",
40 | totalCopies: 25,
41 | videoUrl:
42 | "https://www.shutterstock.com/shutterstock/videos/1107129903/preview/stock-footage-an-open-book-is-on-fire-big-bright-flame-burning-paper-on-old-publication-in-the-dark-book.webm",
43 | summary:
44 | "'Computer Networking: A Top-Down Approach' provides a thorough and accessible introduction to the world of computer networks. James Kurose and Keith Ross present networking concepts by starting with high-level applications like web browsers and email, gradually moving down to the underlying layers of networking protocols. \n\nThe book covers essential topics such as HTTP, DNS, TCP/IP, and network security. Each chapter includes practical examples, hands-on exercises, and real-world scenarios to help readers grasp complex concepts. The authors also explore emerging trends like cloud computing and the Internet of Things, ensuring that the material remains relevant in a rapidly evolving field. \n\nWhether you're a student, professional, or enthusiast, this book offers a clear and engaging path to understanding the architecture and operation of modern computer networks.",
45 | },
46 | ];
47 |
48 | const imagekit = new ImageKit({
49 | publicKey: process.env.NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY!,
50 | privateKey: process.env.IMAGEKIT_PRIVATE_KEY!,
51 | urlEndpoint: process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!,
52 | });
53 |
54 | async function uploadToImageKit(url: string, fileName: string, folder: string) {
55 | try {
56 | const response = await imagekit.upload({
57 | file: url,
58 | fileName: fileName,
59 | folder,
60 | });
61 | return response.filePath;
62 | } catch (error) {
63 | console.error(`Error uploading ${fileName} to ImageKit:`, error);
64 | throw error;
65 | }
66 | }
67 |
68 | async function seed() {
69 | console.log("Seeding books...");
70 |
71 | try {
72 | for (const book of dummyBooks) {
73 | const coverUrl = await uploadToImageKit(
74 | book.coverUrl,
75 | `${book.title}.jpg`,
76 | "/books/covers"
77 | );
78 |
79 | const videoUrl = await uploadToImageKit(
80 | book.videoUrl,
81 | `${book.title}.mp4`,
82 | "/books/videos"
83 | );
84 |
85 | await db.insert(books).values({
86 | ...book,
87 | coverUrl,
88 | videoUrl,
89 | });
90 |
91 | console.log(`Added book: ${book.title}`);
92 | }
93 |
94 | console.log("Seeding completed successfully.");
95 | } catch (error) {
96 | console.error("Error seeding books:", error);
97 | }
98 | }
99 |
100 | seed();
101 |
--------------------------------------------------------------------------------