From 9d0cfc0c9dd5717a5eae53bde85bbe6ed8dd9822 Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Mon, 24 Nov 2025 14:46:08 -0500 Subject: [PATCH 01/51] AI limit v2 --- bookbuddy/frontend/src/Buddy_Recommendation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookbuddy/frontend/src/Buddy_Recommendation.tsx b/bookbuddy/frontend/src/Buddy_Recommendation.tsx index 57a3da7..71d34b4 100644 --- a/bookbuddy/frontend/src/Buddy_Recommendation.tsx +++ b/bookbuddy/frontend/src/Buddy_Recommendation.tsx @@ -54,7 +54,7 @@ function Buddy() { const getUniqueRandomQuestions = async (): Promise => { try { //snags the text file - const res = await fetch("/Questions.txt"); + const res = await fetch("../Questions.txt"); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } From 5a3caf880e70a9f81dd9f02d447c285fcc649c8b Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Mon, 24 Nov 2025 14:59:22 -0500 Subject: [PATCH 02/51] AI limit v3 --- .../src/main/resources/static/Questions.txt | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 bookbuddy/src/main/resources/static/Questions.txt diff --git a/bookbuddy/src/main/resources/static/Questions.txt b/bookbuddy/src/main/resources/static/Questions.txt new file mode 100644 index 0000000..495d3bf --- /dev/null +++ b/bookbuddy/src/main/resources/static/Questions.txt @@ -0,0 +1,166 @@ +Do you want a series or standalone? +What mood are you in? +What inspires you most? +Do you want something familiar or new? +Favorite setting? +Favorite sport? +Do you like to stay at home, go out, or have a mix of the two? +Do you want something written in the past 5 years? +Favorite author? +Which TV show or movie do you wish had a book version? +What is your dream job? +Do you enjoy stories set in the past, present, or future? +If your life were a novel, what genre would it be right now? +If you could be a holiday, which one would you be? +What is your ideal first date activity? +Favorite color? +About how old are you? +Do you prefer worlds set in a dystopia or utopia—or neither? +Hero or villain? +Past or future? +Love or mystery? +City or wilderness? +Real or imaginary settings? +Do you want an educational book? +What age range should the book be targeted at? +What weather do you like to read in? +Do you use emojis when you text? +Apple or Windows computer? +Quickest route or most scenic route? +Do you like being able to relate to your book’s characters? +Are you more a fan of supervised learning or unsupervised learning? +Coffee, tea, or no caffeine? +Would you rather follow a character who changes the world or one who quietly understands it better? +What emotion do you want a book to leave you with when you finish the last page? +What topic, time period, or mystery are you curious about but haven’t explored through reading? +Do you prefer stories told in first, second, or third person? +How important is a satisfying, neat ending versus an ambiguous one? +Do you prefer ensemble casts or a single deeply explored protagonist? +What fictional concept (magic system, technology, etc.) would you most like to see fully explained? +Do you gravitate toward character-driven or plot-driven narratives? +Are you drawn more to stories about creating something or destroying something? +Do you read the last page first or never? +Would you rather read about a “chosen one” who succeeds or a normal person who defies expectations? +How important are accurate historical details in historical fiction? +Do you prefer personal stakes or global stakes? +Do you prefer books with maps or illustrations, or none at all? +Which sense do you associate most with reading (smell, touch, sight, etc.)? +Do you like stark, minimalist cover designs or busy, illustrative ones? +What’s your go-to background noise while reading (silence, music, nature sounds)? +Are you a rereader, or always looking for the next new book? +Do you prefer bright and airy reading locations or dim and cozy ones? +When watching a film adaptation, do you prefer it faithful or creatively different? +Which time of day gives you the best focus (morning, afternoon, evening, night)? +Do you prefer crisp, formal language or casual, conversational prose? +If you had a theme song, what tempo would it be? +Do you prefer muted/earth tones or vibrant/high-contrast colors? +What’s your favorite handwriting style (cursive, print, calligraphy)? +Do you trust institutions (governments, schools, etc.) in fiction? +What belief have you had challenged by something you read? +Do you prefer stories with clear moral lessons or moral ambiguity? +Would you rather master a complex skill or have effortless intuition in everything? +Which fictional character’s philosophy do you align with most? +Do you believe in fate or free will in stories? +What universal truth do you wish more stories acknowledged? +What is the best ending for a memoir (triumphant, reflective, still evolving)? +How do you organize bookshelves (color, author, genre)? +Do you dog-ear pages or use bookmarks? +If you could only use one font forever, what would it be? +Do you listen to audiobooks while doing chores, or only when focused, or not at all? +What’s your preferred reading snack (salty, sweet, savory)? +Do you look up unfamiliar references immediately or keep reading? +Is it better to be the most knowledgeable or the most creative person in the room? +Do you prefer physical books, e-readers, or tablets? +Do you prefer protagonists who start strong or grow significantly? +Are you drawn to magical realism or clearly defined fantasy rules? +Do you like stories that end with major change or quiet resolution? +How much do you enjoy unreliable history or unreliable technology in fiction? +Do you prefer high magic/tech worlds or grounded realism? +What fictional landscape feels most like home (forest, desert, ocean, city)? +Do you prefer stories about building something new or preserving something old? +How do you feel about stories told entirely through documents (letters, emails, reports)? +How do you make decisions: pros/cons list or gut feeling? +Do you like books about isolation or community? +Which historical era do you find most visually interesting? +Do you like it when narrators break the fourth wall? +Are you more interested in why a character acts or what their actions lead to? +Do you prefer tight single-plot narratives or many interwoven subplots? +What fictional object would you want to own? +Do you like characters who are intensely passionate even if it causes problems? +Do you enjoy books that challenge your worldview? +How important is a strong prologue to you? +Do you like witty, quick jokes or long, situational humor? +If you could only listen to one music genre for a year, what would it be? +Do you prefer detailed magical systems or mysterious ones? +Do you like time travel paradox stories? +Do you prefer mystery/suspense or immediate action? +Do you like stories set in extreme weather? +Do you prefer hard sci-fi or soft sci-fi? +Do you prefer reading in the morning, afternoon, or night? +What kind of lighting do you prefer while reading? +Do you like books with a glossary or appendix? +If a character is flawed, do you want to see them redeem themselves? +Are you more interested in beginnings or aftermaths of major events? +Do you like to see characters achieve goals perfectly or imperfectly? +Do you prefer near-future or far-future settings? +Do you like natural-leader protagonists or reluctant followers? +What emotion do you most want a book to evoke? +Do you prefer politics of fictional worlds or daily life of citizens? +Do you prefer complex grammar or simple communication? +Do you prefer emotional impact or intellectual stimulation? +Do you like subtle sequel hints in standalones? +Do you prefer large casts or intimate casts? +What type of non-fiction do you want to read more of? +Do you prefer experts or passionate amateurs as main characters? +Are you drawn to adventure or home-centered stories? +Do you like biased or misleading narrators? +Should stories always teach something new? +What fictional vehicle would you most want to pilot? +Do you prefer interpersonal conflicts or large-scale conflicts? +How important is a book’s title to you? +Do you prefer reading during travel or at home? +Do you like overly optimistic characters or pessimistic ones? +What is your favorite time of year to read? +Do you judge books by their cover? +Do you prefer clear villains or morally gray antagonists? +Do you like long descriptive passages? +Are you drawn to rebellion or order? +Do you prefer older or younger authors? +What is your preferred reading speed? +Do you enjoy experimental page layouts or traditional ones? +Is intricate world-building or atmospheric world-building more important? +Do you like solving mysteries alongside the protagonist? +Do you prefer utopias or dystopias? +Which fictional mentor do you admire most? +Do you like magic that is innate or learned? +Are you drawn to family secrets or societal secrets? +Do you prefer closed endings or ones that imply ongoing struggle? +How do you feel about books with heavy mythological elements? +Do you prefer reading on long trips or short daily sessions? +What’s your ideal drink temperature while reading? +Do you like high-tech futures or de-industrialized societies? +Is motivation or success more important in a character? +Do you prefer long novels or short ones? +Do you enjoy non-linear timelines? +What’s your favorite weather for indoor reading? +Do you prefer royalty/nobility stories or common-folk stories? +Do you like books that teach a skill? +Are you interested in time-travel science or emotional impact? +Do you prefer exceptionally skilled characters or average ones? +Do you prefer fiction close to reality or far from it? +How do you feel about revenge-driven protagonists? +Do you like footnotes or endnotes? +Do you prefer high-stakes/low-emotion books or the reverse? +What fictional historical event would you like to witness? +Do you prefer slow-burn romances or fast-developing ones? +Are you drawn to betrayal or loyalty stories? +Do you like settings that feel like a character themselves? +Which color do you find most appealing? +High-concept sci-fi or contemporary realistic settings? +Do you like invented slang? +Is it more important that a character is brave or kind? +Conflict resolved through dialogue or confrontation? +What fictional museum or library would you want to visit? +Do you like characters who hide secret lives? +Do you prefer characters who challenge norms or uphold them? +What is your favorite non-musical sound? \ No newline at end of file From 5a7db5de1ebcefe9c4a3255d3eed267a258278df Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Mon, 24 Nov 2025 15:00:02 -0500 Subject: [PATCH 03/51] AI limit v4 --- bookbuddy/frontend/src/Buddy_Recommendation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookbuddy/frontend/src/Buddy_Recommendation.tsx b/bookbuddy/frontend/src/Buddy_Recommendation.tsx index 71d34b4..57a3da7 100644 --- a/bookbuddy/frontend/src/Buddy_Recommendation.tsx +++ b/bookbuddy/frontend/src/Buddy_Recommendation.tsx @@ -54,7 +54,7 @@ function Buddy() { const getUniqueRandomQuestions = async (): Promise => { try { //snags the text file - const res = await fetch("../Questions.txt"); + const res = await fetch("/Questions.txt"); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } From 1fc1e2a267a0c337faa6d66debbd2b9a8dc1ad24 Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 20:11:11 -0500 Subject: [PATCH 04/51] Buddy CSS manual v1 --- bookbuddy/frontend/src/App.tsx | 10 +- .../frontend/src/Buddy_Recommendation.tsx | 483 ++++++++++++------ .../src/Styling/Buddy_Recommendation.css | 193 +++++++ .../src/Styling/buddyresultbackground.jpg | Bin 0 -> 30544 bytes .../frontend/src/Styling/getNewQuestion.png | Bin 0 -> 23630 bytes 5 files changed, 517 insertions(+), 169 deletions(-) create mode 100644 bookbuddy/frontend/src/Styling/Buddy_Recommendation.css create mode 100644 bookbuddy/frontend/src/Styling/buddyresultbackground.jpg create mode 100644 bookbuddy/frontend/src/Styling/getNewQuestion.png diff --git a/bookbuddy/frontend/src/App.tsx b/bookbuddy/frontend/src/App.tsx index 331a331..91aa76f 100644 --- a/bookbuddy/frontend/src/App.tsx +++ b/bookbuddy/frontend/src/App.tsx @@ -13,11 +13,13 @@ import Profile from "./Profile"; export default function App() { return ( - } /> - } /> - } /> + {/* Public routes */} + } />e + } /> + {/*} />*/} - }> + {/* Protected routes inside Layout */} + }> } /> } /> } /> diff --git a/bookbuddy/frontend/src/Buddy_Recommendation.tsx b/bookbuddy/frontend/src/Buddy_Recommendation.tsx index 57a3da7..e416614 100644 --- a/bookbuddy/frontend/src/Buddy_Recommendation.tsx +++ b/bookbuddy/frontend/src/Buddy_Recommendation.tsx @@ -1,41 +1,51 @@ import {useNavigate} from "react-router-dom"; -import React, {useState, useEffect} from 'react'; +import React, {useState, useEffect, useRef} from 'react'; import {SendQeustions} from "./api"; import "./components/Searchpage.css"; import WishlistButton from "./Add_Result_to_Wishlist"; -import "./components/Book_loading.css" import { ChangeAiUse } from "./api"; - +import "./components/Book_loading.css"; +import "./Styling/Buddy_Recommendation.css"; +import "./Styling/buddyresultbackground.jpg"; // MAN i gotta learn vim - //our main function. kinda just holds everything function Buddy() { // max answer length const maxInput = 50 // just so meny hooks - //error status + // error status const [error, seterror] = useState(""); // answers from user + const [RQ0, setRQ0] = useState(""); const [RQ1, setRQ1] = useState(""); const [RQ2, setRQ2] = useState(""); const [RQ3, setRQ3] = useState(""); const [RQ4, setRQ4] = useState(""); const [RQ5, setRQ5] = useState(""); + const [randomIndices, setRandomIndices] = useState([]); + const [questions, setQuestions] = useState([]); + const [allQuestionLines, setAllQuestionLines] = useState([]); + // element we use to show user how meny chars they have left const [textlengs, setTextlengs] = useState(maxInput) + // state we use to show the result box + const [isResBoxVisible, setResBoxVisible] = useState(false); // state we use to show the rec div const [isDivVisible, setIsDivVisible] = useState(false); - const [darkbox, setDarkBox] = useState(false); - // hook we use to hold the openAI respones + // hook we use to hold the openAI responses const [bookrec,setBookrec] = useState("I WOULD RECOMMEND THIS BOOK"); // value that holds the book title and sometimes the book author ( basically everything up until the first ',' // this actually works out great because sometimes the book wont show less you enter an auther with it, a good - //example of this is the book 1984 by George Orwell. the book will not show less the author is there too. - //however if you just search George Orwell it wont come up at all so this is epic + // example of this is the book 1984 by George Orwell. the book will not show less the author is there too. + // however if you just search George Orwell it wont come up at all so this is epic const [booktitle, setBooktitle] = useState(""); + const [buttonPressed, setButtonPressed] = useState(false); + const [questionlength, setQuestionlength] = useState(0); + // Auto scroll reference + const resultRef = useRef(null); const parseBookTitle = (responseString: string) => { // Finds the position of the first "," @@ -62,6 +72,7 @@ function Buddy() { // separates the file into questions via splitting at new lines and removing trailing space const lines = text.split('\n').filter(line => line.trim() !== ''); const maxIndex = lines.length; + setQuestionlength(maxIndex); // number of questions we have const numQuestionsToSelect = 5 @@ -77,57 +88,113 @@ function Buddy() { } // Map the random numbers to the corresponding lines (questions) const Questions: string[] = random_Numbers.map(index => lines[index] || "Question not found"); + setAllQuestionLines (Questions); // testing values to show what questions were pulled console.log("Random Indices:", random_Numbers); + return Questions; + } catch (e) { console.error("Error fetching file:", e instanceof Error ? e.message : "Unknown error"); return []; // Return an empty array of the correct type on error } }; - const random_Numbers: number[] = []; - let i = 0; - - // loops until 5 unique random numbers (0-210) are made - while (i < 5) { - let curr_num = Math.floor(Math.random() * 211); - if (!random_Numbers.includes(curr_num)) { - random_Numbers.push(curr_num); - i++; + const random_Numbers: number[] = []; + let i = 0; + + // loops until 5 unique random numbers (0-210) are made + while (i < 5) { + let curr_num = Math.floor(Math.random() * 211); + if (!random_Numbers.includes(curr_num)) { + random_Numbers.push(curr_num); + i++; + } + } + //more hooks to be used in sprint 3 + + const [isLoading, setIsLoading] = useState(true); + + + // Use useEffect to handle the side effect (the async data fetch) + useEffect(() => { + const loadQuestions = async () => { + setIsLoading(true); // Start loading + const loadedQuestions = await getUniqueRandomQuestions(); + setQuestions(loadedQuestions); + setIsLoading(false); // Stop loading once data is set + }; + loadQuestions(); + }, []); + + const handleNewQuestion = async (questionIndexToReplace: number) => { + + let new_index = 0; + let isUnique = false; + + //check if there are questions + if (questionlength === 0) return "Error: Question pool size is zero."; + + // ensure the new index isn't already one of the 5 displayed questions + while (!isUnique) { + new_index = Math.floor(Math.random() * questionlength); + + if (!random_Numbers.includes(new_index)) { + random_Numbers.push(new_index); + isUnique = true; } } - //more hooks to be used in sprint 3 - const [questions, setQuestions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - - // Use useEffect to handle the side effect (the async data fetch) - useEffect(() => { - const loadQuestions = async () => { - setIsLoading(true); // Start loading - const loadedQuestions = await getUniqueRandomQuestions(); - setQuestions(loadedQuestions); - setIsLoading(false); // Stop loading once data is set - }; - loadQuestions(); - }, []); - - const [ - Q1 = '', - Q2 = '', - Q3 = '', - Q4 = '', - Q5 = '' - ] = questions; - - //function that deals with the submit button + try { + console.log(new_index); + const res = await fetch("/Questions.txt"); + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + const text = await res.text(); + + const lines = text.split('\n').filter(line => line.trim() !== ''); + + const newQuestionText: string = lines[new_index] ?? "Question not found"; + + //functional update form + setQuestions(prevQuestions => { + //spread operator, gives the old version of questions + const newQuestions = [...prevQuestions]; + // Replace the question text at the target index + newQuestions[questionIndexToReplace] = newQuestionText; + return newQuestions; + }); + + // Update the indices state to reflect the replacement + + + return newQuestionText; + + } catch (e) { + console.error("Error fetching file:", e instanceof Error ? e.message : "Unknown error"); + return "Failed to load a new question."; + } + } + + let Q0 = "Tell us about the reading experience you're hoping for. Where and why are you reading this book?" + + let [ + Q1 = '', + Q2 = '', + Q3 = '', + Q4 = '', + Q5 = '' + ] = questions; //react hook + + + + //function that deals with the submit button const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if ( + RQ0 === "" || RQ1 === "" || RQ2 === "" || RQ3 === "" || @@ -142,17 +209,29 @@ function Buddy() { await ChangeAiUse(); // session identifies the user } catch (err) { seterror("You have 0 AI uses remaining."); + setButtonPressed(true); // remove button so that the user doesn't try again return; // stop here so AI isn’t called } - setDarkBox(true); + + setResBoxVisible(true) + setButtonPressed(true) + + setTimeout(() => { + resultRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + }, 0); + // CATS each Question with the Answer + const Q_A0 = `${Q0} ${RQ0}`; const Q_A1 = `${Q1} ${RQ1}`; const Q_A2 = `${Q2} ${RQ2}`; const Q_A3 = `${Q3} ${RQ3}`; const Q_A4 = `${Q4} ${RQ4}`; const Q_A5 = `${Q5} ${RQ5}`; - const result = [Q_A1, Q_A2, Q_A3, Q_A4, Q_A5]; + const result = [Q_A0, Q_A1, Q_A2, Q_A3, Q_A4, Q_A5]; // shows the rec div @@ -183,130 +262,204 @@ function Buddy() { } setIsDivVisible(true); + + + const handleNewQuestion = () => { + let new_number; + let isUnique = false; + while (!isUnique) { + new_number = Math.floor(Math.random() * 5); + + // Check if the new number is already in the list of 5 questions + if (!random_Numbers.includes(new_number)) { + isUnique = true; + } + } + } + }; // ya know the basic html we update the react hook each time the user enters a value + return ( -
- <> - -

{error}

-
-
- - setRQ1(e.target.value)}/>

- -
- setRQ2(e.target.value)}/>

- -
- setRQ3(e.target.value)}/>

