diff --git a/backend/src/db/migrations/20260214120000-grant-public-read-books.js b/backend/src/db/migrations/20260214120000-grant-public-read-books.js
new file mode 100644
index 0000000..04a70e1
--- /dev/null
+++ b/backend/src/db/migrations/20260214120000-grant-public-read-books.js
@@ -0,0 +1,38 @@
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ const [roles] = await queryInterface.sequelize.query(
+ "SELECT id FROM roles WHERE name = 'Public' LIMIT 1;"
+ );
+ const publicRoleId = roles[0]?.id;
+
+ if (!publicRoleId) return;
+
+ const [permissions] = await queryInterface.sequelize.query(
+ "SELECT id, name FROM permissions WHERE name IN ('READ_BOOKS', 'READ_AUTHORS', 'READ_CATEGORIES');"
+ );
+
+ const createdAt = new Date();
+ const updatedAt = new Date();
+
+ const rolePermissions = permissions.map(p => ({
+ roles_permissionsId: publicRoleId,
+ permissionId: p.id,
+ createdAt,
+ updatedAt
+ }));
+
+ // Using queryInterface.bulkInsert with raw query or ensuring table name is correct
+ for (const rp of rolePermissions) {
+ await queryInterface.sequelize.query(
+ 'INSERT INTO "rolesPermissionsPermissions" ("roles_permissionsId", "permissionId", "createdAt", "updatedAt") VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING',
+ {
+ replacements: [rp.roles_permissionsId, rp.permissionId, rp.createdAt, rp.updatedAt],
+ type: Sequelize.QueryTypes.INSERT
+ }
+ );
+ }
+ },
+
+ async down(queryInterface, Sequelize) {
+ }
+};
diff --git a/backend/src/index.js b/backend/src/index.js
index c3558d2..75a49bc 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -125,13 +125,13 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
-app.use('/api/authors', passport.authenticate('jwt', {session: false}), authorsRoutes);
+app.use('/api/authors', authorsRoutes);
app.use('/api/publishers', passport.authenticate('jwt', {session: false}), publishersRoutes);
-app.use('/api/categories', passport.authenticate('jwt', {session: false}), categoriesRoutes);
+app.use('/api/categories', categoriesRoutes);
-app.use('/api/books', passport.authenticate('jwt', {session: false}), booksRoutes);
+app.use('/api/books', booksRoutes);
app.use('/api/discount_codes', passport.authenticate('jwt', {session: false}), discount_codesRoutes);
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index 89767ec..ac7f18c 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -26,7 +26,14 @@ trailingSlash: true,
},
],
},
-
+ async rewrites() {
+ return [
+ {
+ source: '/book/:id',
+ destination: '/public/book-details?id=:id',
+ },
+ ]
+ },
}
export default nextConfig
\ No newline at end of file
diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index 72935e6..4ced3eb 100644
--- a/frontend/src/components/NavBarItem.tsx
+++ b/frontend/src/components/NavBarItem.tsx
@@ -1,6 +1,5 @@
-import React, {useEffect, useRef} from 'react'
+import React, {useEffect, useRef, useState} from 'react'
import Link from 'next/link'
-import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
@@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
}
return
{NavBarItemComponentContents}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx
index 1b9907d..26c3572 100644
--- a/frontend/src/layouts/Authenticated.tsx
+++ b/frontend/src/layouts/Authenticated.tsx
@@ -1,5 +1,4 @@
-import React, { ReactNode, useEffect } from 'react'
-import { useState } from 'react'
+import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
@@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
)
-}
+}
\ No newline at end of file
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 6aede5e..72f35e9 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,199 @@
-
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
-import BaseButton from '../components/BaseButton';
-import CardBox from '../components/CardBox';
-import SectionFullScreen from '../components/SectionFullScreen';
+import axios from 'axios';
+import { mdiMagnify, mdiBookOpenVariant, mdiCart, mdiChevronRight, mdiStar } from '@mdi/js';
+import BaseIcon from '../components/BaseIcon';
import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
-import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
+export default function LandingPage() {
+ const [books, setBooks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchQuery, setSearchQuery] = useState('');
-export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('video');
- const [contentPosition, setContentPosition] = useState('left');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
- const title = 'eBook Web Store'
-
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
-
- const imageBlock = (image) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
-
)
- }
+ useEffect(() => {
+ const fetchBooks = async () => {
+ try {
+ // Fetch featured books or just first 8 books
+ const response = await axios.get('/books', {
+ params: { limit: 8, offset: 0 }
+ });
+ setBooks(response.data.rows || []);
+ } catch (error) {
+ console.error('Failed to fetch books:', error);
+ } finally {
+ setLoading(false);
+ }
};
+ fetchBooks();
+ }, []);
return (
-
+
-
{getPageTitle('Starter Page')}
+
{getPageTitle('Home')}
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
-
-
-
-
-
-
+ {/* Navigation */}
+
-
-
-
© 2026 {title}. All rights reserved
-
- Privacy Policy
-
-
+
+ {/* Hero Section */}
+
+
+ {/* Featured Books */}
+
+
+
Featured Books
+
+ View All
+
+
+
+ {loading ? (
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+ ) : (
+
+ {books.map((book: any) => (
+
+
+
+ {book.cover_images?.[0] ? (
+

+ ) : (
+
+
+
+ )}
+
+ ${book.price}
+
+
+
+
+
+
+ {book.title}
+
+
+
+ {book.author?.name || 'Unknown Author'}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* CTA Section */}
+
+
+
Start Your Reading Journey Today
+
+ Join thousands of readers and get access to the world's best eBooks. Free trial available for new members.
+
+
+
+ Get Started for Free
+
+
+ View Plans
+
+
+
+
+
+ {/* Footer */}
+
);
}
-Starter.getLayout = function getLayout(page: ReactElement) {
+LandingPage.getLayout = function getLayout(page: ReactElement) {
return {page};
-};
-
+};
\ No newline at end of file
diff --git a/frontend/src/pages/public/book-details.tsx b/frontend/src/pages/public/book-details.tsx
new file mode 100644
index 0000000..6a9c829
--- /dev/null
+++ b/frontend/src/pages/public/book-details.tsx
@@ -0,0 +1,212 @@
+import React, { useEffect, useState } from 'react';
+import type { ReactElement } from 'react';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import axios from 'axios';
+import { mdiBookOpenVariant, mdiCart, mdiArrowLeft, mdiStar, mdiFormatListBulleted, mdiCalendar, mdiAccount, mdiTag } from '@mdi/js';
+import BaseIcon from '../../components/BaseIcon';
+import LayoutGuest from '../../layouts/Guest';
+import { getPageTitle } from '../../config';
+import Link from 'next/link';
+
+export default function BookDetailsPage() {
+ const router = useRouter();
+ const { id } = router.query;
+ const [book, setBook] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (id && typeof id === 'string') {
+ const fetchBook = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get(`/books/${id}`);
+ setBook(response.data);
+ } catch (err) {
+ console.error('Failed to fetch book:', err);
+ setError(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchBook();
+ }
+ }, [id]);
+
+ if (loading && id) {
+ return (
+
+ );
+ }
+
+ if (error || (!loading && !book && id)) {
+ return (
+
+
Book not found
+
+ Back to Home
+
+
+ );
+ }
+
+ return (
+
+
+
{getPageTitle(book?.title || 'Book Details')}
+
+
+ {/* Breadcrumbs/Nav */}
+
+
+
+ Back to Catalog
+
+
+
+
+
+
+
+ {/* Left Column: Image */}
+
+
+
+ {book?.cover_images?.[0] ? (
+

+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+
+ Book Details
+
+
+ -
+ ISBN:
+ {book?.isbn || 'N/A'}
+
+ -
+ Language:
+ {book?.language || 'English'}
+
+ -
+ Pages:
+ {book?.page_count || 'N/A'}
+
+ -
+ Format:
+ {book?.format || 'Digital'}
+
+
+
+
+
+
+ {/* Right Column: Info */}
+
+
+
+ {book?.categories?.map((cat: any) => (
+
+ {cat.name}
+
+ ))}
+
+
+
+ {book?.title}
+
+ {book?.subtitle && (
+
{book.subtitle}
+ )}
+
+
+
+
+
+
+
+
Author
+
{book?.author?.name || 'Unknown Author'}
+
+
+
+
+
+
+
+
Published
+
{book?.publication_date ? new Date(book.publication_date).getFullYear() : 'N/A'}
+
+
+
+
+
+
+
+
+
+
Price
+
+ ${book?.sale_price || book?.price}
+ {book?.sale_price && book?.price !== book?.sale_price && (
+ ${book?.price}
+ )}
+
+
+
+
+
+
+ Read Preview
+
+
+
+
+
+
+
+
+ );
+}
+
+BookDetailsPage.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};