Skip to content
This repository has been archived by the owner on May 6, 2024. It is now read-only.

Feat/save books #14

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,967 changes: 7,967 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@reduxjs/toolkit": "^2.1.0",
"@t3-oss/env-core": "^0.8.0",
"bun": "^1.0.28",
"cmdk": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -29,32 +30,32 @@
"zod": "^3.22.4"
},
"devDependencies": {
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"@commitlint/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.7.0",
"@types/node": "^20.11.9",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.17",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-simple-import-sort": "^10.0.0",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.0.8",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"postcss": "^8.4.33",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.5",
"standard-version": "^9.5.0",
"@commitlint/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.7.0"
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
58 changes: 50 additions & 8 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,112 @@
// Import global styles to ensure consistent look across the entire application.
import '@/styles/globals.css'

// Import the main page component.
import IndexPage from '@/pages'

// Import context providers for managing global state, authentication, and theming.
import { ClerkProvider } from '@/components/providers/clerk.provider'
import ReduxProvider from '@/components/providers/redux.provider'
import { ThemeProvider } from '@/components/providers/theme.provider'

// Import layout and React essentials.
import { IndexLayout } from '@/pages/+layout'
import React from 'react'
import ReactDOM from 'react-dom/client'

// Import routing components from react-router-dom to manage page routing.
import {
createBrowserRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom'
import CollectionsPage from './pages/collections-page'
import CollectionPage from './pages/collection-page'
import CreateCollectionPopup from './components/CreateCollectionPopup'
import CollectionLayout from './pages/collections.layout'
import UpdateCollectionPopup from './components/UpdateCollectionPopup'

// Define the application's routes using react-router-dom's createBrowserRouter.
// This includes a base route and its children, facilitating nested routing.
export const AppRouter = createBrowserRouter([
{
path: '/',
element: <IndexLayout />,
element: <IndexLayout />, // The layout component wraps around nested routes.
children: [
{
path: '/',
element: <IndexPage />,
element: <IndexPage />, // The main page component to be rendered at the root path.
},
],
},
{
path: '/collections',
element: <CollectionLayout />, // The layout component wraps around nested routes.
children: [
{
path: '/collections',
element: <CollectionsPage />,
},
{
path: '/collections/create',
element: <CreateCollectionPopup />,
},
{
path: '/collections/edit/:slug',
element: <UpdateCollectionPopup />,
},
{
path: '/collections/:slug',
element: <CollectionPage />,
},
],
},
])

// Transform the defined routes into a simpler structure for easy access and manipulation.
// This structure maps parent routes to their children, facilitating navigation logic.
export const AppRoutes = AppRouter.routes.reduce(
(routes: Record<string, string[]> = {}, route: RouteObject) => {
const parentPath = route.path
if (!parentPath) return routes
if (!parentPath) return routes // Skip if the route does not have a path.

const childrenPath: string[] = []
if (route.children?.length) {
for (const childRoute of route.children) {
const childPath = childRoute.path
if (!childPath?.length) continue
if (!childPath?.length) continue // Only add child routes with defined paths.
childrenPath.push(childPath)
}
}

routes[parentPath] = childrenPath
routes[parentPath] = childrenPath // Map parent path to its children paths.
return routes
},
{},
)

// Handle hot module replacement (HMR) for development, ensuring routes are disposed properly.
if (import.meta.hot) import.meta.hot.dispose(() => AppRouter.dispose())

// Define the main App component, wrapping it with various context providers.
// This setup ensures global state, theming, and authentication are available throughout the application.
export const App = () => {
return (
<ReduxProvider>
<ThemeProvider>
<ClerkProvider>
<RouterProvider
<RouterProvider // Provides routing functionality.
router={AppRouter}
fallbackElement={<div>Loading ...</div>}
future={{ v7_startTransition: true }}
fallbackElement={<div>Loading ...</div>} // Displays while routes are loading.
future={{ v7_startTransition: true }} // Enables future react-router-dom features.
/>
</ClerkProvider>
</ThemeProvider>
</ReduxProvider>
)
}

// Render the App component to the DOM, wrapping it in React.StrictMode for highlighting potential problems.
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
Expand Down
26 changes: 26 additions & 0 deletions src/components/BookCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'

interface BookProps {
bookId: number
bookTitle: string
bookAuthor: string
bookUrl: string
}

const BookCard: React.FC<BookProps> = ({ bookTitle, bookAuthor, bookUrl }) => {
return (
<a
href={bookUrl}
target="_blank"
rel="noopener noreferrer"
className="no-underline"
>
<div className="cursor-pointer rounded-lg border p-4 hover:bg-gray-100">
<h2 className="text-lg font-bold">{bookTitle}</h2>
<p className="text-gray-700">{bookAuthor}</p>
</div>
</a>
)
}

export default BookCard
78 changes: 78 additions & 0 deletions src/components/CollectionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Card } from './ui/Card'
import { cn } from '@/utils/dom'
import { useAppDispatch } from '@/data/stores/root'
import { deleteCollectionInStore } from '@/data/stores/collection.slice'

export const CollectionCard = ({
title,
id,
}: {
title: string
id: number
}) => {
const [isOpen, setIsOpen] = useState(false)
const dispatch = useAppDispatch()
const navigate = useNavigate()
const deleteCollection = (e: { preventDefault: () => void }) => {
e.preventDefault()
// delete the collection
dispatch(deleteCollectionInStore(id))
navigate(`/collections`)
}

return (
<Link
to={`/collections/${id}`}
className="group relative m-1 w-1/5 max-w-xs no-underline"
>
<Card
className={cn(
'relative flex h-40 place-content-center place-items-center p-0.5',
'hover:bg-secondary',
'cursor-pointer',
)}
>
<div className="absolute right-0 top-0 p-2">
<button
onClick={(e) => {
e.preventDefault()
setIsOpen(!isOpen)
}}
className="text-gray-600 hover:text-gray-800"
>
<svg
className="h-6 w-6"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 9l-7 7-7-7"></path>
</svg>
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5">
<Link
to={`/collections/edit/${id}`}
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Edit
</Link>
<button
onClick={deleteCollection}
className="block w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100"
>
Delete
</button>
</div>
)}
</div>
<h1 className="text-center">{title}</h1>
</Card>
</Link>
)
}
42 changes: 42 additions & 0 deletions src/components/CreateCollectionPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { addToCollections } from '@/data/stores/collection.slice'
import { useAppDispatch } from '@/data/stores/root'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'

// Popup component for creating a new collection, styled with Tailwind CSS
const CreateCollectionPopup = () => {
const [title, setTitle] = useState('')
const dispatch = useAppDispatch()
const navigate = useNavigate()

const onCreate = () => {
// dispatch redux to add new collection
dispatch(addToCollections(title))
// navigate back to collections
navigate(`/collections`)
}

return (
<div className="fixed left-1/2 top-1/4 z-50 -translate-x-1/2 -translate-y-1/2 transform rounded-lg bg-white p-4 shadow-lg">
<div className="mb-4">
<input
type="text"
placeholder="Collection Title"
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="flex justify-end space-x-2">
<button
className="focus:shadow-outline rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none"
onClick={onCreate}
>
Create
</button>
</div>
</div>
)
}

export default CreateCollectionPopup
Loading
Loading