- -
- setRQ4(e.target.value)}/>

- -
- { - // updating user char count - const newVal = e.target.value; - setRQ5(e.target.value); - const newLength = (maxInput) - newVal.length; - setTextlengs(newLength) - }}/>
- {textlengs} -
- -
- - - - {darkbox && ( - -
- {!isDivVisible && ( -
-
-
-
-
-
-
+
+
+ <> +
+
+ + {/* Initial Filter Question */} +
+ + setRQ0(e.target.value)} + placeholder="I'm looking for a light read to wind down with every night" + maxLength={maxInput} + /> +

+ {maxInput - RQ0.length} / {maxInput} characters remaining +

+
+ + {/* Question 1 */} +
+ +
+ setRQ1(e.target.value)} + placeholder="..." + maxLength={maxInput} + /> +
+

+ {maxInput - RQ1.length} / {maxInput} characters remaining +

+
+ + {/* Question 2 */} +
+ +
+ setRQ2(e.target.value)} + placeholder="..." + maxLength={maxInput} + /> +
+

+ {maxInput - RQ2.length} / {maxInput} characters remaining +

+
+ + {/* Question 3 */} +
+ +
+ setRQ3(e.target.value)} + placeholder="..." + maxLength={maxInput} + /> +
+

+ {maxInput - RQ3.length} / {maxInput} characters remaining +

+
+ + {/* Question 4 */} +
+ +
+ setRQ4(e.target.value)} + placeholder="..." + maxLength={maxInput} + /> +
+

