From 00ba7b33eb555971077905c43499dd1df43d7aad Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 14 Feb 2026 09:16:35 +0000 Subject: [PATCH] [08:43] [prep] Generating schema ... [08:43] [prep] Detecting template and stack ... [08:43] [prep] Generating configuration ... [08:43] [prep] Packaging project files ... [08:43] [net] Queued: creating endpoint [08:43] [db] Queued: initializing workspace [08:43] [build] Queued: configuring app services [08:43] [health] Queued: checking app availability --- .../20260214120000-grant-public-read-books.js | 38 +++ backend/src/index.js | 6 +- frontend/next.config.mjs | 9 +- frontend/src/components/NavBarItem.tsx | 5 +- frontend/src/layouts/Authenticated.tsx | 5 +- frontend/src/pages/index.tsx | 323 ++++++++++-------- frontend/src/pages/public/book-details.tsx | 212 ++++++++++++ 7 files changed, 443 insertions(+), 155 deletions(-) create mode 100644 backend/src/db/migrations/20260214120000-grant-public-read-books.js create mode 100644 frontend/src/pages/public/book-details.tsx 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) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } + 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} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
+ {/* Navigation */} +
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+ + {/* Hero Section */} +
+
+

+ Discover Your Next Favorite Book +

+

+ Access thousands of digital books across all genres. Read anytime, anywhere on any device. +

+
+
+ +
+ setSearchQuery(e.target.value)} + /> + +
+
+
+ + {/* Featured Books */} +
+
+

Featured Books

+ + View All + +
+ + {loading ? ( +
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+ ))} +
+ ) : ( +
+ {books.map((book: any) => ( +
+ +
+ {book.cover_images?.[0] ? ( + {book.title} + ) : ( +
+ +
+ )} +
+ ${book.price} +
+
+ +
+ +

+ {book.title} +

+ +

+ {book.author?.name || 'Unknown Author'} +

+
+
+ + 4.8 +
+ + + +
+
+
+ ))} +
+ )} +
+ + {/* 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 */} +
+
+
+ + eBookStore +
+

+ The best place to find and enjoy your favorite digital books. +

+
+ Privacy Policy + Terms of Service + Contact Us +
+
+ © 2026 eBookStore. All rights reserved. Built with Flatlogic. +
+
+
); } -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.title} + ) : ( +
+ +
+ )} +
+
+ +
+
+

+ + 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'}

+
+
+
+
+ +
+
+

Rating

+

4.8 / 5.0

+
+
+
+ +
+

Description

+
+
+ +
+
+

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}; +};