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/data-table-pagination.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from "@tanstack/react-table"
2 | import {
3 | ChevronLeft,
4 | ChevronRight,
5 | ChevronsLeft,
6 | ChevronsRight,
7 | } from "lucide-react"
8 |
9 | import { Button } from "@/components/ui/button"
10 | import {
11 | Select,
12 | SelectContent,
13 | SelectItem,
14 | SelectTrigger,
15 | SelectValue,
16 | } from "@/components/ui/select"
17 |
18 | interface DataTablePaginationProps {
19 | table: Table
20 | }
21 |
22 | export function DataTablePagination({
23 | table,
24 | }: DataTablePaginationProps) {
25 | return (
26 |
27 |
28 | {table.getFilteredSelectedRowModel().rows.length} of{" "}
29 | {table.getFilteredRowModel().rows.length} row(s) selected.
30 |
31 |
32 |
33 | Rows per page
34 |
51 |
52 |
53 | Page {table.getState().pagination.pageIndex + 1} of{" "}
54 | {table.getPageCount()}
55 |
56 |
57 |
66 |
75 |
84 |
93 |
94 |
95 |
96 | )
97 | }
98 |
--------------------------------------------------------------------------------
/components/add-category-dialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
3 | import { Form, FormControl, FormField, FormItem, FormLabel } from './ui/form'
4 | import { zodResolver } from '@hookform/resolvers/zod'
5 | import { z } from 'zod'
6 | import { useForm } from 'react-hook-form'
7 | import { Input } from './ui/input'
8 | import { Button } from './ui/button'
9 | import { addCategory, updateCategory } from '@/actions/actions'
10 | import { usePathname } from 'next/navigation'
11 | import { useToast } from '@/hooks/use-toast'
12 | import { Category } from '@/app/(admin)/admin/categories/columns'
13 | type Props = {
14 | open: boolean,
15 | setOpen: React.Dispatch>,
16 | category?: Category
17 | }
18 |
19 | const formSchema = z.object({
20 | id: z.number().default(-1),
21 | name: z.string().min(2, {
22 | message: 'Category must be entered'
23 | }).max(20)
24 | })
25 |
26 | function AddCategoryDialog({ setOpen, open, category }: Props) {
27 | const { toast } = useToast()
28 | const path = usePathname()
29 |
30 | const form = useForm>({
31 | resolver: zodResolver(formSchema),
32 | defaultValues: { name: '' }
33 | })
34 |
35 | useEffect(() => {
36 | if (category) {
37 | form.setValue('id', category.category_id)
38 | form.setValue('name', category.category_name)
39 | }
40 | }, [category, form])
41 |
42 | const onSubmit = async (values: z.infer) => {
43 | try {
44 | let message = 'Category has been saved'
45 | if (category) {
46 | await updateCategory(category.category_id, values.name, path)
47 | message = "category updated"
48 | } else {
49 | await addCategory(values.name, path)
50 | }
51 |
52 | toast({
53 | description: message
54 | })
55 | form.reset()
56 |
57 | } catch(error) {
58 | console.log(error)
59 | toast({
60 | description: 'Failed to perform action',
61 | })
62 | }
63 |
64 | }
65 |
66 | return (
67 |
93 | )
94 | }
95 |
96 | export default AddCategoryDialog
--------------------------------------------------------------------------------
/components/member-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from './ui/sidebar'
3 | import { BookCheck, Home, IdCard, Library, MapIcon, PartyPopper, Settings, Stamp } from 'lucide-react'
4 | import Link from 'next/link'
5 | import UserButton from './user-button'
6 |
7 | // Menu items.
8 | const menu_items = [
9 | {
10 | title: "Home",
11 | url: "/",
12 | icon: Home,
13 | },
14 | {
15 | title: "Catalog",
16 | url: "/cataloge",
17 | icon: Library,
18 | },
19 | {
20 | title: "Locations",
21 | url: "/locations",
22 | icon: MapIcon,
23 | },
24 | {
25 | title: "Activities",
26 | url: "/activities",
27 | icon: PartyPopper,
28 | },
29 | {
30 | title: "Library resources",
31 | url: "#",
32 | icon: Settings,
33 | },
34 | ]
35 |
36 | const resources_items = [
37 | {
38 | title: "Library card",
39 | url: "/",
40 | icon: IdCard,
41 | },
42 | {
43 | title: "Equipment rental",
44 | url: "/equipment-rental",
45 | icon: Stamp,
46 | },
47 | {
48 | title: "Book a room",
49 | url: "/book-a-room",
50 | icon: BookCheck,
51 | }
52 | ]
53 |
54 | function MemberSidebar() {
55 | return (
56 |
57 |
58 |
59 | The Library
60 |
61 |
62 |
63 | {
64 | menu_items.map(item => (
65 |
66 |
67 |
68 |
69 |
70 | {item.title}
71 |
72 |
73 |
74 | ))
75 | }
76 |
77 |
78 | Library resources
79 |
80 |
81 | {
82 | resources_items.map(item => (
83 |
84 |
85 |
86 |
87 | {item.title}
88 |
89 |
90 |
91 | ))
92 | }
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | )
107 | }
108 |
109 | export default MemberSidebar
--------------------------------------------------------------------------------
/components/checkout.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from '@/auth'
2 | import { prisma } from '@/lib/prisma'
3 | import { formatISBN, getDateWithOffset } from '@/lib/utils'
4 | import { differenceInCalendarDays, format } from 'date-fns'
5 | import Image from 'next/image'
6 | import React from 'react'
7 |
8 | async function Checkout() {
9 | const session = await auth()
10 |
11 | const results = await prisma.borrowings.findMany({
12 | where: {
13 | user_id: session?.user.user_id,
14 | return_date: null
15 | },
16 | include: {
17 | books: {
18 | select: {
19 | name: true,
20 | author: true,
21 | isbn: true,
22 | book_photos: {
23 | select: { url: true }
24 | }
25 | }
26 | }
27 | }
28 | })
29 |
30 | const daysBookDue = (refDate: Date) => {
31 | return differenceInCalendarDays(refDate, new Date())
32 | }
33 |
34 | return (
35 |
36 |
37 | {
38 | results.length > 0 ?
39 | results.map(result => (
40 |
41 |
42 |
43 |
50 |
51 | {result.books?.name}
52 |
53 | By: {result.books.author}
54 |
55 | {formatISBN(result.books.isbn)}
56 |
57 |
58 |
59 |
60 |
61 | {
62 | daysBookDue(result.due_date) < 0 ?
63 |
64 |
65 | {Math.abs(daysBookDue(result.due_date))} days overdue.
66 |
67 |
68 | :
69 |
70 | Due in {daysBookDue(result.due_date)} days.
71 |
72 | }
73 | Checkout date: {format(getDateWithOffset(result.borrow_date), 'MMM dd, yyyy')}
74 | Due date: {format(getDateWithOffset(result.due_date), 'MMM dd, yyyy')}
75 |
76 |
77 | ))
78 | :
79 | No items checked out.
80 | }
81 |
82 | )
83 | }
84 |
85 | export default Checkout
--------------------------------------------------------------------------------
/app/profile/profile-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { State, updateProfile } from '@/actions/actions'
4 | import { Button } from '@/components/ui/button'
5 | import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'
6 | import { Input } from '@/components/ui/input'
7 | import { zodResolver } from '@hookform/resolvers/zod'
8 | import { Eye, EyeOff } from 'lucide-react'
9 | import React, { useActionState, useState } from 'react'
10 | import { useForm } from 'react-hook-form'
11 | import { z } from 'zod'
12 |
13 | const passwordFormSchema = z.object({
14 | old_password: z.string(),
15 | new_password: z.string().min(8)
16 | })
17 |
18 | function ProfileForm() {
19 |
20 | const initialState: State = { message: null }
21 | const [state, formAction] = useActionState(updateProfile, initialState)
22 | const [showPassword, setShowPassword] = useState(false)
23 |
24 | const form = useForm>({
25 | resolver: zodResolver(passwordFormSchema),
26 | defaultValues: {
27 | old_password: '',
28 | new_password: ''
29 | }
30 | })
31 | return (
32 | <>
33 |
86 |
87 | >
88 | )
89 | }
90 |
91 | export default ProfileForm
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/app/(home)/search/page.tsx:
--------------------------------------------------------------------------------
1 | import { prisma } from '@/lib/prisma'
2 | import Image from 'next/image'
3 | import Link from 'next/link'
4 | import React from 'react'
5 |
6 | type ResultType = {
7 | book_id: number
8 | author: string
9 | name: string
10 | book_category_links: {
11 | category_id: number
12 | book_categories: {
13 | category_name: string
14 | }
15 | }[],
16 | book_photos: { url: string }[]
17 | }
18 |
19 | async function SearchPage({
20 | searchParams
21 | }: { searchParams: { query: string, search_by: string } }
22 | ) {
23 | const params = await searchParams
24 | const { query, search_by } = params
25 | let results: ResultType[] = []
26 |
27 | if (search_by === 'title') {
28 | results = await prisma.books.findMany({
29 | where: {
30 | OR: [
31 | { name: { contains: query } },
32 | { name: { startsWith: query } },
33 | { name: { endsWith: query } }
34 | ]
35 | },
36 | include: {
37 | book_photos: {
38 | select: { url: true }
39 | },
40 | book_category_links: {
41 | include: {
42 | book_categories: {
43 | select: { category_name: true }
44 | }
45 | }
46 | }
47 | }
48 | })
49 | } if (search_by === 'category') {
50 | results = await prisma.books.findMany({
51 | where: {
52 | book_category_links: {
53 | some: {
54 | book_categories: {
55 | category_name: { contains: query }
56 | }
57 | }
58 | }
59 | },
60 | include: {
61 | book_photos: {
62 | select: { url: true }
63 | },
64 | book_category_links: {
65 | include: {
66 | book_categories: {
67 | select: { category_name: true }
68 | }
69 | }
70 | }
71 | }
72 | })
73 | }
74 |
75 | return (
76 | <>
77 |
78 | Search results
79 | {
80 | results && results.length > 0 && (
81 | results.map(result => (
82 |
83 | {
84 | result.book_photos &&
85 |
86 |
93 |
94 | }
95 |
96 |
97 | {result.name}
98 | {result.author}
99 |
100 | {
101 | result.book_category_links && result.book_category_links.map(bcl => (
102 |
103 | {bcl.book_categories.category_name}
104 |
105 | ))
106 | }
107 |
108 |
109 |
110 | ))
111 | )
112 | }
113 | {
114 | results && results.length === 0 && No results found
115 | }
116 |
117 | >
118 | )
119 | }
120 |
121 | export default SearchPage
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/app/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import Rating from "@/components/rating";
2 | import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel";
3 | import { prisma } from "@/lib/prisma";
4 | import Image from "next/image";
5 | import Link from "next/link";
6 |
7 | export default async function HomePage() {
8 |
9 | const arrivals = await prisma.books.findMany({
10 | skip: 0,
11 | take: 10,
12 | include: {
13 | book_photos: {
14 | select: { url: true }
15 | }
16 | },
17 | orderBy: {
18 | created_at: 'desc'
19 | }
20 | })
21 |
22 | const recently_reviewed = await prisma.ratings.findMany({
23 | skip: 0,
24 | take: 10,
25 | distinct: ['book_id'],
26 | orderBy: {
27 | created_at: 'desc'
28 | },
29 | include: {
30 | books: {
31 | include: {
32 | book_photos: { select: { url: true } }
33 | }
34 | }
35 | }
36 | })
37 |
38 | const staff_picks = await prisma.staff_picks.findMany({
39 | skip: 0,
40 | take: 10,
41 | distinct: ['book_id'],
42 | include: {
43 | books: {
44 | include: {
45 | book_photos: { select: { url: true } }
46 | }
47 | },
48 | users: {
49 | select: {
50 | name: true
51 | }
52 | }
53 | }
54 | })
55 |
56 | return (
57 | <>
58 |
59 | {/* new arrivals */}
60 |
61 | New arrivals
62 |
69 |
70 | {
71 | arrivals.map(arrival => (
72 |
73 |
74 |
80 |
81 |
82 | ))
83 | }
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {/* recently reviewed */}
93 |
94 | Recently reviewed
95 |
102 |
103 | {
104 | recently_reviewed.map(rr => (
105 |
106 |
107 |
113 |
114 |
115 |
116 | ))
117 | }
118 |
119 |
120 |
121 |
122 |
123 |
124 | {/* staff picks */}
125 |
126 | Staff picks
127 |
134 |
135 | {
136 | staff_picks.map(sp => (
137 |
138 |
139 |
145 |
146 | By: {sp.users.name}
147 |
148 | ))
149 | }
150 |
151 |
152 |
153 |
154 |
155 |
156 | >
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 font-medium transition-colors hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | const CommandDialog = ({ children, ...props }: DialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/components/data-table.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | ColumnDef,
5 | ColumnFiltersState,
6 | flexRender,
7 | getCoreRowModel,
8 | getFilteredRowModel,
9 | getPaginationRowModel,
10 | getSortedRowModel,
11 | PaginationState,
12 | RowData,
13 | SortingState,
14 | useReactTable,
15 | VisibilityState,
16 | } from "@tanstack/react-table"
17 |
18 | import {
19 | Table,
20 | TableBody,
21 | TableCell,
22 | TableHead,
23 | TableHeader,
24 | TableRow,
25 | } from "@/components/ui/table"
26 | import { DataTableViewOptions } from "./data-table-view-options"
27 | import { DataTablePagination } from "./data-table-pagination"
28 | import DataTableFilterInput from "./data-table-input-filter"
29 | import React, { useEffect } from "react"
30 | import { usePathname, useRouter } from "next/navigation"
31 | import { pageSize } from "@/lib/utils"
32 |
33 | declare module '@tanstack/react-table' {
34 | interface TableMeta {
35 | onDelete: (item: TData) => void,
36 | onEdit: (item: TData) => void
37 | }
38 | }
39 |
40 | interface DataTableProps {
41 | columns: ColumnDef[]
42 | data: TData[],
43 | total: number,
44 | filter_column: string,
45 | onRowDelete: (item: TData) => void,
46 | onRowEdit: (item: TData) => void,
47 | }
48 |
49 | export function DataTable({
50 | columns,
51 | data,
52 | total,
53 | filter_column,
54 | onRowDelete,
55 | onRowEdit,
56 | }: DataTableProps) {
57 |
58 | const [sorting, setSorting] = React.useState([])
59 | const [columnFilters, setColumnFilters] = React.useState(
60 | [])
61 | const [columnVisibility, setColumnVisibility] =
62 | React.useState({})
63 | const [pagination, setPagination] = React.useState({
64 | pageIndex: 0,
65 | pageSize: pageSize
66 | })
67 |
68 | const table = useReactTable({
69 | data,
70 | columns,
71 | getCoreRowModel: getCoreRowModel(),
72 | onSortingChange: setSorting,
73 | getSortedRowModel: getSortedRowModel(),
74 | onColumnFiltersChange: setColumnFilters,
75 | onColumnVisibilityChange: setColumnVisibility,
76 | getFilteredRowModel: getFilteredRowModel(),
77 | getPaginationRowModel: getPaginationRowModel(),
78 | onPaginationChange: setPagination,
79 | manualPagination: true,
80 | rowCount: total,
81 | meta: {
82 | onDelete: (item) => onRowDelete(item),
83 | onEdit: (item) => onRowEdit(item)
84 | },
85 | state: {
86 | sorting,
87 | columnFilters,
88 | columnVisibility,
89 | pagination
90 | },
91 | })
92 |
93 | const pathname = usePathname()
94 | const router = useRouter()
95 |
96 | useEffect(() => {
97 |
98 | const page = pagination.pageIndex * pageSize
99 | const take = (pagination.pageIndex * pageSize) + pageSize
100 |
101 | router.push(`${pathname}?page=${page}&limit=${take}`)
102 | }, [pagination, pathname, router])
103 | return (
104 |
105 |
106 |
107 |
108 |
109 |
110 | {table.getHeaderGroups().map((headerGroup) => (
111 |
112 | {headerGroup.headers.map((header) => {
113 | return (
114 |
115 | {header.isPlaceholder
116 | ? null
117 | : flexRender(
118 | header.column.columnDef.header,
119 | header.getContext()
120 | )}
121 |
122 | )
123 | })}
124 |
125 | ))}
126 |
127 |
128 | {table.getRowModel().rows?.length ? (
129 | table.getRowModel().rows.map((row) => (
130 |
134 | {row.getVisibleCells().map((cell) => (
135 |
136 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
137 |
138 | ))}
139 |
140 | ))
141 | ) : (
142 |
143 |
144 | No results.
145 |
146 |
147 | )}
148 |
149 |
150 |
151 |
152 |
153 |
154 | )
155 | }
156 |
--------------------------------------------------------------------------------
|