+ {maxInput - RQ4.length} / {maxInput} characters remaining +

+
+ + {/* Question 5 */} +
+ +
+ setRQ5(e.target.value)} + placeholder="..." + maxLength={maxInput} + /> +
+

+ {maxInput - RQ5.length} / {maxInput} characters remaining +

+
+ {!buttonPressed && ( + + )} + + {error && ( +
+ {error} +
+ )} + +
+
+ + + {isResBoxVisible && ( +
+ {!isDivVisible && ( +
+
+
+
+
+
+
+
)} + + {isDivVisible && ( +
+
+ {isDivVisible && } +
+
+

{bookrec}

+
+
)}
)} - - {isDivVisible && ( -
-

{bookrec}

- {isDivVisible && } - -
)} - {/* Could add a spinner component here - that is what ill be doing however i still feel petty about this comment */} - -
)} - +
- ) } diff --git a/bookbuddy/frontend/src/Styling/Buddy_Recommendation.css b/bookbuddy/frontend/src/Styling/Buddy_Recommendation.css new file mode 100644 index 0000000..359ca96 --- /dev/null +++ b/bookbuddy/frontend/src/Styling/Buddy_Recommendation.css @@ -0,0 +1,193 @@ + /*Form CSS*/ + .pageBackground { + width: 100vw; + height: 100vh; + background-image: url("buddyresultbackground.jpg"); + background-repeat: repeat; + background-position: top left; + background-size: auto; + margin: 0; + padding: 0; + top: 0; + left: 0; + position: absolute; + overflow-y: scroll; + } + + .questions-container { + max-width: 800px; + margin: 80px auto; + padding: 30px; + border: 1px solid #ccc; + border-radius: 10px; + box-shadow: 0px 4px 8px rgba(0,0,0,0.1); + background: whitesmoke; + text-align: center; + } + /*#ECF5FF*/ + .questionBlock { + margin-bottom: 24px; + } + + .questionLabel { + display: block; + margin-bottom: 10px; + font-weight: 500; + color: #333; + font-size: 1.1rem; + } + + .aBox { + width: 100%; + padding: 12px; + max-width: 700px; + margin-top: 4px; + border: 1px solid #ccc; + border-radius: 18px; + background: white; + outline: #0056b3; + } + + .aBox:hover { + transform: translateY(-3px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); + } + + .aBox:focus { + border-color: #007bff; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.3); + transform: translateY(-3px); + } + + .charCounter { + margin-top: 4px; + font-size: 12px; + color: black; + } + + .submitButton { + padding: 10px; + width: 100%; + max-width: 300px; + background: #F1DADE; + border: none; + color: #000; + border-radius: 15px; + font-size: 16px; + cursor: pointer; + outline: 1px solid #ccc; + margin-bottom: 15px; + } + + .submitButton:hover { + background: #E2B4BD; + } + + .filterQuestionBlock { + margin-bottom: 24px; + } + + .filterQuestionLabel { + display: block; + margin-bottom: 12px; + font-weight: 700; + color: #333; + font-size: 1.1rem; + } + + /*Results CSS*/ + /*This box is invisible, I just don't want to change the programming in the main file*/ + .resultBox { + position: relative; + max-width: 90%; + max-height: 100%; + min-width: 200px; + min-height: 200px; + margin: 2rem auto; + background-position: center; + padding: 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow-y: auto; + overflow-x: hidden; + word-wrap: break-word; + text-align: center; + z-index: 10; + color: #000; + font-size: 20px; + } + + .eStyle { + /*display: block;*/ + /*margin-bottom: 10px;*/ + /*font-weight: 500;*/ + /*color: palevioletred;*/ + /*font-size: 1.1rem;*/ + background: #fee2e2; + color: #7f1d1d; + margin-top: 4px; + padding: var(--bb-space-2) var(--bb-space-3); + border-radius: .75rem; + margin-bottom: var(--bb-space-3); + } + + .resultContainer { + display: flex; + flex-direction: row; + gap: 2rem; + align-items: flex-start; + padding: 2rem; + background: whitesmoke; + border: 5px solid black; + border-radius: 12px; + max-width: 900px; + margin: 2rem auto; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + } + + .resultLeft { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + min-width: 200px; + flex-shrink: 0; + } + + .resultRight { + flex: 1; + min-width: 0; + } + + .refreshButton { + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid #ccc; + background-image: url("getNewQuestion.png"); + background-color: #77C3EC; /*#ECF5FF*/ + background-repeat: no-repeat; + background-position: center; + background-size: 24px 24px; + margin-top: 9px; + margin-left: 9px; + align-items: center; + cursor: pointer; + transition: all 0.2s ease; + position: absolute; + } + + .refreshButton:hover { + background-color: #5a96dc; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); + transform: translateY(-3px); + } + + .answerBlock { + position: relative; + display: inline-block; + width: 100%; + text-align: left; + } \ No newline at end of file diff --git a/bookbuddy/frontend/src/Styling/buddyresultbackground.jpg b/bookbuddy/frontend/src/Styling/buddyresultbackground.jpg new file mode 100644 index 0000000000000000000000000000000000000000..14fb04769451b563f919fc66c68cd8474962c68d GIT binary patch literal 30544 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 literal 0 HcmV?d00001 From d7a76c48897f7dfb030ded430e850fd0917336e4 Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 21:59:14 -0500 Subject: [PATCH 05/51] Profile CSS manual v1 --- bookbuddy/frontend/src/Profile.tsx | 72 ++++++---------- bookbuddy/frontend/src/Styling/Profile.css | 95 ++++++++++++++++++++++ 2 files changed, 119 insertions(+), 48 deletions(-) create mode 100644 bookbuddy/frontend/src/Styling/Profile.css diff --git a/bookbuddy/frontend/src/Profile.tsx b/bookbuddy/frontend/src/Profile.tsx index 63d67f4..a725049 100644 --- a/bookbuddy/frontend/src/Profile.tsx +++ b/bookbuddy/frontend/src/Profile.tsx @@ -1,64 +1,40 @@ -import React, { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import type { AccountDto } from "./types/AccountDto"; -import type { BookDto } from "./types/BookDto"; -import {getAccountById, getCurrentUser, getMyLibrary} from "./api"; +import {useEffect, useState} from "react"; +import {AccountDto} from "./types/AccountDto"; +import {getCurrentUser, getMyLibrary} from "./api"; +import {BookDto} from "./types/BookDto"; +import {useNavigate} from "react-router-dom"; +import "./Styling/Profile.css"; + export default function Profile() { const navigate = useNavigate(); - const [account, setAccount] = useState(null); - const [books, setBooks] = useState([]); + @@ -11,7 +13,6 @@ export default function Profile() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Calculate total pages const totalPages = books.reduce((sum, b) => sum + (b.pagecount || 0), 0); useEffect(() => { - (async () => { - try { - const acct = await getCurrentUser(); - setAccount(acct); - - const lib = await getMyLibrary(); - setBooks(lib); - } catch (e: any) { - console.error(e); - navigate("/login"); - } finally { - setLoading(false); - } - })(); - }, [navigate]); - - if (loading) { - return ( -
-

Loading profile...

-
+ @@ -39,26 +40,22 @@ export default function Profile() { ); } - if (error) { - return ( + return ( +
-

Profile

-

{error}

-
- ); - } - return ( -
-

Hello, {account?.name || "Reader"}!

-
-

Books in Library: {books.length}

-

Total Pages: {totalPages.toLocaleString()}

-
+

Hello, {account?.name || "Reader"}!

- +
+

Books in Library: {books.length}

+

Total Pages: {totalPages.toLocaleString()}

+

AI Uses Remaining: {account?.aiLimit ?? 0}

+
+ + + +
- ); -} \ No newline at end of file + );} \ No newline at end of file diff --git a/bookbuddy/frontend/src/Styling/Profile.css b/bookbuddy/frontend/src/Styling/Profile.css new file mode 100644 index 0000000..6445576 --- /dev/null +++ b/bookbuddy/frontend/src/Styling/Profile.css @@ -0,0 +1,95 @@ +.profile-page { + position: fixed; + inset: 0; /* top:0 left:0 right:0 bottom:0 */ + width: 100vw; + height: 100vh; + + background-image: url("./buddyresultbackground.jpg"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + + display: flex; + align-items: center; /* vertical center */ + justify-content: center; /* horizontal center */ +} + +/* Center floating card */ +.wrap { + width: 90%; + max-width: 420px; + + background: rgba(240, 240, 240, 0.75); /* light grey transparent */ + backdrop-filter: blur(4px); + + padding: 28px 32px; + border-radius: 16px; + + box-shadow: + 0 6px 20px rgba(0,0,0,0.18), + 0 12px 28px rgba(0,0,0,0.12); + + color: #1e293b; + animation: fadeIn 0.4s ease-in-out; +} + +/* Header */ +.wrap h1 { + font-size: 1.9rem; + margin-bottom: 18px; + font-weight: 700; + color: #1f2937; + text-align: center; +} + +/* Stats card */ +.bb-card { + background: rgba(255, 255, 255, 0.7); + border-radius: 10px; + padding: 18px 20px; + margin-bottom: 20px; + backdrop-filter: blur(3px); + box-shadow: + 0 2px 5px rgba(0,0,0,0.08), + 0 6px 14px rgba(0,0,0,0.05); +} + +/* Stats text */ +.bb-card p { + margin: 6px 0; + font-size: 0.95rem; +} + +.bb-card p strong { + color: #374151; +} + +.bb-btn { + width: 100%; + background: #d66a6a; + color: white; + font-size: 1.05rem; + padding: 12px 16px; + border-radius: 10px; + border: none; + cursor: pointer; + font-weight: 600; + transition: 0.18s ease; +} + +.bb-btn:hover { + background: #c65454; + transform: translateY(-2px); +} + +/* Fade animation */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file From 17107c337078fd5c77b3b701b9fc3180a6811bcf Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 22:03:53 -0500 Subject: [PATCH 06/51] Profile CSS manual v2 --- bookbuddy/frontend/src/Profile.tsx | 38 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/bookbuddy/frontend/src/Profile.tsx b/bookbuddy/frontend/src/Profile.tsx index a725049..c4619e8 100644 --- a/bookbuddy/frontend/src/Profile.tsx +++ b/bookbuddy/frontend/src/Profile.tsx @@ -1,23 +1,38 @@ -import {useEffect, useState} from "react"; -import {AccountDto} from "./types/AccountDto"; -import {getCurrentUser, getMyLibrary} from "./api"; -import {BookDto} from "./types/BookDto"; -import {useNavigate} from "react-router-dom"; +import { useEffect, useState } from "react"; +import { AccountDto } from "./types/AccountDto"; +import { getCurrentUser, getMyLibrary } from "./api"; +import { BookDto } from "./types/BookDto"; +import { useNavigate } from "react-router-dom"; import "./Styling/Profile.css"; - export default function Profile() { const navigate = useNavigate(); - @@ -11,7 +13,6 @@ export default function Profile() { + + const [account, setAccount] = useState(null); + const [books, setBooks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const totalPages = books.reduce((sum, b) => sum + (b.pagecount || 0), 0); useEffect(() => { - @@ -39,26 +40,22 @@ export default function Profile() { - ); - } + async function loadData() { + try { + const userData = await getCurrentUser(); + const libraryData = await getMyLibrary(); + setAccount(userData); + setBooks(libraryData); + } catch (err) { + setError("Failed to load profile data"); + } finally { + setLoading(false); + } + } + loadData(); + }, []); + + if (loading) return
Loading...
; + if (error) return
{error}
; return (
@@ -37,4 +52,5 @@ export default function Profile() {
- );} \ No newline at end of file + ); +} From c7ee2a01fc75e5da5dd7603411bfe09381392601 Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 22:10:16 -0500 Subject: [PATCH 07/51] Profile CSS manual v3 --- bookbuddy/frontend/src/types/AccountDto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookbuddy/frontend/src/types/AccountDto.ts b/bookbuddy/frontend/src/types/AccountDto.ts index 9ffa5a3..7f41ca0 100644 --- a/bookbuddy/frontend/src/types/AccountDto.ts +++ b/bookbuddy/frontend/src/types/AccountDto.ts @@ -1 +1 @@ -export type AccountDto = { accountId?: number; name: string, password: string} \ No newline at end of file +export type AccountDto = { accountId?: number; name: string, password: string, aiLimit:number} \ No newline at end of file From 40ff98cd1370b9c1438eae356d81c20360e1fcc7 Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 22:13:05 -0500 Subject: [PATCH 08/51] Profile CSS manual v4 --- bookbuddy/frontend/src/types/AccountDto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookbuddy/frontend/src/types/AccountDto.ts b/bookbuddy/frontend/src/types/AccountDto.ts index 7f41ca0..7f07723 100644 --- a/bookbuddy/frontend/src/types/AccountDto.ts +++ b/bookbuddy/frontend/src/types/AccountDto.ts @@ -1 +1 @@ -export type AccountDto = { accountId?: number; name: string, password: string, aiLimit:number} \ No newline at end of file +export type AccountDto = { accountId?: number; name: string, password: string, aiLimit?:number} \ No newline at end of file From c7efc68f09061349ac48300acf797772e0a48d6d Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 22:28:05 -0500 Subject: [PATCH 09/51] Profile CSS manual v5 --- bookbuddy/frontend/src/Profile.tsx | 14 ++-- bookbuddy/frontend/src/Styling/Profile.css | 82 ++++++++++------------ 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/bookbuddy/frontend/src/Profile.tsx b/bookbuddy/frontend/src/Profile.tsx index c4619e8..e09dacc 100644 --- a/bookbuddy/frontend/src/Profile.tsx +++ b/bookbuddy/frontend/src/Profile.tsx @@ -23,7 +23,7 @@ export default function Profile() { setAccount(userData); setBooks(libraryData); } catch (err) { - setError("Failed to load profile data"); + setError("Failed to load profile"); } finally { setLoading(false); } @@ -31,22 +31,22 @@ export default function Profile() { loadData(); }, []); - if (loading) return
Loading...
; - if (error) return
{error}
; + if (loading) return
Loading...
; + if (error) return
{error}
; return (
-
+
-

Hello, {account?.name || "Reader"}!

+

Hello, {account?.name || "Reader"}!

-
+

Books in Library: {books.length}

Total Pages: {totalPages.toLocaleString()}

AI Uses Remaining: {account?.aiLimit ?? 0}

- diff --git a/bookbuddy/frontend/src/Styling/Profile.css b/bookbuddy/frontend/src/Styling/Profile.css index 6445576..1bc71d2 100644 --- a/bookbuddy/frontend/src/Styling/Profile.css +++ b/bookbuddy/frontend/src/Styling/Profile.css @@ -1,6 +1,7 @@ +/* === Page Background === */ .profile-page { position: fixed; - inset: 0; /* top:0 left:0 right:0 bottom:0 */ + inset: 0; width: 100vw; height: 100vh; @@ -10,66 +11,65 @@ background-repeat: no-repeat; display: flex; - align-items: center; /* vertical center */ - justify-content: center; /* horizontal center */ + justify-content: center; + align-items: center; } -/* Center floating card */ -.wrap { +/* === Main Card === */ +.profile-card { width: 90%; max-width: 420px; - background: rgba(240, 240, 240, 0.75); /* light grey transparent */ + background: rgba(240,240,240,0.78); backdrop-filter: blur(4px); - padding: 28px 32px; + padding: 30px 32px; border-radius: 16px; box-shadow: - 0 6px 20px rgba(0,0,0,0.18), - 0 12px 28px rgba(0,0,0,0.12); + 0 6px 20px rgba(0,0,0,0.18), + 0 12px 28px rgba(0,0,0,0.12); - color: #1e293b; - animation: fadeIn 0.4s ease-in-out; + animation: profileFadeIn 0.4s ease-in-out; } -/* Header */ -.wrap h1 { +/* Title */ +.profile-title { font-size: 1.9rem; - margin-bottom: 18px; font-weight: 700; - color: #1f2937; + color: #1e293b; + margin-bottom: 22px; text-align: center; } -/* Stats card */ -.bb-card { - background: rgba(255, 255, 255, 0.7); +/* Stats Container */ +.profile-stats { + background: rgba(255,255,255,0.72); border-radius: 10px; - padding: 18px 20px; - margin-bottom: 20px; - backdrop-filter: blur(3px); + padding: 18px 22px; + margin-bottom: 22px; + box-shadow: - 0 2px 5px rgba(0,0,0,0.08), - 0 6px 14px rgba(0,0,0,0.05); + 0 2px 6px rgba(0,0,0,0.09), + 0 6px 14px rgba(0,0,0,0.05); } -/* Stats text */ -.bb-card p { +.profile-stats p { margin: 6px 0; - font-size: 0.95rem; + font-size: 0.97rem; } -.bb-card p strong { +.profile-stats strong { color: #374151; } -.bb-btn { +/* Button */ +.profile-btn { width: 100%; background: #d66a6a; color: white; - font-size: 1.05rem; - padding: 12px 16px; + font-size: 1.08rem; + padding: 12px 14px; border-radius: 10px; border: none; cursor: pointer; @@ -77,19 +77,13 @@ transition: 0.18s ease; } -.bb-btn:hover { - background: #c65454; - transform: translateY(-2px); +.profile-btn:hover { + background: #c65252; + transform: translateY(-2px); } -/* Fade animation */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} \ No newline at end of file +/* Fade-in animation */ +@keyframes profileFadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} From e57d396803b1698c7ee9fe01d2af2e76463d751c Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 23:10:49 -0500 Subject: [PATCH 10/51] Nicks changes v2 --- bookbuddy/frontend/src/addBooksViaCSV.tsx | 116 +++++++----------- bookbuddy/frontend/src/searchBookViaTitle.tsx | 30 ++++- 2 files changed, 66 insertions(+), 80 deletions(-) diff --git a/bookbuddy/frontend/src/addBooksViaCSV.tsx b/bookbuddy/frontend/src/addBooksViaCSV.tsx index e187a42..e12a31c 100644 --- a/bookbuddy/frontend/src/addBooksViaCSV.tsx +++ b/bookbuddy/frontend/src/addBooksViaCSV.tsx @@ -19,13 +19,10 @@ async function delay(ms: number) { const CSVReader: React.FC = () => { const [columnData, setColumnData] = useState([]); - const [books, setBooks] = useState([]); // kept in case you use later const [bookMessages, setBookMessages] = useState([]); const [fileName, setFileName] = useState("No File Chosen!"); const [isLoading, setIsLoading] = useState(false); - const [currentMessage, setCurrentMessage] = useState( - null - ); + const [currentMessage, setCurrentMessage] = useState(null); const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -50,30 +47,20 @@ const CSVReader: React.FC = () => { const csvSplitRegExp = /,(?=(?:[^"]*"[^"]*")*[^"]*$)/; - // Assume first row is header + // HEADER row const headerCells = lines[0].split(csvSplitRegExp); - let titleColIndex = -1; - - for (let i = 0; i < headerCells.length; i++) { - if (headerCells[i].replace(/"/g, "").trim().toLowerCase() === "title") { - titleColIndex = i; - break; - } - } + let titleColIndex = headerCells.findIndex( + (h) => h.replace(/"/g, "").trim().toLowerCase() === "title" + ); - // If we couldn't find the "title" column, bail + // If no title column found, fallback → first column if (titleColIndex === -1) { - // Fallback: try first column as titles - const firstColumn = lines.map( - (line) => line.split(csvSplitRegExp)[0] ?? "" - ); - setColumnData(firstColumn); + setColumnData(lines.map((line) => line.split(csvSplitRegExp)[0] ?? "")); return; } - const titleColumn: string[] = lines.map((line) => { - const cells = line.split(csvSplitRegExp); - return cells[titleColIndex] ?? ""; + const titleColumn = lines.map((line) => { + return line.split(csvSplitRegExp)[titleColIndex] ?? ""; }); setColumnData(titleColumn); @@ -86,43 +73,33 @@ const CSVReader: React.FC = () => { if (columnData.length === 0) return; async function processBooks() { - // Copy so we don't mutate state directly - let titles = [...columnData]; - - // Skip header row - titles = titles.slice(1); - - // Limit to 25 books (plus header originally) - titles = titles.slice(0, 25); - + let titles = [...columnData].slice(1).slice(0, 25); // skip header, limit 25 if (titles.length === 0) return; setIsLoading(true); - for (const title of titles) { - const titleClean = title.replaceAll("/", "").replaceAll("#", "").trim(); - + for (const rawTitle of titles) { + const titleClean = rawTitle.replaceAll("/", "").replaceAll("#", "").trim(); if (!titleClean) continue; - // 1. Search the book const found = await searchBookViaTitle(titleClean, BASE); + if (!found) { - const msg: BookMessage = { + const msg = { title: titleClean, message: "No result found", success: false, }; setCurrentMessage(msg); setBookMessages((prev) => [...prev, msg]); - await delay(25); + await delay(50); continue; } - // 2. Add book to backend const result = await addCSVBooks(found, BASE); const msg: BookMessage = { - title: found.bookname, + title: found.bookname ?? titleClean, message: result.ok ? "Added successfully" : result.message || "Failed", success: result.ok, }; @@ -142,38 +119,30 @@ const CSVReader: React.FC = () => { return (
-
- -
+ {isLoading && typeof document !== "undefined" && @@ -209,8 +178,7 @@ const CSVReader: React.FC = () => { color: "#B6D15C", }} > - For now, users are limited to 25 Books from their imported - library! + Users are limited to 25 books for now!
{ }} > {currentMessage - ? `${currentMessage.title}: ${currentMessage.message}` + ? `${currentMessage.title}: ${currentMessage.message}` : "Starting import..."}
, diff --git a/bookbuddy/frontend/src/searchBookViaTitle.tsx b/bookbuddy/frontend/src/searchBookViaTitle.tsx index 5e5fcfe..44c489b 100644 --- a/bookbuddy/frontend/src/searchBookViaTitle.tsx +++ b/bookbuddy/frontend/src/searchBookViaTitle.tsx @@ -1,9 +1,27 @@ import type { BookDto } from "./types/BookDto"; -export async function searchBookViaTitle(title: string, BASE: string): Promise { - const res = await fetch(`${BASE}/googlebooks/search/${encodeURIComponent(title)}`); - if (!res.ok) return; - const data = await res.json(); +/** + * Searches for a book by title and returns the **first result** + * Always returns BookDto OR null (never undefined) + */ +export async function searchBookViaTitle( + title: string, + BASE: string +): Promise { + try { + const res = await fetch( + `${BASE}/googlebooks/search/${encodeURIComponent(title)}` + ); - return (data.docs?.[0] as BookDto) ?? null; //returns the first book -} \ No newline at end of file + // If request fails → return null instead of undefined + if (!res.ok) return null; + + const data = await res.json(); + + // Guarantee output type: BookDto | null + return (data?.docs?.[0] as BookDto) ?? null; + } catch (err) { + console.error("searchBookViaTitle error:", err); + return null; + } +} From 2b5e740e7b61349ff12984eb46c391c51a4400b0 Mon Sep 17 00:00:00 2001 From: Noah Schaible Date: Tue, 25 Nov 2025 23:18:17 -0500 Subject: [PATCH 11/51] Nicks changes v3 --- bookbuddy/frontend/src/addBooksViaCSV.tsx | 71 ++++++----------------- 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/bookbuddy/frontend/src/addBooksViaCSV.tsx b/bookbuddy/frontend/src/addBooksViaCSV.tsx index e12a31c..b9d22b6 100644 --- a/bookbuddy/frontend/src/addBooksViaCSV.tsx +++ b/bookbuddy/frontend/src/addBooksViaCSV.tsx @@ -26,10 +26,7 @@ const CSVReader: React.FC = () => { const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; - if (!file) { - setFileName("No File Chosen!"); - return; - } + if (!file) return setFileName("No File Chosen!"); setFileName(file.name); @@ -47,23 +44,19 @@ const CSVReader: React.FC = () => { const csvSplitRegExp = /,(?=(?:[^"]*"[^"]*")*[^"]*$)/; - // HEADER row + // HEADER const headerCells = lines[0].split(csvSplitRegExp); let titleColIndex = headerCells.findIndex( (h) => h.replace(/"/g, "").trim().toLowerCase() === "title" ); - // If no title column found, fallback → first column + // fallback → first column if (titleColIndex === -1) { setColumnData(lines.map((line) => line.split(csvSplitRegExp)[0] ?? "")); return; } - const titleColumn = lines.map((line) => { - return line.split(csvSplitRegExp)[titleColIndex] ?? ""; - }); - - setColumnData(titleColumn); + setColumnData(lines.map((line) => line.split(csvSplitRegExp)[titleColIndex] ?? "")); }; reader.readAsText(file); @@ -73,17 +66,19 @@ const CSVReader: React.FC = () => { if (columnData.length === 0) return; async function processBooks() { - let titles = [...columnData].slice(1).slice(0, 25); // skip header, limit 25 + let titles = [...columnData].slice(1).slice(0, 25); // skip header + limit 25 if (titles.length === 0) return; setIsLoading(true); for (const rawTitle of titles) { - const titleClean = rawTitle.replaceAll("/", "").replaceAll("#", "").trim(); + const titleClean = rawTitle?.replaceAll("/", "").replaceAll("#", "").trim() ?? ""; + if (!titleClean) continue; const found = await searchBookViaTitle(titleClean, BASE); + // 🔥 MUST check this or TS2532 triggers if (!found) { const msg = { title: titleClean, @@ -96,10 +91,11 @@ const CSVReader: React.FC = () => { continue; } + // BOOK IS CONFIRMED SAFE HERE — NO UNDEFINED ACCESS const result = await addCSVBooks(found, BASE); const msg: BookMessage = { - title: found.bookname ?? titleClean, + title: found.bookname ?? titleClean, // ensures never undefined message: result.ok ? "Added successfully" : result.message || "Failed", success: result.ok, }; @@ -109,7 +105,7 @@ const CSVReader: React.FC = () => { await delay(1000); } - await delay(3000); + await delay(2000); setIsLoading(false); window.location.reload(); } @@ -122,7 +118,7 @@ const CSVReader: React.FC = () => {