diff --git a/.gitignore b/.gitignore index 4d29575..a9b46ee 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,9 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +flask-server/.env +flask-server/venv/ +flask-server/__pycache__/ +**/__pycache__/ +*.pyc \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Introverse.iml b/.idea/Introverse.iml new file mode 100644 index 0000000..81b3eaa --- /dev/null +++ b/.idea/Introverse.iml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Final project document/Project Document Group 1.docx b/Final project document/Project Document Group 1.docx deleted file mode 100644 index 812f94f..0000000 Binary files a/Final project document/Project Document Group 1.docx and /dev/null differ diff --git a/Final project document/Project Document Group 1.pdf b/Final project document/Project Document Group 1.pdf deleted file mode 100644 index c6028e7..0000000 Binary files a/Final project document/Project Document Group 1.pdf and /dev/null differ diff --git a/Final project document/PythonREADME.md b/Final project document/PythonREADME.md deleted file mode 100644 index f69cc62..0000000 --- a/Final project document/PythonREADME.md +++ /dev/null @@ -1,32 +0,0 @@ -# Server for IntroVerse project -Description about IntroVerse Group 1 Project -## To set up and run the server -1. Create a virtual python environment (can do this with command or VScode should prompt you when try to run a file) and select it -``` -python -m venv path -``` -2. Install the required packages with pip -``` -pip install -r requirements.txt -``` -If you have any issues with modules not being installed properly try pip install manually. If you are a windows user you may have to do this instead: -``` -pip install --user -r requirements.txt -``` -If they are installed but the file shows not imported properly try changing the Python interpreter (Ctrl+Shift+P in VS code) and reinstalling if necessary. Have the virtual environment selected. -3. Change mysqlconfig.py to your credentials -4. Create the database if does not already exist - either through the SQL script or with Python -To create from Python run the create_db.py file -``` -python create_db.py -``` -5. Create the message_board table directly from the SQL file. Then the other tables (if do not already exist) can be created either through the SQL script or with Python (create_tables.py) -``` -python create_tables.py -``` -6. Insert the message_board, book, game, and anime data from the SQL file -7. Run routes.py to start the server -``` -python routes.py -``` -Enjoy! \ No newline at end of file diff --git a/README.md b/README.md index 63f695c..46b746d 100644 --- a/README.md +++ b/README.md @@ -13,37 +13,35 @@ Follow the following steps to get frontend of application running in your termin 3. npm start ``` ``` -5. Enjoy exploring our website. +4. Enjoy exploring our website. ``` # Backend +See [Flask Readme](flask-server/README.md) for more information ## To set up and run the server -1. Create a virtual python environment (can do this with command or VScode should prompt you when try to run a file) and select it +1. Create a virtual python environment (can do this with command or VScode should prompt you when try to run a file) and select it. ``` -python -m venv path +python -m venv venv ``` -2. Install the required packages with pip +2. Install the required packages with pip (or pip3 for mac). ``` pip install -r requirements.txt ``` -If you have any issues with modules not being installed properly try pip install manually. If you are a windows user you may have to do this instead: -``` -pip install --user -r requirements.txt -``` -If they are installed but the file shows not imported properly try changing the Python interpreter (Ctrl+Shift+P in VS code) and reinstalling if necessary. Have the virtual environment selected. -3. Change mysqlconfig.py to your credentials -4. Create the database if does not already exist - either through the SQL script or with Python -To create from Python run the create_db.py file +3. Set up the .env file, can create a copy of (or rename) .env.example and save as .env. Follow the instructions to add a secret key and your MySQL credentials. +4. Create the databases, either through the SQL script or with Python. Running create_db.py will create them if they don't exist or print if they do. ``` python create_db.py ``` -5. Create the message_board table directly from the SQL file. Then the other tables (if do not already exist) can be created either through the SQL script or with Python (create_tables.py) +5. Create tables of the development database through the SQL script or running create_tables.py (recommended). Can use flask migrate, flask db upgrade, to get the latest migrations of tables. Alternatively you can also uncomment with app context db.create_all in the app.py factory function. ``` python create_tables.py ``` -6. Insert the message_board, book, game, and anime data from the SQL file -7. Run routes.py to start the server ``` -python routes.py +flask db upgrade +``` +6. Insert the book, game, and anime data from the SQL script file into your database so that can use the recommendation feature. +7. Run app.py to start the server +``` +python app.py ``` -Enjoy! \ No newline at end of file +Enjoy! diff --git a/express-server/Images/ACOTAR.jpg b/express-server/Images/ACOTAR.jpg deleted file mode 100644 index db188a3..0000000 Binary files a/express-server/Images/ACOTAR.jpg and /dev/null differ diff --git a/express-server/Images/AttackOnTitan.jpg b/express-server/Images/AttackOnTitan.jpg deleted file mode 100644 index b6347a9..0000000 Binary files a/express-server/Images/AttackOnTitan.jpg and /dev/null differ diff --git a/express-server/Images/Berserk.jpg b/express-server/Images/Berserk.jpg deleted file mode 100644 index 4a347b3..0000000 Binary files a/express-server/Images/Berserk.jpg and /dev/null differ diff --git a/express-server/Images/BlackClover.jpg b/express-server/Images/BlackClover.jpg deleted file mode 100644 index b64454b..0000000 Binary files a/express-server/Images/BlackClover.jpg and /dev/null differ diff --git a/express-server/Images/Bleach.jpg b/express-server/Images/Bleach.jpg deleted file mode 100644 index 0da9079..0000000 Binary files a/express-server/Images/Bleach.jpg and /dev/null differ diff --git a/express-server/Images/ChainsawMan.jpg b/express-server/Images/ChainsawMan.jpg deleted file mode 100644 index b8880f3..0000000 Binary files a/express-server/Images/ChainsawMan.jpg and /dev/null differ diff --git a/express-server/Images/CozyGrove.jpg b/express-server/Images/CozyGrove.jpg deleted file mode 100644 index 313e16a..0000000 Binary files a/express-server/Images/CozyGrove.jpg and /dev/null differ diff --git a/express-server/Images/DeathNote.jpg b/express-server/Images/DeathNote.jpg deleted file mode 100644 index e96fac1..0000000 Binary files a/express-server/Images/DeathNote.jpg and /dev/null differ diff --git a/express-server/Images/Dracula.jpg b/express-server/Images/Dracula.jpg deleted file mode 100644 index 68eedef..0000000 Binary files a/express-server/Images/Dracula.jpg and /dev/null differ diff --git a/express-server/Images/DungeonMaster.jpg b/express-server/Images/DungeonMaster.jpg deleted file mode 100644 index 16fcc18..0000000 Binary files a/express-server/Images/DungeonMaster.jpg and /dev/null differ diff --git a/express-server/Images/ElizabethBathory.jpg b/express-server/Images/ElizabethBathory.jpg deleted file mode 100644 index af5c036..0000000 Binary files a/express-server/Images/ElizabethBathory.jpg and /dev/null differ diff --git a/express-server/Images/FaeFarm.jpg b/express-server/Images/FaeFarm.jpg deleted file mode 100644 index b9d704c..0000000 Binary files a/express-server/Images/FaeFarm.jpg and /dev/null differ diff --git a/express-server/Images/FourthWing.jpg b/express-server/Images/FourthWing.jpg deleted file mode 100644 index dce8dfc..0000000 Binary files a/express-server/Images/FourthWing.jpg and /dev/null differ diff --git a/express-server/Images/GameDevTycoon.jpg b/express-server/Images/GameDevTycoon.jpg deleted file mode 100644 index f210ba8..0000000 Binary files a/express-server/Images/GameDevTycoon.jpg and /dev/null differ diff --git a/express-server/Images/GodOfWar.jpg b/express-server/Images/GodOfWar.jpg deleted file mode 100644 index e8edb05..0000000 Binary files a/express-server/Images/GodOfWar.jpg and /dev/null differ diff --git a/express-server/Images/HarryPotter.jpg b/express-server/Images/HarryPotter.jpg deleted file mode 100644 index eb9360a..0000000 Binary files a/express-server/Images/HarryPotter.jpg and /dev/null differ diff --git a/express-server/Images/HogwartsLegacy.jpg b/express-server/Images/HogwartsLegacy.jpg deleted file mode 100644 index 7a67bdd..0000000 Binary files a/express-server/Images/HogwartsLegacy.jpg and /dev/null differ diff --git a/express-server/Images/JJK.jpg b/express-server/Images/JJK.jpg deleted file mode 100644 index 2effc1c..0000000 Binary files a/express-server/Images/JJK.jpg and /dev/null differ diff --git a/express-server/Images/Jojo.jpg b/express-server/Images/Jojo.jpg deleted file mode 100644 index 08a95ab..0000000 Binary files a/express-server/Images/Jojo.jpg and /dev/null differ diff --git a/express-server/Images/Landlines.jpg b/express-server/Images/Landlines.jpg deleted file mode 100644 index e652047..0000000 Binary files a/express-server/Images/Landlines.jpg and /dev/null differ diff --git a/express-server/Images/LiesOfP.jpg b/express-server/Images/LiesOfP.jpg deleted file mode 100644 index e736249..0000000 Binary files a/express-server/Images/LiesOfP.jpg and /dev/null differ diff --git a/express-server/Images/LinkClick.jpg b/express-server/Images/LinkClick.jpg deleted file mode 100644 index 6a540d7..0000000 Binary files a/express-server/Images/LinkClick.jpg and /dev/null differ diff --git a/express-server/Images/LittleWitch.jpg b/express-server/Images/LittleWitch.jpg deleted file mode 100644 index 769baf8..0000000 Binary files a/express-server/Images/LittleWitch.jpg and /dev/null differ diff --git a/express-server/Images/Naruto.jpg b/express-server/Images/Naruto.jpg deleted file mode 100644 index 7a8ced5..0000000 Binary files a/express-server/Images/Naruto.jpg and /dev/null differ diff --git a/express-server/Images/OnePiece.jpg b/express-server/Images/OnePiece.jpg deleted file mode 100644 index 057aa94..0000000 Binary files a/express-server/Images/OnePiece.jpg and /dev/null differ diff --git a/express-server/Images/SpellcasterUniversity.jpg b/express-server/Images/SpellcasterUniversity.jpg deleted file mode 100644 index f5ff227..0000000 Binary files a/express-server/Images/SpellcasterUniversity.jpg and /dev/null differ diff --git a/express-server/Images/Spiderman2.jpg b/express-server/Images/Spiderman2.jpg deleted file mode 100644 index 08680f4..0000000 Binary files a/express-server/Images/Spiderman2.jpg and /dev/null differ diff --git a/express-server/Images/StardewValley.jpg b/express-server/Images/StardewValley.jpg deleted file mode 100644 index b1e933a..0000000 Binary files a/express-server/Images/StardewValley.jpg and /dev/null differ diff --git a/express-server/Images/TheExorcist.jpg b/express-server/Images/TheExorcist.jpg deleted file mode 100644 index 35d3c58..0000000 Binary files a/express-server/Images/TheExorcist.jpg and /dev/null differ diff --git a/express-server/Images/TheGreatEmpires.jpg b/express-server/Images/TheGreatEmpires.jpg deleted file mode 100644 index 03e3018..0000000 Binary files a/express-server/Images/TheGreatEmpires.jpg and /dev/null differ diff --git a/express-server/Images/TheHaunting.jpg b/express-server/Images/TheHaunting.jpg deleted file mode 100644 index f7f7d04..0000000 Binary files a/express-server/Images/TheHaunting.jpg and /dev/null differ diff --git a/express-server/Images/TheShining.jpg b/express-server/Images/TheShining.jpg deleted file mode 100644 index df1d2a4..0000000 Binary files a/express-server/Images/TheShining.jpg and /dev/null differ diff --git a/express-server/Images/ToKillAKingdom.jpg b/express-server/Images/ToKillAKingdom.jpg deleted file mode 100644 index 215cafd..0000000 Binary files a/express-server/Images/ToKillAKingdom.jpg and /dev/null differ diff --git a/express-server/Images/TokyoGhoul.jpg b/express-server/Images/TokyoGhoul.jpg deleted file mode 100644 index 6c9b46a..0000000 Binary files a/express-server/Images/TokyoGhoul.jpg and /dev/null differ diff --git a/express-server/Images/TwoPointHospital.jpg b/express-server/Images/TwoPointHospital.jpg deleted file mode 100644 index f949c97..0000000 Binary files a/express-server/Images/TwoPointHospital.jpg and /dev/null differ diff --git a/express-server/Images/Unbroken.jpg b/express-server/Images/Unbroken.jpg deleted file mode 100644 index 237c889..0000000 Binary files a/express-server/Images/Unbroken.jpg and /dev/null differ diff --git a/express-server/anime.json b/express-server/anime.json deleted file mode 100644 index 944d4d3..0000000 --- a/express-server/anime.json +++ /dev/null @@ -1,86 +0,0 @@ -[ - { - "Anime_ID" : 1, - "Anime_Name" : "Bleach", - "Anime_Genre" : "Shonen", - "Where_TW" : "Disney+", - "Anime_Script" : "Ichigo Kurosaki is a teenager from Karakura Town who can see ghosts, a talent allowing him to meet a supernatural human Rukia Kuchiki, who enters the town in search of a Hollow, a kind of monstrous lost soul who can harm both ghosts and humans." - }, - { - "Anime_ID" : 2, - "Anime_Name" : "Naruto", - "Anime_Genre" : "Shonen", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "The Village Hidden in the Leaves is home to the stealthiest ninja. But twelve years earlier, a fearsome Nine-tailed Fox terrorized the village before it was subdued and its spirit sealed within the body of a baby boy." - }, - { - "Anime_ID" : 3, - "Anime_Name" : "Jujutsu Kaisen", - "Anime_Genre" : "Shonen", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "Yuji Itadori is a boy with tremendous physical strength, though he lives a completely ordinary high school life. One day, to save a classmate who has been attacked by curses, he eats the finger of Ryomen Sukuna, taking the curse into his own soul. From then on, he shares one body with Ryomen Sukuna. Guided by the most powerful of sorcerers, Satoru Gojo, Itadori is admitted to Tokyo Jujutsu High School, an organization that fights the curses... and thus begins the heroic tale of a boy who became a curse to exorcise a curse, a life from which he could never turn back." - }, - { - "Anime_ID" : 4, - "Anime_Name" : "One Piece", - "Anime_Genre" : "Shonen", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "Monkey. D. Luffy refuses to let anyone or anything stand in the way of his quest to become the king of all pirates. With a course charted for the treacherous waters of the Grand Line and beyond, this is one captain who will never give up until he has claimed the greatest treasure on Earth: the Legendary One Piece!" - }, - { - "Anime_ID" : 5, - "Anime_Name" : "Attack on Titan", - "Anime_Genre" : "Seinen", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "Known in Japan as Shingeki no Kyojin, many years ago, the last remnants of humanity were forced to retreat behind the towering walls of a fortified city to escape the massive, man-eating Titans that roamed the land outside their fortress. Only the heroic members of the Scouting Legion dared to stray beyond the safety of the walls – but even those brave warriors seldom returned alive. Those within the city clung to the illusion of a peaceful existence until the day that dream was shattered, and their slim chance at survival was reduced to one horrifying choice: kill – or be devoured!" - }, - { - "Anime_ID" : 6, - "Anime_Name" : "Tokyo Ghoul", - "Anime_Genre" : "Seinen", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "Haise Sasaki has been tasked with teaching Qs Squad how to be outstanding investigators, but his assignment is complicated by the troublesome personalities of his students and his own uncertain grasp of his Ghoul powers. Can he pull them together as a team, or will Qs Squad first assignment be their last?" - }, - { - "Anime_ID" : 7, - "Anime_Name" : "Berserk", - "Anime_Genre" : "Seinen", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "Spurred by the flame raging in his heart, the Black Swordsman Guts continues his seemingly endless quest for revenge. Standing in his path are heinous outlaws, delusional evil spirits, and a devout child of god.Even as it chips away at his life, Guts continues to fight his enemies, who wield repulsive and inhumane power, with nary but his body and sword—his strength as a human. What lies at the end of his travels? The answer is shrouded in the night." - }, - { - "Anime_ID" : 8, - "Anime_Name" : "Death Note", - "Anime_Genre" : "Seinen", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "An intelligent high school student goes on a secret crusade to eliminate criminals from the world after discovering a notebook capable of killing anyone whose name is written into it." - }, - { - "Anime_ID" : 9, - "Anime_Name" : "Chainsaw Man", - "Anime_Genre" : "Fantasy", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "Denji is a young boy who works as a Devil Hunter with the Chainsaw Devil Pochita. One day, as he was living his miserable life trying to pay off the debt he inherited from his parents, he got betrayed and killed. As he was losing his consciousness, he made a deal with Pochita, and got resurrected as the Chainsaw Man: the owner of the Devil’s heart." - }, - { - "Anime_ID" : 10, - "Anime_Name" : "JoJos Bizarre Adventure", - "Anime_Genre" : "Fantasy", - "Where_TW" : "Netflix", - "Anime_Script" : "In ancient Mexico, people of Aztec had prospered. They had historic and strange Stone Mask. It was a miraculous mask which brings eternal life and the power of authentic ruler. But the mask suddenly disappeared. A long time after that, in late 19th centuries when the thought and life of people were suddenly changing, Jonathan Joestar met with Dio Brando. They spend time together through boyhood to youth, and the Stone Mask brings curious fate to them." - }, - { - "Anime_ID" : 11, - "Anime_Name" : "Black Clover", - "Anime_Genre" : "Fantasy", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "In a world where magic is everything, Asta and Yuno are both found abandoned at a church on the same day. While Yuno is gifted with exceptional magical powers, Asta is the only one in this world without any. At the age of fifteen, both receive grimoires, magic books that amplify their holder’s magic. Asta’s is a rare Grimoire of Anti-Magic that negates and repels his opponent’s spells. Being opposite but good rivals, Yuno and Asta are ready for the hardest of challenges to achieve their common dream: to be the Wizard King. Giving up is never an option!" - }, - { - "Anime_ID" : 12, - "Anime_Name" : "Link Click", - "Anime_Genre" : "Fantasy", - "Where_TW" : "Crunchyroll", - "Anime_Script" : "Using superpowers to enter their clientele’s photos one by one, Cheng Xiaoshi and Lu Guang take their work seriously at Time Photo Studio, a small photography shop set in the backdrop of a modern metropolis. Each job can be full of danger, but nothing is more important than fulfilling every order, no matter the scale…or peril involved!" - } -] diff --git a/express-server/animeRoutes.js b/express-server/animeRoutes.js deleted file mode 100644 index 25ef2c9..0000000 --- a/express-server/animeRoutes.js +++ /dev/null @@ -1,23 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const connection = require('./db'); -const handleDatabaseErrors = require('./errorHandling').default; - -router.get('/', (req, res) => { - console.log('Fetching anime from the database...'); - connection.query('SELECT * FROM Anime', (err, results) => { - if (err) { - console.error('Error querying MySQL - Anime:', err); - res.status(500).json({ error: 'Error querying MySQL Anime. Please check your query' }); - return; - } - if (results.length === 0) { - console.log('No anime found in the database.'); - } else { - console.log('Anime fetched successfully.'); - } - res.json(results); - }); -}); - -module.exports = { router, connection, handleDatabaseErrors }; \ No newline at end of file diff --git a/express-server/bookRoutes.js b/express-server/bookRoutes.js deleted file mode 100644 index 8b309e5..0000000 --- a/express-server/bookRoutes.js +++ /dev/null @@ -1,23 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const connection = require('./db'); -const handleDatabaseErrors = require('./errorHandling').default; - -router.get('/', (req, res) => { - console.log('Fetching books from the database...'); - connection.query('SELECT * FROM Books', (err, results) => { - if (err) { - console.error('Error querying MySQL - Books: Please check your query.', err); - res.status(500).json({ error: 'Error querying MySQL Books' }); - return; - } - if (results.length === 0) { - console.log('No books found in the database.'); - } else { - console.log('Books fetched successfully.'); - } - res.json(results); - }); -}); - -module.exports = { router, connection, handleDatabaseErrors }; diff --git a/express-server/books.json b/express-server/books.json deleted file mode 100644 index f90d1cf..0000000 --- a/express-server/books.json +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - "Book_ID" : 1, - "Book_Name" : "Fourth Wing", - "Book_Author" : "Rebecca Yarros", - "Book_Genre" : "Fantasy", - "Price" : 9.19, - "Book_Script" : "Twenty-year-old Violet Sorrengail was supposed to enter the Scribe Quadrant, living a quiet life among books and history. Now, the commanding general-also known as her tough-as-talons mother-has ordered Violet to join the hundreds of candidates striving to become the elite of Navarre: dragon riders." - }, - { - "Book_ID" : 2, - "Book_Name" : "The Harry Potter Series", - "Book_Author" : "J.K. Rowling", - "Book_Genre" : "Fantasy", - "Price" : 51.65, - "Book_Script" : "The Harry Potter books follow a young wizard named Harry as he attends Hogwarts School of Witchcraft and Wizardry. Alongside his friends Ron and Hermione, Harry faces challenges, discovers his past, and confronts the dark wizard Voldemort across seven books, filled with magic, friendship, and the battle between good and evil." - }, - { - "Book_ID" : 3, - "Book_Name" : "A Court of Thorns and Roses Series", - "Book_Author" : "Sarah J. Maas", - "Book_Genre" : "Fantasy", - "Price" : 31.74, - "Book_Script" : "ACOTAR Follows Feyre, a huntress who accidentally kills a faerie and is taken to the faerie lands as punishment. There, she navigates faerie politics, forms relationships with powerful fae like Tamlin and Rhysand, and becomes involved in a high-stakes battle that could impact both human and faerie realms across several books filled with magic, romance, and conflicts." - }, - { - "Book_ID" : 4, - "Book_Name" : "To Kill a Kingdom", - "Book_Author" : "Alexandra Christo", - "Book_Genre" : "Fantasy", - "Price" : 4.67, - "Book_Script" : "Princess Lira is siren royalty and the most lethal of them all. With the hearts of seventeen princes in her collection, she is revered across the sea. Until a twist of fate forces her to kill one of her own. To punish her daughter, the Sea Queen transforms Lira into the one thing they loathe most - a human. Robbed of her song, Lira has until the winter solstice to deliver Prince Elians heart to the Sea Queen or remain a human forever." - }, - { - "Book_ID" : 5, - "Book_Name" : "Elizabeth Bathory: Life and Legacy of Historys Most Prolific Female Serial Killer", - "Book_Author" : "James Oliver", - "Book_Genre" : "History", - "Price" : 6.12, - "Book_Script" : "This book explains the life and times of this powerful woman - and how she came to be accused of so many heinous crimes. Youll gain access to a variety of historical versions, perspectives, and accounts of her life - some of which paint her as a villain and others as a victim!" - }, - { - "Book_ID" : 6, - "Book_Name" : "The Great Empires of the Ancient World", - "Book_Author" : "Thomas Harrison", - "Book_Genre" : "History", - "Price" : 11.63, - "Book_Script" : "A distinguished team of internationally renowned scholars surveys the great empires from 1600 BC to AD 500, from the ancient Mediterranean to China. Exploring the very nature of empire itself, the authors show how profoundly imperialism in the distant past influenced the 19th-century powers and the modern United States." - }, - { - "Book_ID" : 7, - "Book_Name" : "Landlines", - "Book_Author" : "Raynor Winn", - "Book_Genre" : "History", - "Price" : 6, - "Book_Script" : "Embarking on a journey across the Cape Wrath Trail, over 200 miles of gruelling terrain through Scotlands remotest mountains and lochs, Raynor and Moth look to an uncertain future. Fearing that miracles dont often repeat themselves." - }, - { - "Book_ID" : 8, - "Book_Name" : "Unbroken: A World War II Story of Survival", - "Book_Author" : "Lauren Hillenbrand", - "Book_Genre" : "History", - "Price" : 4.71, - "Book_Script" : "On a May afternoon in 1943, an Army Air Forces bomber crashed into the Pacific Ocean and disappeared, leaving only a spray of debris and a slick of oil, gasoline, and blood. Then, on the ocean surface, a face appeared. It was that of a young lieutenant, the planes bombardier, who was struggling to a life raft and pulling himself aboard. So began one of the most extraordinary odysseys of the Second World War." - }, - { - "Book_ID" : 9, - "Book_Name" : "The Haunting of Hill House", - "Book_Author" : "Shirley Jackson", - "Book_Genre" : "Horror", - "Price" : 9.9, - "Book_Script" : "Welcome to Hill House, an eerie mansion with a chilling past. When a group of individuals sets out to uncover its supernatural secrets, they find themselves trapped in a world where reality blurs with the terrifying unknown. Shirley Jacksons classic tale weaves a haunting narrative that explores the eerie power of a house that seems to have a mind of its own." - }, - { - "Book_ID" : 10, - "Book_Name" : "Dracula", - "Book_Author" : "Bram Stoker", - "Book_Genre" : "Horror", - "Price" : 14.29, - "Book_Script" : "When Jonathan Harker visits Transylvania to help Count Dracula with the purchase of a London house, he makes a series of horrific discoveries about his client. Soon afterwards, various bizarre incidents unfold in England: an apparently unmanned ship is wrecked off the coast of Whitby; a young woman discovers strange puncture marks on her neck; and the inmate of a lunatic asylum raves about the Master and his imminent arrival." - }, - { - "Book_ID" : 11, - "Book_Name" : "The Shining", - "Book_Author" : "Stephen King", - "Book_Genre" : "Horror", - "Price" : 10.11, - "Book_Script" : "Danny is only five years old, but in the words of old Mr Hallorann he is a shiner, aglow with psychic voltage. When his father becomes caretaker of the Overlook Hotel, Dannys visions grow out of control. As winter closes in and blizzards cut them off, the hotel seems to develop a life of its own. It is meant to be empty. So who is the lady in Room 217 and who are the masked guests going up and down in the elevator? And why do the hedges shaped like animals seem so alive? Somewhere, somehow, there is an evil force in the hotel - and that, too, is beginning to shine." - }, - { - "Book_ID" : 12, - "Book_Name" : "The Exorcist", - "Book_Author" : "William Peter Blatty", - "Book_Genre" : "Horror", - "Price" : 9.19, - "Book_Script" : "The terror begins unobtrusively. Noises in the attic. In the childs room, an odd smell, the displacement of furniture, an icy chill. At first, easy explanations are offered. Then frightening changes begin to appear in eleven-year-old Regan. Medical tests fail to shed any light on her symptoms, but it is as if a different personality has invaded her body." - } -] diff --git a/express-server/config.js b/express-server/config.js deleted file mode 100644 index b7c641c..0000000 --- a/express-server/config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - database: { - host: '127.0.0.1', - user: 'root', - password: '', - databaseName: 'genre_content' - }, - server: { - port: process.env.PORT || 8080 - } -}; \ No newline at end of file diff --git a/express-server/db.js b/express-server/db.js deleted file mode 100644 index fb1d9eb..0000000 --- a/express-server/db.js +++ /dev/null @@ -1,19 +0,0 @@ -const mysql = require('mysql'); -const config = require('./config'); - -const connection = mysql.createConnection({ - host: config.database.host, - user: config.database.user, - password: config.database.password, - database: config.database.databaseName -}); - -connection.connect(err => { - if (err) { - console.error('Error connecting to database:', err); - return; - } - console.log('Connected to MySQL database'); -}); - -module.exports = connection; diff --git a/express-server/errorHandling.js b/express-server/errorHandling.js deleted file mode 100644 index 5651309..0000000 --- a/express-server/errorHandling.js +++ /dev/null @@ -1,6 +0,0 @@ -function handleDatabaseErrors(res, err, tablename, genre) { - console.error(`Error fetching ${tablename} suggestions for ${genre}:`, err); - res.status(500).json({ error: `Error fetching ${tablename} suggestions for ${genre}` }); -} - -module.exports = {handleDatabaseErrors} ; \ No newline at end of file diff --git a/express-server/gameRoutes.js b/express-server/gameRoutes.js deleted file mode 100644 index ee35941..0000000 --- a/express-server/gameRoutes.js +++ /dev/null @@ -1,23 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const connection = require('./db'); -const handleDatabaseErrors = require('./errorHandling').default; - -router.get('/', (req, res) => { - console.log('Fetching games from the database...'); - connection.query('SELECT * FROM Games', (err, results) => { - if (err) { - console.error('Error querying MySQL - Games: Please check your query.', err); - res.status(500).json({ error: 'Error querying MySQL Games' }); - return; - } - if (results.length === 0) { - console.log('No games found in the database.'); - } else { - console.log('Games fetched successfully.'); - } - res.json(results); - }); -}); - -module.exports = { router, connection, handleDatabaseErrors }; \ No newline at end of file diff --git a/express-server/games.json b/express-server/games.json deleted file mode 100644 index 32dfe04..0000000 --- a/express-server/games.json +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - "Game_ID" : 1, - "Game_Name" : "Fae Farm", - "Game_Genre" : "Cozy Games", - "W_Console" : "PC, NINTENDO SWITCH", - "Price" : 29.99, - "Game_Script" : "Escape to the magical life of your dreams in Fae Farm, a farm sim RPG for 1-4 players. Craft, cultivate, and decorate to grow your homestead, and use spells to explore the enchanted island of Azoria!" - }, - { - "Game_ID" : 2, - "Game_Name" : "Spellcaster University", - "Game_Genre" : "Cozy Games", - "W_Console" : "PC", - "Price" : 19.49, - "Game_Script" : "Develop a prestigious university of mages. Build rooms, train your students, fight orcs, slay the bureaucrats, manage your budget... a directors life is not a quiet one." - }, - { - "Game_ID" : 3, - "Game_Name" : "Little Witch in the Woods", - "Game_Genre" : "Cozy Games", - "W_Console" : "PC, NINTENDO SWITCH", - "Price" : 12.39, - "Game_Script" : " Little Witch in the Woods tells the story of Ellie, an apprentice witch. Explore the mystical forest, help the charming residents, and experience the daily life of the witch." - }, - { - "Game_ID" : 4, - "Game_Name" : "Cozy Grove", - "Game_Genre" : "Cozy Games", - "W_Console" : "PC, NINTENDO SWITCH", - "Price" : 11.39, - "Game_Script" : "Welcome to Cozy Grove, a game about camping on a haunted, ever-changing island. As a Spirit Scout, youll wander the islands forest each day, finding new hidden secrets and helping soothe the local ghosts. With a little time and a lot of crafting, youll bring color and joy back to Cozy Grove!" - }, - { - "Game_ID" : 5, - "Game_Name" : "Hogwarts Legacy", - "Game_Genre" : "RPG", - "W_Console" : "PC, XBOX, PLAYSTATION, NINTENDO SWITCH", - "Price" : 49.99, - "Game_Script" : "Hogwarts Legacy is an immersive, open-world action RPG. Now you can take control of the action and be at the center of your own adventure in the wizarding world." - }, - { - "Game_ID" : 6, - "Game_Name" : "God of War", - "Game_Genre" : "RPG", - "W_Console" : "PLAYSTATION", - "Price" : 39.99, - "Game_Script" : "Against a backdrop of Norse Realms torn asunder by the fury of the Aesir, they’ve been trying their utmost to undo the end times. But despite their best efforts, Fimbulwinter presses onward. Witness the changing dynamic of the father-son relationship as they fight for survival. Atreus thirsts for knowledge to help him understand the prophecy of Loki, as Kratos struggles to break free of his past and be the father his son needs." - }, - { - "Game_ID" : 7, - "Game_Name" : "Lies of P", - "Game_Genre" : "RPG", - "W_Console" : "PLAYSTATION, PC, XBOX", - "Price" : 49.99, - "Game_Script" : "Lies of P is a thrilling soulslike that takes the story of Pinocchio, turns it on its head, and sets it against the darkly elegant backdrop of the Belle Epoque era." - }, - { - "Game_ID" : 8, - "Game_Name" : "Marvels Spider Man 2", - "Game_Genre" : "RPG", - "W_Console" : "PLAYSTATION", - "Price" : 69.99, - "Game_Script" : "Peter Parker and Miles Morales return for an exciting new adventure in the critically acclaimed Marvel’s Spider-Man franchise. Swing, jump and utilize the new Web Wings to travel across Marvel’s New York, quickly switching between Peter Parker and Miles Morales to experience different stories and epic new powers, as the iconic villain Venom threatens to destroy their lives, their city and the ones they love." - }, - { - "Game_ID" : 9, - "Game_Name" : "STARDEW VALLEY", - "Game_Genre" : "SIMULATION", - "W_Console" : "PC, NINTENDO SWITCH, XBOX, PLAYSTATION", - "Price" : 10.99, - "Game_Script" : "Youve inherited your grandfathers old farm plot in Stardew Valley. Armed with hand-me-down tools and a few coins, you set out to begin your new life. Can you learn to live off the land and turn these overgrown fields into a thriving home?" - }, - { - "Game_ID" : 10, - "Game_Name" : "Two Point Hospital", - "Game_Genre" : "SIMULATION", - "W_Console" : "PC, NINTENDO SWITCH, XBOX, PLAYSTATION", - "Price" : 24.99, - "Game_Script" : "Design stunning hospitals, cure peculiar illnesses and manage troublesome staff as you spread your budding healthcare organisation across Two Point County." - }, - { - "Game_ID" : 11, - "Game_Name" : "GAME DEV TYCOON", - "Game_Genre" : "SIMULATION", - "W_Console" : "PC", - "Price" : 8.5, - "Game_Script" : "In Game Dev Tycoon you replay the history of the gaming industry by starting your own video game development company in the 80s. Create best selling games. Research new technologies and invent new game types. Become the leader of the market and gain worldwide fans." - }, - { - "Game_ID" : 12, - "Game_Name" : "NAHEULBEUKS DUNGEON MASTER", - "Game_Genre" : "SIMULATION", - "W_Console" : "PC", - "Price" : 20.99, - "Game_Script" : "A dungeon in danger ! Build, manage, and defend your tower in the satirical heroic fantasy universe of Dungeon of Naheulbeuk. From a shaky establishment to an infamous lair!" - } -] diff --git a/express-server/genre_content.sql b/express-server/genre_content.sql deleted file mode 100644 index 0e9c091..0000000 --- a/express-server/genre_content.sql +++ /dev/null @@ -1,76 +0,0 @@ -CREATE DATABASE genre_content; -USE genre_content; - -CREATE TABLE Anime ( - Anime_ID INTEGER PRIMARY KEY NOT NULL, - Anime_Name VARCHAR(50) UNIQUE NOT NULL, - Anime_Genre VARCHAR(25) NOT NULL, - Where_TW VARCHAR(25), - Anime_Script VARCHAR(1000) - ); - -CREATE TABLE Books ( - Book_ID INTEGER PRIMARY KEY NOT NULL, - Book_Name VARCHAR(100) UNIQUE NOT NULL, - Book_Author VARCHAR(30) NOT NULL, - Book_Genre VARCHAR(25) NOT NULL, - Price FLOAT NOT NULL, - Book_Script VARCHAR(1000) - ); - -CREATE TABLE Games ( - Game_ID INTEGER PRIMARY KEY NOT NULL, - Game_Name VARCHAR(50) UNIQUE NOT NULL, - Game_Genre VARCHAR(30) NOT NULL, - W_Console VARCHAR(100), - Price FLOAT NOT NULL, - Game_Script VARCHAR(1000) - ); - -INSERT INTO Books -(Book_ID, Book_Name, Book_Author, Book_Genre, Price, Book_Script) -VALUES -(1, 'Fourth Wing', 'Rebecca Yarros', 'Fantasy', 9.19, 'Twenty-year-old Violet Sorrengail was supposed to enter the Scribe Quadrant, living a quiet life among books and history. Now, the commanding general-also known as her tough-as-talons mother-has ordered Violet to join the hundreds of candidates striving to become the elite of Navarre: dragon riders.'), -(2, 'The Harry Potter Series', 'J.K. Rowling', 'Fantasy', 51.65, 'The Harry Potter books follow a young wizard named Harry as he attends Hogwarts School of Witchcraft and Wizardry. Alongside his friends Ron and Hermione, Harry faces challenges, discovers his past, and confronts the dark wizard Voldemort across seven books, filled with magic, friendship, and the battle between good and evil.'), -(3, 'A Court of Thorns and Roses Series', 'Sarah J. Maas', 'Fantasy', 31.74, 'ACOTAR Follows Feyre, a huntress who accidentally kills a faerie and is taken to the faerie lands as punishment. There, she navigates faerie politics, forms relationships with powerful fae like Tamlin and Rhysand, and becomes involved in a high-stakes battle that could impact both human and faerie realms across several books filled with magic, romance, and conflicts.'), -(4, 'To Kill a Kingdom', 'Alexandra Christo', 'Fantasy', 4.67, 'Princess Lira is siren royalty and the most lethal of them all. With the hearts of seventeen princes in her collection, she is revered across the sea. Until a twist of fate forces her to kill one of her own. To punish her daughter, the Sea Queen transforms Lira into the one thing they loathe most - a human. Robbed of her song, Lira has until the winter solstice to deliver Prince Elians heart to the Sea Queen or remain a human forever.'), -(5, 'Elizabeth Bathory: Life and Legacy of Historys Most Prolific Female Serial Killer', 'James Oliver', 'History', 6.12, 'This book explains the life and times of this powerful woman - and how she came to be accused of so many heinous crimes. Youll gain access to a variety of historical versions, perspectives, and accounts of her life - some of which paint her as a villain and others as a victim!'), -(6, 'The Great Empires of the Ancient World', 'Thomas Harrison', 'History', 11.63, 'A distinguished team of internationally renowned scholars surveys the great empires from 1600 BC to AD 500, from the ancient Mediterranean to China. Exploring the very nature of empire itself, the authors show how profoundly imperialism in the distant past influenced the 19th-century powers and the modern United States.'), -(7, 'Landlines', 'Raynor Winn', 'History', 6.00, 'Embarking on a journey across the Cape Wrath Trail, over 200 miles of gruelling terrain through Scotlands remotest mountains and lochs, Raynor and Moth look to an uncertain future. Fearing that miracles dont often repeat themselves.'), -(8, 'Unbroken: A World War II Story of Survival', 'Lauren Hillenbrand', 'History', 4.71, 'On a May afternoon in 1943, an Army Air Forces bomber crashed into the Pacific Ocean and disappeared, leaving only a spray of debris and a slick of oil, gasoline, and blood. Then, on the ocean surface, a face appeared. It was that of a young lieutenant, the planes bombardier, who was struggling to a life raft and pulling himself aboard. So began one of the most extraordinary odysseys of the Second World War.'), -(9, 'The Haunting of Hill House', 'Shirley Jackson', 'Horror', 9.90, 'Welcome to Hill House, an eerie mansion with a chilling past. When a group of individuals sets out to uncover its supernatural secrets, they find themselves trapped in a world where reality blurs with the terrifying unknown. Shirley Jacksons classic tale weaves a haunting narrative that explores the eerie power of a house that seems to have a mind of its own.'), -(10, 'Dracula', 'Bram Stoker', 'Horror', 14.29, 'When Jonathan Harker visits Transylvania to help Count Dracula with the purchase of a London house, he makes a series of horrific discoveries about his client. Soon afterwards, various bizarre incidents unfold in England: an apparently unmanned ship is wrecked off the coast of Whitby; a young woman discovers strange puncture marks on her neck; and the inmate of a lunatic asylum raves about the Master and his imminent arrival.'), -(11, 'The Shining', 'Stephen King', 'Horror', 10.11, 'Danny is only five years old, but in the words of old Mr Hallorann he is a shiner, aglow with psychic voltage. When his father becomes caretaker of the Overlook Hotel, Dannys visions grow out of control. As winter closes in and blizzards cut them off, the hotel seems to develop a life of its own. It is meant to be empty. So who is the lady in Room 217 and who are the masked guests going up and down in the elevator? And why do the hedges shaped like animals seem so alive? Somewhere, somehow, there is an evil force in the hotel - and that, too, is beginning to shine.'), -(12, 'The Exorcist', 'William Peter Blatty', 'Horror', 9.19, 'The terror begins unobtrusively. Noises in the attic. In the childs room, an odd smell, the displacement of furniture, an icy chill. At first, easy explanations are offered. Then frightening changes begin to appear in eleven-year-old Regan. Medical tests fail to shed any light on her symptoms, but it is as if a different personality has invaded her body.'); - -INSERT INTO Anime -(Anime_ID, Anime_Name, Anime_Genre, Where_TW, Anime_Script) -VALUES -(1, 'Bleach', 'Shonen', 'Disney+', 'Ichigo Kurosaki is a teenager from Karakura Town who can see ghosts, a talent allowing him to meet a supernatural human Rukia Kuchiki, who enters the town in search of a Hollow, a kind of monstrous lost soul who can harm both ghosts and humans.'), -(2, 'Naruto', 'Shonen', 'Crunchyroll', 'The Village Hidden in the Leaves is home to the stealthiest ninja. But twelve years earlier, a fearsome Nine-tailed Fox terrorized the village before it was subdued and its spirit sealed within the body of a baby boy.'), -(3, 'Jujutsu Kaisen', 'Shonen', 'Crunchyroll', 'Yuji Itadori is a boy with tremendous physical strength, though he lives a completely ordinary high school life. One day, to save a classmate who has been attacked by curses, he eats the finger of Ryomen Sukuna, taking the curse into his own soul. From then on, he shares one body with Ryomen Sukuna. Guided by the most powerful of sorcerers, Satoru Gojo, Itadori is admitted to Tokyo Jujutsu High School, an organization that fights the curses... and thus begins the heroic tale of a boy who became a curse to exorcise a curse, a life from which he could never turn back.'), -(4, 'One Piece', 'Shonen', 'Crunchyroll', 'Monkey. D. Luffy refuses to let anyone or anything stand in the way of his quest to become the king of all pirates. With a course charted for the treacherous waters of the Grand Line and beyond, this is one captain who will never give up until he has claimed the greatest treasure on Earth: the Legendary One Piece!'), -(5, 'Attack on Titan', 'Seinen', 'Crunchyroll', 'Known in Japan as Shingeki no Kyojin, many years ago, the last remnants of humanity were forced to retreat behind the towering walls of a fortified city to escape the massive, man-eating Titans that roamed the land outside their fortress. Only the heroic members of the Scouting Legion dared to stray beyond the safety of the walls – but even those brave warriors seldom returned alive. Those within the city clung to the illusion of a peaceful existence until the day that dream was shattered, and their slim chance at survival was reduced to one horrifying choice: kill – or be devoured!'), -(6, 'Tokyo Ghoul', 'Seinen', 'Crunchyroll', 'Haise Sasaki has been tasked with teaching Qs Squad how to be outstanding investigators, but his assignment is complicated by the troublesome personalities of his students and his own uncertain grasp of his Ghoul powers. Can he pull them together as a team, or will Qs Squad first assignment be their last?'), -(7, 'Berserk', 'Seinen', 'Crunchyroll', 'Spurred by the flame raging in his heart, the Black Swordsman Guts continues his seemingly endless quest for revenge. Standing in his path are heinous outlaws, delusional evil spirits, and a devout child of god.Even as it chips away at his life, Guts continues to fight his enemies, who wield repulsive and inhumane power, with nary but his body and sword—his strength as a human. What lies at the end of his travels? The answer is shrouded in the night.'), -(8, 'Death Note', 'Seinen', 'Crunchyroll', 'An intelligent high school student goes on a secret crusade to eliminate criminals from the world after discovering a notebook capable of killing anyone whose name is written into it.'), -(9, 'Chainsaw Man', 'Fantasy', 'Crunchyroll', 'Denji is a young boy who works as a Devil Hunter with the Chainsaw Devil Pochita. One day, as he was living his miserable life trying to pay off the debt he inherited from his parents, he got betrayed and killed. As he was losing his consciousness, he made a deal with Pochita, and got resurrected as the Chainsaw Man: the owner of the Devil’s heart.'), -(10, 'JoJos Bizarre Adventure', 'Fantasy', 'Netflix', 'In ancient Mexico, people of Aztec had prospered. They had historic and strange Stone Mask. It was a miraculous mask which brings eternal life and the power of authentic ruler. But the mask suddenly disappeared. A long time after that, in late 19th centuries when the thought and life of people were suddenly changing, Jonathan Joestar met with Dio Brando. They spend time together through boyhood to youth, and the Stone Mask brings curious fate to them.'), -(11, 'Black Clover', 'Fantasy', 'Crunchyroll', 'In a world where magic is everything, Asta and Yuno are both found abandoned at a church on the same day. While Yuno is gifted with exceptional magical powers, Asta is the only one in this world without any. At the age of fifteen, both receive grimoires, magic books that amplify their holder’s magic. Asta’s is a rare Grimoire of Anti-Magic that negates and repels his opponent’s spells. Being opposite but good rivals, Yuno and Asta are ready for the hardest of challenges to achieve their common dream: to be the Wizard King. Giving up is never an option!'), -(12, 'Link Click', 'Fantasy', 'Crunchyroll', 'Using superpowers to enter their clientele’s photos one by one, Cheng Xiaoshi and Lu Guang take their work seriously at Time Photo Studio, a small photography shop set in the backdrop of a modern metropolis. Each job can be full of danger, but nothing is more important than fulfilling every order, no matter the scale…or peril involved!'); - -INSERT INTO Games -(Game_ID, Game_Name, Game_Genre, W_Console, Price, Game_Script) -VALUES -(1, 'Fae Farm', 'Cozy Games', 'PC, NINTENDO SWITCH', 29.99, 'Escape to the magical life of your dreams in Fae Farm, a farm sim RPG for 1-4 players. Craft, cultivate, and decorate to grow your homestead, and use spells to explore the enchanted island of Azoria!'), -(2, 'Spellcaster University', 'Cozy Games', 'PC', 19.49, 'Develop a prestigious university of mages. Build rooms, train your students, fight orcs, slay the bureaucrats, manage your budget... a directors life is not a quiet one.'), -(3, 'Little Witch in the Woods', 'Cozy Games', 'PC, NINTENDO SWITCH', 12.39, ' Little Witch in the Woods tells the story of Ellie, an apprentice witch. Explore the mystical forest, help the charming residents, and experience the daily life of the witch.'), -(4, 'Cozy Grove', 'Cozy Games', 'PC, NINTENDO SWITCH', 11.39, 'Welcome to Cozy Grove, a game about camping on a haunted, ever-changing island. As a Spirit Scout, youll wander the islands forest each day, finding new hidden secrets and helping soothe the local ghosts. With a little time and a lot of crafting, youll bring color and joy back to Cozy Grove!'), -(5, 'Hogwarts Legacy', 'RPG', 'PC, XBOX, PLAYSTATION, NINTENDO SWITCH', 49.99, 'Hogwarts Legacy is an immersive, open-world action RPG. Now you can take control of the action and be at the center of your own adventure in the wizarding world.'), -(6, 'God of War', 'RPG', 'PLAYSTATION', 39.99, 'Against a backdrop of Norse Realms torn asunder by the fury of the Aesir, they’ve been trying their utmost to undo the end times. But despite their best efforts, Fimbulwinter presses onward. Witness the changing dynamic of the father-son relationship as they fight for survival. Atreus thirsts for knowledge to help him understand the prophecy of Loki, as Kratos struggles to break free of his past and be the father his son needs.'), -(7, 'Lies of P', 'RPG', 'PLAYSTATION, PC, XBOX', 49.99, 'Lies of P is a thrilling soulslike that takes the story of Pinocchio, turns it on its head, and sets it against the darkly elegant backdrop of the Belle Epoque era.'), -(8, 'Marvels Spider Man 2', 'RPG', 'PLAYSTATION', 69.99, 'Peter Parker and Miles Morales return for an exciting new adventure in the critically acclaimed Marvel’s Spider-Man franchise. Swing, jump and utilize the new Web Wings to travel across Marvel’s New York, quickly switching between Peter Parker and Miles Morales to experience different stories and epic new powers, as the iconic villain Venom threatens to destroy their lives, their city and the ones they love.'), -(9, 'STARDEW VALLEY', 'SIMULATION', 'PC, NINTENDO SWITCH, XBOX, PLAYSTATION', 10.99, 'Youve inherited your grandfathers old farm plot in Stardew Valley. Armed with hand-me-down tools and a few coins, you set out to begin your new life. Can you learn to live off the land and turn these overgrown fields into a thriving home?'), -(10, 'Two Point Hospital', 'SIMULATION', 'PC, NINTENDO SWITCH, XBOX, PLAYSTATION', 24.99, 'Design stunning hospitals, cure peculiar illnesses and manage troublesome staff as you spread your budding healthcare organisation across Two Point County.'), -(11, 'GAME DEV TYCOON', 'SIMULATION', 'PC', 8.50, 'In Game Dev Tycoon you replay the history of the gaming industry by starting your own video game development company in the 80s. Create best selling games. Research new technologies and invent new game types. Become the leader of the market and gain worldwide fans.'), -(12, 'NAHEULBEUKS DUNGEON MASTER', 'SIMULATION', 'PC', 20.99, 'A dungeon in danger ! Build, manage, and defend your tower in the satirical heroic fantasy universe of Dungeon of Naheulbeuk. From a shaky establishment to an infamous lair!'); \ No newline at end of file diff --git a/express-server/index.js b/express-server/index.js deleted file mode 100644 index bead55e..0000000 --- a/express-server/index.js +++ /dev/null @@ -1,18 +0,0 @@ -const express = require('express'); -const app = express(); -const PORT = process.env.PORT || 8080; -const cors = require("cors"); - -const bookRoutes = require('./bookRoutes'); -const animeRoutes = require('./animeRoutes'); -const gameRoutes = require('./gameRoutes'); -const suggestions = require('./suggestions'); - -app.use('/api/Books', bookRoutes); -app.use('/api/Anime', animeRoutes); -app.use('/api/Games', gameRoutes); -app.use('/api/suggestions', suggestions); - -app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); -}); \ No newline at end of file diff --git a/express-server/server.js b/express-server/server.js deleted file mode 100644 index 74204d2..0000000 --- a/express-server/server.js +++ /dev/null @@ -1,35 +0,0 @@ -//REST API demo in Node.js -let express = require('express'); // requre the express framework -let app = express(); -let fs = require('fs'); //require file system object - -// Endpoint to Get a list of genres -app.get('/getGames', function(req, res){ - fs.readFile(__dirname + "/" + "games.json", 'utf8', function(err, data){ - console.log(data); - res.end(data); // you can also use res.send() - }); -}) - -// Endpoint to Get a list of genres -app.get('/getAnime', function(req, res){ - fs.readFile(__dirname + "/" + "anime.json", 'utf8', function(err, data){ - console.log(data); - res.end(data); // you can also use res.send() - }); -}) - -// Endpoint to Get a list of genres -app.get('/getBooks', function(req, res){ - fs.readFile(__dirname + "/" + "books.json", 'utf8', function(err, data){ - console.log(data); - res.end(data); // you can also use res.send() - }); -}) - -// Create a server to listen at port 8080 -let server = app.listen(8080, function(){ - let host = server.address().address - let port = server.address().port - console.log("REST API demo app listening at http://localhost:8080") -}) \ No newline at end of file diff --git a/express-server/suggestions.js b/express-server/suggestions.js deleted file mode 100644 index 3df7d04..0000000 --- a/express-server/suggestions.js +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const db = require('./db'); -const suggestionsData = require('./suggestionsData'); - -router.get('/:tablename/:genre', (req, res) => { - const { tablename, genre } = req.params; - - const query = `SELECT * FROM ${tablename} WHERE ${tablename}_genre = ?`; - - db.query(query, [genre], (err, results) => { - if (err) { - console.error(`Error fetching ${tablename} suggestions for ${genre}:`, err); - res.status(500).json({ error: `Error fetching ${tablename} suggestions for ${genre}` }); - return; - } - - const mergedResults = results.map(result => { - const title = result[`${tablename}_Title`]; // Get the title from the fetched results - const matchingSuggestion = suggestionsData[tablename][genre].find(suggestion => suggestion.title === title); - - if (matchingSuggestion) { - return { - ...result, - image: matchingSuggestion.image - }; - } - return result; - }); - - res.json(mergedResults); - }); -}); - -// export default router; -module.exports = router; diff --git a/express-server/suggestionsData.js b/express-server/suggestionsData.js deleted file mode 100644 index 1440981..0000000 --- a/express-server/suggestionsData.js +++ /dev/null @@ -1,64 +0,0 @@ -const suggestionsData = { - Books: { - Fantasy: [ - { id: 1, title: 'Fourth Wing', image: 'FourthWing.jpg' }, - { id: 2, title: 'The Harry Potter Series', image: 'HarryPotter.jpg' }, - { id: 3, title: 'A Court of Thorns and Roses Series', image: 'ACOTAR.jpg' }, - { id: 4, title: 'To Kill a Kingdom', image: 'ToKillAKingdom.jpg' } - ], - History: [ - { id: 5, title: 'Elizabeth Bathory: Life and Legacy of Historys Most Prolific Female Serial Killer', image: 'ElizabethBathory.jpg' }, - { id: 6, title: 'The Great Empires of the Ancient World', image: 'TheGreatEmpires.jpg' }, - { id: 7, title: 'Landlines', image: 'Landlines.jpg' }, - { id: 8, title: 'Unbroken: A World War II Story of Survival', image: 'Unbroken.jpg' } - ], - Horror: [ - { id: 9, title: 'The Haunting of Hill House', image: 'TheHaunting' }, - { id: 10, title: 'Dracula', image: 'Dracula.jpg' }, - { id: 11, title: 'The Shining', image: 'TheShining.jpg' }, - { id: 12, title: 'The Exorcist', image: 'TheExorcist.jpg' } - ] - }, - Anime: { - Shonen: [ - { id: 1, title: 'Bleach', image: 'Bleach.jpg' }, - { id: 2, title: 'Naruto', image: 'Naruto.jpg' }, - { id: 3, title: 'Jujutsu Kaisen', image: 'JJK.jpg' }, - { id: 4, title: 'One Piece', image: 'OnePiece.jpg' } - ], - Seinen: [ - { id: 5, title: 'Attack on Titan', image: 'AttackOnTitan.jpg' }, - { id: 6, title: 'Tokyo Ghoul', image: 'TokyoGhoul.jpg' }, - { id: 7, title: 'Berserk', image: 'Berserk.jpg' }, - { id: 8, title: 'Death Note', image: 'DeathNote.jpg' } - ], - Fantasy: [ - { id: 9, title: 'Chainsaw Man', image: 'ChainsawMan.jpg' }, - { id: 10, title: 'JoJos Bizarre Adventure', image: 'Jojo.jpg' }, - { id: 11, title: 'Black Clover', image: 'BlackClover.jpg' }, - { id: 12, title: 'Link Click', image: 'LinkClick.jpg' } - ] - }, - Games: { - 'Cozy games': [ - { id: 1, title: 'Fae Farm', image: 'FaeFarm.jpg' }, - { id: 2, title: 'Spellcaster University', image: 'SpellcasterUniversity.jpg' }, - { id: 3, title: 'Little Witch in the Woods', image: 'LittleWitch.jpg' }, - { id: 4, title: 'Cozy Grove', image: 'CozyGrove.jpg' } - ], - RPG: [ - { id: 5, title: 'Hogwarts Legacy', image: 'HogwartsLegacy.jpg' }, - { id: 6, title: 'God of War', image: 'GodOfWar.jpg' }, - { id: 7, title: 'Lies of P', image: 'LiesOfP.jpg' }, - { id: 8, title: 'Marvels Spider-Man 2', image: 'Spiderman2.jpg' } - ], - SIMULATION: [ - { id: 9, title: 'STARDEW VALLEY', image: 'StardewValley.jpg' }, - { id: 10, title: 'Two Point Hospital', image: 'TwoPointHospital.jpg' }, - { id: 11, title: 'GAME DEV TYCOON', image: 'GameDevTycoon.jpg' }, - { id: 12, title: 'NAHEULBEUKS DUNGEON MASTER', image: 'DungeonMaster.jpg' } - ] - } -}; - -module.exports = suggestionsData; \ No newline at end of file diff --git a/flask-server/.env b/flask-server/.env deleted file mode 100644 index fce27e6..0000000 --- a/flask-server/.env +++ /dev/null @@ -1 +0,0 @@ -SECRET_KEY=waeiuhiawehaiuwehiehiaehiew \ No newline at end of file diff --git a/flask-server/.env.example b/flask-server/.env.example new file mode 100644 index 0000000..9f0be3d --- /dev/null +++ b/flask-server/.env.example @@ -0,0 +1,13 @@ +# Rename or create a copy of this file as ".env" + +# Create secret keys +SECRET_KEY= +JWT_SECRET_KEY= + +# Add your database credentials +HOST=localhost +USER=root +PASSWORD= +PORT=3306 +DATABASE=introverse_dev +TESTDB=introverse_test diff --git a/flask-server/README.md b/flask-server/README.md index f69cc62..3372772 100644 --- a/flask-server/README.md +++ b/flask-server/README.md @@ -1,32 +1,143 @@ # Server for IntroVerse project -Description about IntroVerse Group 1 Project -## To set up and run the server -1. Create a virtual python environment (can do this with command or VScode should prompt you when try to run a file) and select it + +This is the flask server for Queens of Code's project IntroVerse. IntroVerse is a website which started out as our final project for the CFGdegree. It is designed to be a safe nurturing environment for introverts who love anime, reading and gaming; to get recommendations, discuss and connect with others, and find resources, both for mental wellbeing and times of need. + +## Prerequisites + +This project uses MySQL for the development and testing databases. However, if you do not have MySQL it is also possible to run it with a SQLite database. To do this, open config.py and change the database connections in the DevConfig and TestConfig classes to use the sqlite DB variables instead. It hasn't been developed with SQLite in mind, so is not tested, but it should be possible. + +## Set up + +1. Create a virtual python environment (can do this with command or VScode should prompt you when try to run a file) and select it. + ``` -python -m venv path +python -m venv venv ``` -2. Install the required packages with pip + +2. Install the required packages with pip (or pip3 for mac). + ``` pip install -r requirements.txt ``` -If you have any issues with modules not being installed properly try pip install manually. If you are a windows user you may have to do this instead: -``` -pip install --user -r requirements.txt -``` -If they are installed but the file shows not imported properly try changing the Python interpreter (Ctrl+Shift+P in VS code) and reinstalling if necessary. Have the virtual environment selected. -3. Change mysqlconfig.py to your credentials -4. Create the database if does not already exist - either through the SQL script or with Python -To create from Python run the create_db.py file + +3. Set up the .env file, can create a copy of (or rename) .env.example and save as .env. Follow the instructions to add a secret key and your MySQL credentials. +4. Create the databases, either through the SQL script or with Python. Running create_db.py will create them if they don't exist or print if they do. + ``` python create_db.py ``` -5. Create the message_board table directly from the SQL file. Then the other tables (if do not already exist) can be created either through the SQL script or with Python (create_tables.py) + +5. Create tables of the development database through the SQL script or running create_tables.py (recommended). Can use flask migrate, flask db upgrade, to get the latest migrations of tables. Alternatively you can also uncomment with app context db.create_all in the app.py factory function. + ``` python create_tables.py ``` -6. Insert the message_board, book, game, and anime data from the SQL file -7. Run routes.py to start the server + +``` +flask db upgrade +``` + +6. Insert the book, game, and anime data from the SQL script file into your database so that can use the recommendation feature. +7. Run app.py to start the server + +``` +python app.py +``` + +Enjoy! + +## How it is built and features + +The frontend of our website uses React and the backend uses Flask. The backend also uses SQLAlchemy and MySQL as the database, and Flask-RESTX (a fork of Flask-RESTPlus) with the swagger docs for the server on the base path. Tests have been written using unittest, in the hope of covering the most common scenarios but welcome to suggestions.

+The API features are: + +- User registration, login, and logout, which uses flask-jwt-extended (and react-auth-token on the frontend) +- Endpoints to view and edit profile +- Forum message board with ability to create, read, edit, and delete posts +- Filtering for forum messages based on category and author +- Content recommendation API for books, anime, and games (this is currently fairly limited in size for demonstration but we intend to grow it) +- Filtering for recommendations based on various fields such as author, console, genre etc + +## Structure + +The backend structure of this project is depicted below. + +``` +flask-server +├── migrations +├── models +| ├── content_models.py +| ├── forum_models.py +| └── user_models.py +├── routes +| ├── content.py +| ├── forum.py +| └── user.py +├── tests +| ├── mock_data.py +| ├── test_anime_api.py +| ├── test_auth_api.py +| ├── test_books_api.py +| ├── test_forum_api.py +| ├── test_games_api.py +| └── test_user_api.py +├── venv +├── .env +├── .env.example +├── app.py +├── config.py +├── create_db.py +├── create_tables.py +├── exts.py +├── introverseSQL.sql +├── README.md +└── requirements.txt +``` + +- migrations - Folder containing the flask migrate files +- models - Contains the database model classes split according to namespace +- routes - Contains the different API namespaces and endpoints as well as their respective data transfer objects +- tests - Contains all the test files for each of the API endpoints and mock data file for content filtering tests, uses the testing database (also MySQL by default) +- venv - Virtual environment files +- .env and .env.example - Environment variables file (create and fill in if does not exist) and template +- app.py - Factory function for creating the app and run file +- config.py - Application config classes for connecting to database with the app +- create_db.py - Can run to create databases or check if they exist +- create_tables.py - Can run to create tables if want to +- exts.py - Extensions file, contains an instance of SQLAlchemy and JWTManager so that they can be imported into app.py and other files from one place +- introverseSQL.sql - SQL database file containing table creation if wish and the data to insert for recommendation API +- README.md - This guy +- requirements.txt - Requirements to pip install + +## Troubleshooting and known issues + +### Pip requirements + +If you have any issues with modules not being installed properly try pip install manually. If you are a windows user you may have to do this instead: + +``` +pip install --user -r requirements.txt +``` + +If they are installed but the file shows things not imported properly try changing the Python interpreter (Ctrl+Shift+P in VS code) and then reinstalling packages if necessary. Have the virtual environment selected. + +### Environment variables + +The config.py file will use the environment variables from .env to connect to your database in MySQL. The load_dotenv function loads them so they can be used. However, if you have system environment variables of the same name it will use those unless override is set to true. "USER" is a common one, we found this out the hard way when database would not connect on another computer, so added override=True. + ``` -python routes.py +load_dotenv(override=True) ``` -Enjoy! \ No newline at end of file + +### Localhost 5000 + +This server uses the default port for Flask (5000) and the frontend is set up to use endpoints with that localhost. Mac users may need to disable AirPlay which also runs on port 5000 (we found this out the hard way as well from our Mac user). + +## Useful docs + +[Flask](https://flask.palletsprojects.com/en/3.0.x/)\ +[SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/)\ +[Flask-RESTX](https://flask-restx.readthedocs.io/en/latest/)\ +[Flask-RESTPlus](https://flask-restplus.readthedocs.io/en/stable/)\ +[Flask-JWT-Extended](https://flask-jwt-extended.readthedocs.io/en/stable/)\ +[Flask-Migrate](https://flask-migrate.readthedocs.io/en/latest/) diff --git a/flask-server/__pycache__/app.cpython-312.pyc b/flask-server/__pycache__/app.cpython-312.pyc index 3c147db..6ef89cf 100644 Binary files a/flask-server/__pycache__/app.cpython-312.pyc and b/flask-server/__pycache__/app.cpython-312.pyc differ diff --git a/flask-server/__pycache__/config.cpython-312.pyc b/flask-server/__pycache__/config.cpython-312.pyc index b95e9b6..3fd5128 100644 Binary files a/flask-server/__pycache__/config.cpython-312.pyc and b/flask-server/__pycache__/config.cpython-312.pyc differ diff --git a/flask-server/__pycache__/mysqlconfig.cpython-312.pyc b/flask-server/__pycache__/mysqlconfig.cpython-312.pyc index 109a59e..469bb7c 100644 Binary files a/flask-server/__pycache__/mysqlconfig.cpython-312.pyc and b/flask-server/__pycache__/mysqlconfig.cpython-312.pyc differ diff --git a/flask-server/app.py b/flask-server/app.py index 3c36e47..41a777e 100644 --- a/flask-server/app.py +++ b/flask-server/app.py @@ -1,32 +1,70 @@ -from flask import Flask -from flask_sqlalchemy import SQLAlchemy +from flask import Flask, jsonify from flask_bcrypt import Bcrypt from flask_cors import CORS -from config import ApplicationConfig, TestConfig -from flask_jwt_extended import JWTManager +from flask_restx import Api +from flask_migrate import Migrate +from exts import db, jwt +from config import DevConfig, TestConfig +from models.user_models import User, Profile +from models.forum_models import Message +from models.content_models import Books, Anime, Games +from routes.user import user_ns +from routes.content import content_ns +from routes.forum import forum_ns - -# Separated out routes so file doesn't get too big, this file just sets up the app - -db = SQLAlchemy() bcrypt = Bcrypt() cors = CORS() -jwt = JWTManager() +migrate = Migrate() -def create_app(test_config=None): # Changed function to take in a config so can unit test with a test config - app = Flask(__name__) +def create_app(test_config=None): + """Application factory function""" + app = Flask(__name__, instance_relative_config=True) if test_config is None: - # Load the instance config when not testing - app.config.from_object(ApplicationConfig) + app.config.from_object(DevConfig) else: - # Load the test config if passwed in app.config.from_object(TestConfig) db.init_app(app) + migrate.init_app(app,db) bcrypt.init_app(app) cors.init_app(app, supports_credentials=True) jwt.init_app(app) + + # with app.app_context(): + # db.create_all() + + api = Api(app) + + api.add_namespace(user_ns) + api.add_namespace(content_ns) + api.add_namespace(forum_ns) + + @jwt.expired_token_loader + def expired_token_callback(jwt_header, jwt_data): + return jsonify({"message": "Token has expired", "error": "token_expired"}), 401 + @jwt.invalid_token_loader + def invalid_token_callback(error): + return jsonify({"message": "Signature verification failed", "error": "invalid_token"}), 401 + @jwt.unauthorized_loader + def missing_token_callback(error): + return jsonify({"message": "Request does not contain a valid token", "error": "authorisation_token"}), 401 + + @app.shell_context_processor + def make_shell_context(): + return { + "db": db, + "User": User, + "Profile": Profile, + "Message": Message, + "Books": Books, + "Anime": Anime, + "Games": Games + } + return app +if __name__ == "__main__": + app = create_app() + app.run(debug=True) \ No newline at end of file diff --git a/flask-server/config.py b/flask-server/config.py index 3ef1577..41ce542 100644 --- a/flask-server/config.py +++ b/flask-server/config.py @@ -1,23 +1,43 @@ from dotenv import load_dotenv import os -import redis -from mysqlconfig import HOST, USER, PASSWORD -load_dotenv() +load_dotenv(override=True) +host = os.getenv("HOST") +user = os.getenv("USER") +password = os.getenv("PASSWORD") +port = os.getenv("PORT") +database = os.getenv("DATABASE") +test_database = os.getenv("TESTDB") -class ApplicationConfig: +# MySQL database connection +mysql_uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}" +mysql_test = f"mysql+pymysql://{user}:{password}@{host}:{port}/{test_database}" + +# For SQLite DB, use these instead +sqlite_uri = r"sqlite:///./db.sqlite" +sqlite_test = r"sqlite:///./testdb.sqlite" + + +class Config: + """Base config class""" SECRET_KEY = os.environ["SECRET_KEY"] SQLALCHEMY_TRACK_MODIFICATIONS = False + + +class DevConfig(Config): + """Development config""" + SQLALCHEMY_DATABASE_URI = mysql_uri SQLALCHEMY_ECHO = True - SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{USER}:{PASSWORD}@{HOST}:3306/introverse" # Assuming default 3306 port for MySQL, change if not default - # Can comment the above and uncomment below to use the SQLite DB instead - # SQLALCHEMY_DATABASE_URI = r"sqlite:///./db.sqlite" +class ProdConfig(Config): + """Production config""" + pass -class TestConfig: - SECRET_KEY = os.environ["SECRET_KEY"] + +class TestConfig(Config): + """Testing config""" + SQLALCHEMY_DATABASE_URI = mysql_test SQLALCHEMY_ECHO = False - SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{USER}:{PASSWORD}@{HOST}:3306/introverse" - Testing=True # No data gets actually passed into the database \ No newline at end of file + TESTING = True \ No newline at end of file diff --git a/flask-server/create_db.py b/flask-server/create_db.py index d94d4a0..c3184ec 100644 --- a/flask-server/create_db.py +++ b/flask-server/create_db.py @@ -1,18 +1,27 @@ -# Run this file only if database is not yet created +from dotenv import load_dotenv +import os import mysql.connector -from mysqlconfig import HOST, USER, PASSWORD + +load_dotenv(override=True) mydb = mysql.connector.connect( - host=HOST, - user=USER, - passwd=PASSWORD, + host=os.getenv("HOST"), + user=os.getenv("USER"), + passwd=os.getenv("PASSWORD"), ) +database = os.getenv("DATABASE") +test_database = os.getenv("TESTDB") my_cursor = mydb.cursor() -my_cursor.execute("CREATE DATABASE introverse") - -# Can run this bit below too if you want to see all the databases in your MySQL (and will confirm creation of new one) -my_cursor.execute("SHOW DATABASES") -for db in my_cursor: - print(db) \ No newline at end of file +def check_db_create_if_not_exist(dbname): + """Will check if database already exists and create it if not""" + my_cursor.execute(f"SHOW DATABASES LIKE '{dbname}';") + for db in my_cursor: + return f"{dbname} already exists" + else: + my_cursor.execute(f"CREATE DATABASE {dbname};") + return f"{dbname} has been created" + +print(check_db_create_if_not_exist(database)) +print(check_db_create_if_not_exist(test_database)) diff --git a/flask-server/create_tables.py b/flask-server/create_tables.py index ec2c95d..adf855d 100644 --- a/flask-server/create_tables.py +++ b/flask-server/create_tables.py @@ -1,14 +1,12 @@ -# This file will create the tables if they do not already exist -from config import ApplicationConfig - def deploy(): + """Run create all tables for the default database (development database)""" from app import create_app, db - from models.user_models import User, Profile, Message + from models.user_models import User, Profile + from models.forum_models import Message from models.content_models import Books, Anime, Games - app = create_app(ApplicationConfig) + app = create_app() app.app_context().push() - # Create tables db.create_all() -deploy() +deploy() \ No newline at end of file diff --git a/flask-server/exts.py b/flask-server/exts.py new file mode 100644 index 0000000..8759316 --- /dev/null +++ b/flask-server/exts.py @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager + +db = SQLAlchemy() +jwt = JWTManager() diff --git a/flask-server/instance/db.sqlite b/flask-server/instance/db.sqlite deleted file mode 100644 index 6777483..0000000 Binary files a/flask-server/instance/db.sqlite and /dev/null differ diff --git a/flask-server/introverseSQL.sql b/flask-server/introverseSQL.sql index d351eed..9744fd2 100644 --- a/flask-server/introverseSQL.sql +++ b/flask-server/introverseSQL.sql @@ -1,22 +1,11 @@ -- Run create database -CREATE DATABASE introverse; -USE introverse; +CREATE DATABASE introverse_dev; +CREATE DATABASE introverse_test; +USE introverse_dev; - --- Table for forum message board, this table needs to be created in MySQL so that it can have the MySQL default method of getting the time for the example data --- Going to drop the foreign key constraint on author to prevent any errors from creating mock posts to display the messages -CREATE TABLE message_board ( - post_id INT PRIMARY KEY AUTO_INCREMENT UNIQUE, - post_content TEXT NOT NULL, - post_category VARCHAR(50) NOT NULL, - post_author VARCHAR(30) NOT NULL, - post_date DATETIME NOT NULL DEFAULT NOW() -); - - --- Tables for user profile and accounts (can also create them from Python - recommend create from python) +-- Creating tables, recommend creating from Python CREATE TABLE user_profiles ( - username VARCHAR(30) NOT NULL, + username VARCHAR(30) NOT NULL AUTO_INCREMENT DEFAULT(UUID()), first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, email VARCHAR(254) NOT NULL, @@ -37,76 +26,63 @@ CREATE TABLE user_accounts ( UNIQUE (username) ); - - --- Mock posts -INSERT INTO message_board -(post_content, post_category, post_author) -VALUES -("What new Anime can I watch everyone?", "Anime", "BlueMonkey"), -("Is there a new season of FairyTail coming out?", "Anime", "MarshmellowDestroyer"), -("Should really get around to finishing Hokuto no Ken/FoTNS already...it's bad ass...", "Anime", "BlueMonkey"), -("Look to the 80's and 90's for anime that isn't trying to give you a stiffy. Sure, there's still women in skimpy outfits, but it's not a primary goal of the anime.", "Anime", "randomDUDEEEEEE"), -("I'm starting Welcome to the N.H.K myself. Reading the LN to and figure I might as well do a side by side comparison.", "Anime", "BloodLord55"), -("D.Gray man's OST is really good. Only watched the first two seasons but those two have both of some my favourite openings of all time.", "Anime", "DogWar"); - -INSERT INTO message_board -(post_content, post_category, post_author) -VALUES -("Hello I am BlueMonkey here I got the nickname from my friends a while back", "Introduce", "BlueMonkey"), -("Sup dudes, anyone play anything then?", "Introduce", "MarshmellowDestroyer"), -("So what are rules to these forums then?", "Introduce", "MonkeyFivesss"), -("Just some sound dude from new york.", "Introduce", "randomDUDEEEEEE"), -("I got a really cute puppy anyone wanna see?", "Introduce", "BloodLord55"), -("Just here for cool community!", "Introduce", "DogWar"); +CREATE TABLE message_board ( + post_id INT PRIMARY KEY AUTO_INCREMENT UNIQUE, + post_content TEXT NOT NULL, + post_category VARCHAR(50) NOT NULL, + post_author VARCHAR(30) NOT NULL, + post_date DATETIME NOT NULL DEFAULT NOW() +); -- Content tables for recommendations -CREATE TABLE Books ( - Book_ID INTEGER PRIMARY KEY NOT NULL, - Book_Name VARCHAR(100) UNIQUE NOT NULL, - Book_Author VARCHAR(30), - Book_Genre VARCHAR(25), - Price FLOAT NOT NULL, - Book_Script VARCHAR(1000) +CREATE TABLE books ( + book_id INTEGER PRIMARY KEY NOT NULL, + book_name VARCHAR(100) UNIQUE NOT NULL, + book_author VARCHAR(30), + book_genre VARCHAR(25), + price FLOAT NOT NULL, + book_script VARCHAR(1000), + book_image VARCHAR(100) UNIQUE ); -CREATE TABLE Anime ( - Anime_ID INTEGER PRIMARY KEY NOT NULL, - Anime_Name VARCHAR(50) UNIQUE NOT NULL, - Anime_Genre VARCHAR(25), - Where_TW VARCHAR(25), - Anime_Script VARCHAR(1000) +CREATE TABLE anime ( + anime_id INTEGER PRIMARY KEY NOT NULL, + anime_name VARCHAR(50) UNIQUE NOT NULL, + anime_genre VARCHAR(25), + where_tw VARCHAR(25), + anime_script VARCHAR(1000), + anime_image VARCHAR(100) UNIQUE ); -CREATE TABLE Games ( - Game_ID INTEGER PRIMARY KEY NOT NULL, - Game_Name VARCHAR(50) UNIQUE NOT NULL, - Game_Genre VARCHAR(30), - W_Console VARCHAR(100), - Price FLOAT NOT NULL, - Game_Script VARCHAR(1000) +CREATE TABLE games ( + game_id INTEGER PRIMARY KEY NOT NULL, + game_name VARCHAR(50) UNIQUE NOT NULL, + game_genre VARCHAR(30), + w_console VARCHAR(100), + price FLOAT NOT NULL, + game_script VARCHAR(1000), + game_image VARCHAR(100) UNIQUE ); - --- Values for recommendation tables -INSERT INTO Books -(Book_ID, Book_Name, Book_Author, Book_Genre, Price, Book_Script) +-- Data for recommendation tables, INSERT THESE BEFORE RUNNING TO USE CONTENT RECOMMENDATION FEATURE +INSERT INTO books +(book_id, book_name, book_author, book_genre, price, book_script, book_image) VALUES -(1, 'Fourth Wing', 'Rebecca Yarros', 'Fantasy', 9.19, 'Twenty-year-old Violet Sorrengail was supposed to enter the Scribe Quadrant, living a quiet life among books and history. Now, the commanding general-also known as her tough-as-talons mother-has ordered Violet to join the hundreds of candidates striving to become the elite of Navarre: dragon riders.'), -(2, 'The Harry Potter Series', 'J.K. Rowling', 'Fantasy', 51.65, 'The Harry Potter books follow a young wizard named Harry as he attends Hogwarts School of Witchcraft and Wizardry. Alongside his friends Ron and Hermione, Harry faces challenges, discovers his past, and confronts the dark wizard Voldemort across seven books, filled with magic, friendship, and the battle between good and evil.'), -(3, 'A Court of Thorns and Roses Series', 'Sarah J. Maas', 'Fantasy', 31.74, 'ACOTAR Follows Feyre, a huntress who accidentally kills a faerie and is taken to the faerie lands as punishment. There, she navigates faerie politics, forms relationships with powerful fae like Tamlin and Rhysand, and becomes involved in a high-stakes battle that could impact both human and faerie realms across several books filled with magic, romance, and conflicts.'), -(4, 'To Kill a Kingdom', 'Alexandra Christo', 'Fantasy', 4.67, 'Princess Lira is siren royalty and the most lethal of them all. With the hearts of seventeen princes in her collection, she is revered across the sea. Until a twist of fate forces her to kill one of her own. To punish her daughter, the Sea Queen transforms Lira into the one thing they loathe most - a human. Robbed of her song, Lira has until the winter solstice to deliver Prince Elians heart to the Sea Queen or remain a human forever.'), -(5, 'Elizabeth Bathory: Life and Legacy of Historys Most Prolific Female Serial Killer', 'James Oliver', 'History', 6.12, 'This book explains the life and times of this powerful woman - and how she came to be accused of so many heinous crimes. Youll gain access to a variety of historical versions, perspectives, and accounts of her life - some of which paint her as a villain and others as a victim!'), -(6, 'The Great Empires of the Ancient World', 'Thomas Harrison', 'History', 11.63, 'A distinguished team of internationally renowned scholars surveys the great empires from 1600 BC to AD 500, from the ancient Mediterranean to China. Exploring the very nature of empire itself, the authors show how profoundly imperialism in the distant past influenced the 19th-century powers and the modern United States.'), -(7, 'Landlines', 'Raynor Winn', 'History', 6.00, 'Embarking on a journey across the Cape Wrath Trail, over 200 miles of gruelling terrain through Scotlands remotest mountains and lochs, Raynor and Moth look to an uncertain future. Fearing that miracles dont often repeat themselves.'), -(8, 'Unbroken: A World War II Story of Survival', 'Lauren Hillenbrand', 'History', 4.71, 'On a May afternoon in 1943, an Army Air Forces bomber crashed into the Pacific Ocean and disappeared, leaving only a spray of debris and a slick of oil, gasoline, and blood. Then, on the ocean surface, a face appeared. It was that of a young lieutenant, the planes bombardier, who was struggling to a life raft and pulling himself aboard. So began one of the most extraordinary odysseys of the Second World War.'), -(9, 'The Haunting of Hill House', 'Shirley Jackson', 'Horror', 9.90, 'Welcome to Hill House, an eerie mansion with a chilling past. When a group of individuals sets out to uncover its supernatural secrets, they find themselves trapped in a world where reality blurs with the terrifying unknown. Shirley Jacksons classic tale weaves a haunting narrative that explores the eerie power of a house that seems to have a mind of its own.'), -(10, 'Dracula', 'Bram Stoker', 'Horror', 14.29, 'When Jonathan Harker visits Transylvania to help Count Dracula with the purchase of a London house, he makes a series of horrific discoveries about his client. Soon afterwards, various bizarre incidents unfold in England: an apparently unmanned ship is wrecked off the coast of Whitby; a young woman discovers strange puncture marks on her neck; and the inmate of a lunatic asylum raves about the Master and his imminent arrival.'), -(11, 'The Shining', 'Stephen King', 'Horror', 10.11, 'Danny is only five years old, but in the words of old Mr Hallorann he is a shiner, aglow with psychic voltage. When his father becomes caretaker of the Overlook Hotel, Dannys visions grow out of control. As winter closes in and blizzards cut them off, the hotel seems to develop a life of its own. It is meant to be empty. So who is the lady in Room 217 and who are the masked guests going up and down in the elevator? And why do the hedges shaped like animals seem so alive? Somewhere, somehow, there is an evil force in the hotel - and that, too, is beginning to shine.'), -(12, 'The Exorcist', 'William Peter Blatty', 'Horror', 9.19, 'The terror begins unobtrusively. Noises in the attic. In the childs room, an odd smell, the displacement of furniture, an icy chill. At first, easy explanations are offered. Then frightening changes begin to appear in eleven-year-old Regan. Medical tests fail to shed any light on her symptoms, but it is as if a different personality has invaded her body.'); - -INSERT INTO Anime -(Anime_ID, Anime_Name, Anime_Genre, Where_TW, Anime_Script) +(1, 'Fourth Wing', 'Rebecca Yarros', 'Fantasy', 9.19, 'Twenty-year-old Violet Sorrengail was supposed to enter the Scribe Quadrant, living a quiet life among books and history. Now, the commanding general-also known as her tough-as-talons mother-has ordered Violet to join the hundreds of candidates striving to become the elite of Navarre: dragon riders.', 'https://rb.gy/a7n2nv'), +(2, 'The Harry Potter Series', 'J.K. Rowling', 'Fantasy', 51.65, 'The Harry Potter books follow a young wizard named Harry as he attends Hogwarts School of Witchcraft and Wizardry. Alongside his friends Ron and Hermione, Harry faces challenges, discovers his past, and confronts the dark wizard Voldemort across seven books, filled with magic, friendship, and the battle between good and evil.', 'https://rb.gy/mioh6n'), +(3, 'A Court of Thorns and Roses Series', 'Sarah J. Maas', 'Fantasy', 31.74, 'ACOTAR Follows Feyre, a huntress who accidentally kills a faerie and is taken to the faerie lands as punishment. There, she navigates faerie politics, forms relationships with powerful fae like Tamlin and Rhysand, and becomes involved in a high-stakes battle that could impact both human and faerie realms across several books filled with magic, romance, and conflicts.', 'https://rb.gy/ydq70j'), +(4, 'To Kill a Kingdom', 'Alexandra Christo', 'Fantasy', 4.67, 'Princess Lira is siren royalty and the most lethal of them all. With the hearts of seventeen princes in her collection, she is revered across the sea. Until a twist of fate forces her to kill one of her own. To punish her daughter, the Sea Queen transforms Lira into the one thing they loathe most - a human. Robbed of her song, Lira has until the winter solstice to deliver Prince Elians heart to the Sea Queen or remain a human forever.', 'https://rb.gy/09jtlo'), +(5, 'Elizabeth Bathory: Life and Legacy of Historys Most Prolific Female Serial Killer', 'James Oliver', 'History', 6.12, 'This book explains the life and times of this powerful woman - and how she came to be accused of so many heinous crimes. Youll gain access to a variety of historical versions, perspectives, and accounts of her life - some of which paint her as a villain and others as a victim!', 'https://rb.gy/du98fq'), +(6, 'The Great Empires of the Ancient World', 'Thomas Harrison', 'History', 11.63, 'A distinguished team of internationally renowned scholars surveys the great empires from 1600 BC to AD 500, from the ancient Mediterranean to China. Exploring the very nature of empire itself, the authors show how profoundly imperialism in the distant past influenced the 19th-century powers and the modern United States.', 'tiny.cc/2iitvz'), +(7, 'Landlines', 'Raynor Winn', 'History', 6.00, 'Embarking on a journey across the Cape Wrath Trail, over 200 miles of gruelling terrain through Scotlands remotest mountains and lochs, Raynor and Moth look to an uncertain future. Fearing that miracles dont often repeat themselves.', 'tiny.cc/ziitvz'), +(8, 'Unbroken: A World War II Story of Survival', 'Lauren Hillenbrand', 'History', 4.71, 'On a May afternoon in 1943, an Army Air Forces bomber crashed into the Pacific Ocean and disappeared, leaving only a spray of debris and a slick of oil, gasoline, and blood. Then, on the ocean surface, a face appeared. It was that of a young lieutenant, the planes bombardier, who was struggling to a life raft and pulling himself aboard. So began one of the most extraordinary odysseys of the Second World War.', 'tiny.cc/djitvz'), +(9, 'The Haunting of Hill House', 'Shirley Jackson', 'Horror', 9.90, 'Welcome to Hill House, an eerie mansion with a chilling past. When a group of individuals sets out to uncover its supernatural secrets, they find themselves trapped in a world where reality blurs with the terrifying unknown. Shirley Jacksons classic tale weaves a haunting narrative that explores the eerie power of a house that seems to have a mind of its own.', 'tiny.cc/sjitvz'), +(10, 'Dracula', 'Bram Stoker', 'Horror', 14.29, 'When Jonathan Harker visits Transylvania to help Count Dracula with the purchase of a London house, he makes a series of horrific discoveries about his client. Soon afterwards, various bizarre incidents unfold in England: an apparently unmanned ship is wrecked off the coast of Whitby; a young woman discovers strange puncture marks on her neck; and the inmate of a lunatic asylum raves about the Master and his imminent arrival.', 'tiny.cc/1kitvz'), +(11, 'The Shining', 'Stephen King', 'Horror', 10.11, 'Danny is only five years old, but in the words of old Mr Hallorann he is a shiner, aglow with psychic voltage. When his father becomes caretaker of the Overlook Hotel, Dannys visions grow out of control. As winter closes in and blizzards cut them off, the hotel seems to develop a life of its own. It is meant to be empty. So who is the lady in Room 217 and who are the masked guests going up and down in the elevator? And why do the hedges shaped like animals seem so alive? Somewhere, somehow, there is an evil force in the hotel - and that, too, is beginning to shine.', 'tiny.cc/kkitvz'), +(12, 'The Exorcist', 'William Peter Blatty', 'Horror', 9.19, 'The terror begins unobtrusively. Noises in the attic. In the childs room, an odd smell, the displacement of furniture, an icy chill. At first, easy explanations are offered. Then frightening changes begin to appear in eleven-year-old Regan. Medical tests fail to shed any light on her symptoms, but it is as if a different personality has invaded her body.', 'tiny.cc/9litvz'); + +INSERT INTO anime +(anime_id, anime_name, anime_genre, where_tw, anime_script) VALUES (1, 'Bleach', 'Shonen', 'Disney+', 'Ichigo Kurosaki is a teenager from Karakura Town who can see ghosts, a talent allowing him to meet a supernatural human Rukia Kuchiki, who enters the town in search of a Hollow, a kind of monstrous lost soul who can harm both ghosts and humans.'), (2, 'Naruto', 'Shonen', 'Crunchyroll', 'The Village Hidden in the Leaves is home to the stealthiest ninja. But twelve years earlier, a fearsome Nine-tailed Fox terrorized the village before it was subdued and its spirit sealed within the body of a baby boy.'), @@ -121,8 +97,8 @@ VALUES (11, 'Black Clover', 'Fantasy', 'Crunchyroll', 'In a world where magic is everything, Asta and Yuno are both found abandoned at a church on the same day. While Yuno is gifted with exceptional magical powers, Asta is the only one in this world without any. At the age of fifteen, both receive grimoires, magic books that amplify their holder’s magic. Asta’s is a rare Grimoire of Anti-Magic that negates and repels his opponent’s spells. Being opposite but good rivals, Yuno and Asta are ready for the hardest of challenges to achieve their common dream: to be the Wizard King. Giving up is never an option!'), (12, 'Link Click', 'Fantasy', 'Crunchyroll', 'Using superpowers to enter their clientele’s photos one by one, Cheng Xiaoshi and Lu Guang take their work seriously at Time Photo Studio, a small photography shop set in the backdrop of a modern metropolis. Each job can be full of danger, but nothing is more important than fulfilling every order, no matter the scale…or peril involved!'); -INSERT INTO Games -(Game_ID, Game_Name, Game_Genre, W_Console, Price, Game_Script) +INSERT INTO games +(game_id, game_name, game_genre, w_console, price, game_script) VALUES (1, 'Fae Farm', 'Cozy Games', 'PC, NINTENDO SWITCH', 29.99, 'Escape to the magical life of your dreams in Fae Farm, a farm sim RPG for 1-4 players. Craft, cultivate, and decorate to grow your homestead, and use spells to explore the enchanted island of Azoria!'), (2, 'Spellcaster University', 'Cozy Games', 'PC', 19.49, 'Develop a prestigious university of mages. Build rooms, train your students, fight orcs, slay the bureaucrats, manage your budget... a directors life is not a quiet one.'), @@ -137,16 +113,23 @@ VALUES (11, 'GAME DEV TYCOON', 'SIMULATION', 'PC', 8.50, 'In Game Dev Tycoon you replay the history of the gaming industry by starting your own video game development company in the 80s. Create best selling games. Research new technologies and invent new game types. Become the leader of the market and gain worldwide fans.'), (12, 'NAHEULBEUKS DUNGEON MASTER', 'SIMULATION', 'PC', 20.99, 'A dungeon in danger ! Build, manage, and defend your tower in the satirical heroic fantasy universe of Dungeon of Naheulbeuk. From a shaky establishment to an infamous lair!'); --- Values for user tables, need to update and best not to insert directly from SQL because need to hash passwords, but keeping them here in meantime for an idea --- Just for reference of some of the users added through the website --- INSERT INTO Users (UserID, Username, Email, Name, DateOfBirth, Interests, Password) --- VALUES --- (1,'the_kickboxer', 'kathoop@email.com', 'Katherine Hooper', '1990-01-01', 'Gaming ', 'password1'), --- (2,'pokemon_girl', 'angel.pika@email.com', 'Angel Witchell', '2001-02-02', 'Shonen' , 'password2' ) , --- (3,'lover_ofbooks', 'agd@email.com', 'Abbie-Gayle Daniel', '2002-03-03', 'Reading', 'password3'), --- (4, 'dog_mum', 'haiyingl@email.com', 'Haiying Liao', '2003-04-04', 'Cozy_games', 'password4'), --- (5, 'the_baroness', 'katbray@email.com', 'Katalin Bray', '1920-04-04', 'History', 'password5'), --- (6, 'friday_13', 'jimmychamp@email.com', 'Jimmy Champagne', '1970-05-05', 'Horror', 'password6'), --- (7, 'elder_scrolls', 'pewdiepie@email.com', 'Felix Kjellberg', '1995-06-06', 'Adventure', 'password7'), --- (8,'nerdrotic', 'garyb@email.com', 'Gary Brown', '1950-07-07', 'Fantasy', 'password8'), --- (9, 'critical_drinker', 'willjordan@email.com', 'Will Jordan', '1980-07-07', 'Simulation', 'password8'); \ No newline at end of file +-- Mock posts for forum, can use if want or post your own on the frontend +INSERT INTO message_board +(post_content, post_category, post_author, post_date) +VALUES +("What new Anime can I watch everyone?", "Anime", "BlueMonkey", NOW()), +("Is there a new season of FairyTail coming out?", "Anime", "MarshmellowDestroyer", NOW()), +("Should really get around to finishing Hokuto no Ken/FoTNS already...it's bad ass...", "Anime", "BlueMonkey", NOW()), +("Look to the 80's and 90's for anime that isn't trying to give you a stiffy. Sure, there's still women in skimpy outfits, but it's not a primary goal of the anime.", "Anime", "randomDUDEEEEEE", NOW()), +("I'm starting Welcome to the N.H.K myself. Reading the LN to and figure I might as well do a side by side comparison.", "Anime", "BloodLord55", NOW()), +("D.Gray man's OST is really good. Only watched the first two seasons but those two have both of some my favourite openings of all time.", "Anime", "DogWar", NOW()); + +INSERT INTO message_board +(post_content, post_category, post_author, post_date) +VALUES +("Hello I am BlueMonkey here I got the nickname from my friends a while back", "Introduce", "BlueMonkey", NOW()), +("Sup dudes, anyone play anything then?", "Introduce", "MarshmellowDestroyer", NOW()), +("So what are rules to these forums then?", "Introduce", "MonkeyFivesss", NOW()), +("Just some sound dude from new york.", "Introduce", "randomDUDEEEEEE", NOW()), +("I got a really cute puppy anyone wanna see?", "Introduce", "BloodLord55", NOW()), +("Just here for cool community!", "Introduce", "DogWar", NOW()); \ No newline at end of file diff --git a/flask-server/migrations/README b/flask-server/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/flask-server/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/flask-server/migrations/alembic.ini b/flask-server/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/flask-server/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/flask-server/migrations/env.py b/flask-server/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/flask-server/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/flask-server/migrations/script.py.mako b/flask-server/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/flask-server/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/flask-server/migrations/versions/4a803065cd9a_tables.py b/flask-server/migrations/versions/4a803065cd9a_tables.py new file mode 100644 index 0000000..dca9df1 --- /dev/null +++ b/flask-server/migrations/versions/4a803065cd9a_tables.py @@ -0,0 +1,99 @@ +"""tables + +Revision ID: 4a803065cd9a +Revises: +Create Date: 2024-01-26 17:44:34.021758 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4a803065cd9a' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('Anime', + sa.Column('Anime_ID', sa.Integer(), nullable=False), + sa.Column('Anime_Name', sa.String(length=50), nullable=False), + sa.Column('Anime_Genre', sa.String(length=25), nullable=False), + sa.Column('Where_TW', sa.String(length=25), nullable=True), + sa.Column('Anime_Script', sa.String(length=1000), nullable=True), + sa.Column('Anime_Image', sa.String(length=100), nullable=True), + sa.PrimaryKeyConstraint('Anime_ID'), + sa.UniqueConstraint('Anime_ID'), + sa.UniqueConstraint('Anime_Image'), + sa.UniqueConstraint('Anime_Name') + ) + op.create_table('Books', + sa.Column('Book_ID', sa.Integer(), nullable=False), + sa.Column('Book_Name', sa.String(length=100), nullable=False), + sa.Column('Book_Author', sa.String(length=30), nullable=False), + sa.Column('Book_Genre', sa.String(length=25), nullable=False), + sa.Column('Price', sa.Float(), nullable=False), + sa.Column('Book_Script', sa.String(length=1000), nullable=True), + sa.Column('Book_Image', sa.String(length=100), nullable=True), + sa.PrimaryKeyConstraint('Book_ID'), + sa.UniqueConstraint('Book_ID'), + sa.UniqueConstraint('Book_Image'), + sa.UniqueConstraint('Book_Name') + ) + op.create_table('Games', + sa.Column('Game_ID', sa.Integer(), nullable=False), + sa.Column('Game_Name', sa.String(length=50), nullable=False), + sa.Column('Game_Genre', sa.String(length=30), nullable=False), + sa.Column('W_Console', sa.String(length=100), nullable=True), + sa.Column('Price', sa.Float(), nullable=False), + sa.Column('Game_Script', sa.String(length=1000), nullable=True), + sa.Column('Game_Image', sa.String(length=100), nullable=True), + sa.PrimaryKeyConstraint('Game_ID'), + sa.UniqueConstraint('Game_ID'), + sa.UniqueConstraint('Game_Image'), + sa.UniqueConstraint('Game_Name') + ) + op.create_table('message_board', + sa.Column('post_id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('post_content', sa.Text(), nullable=False), + sa.Column('post_category', sa.String(length=50), nullable=False), + sa.Column('post_author', sa.String(length=30), nullable=False), + sa.Column('post_date', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('post_id'), + sa.UniqueConstraint('post_id') + ) + op.create_table('user_accounts', + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('username', sa.String(length=30), nullable=False), + sa.Column('password', sa.String(length=60), nullable=False), + sa.PrimaryKeyConstraint('user_id'), + sa.UniqueConstraint('user_id'), + sa.UniqueConstraint('username') + ) + op.create_table('user_profiles', + sa.Column('username', sa.String(length=30), nullable=False), + sa.Column('first_name', sa.String(length=50), nullable=False), + sa.Column('last_name', sa.String(length=50), nullable=False), + sa.Column('email', sa.String(length=254), nullable=False), + sa.Column('date_of_birth', sa.Date(), nullable=True), + sa.Column('interests', sa.Text(), nullable=True), + sa.Column('date_joined', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('username'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user_profiles') + op.drop_table('user_accounts') + op.drop_table('message_board') + op.drop_table('Games') + op.drop_table('Books') + op.drop_table('Anime') + # ### end Alembic commands ### diff --git a/flask-server/migrations/versions/__pycache__/05857013da77_test_2.cpython-312.pyc b/flask-server/migrations/versions/__pycache__/05857013da77_test_2.cpython-312.pyc new file mode 100644 index 0000000..9f329b5 Binary files /dev/null and b/flask-server/migrations/versions/__pycache__/05857013da77_test_2.cpython-312.pyc differ diff --git a/flask-server/migrations/versions/__pycache__/4a803065cd9a_tables.cpython-312.pyc b/flask-server/migrations/versions/__pycache__/4a803065cd9a_tables.cpython-312.pyc new file mode 100644 index 0000000..5a82e8e Binary files /dev/null and b/flask-server/migrations/versions/__pycache__/4a803065cd9a_tables.cpython-312.pyc differ diff --git a/flask-server/migrations/versions/__pycache__/5b78a0798085_initial.cpython-312.pyc b/flask-server/migrations/versions/__pycache__/5b78a0798085_initial.cpython-312.pyc new file mode 100644 index 0000000..b8cde9f Binary files /dev/null and b/flask-server/migrations/versions/__pycache__/5b78a0798085_initial.cpython-312.pyc differ diff --git a/flask-server/migrations/versions/__pycache__/a7a4d216a7b9_test_update.cpython-312.pyc b/flask-server/migrations/versions/__pycache__/a7a4d216a7b9_test_update.cpython-312.pyc new file mode 100644 index 0000000..a5838db Binary files /dev/null and b/flask-server/migrations/versions/__pycache__/a7a4d216a7b9_test_update.cpython-312.pyc differ diff --git a/flask-server/migrations/versions/e11b182cd2e2_updated_table_and_column_names_for_.py b/flask-server/migrations/versions/e11b182cd2e2_updated_table_and_column_names_for_.py new file mode 100644 index 0000000..ba579b9 --- /dev/null +++ b/flask-server/migrations/versions/e11b182cd2e2_updated_table_and_column_names_for_.py @@ -0,0 +1,154 @@ +"""Updated table and column names for content tables to be lowercase + +Revision ID: e11b182cd2e2 +Revises: 4a803065cd9a +Create Date: 2024-02-14 11:44:06.282191 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'e11b182cd2e2' +down_revision = '4a803065cd9a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('anime', schema=None) as batch_op: + batch_op.add_column(sa.Column('anime_id', sa.Integer(), nullable=False)) + batch_op.add_column(sa.Column('anime_name', sa.String(length=50), nullable=False)) + batch_op.add_column(sa.Column('anime_genre', sa.String(length=25), nullable=False)) + batch_op.add_column(sa.Column('where_tw', sa.String(length=25), nullable=True)) + batch_op.add_column(sa.Column('anime_script', sa.String(length=1000), nullable=True)) + batch_op.add_column(sa.Column('anime_image', sa.String(length=100), nullable=True)) + batch_op.drop_index('Anime_ID') + batch_op.drop_index('Anime_Image') + batch_op.drop_index('Anime_Name') + batch_op.create_unique_constraint(None, ['anime_image']) + batch_op.create_unique_constraint(None, ['anime_id']) + batch_op.create_unique_constraint(None, ['anime_name']) + batch_op.drop_column('Anime_Name') + batch_op.drop_column('Anime_Script') + batch_op.drop_column('Anime_Genre') + batch_op.drop_column('Where_TW') + batch_op.drop_column('Anime_ID') + batch_op.drop_column('Anime_Image') + + with op.batch_alter_table('books', schema=None) as batch_op: + batch_op.add_column(sa.Column('book_id', sa.Integer(), nullable=False)) + batch_op.add_column(sa.Column('book_name', sa.String(length=100), nullable=False)) + batch_op.add_column(sa.Column('book_author', sa.String(length=30), nullable=False)) + batch_op.add_column(sa.Column('book_genre', sa.String(length=25), nullable=False)) + batch_op.add_column(sa.Column('price', sa.Float(), nullable=False)) + batch_op.add_column(sa.Column('book_script', sa.String(length=1000), nullable=True)) + batch_op.add_column(sa.Column('book_image', sa.String(length=100), nullable=True)) + batch_op.drop_index('Book_ID') + batch_op.drop_index('Book_Image') + batch_op.drop_index('Book_Name') + batch_op.create_unique_constraint(None, ['book_name']) + batch_op.create_unique_constraint(None, ['book_id']) + batch_op.create_unique_constraint(None, ['book_image']) + batch_op.drop_column('Book_ID') + batch_op.drop_column('Book_Genre') + batch_op.drop_column('Book_Name') + batch_op.drop_column('Book_Author') + batch_op.drop_column('Book_Script') + batch_op.drop_column('Book_Image') + batch_op.drop_column('Price') + + with op.batch_alter_table('games', schema=None) as batch_op: + batch_op.add_column(sa.Column('game_id', sa.Integer(), nullable=False)) + batch_op.add_column(sa.Column('game_name', sa.String(length=50), nullable=False)) + batch_op.add_column(sa.Column('game_genre', sa.String(length=30), nullable=False)) + batch_op.add_column(sa.Column('w_console', sa.String(length=100), nullable=True)) + batch_op.add_column(sa.Column('price', sa.Float(), nullable=False)) + batch_op.add_column(sa.Column('game_script', sa.String(length=1000), nullable=True)) + batch_op.add_column(sa.Column('game_image', sa.String(length=100), nullable=True)) + batch_op.drop_index('Game_ID') + batch_op.drop_index('Game_Image') + batch_op.drop_index('Game_Name') + batch_op.create_unique_constraint(None, ['game_name']) + batch_op.create_unique_constraint(None, ['game_image']) + batch_op.create_unique_constraint(None, ['game_id']) + batch_op.drop_column('Game_Image') + batch_op.drop_column('Game_ID') + batch_op.drop_column('Game_Script') + batch_op.drop_column('W_Console') + batch_op.drop_column('Price') + batch_op.drop_column('Game_Name') + batch_op.drop_column('Game_Genre') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('games', schema=None) as batch_op: + batch_op.add_column(sa.Column('Game_Genre', mysql.VARCHAR(length=30), nullable=False)) + batch_op.add_column(sa.Column('Game_Name', mysql.VARCHAR(length=50), nullable=False)) + batch_op.add_column(sa.Column('Price', mysql.FLOAT(), nullable=False)) + batch_op.add_column(sa.Column('W_Console', mysql.VARCHAR(length=100), nullable=True)) + batch_op.add_column(sa.Column('Game_Script', mysql.VARCHAR(length=1000), nullable=True)) + batch_op.add_column(sa.Column('Game_ID', mysql.INTEGER(), autoincrement=True, nullable=False)) + batch_op.add_column(sa.Column('Game_Image', mysql.VARCHAR(length=100), nullable=True)) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.create_index('Game_Name', ['Game_Name'], unique=True) + batch_op.create_index('Game_Image', ['Game_Image'], unique=True) + batch_op.create_index('Game_ID', ['Game_ID'], unique=True) + batch_op.drop_column('game_image') + batch_op.drop_column('game_script') + batch_op.drop_column('price') + batch_op.drop_column('w_console') + batch_op.drop_column('game_genre') + batch_op.drop_column('game_name') + batch_op.drop_column('game_id') + + with op.batch_alter_table('books', schema=None) as batch_op: + batch_op.add_column(sa.Column('Price', mysql.FLOAT(), nullable=False)) + batch_op.add_column(sa.Column('Book_Image', mysql.VARCHAR(length=100), nullable=True)) + batch_op.add_column(sa.Column('Book_Script', mysql.VARCHAR(length=1000), nullable=True)) + batch_op.add_column(sa.Column('Book_Author', mysql.VARCHAR(length=30), nullable=False)) + batch_op.add_column(sa.Column('Book_Name', mysql.VARCHAR(length=100), nullable=False)) + batch_op.add_column(sa.Column('Book_Genre', mysql.VARCHAR(length=25), nullable=False)) + batch_op.add_column(sa.Column('Book_ID', mysql.INTEGER(), autoincrement=True, nullable=False)) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.create_index('Book_Name', ['Book_Name'], unique=True) + batch_op.create_index('Book_Image', ['Book_Image'], unique=True) + batch_op.create_index('Book_ID', ['Book_ID'], unique=True) + batch_op.drop_column('book_image') + batch_op.drop_column('book_script') + batch_op.drop_column('price') + batch_op.drop_column('book_genre') + batch_op.drop_column('book_author') + batch_op.drop_column('book_name') + batch_op.drop_column('book_id') + + with op.batch_alter_table('anime', schema=None) as batch_op: + batch_op.add_column(sa.Column('Anime_Image', mysql.VARCHAR(length=100), nullable=True)) + batch_op.add_column(sa.Column('Anime_ID', mysql.INTEGER(), autoincrement=True, nullable=False)) + batch_op.add_column(sa.Column('Where_TW', mysql.VARCHAR(length=25), nullable=True)) + batch_op.add_column(sa.Column('Anime_Genre', mysql.VARCHAR(length=25), nullable=False)) + batch_op.add_column(sa.Column('Anime_Script', mysql.VARCHAR(length=1000), nullable=True)) + batch_op.add_column(sa.Column('Anime_Name', mysql.VARCHAR(length=50), nullable=False)) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.create_index('Anime_Name', ['Anime_Name'], unique=True) + batch_op.create_index('Anime_Image', ['Anime_Image'], unique=True) + batch_op.create_index('Anime_ID', ['Anime_ID'], unique=True) + batch_op.drop_column('anime_image') + batch_op.drop_column('anime_script') + batch_op.drop_column('where_tw') + batch_op.drop_column('anime_genre') + batch_op.drop_column('anime_name') + batch_op.drop_column('anime_id') + + # ### end Alembic commands ### diff --git a/flask-server/models/__pycache__/content_models.cpython-312.pyc b/flask-server/models/__pycache__/content_models.cpython-312.pyc index ad7b64b..a72a7bb 100644 Binary files a/flask-server/models/__pycache__/content_models.cpython-312.pyc and b/flask-server/models/__pycache__/content_models.cpython-312.pyc differ diff --git a/flask-server/models/__pycache__/user_models.cpython-312.pyc b/flask-server/models/__pycache__/user_models.cpython-312.pyc index db169d3..5012594 100644 Binary files a/flask-server/models/__pycache__/user_models.cpython-312.pyc and b/flask-server/models/__pycache__/user_models.cpython-312.pyc differ diff --git a/flask-server/models/content_models.py b/flask-server/models/content_models.py index 4958bc7..6375be1 100644 --- a/flask-server/models/content_models.py +++ b/flask-server/models/content_models.py @@ -1,36 +1,138 @@ -from flask_sqlalchemy import SQLAlchemy -from app import db +import sys +sys.path.append("..") +from exts import db -# db = SQLAlchemy() - -# Class for Books table class Books(db.Model): - __tablename__ = "Books" - Book_ID = db.Column(db.Integer, primary_key=True, unique=True) - Book_Name = db.Column(db.String(100), unique=True, nullable=False) - Book_Author = db.Column(db.String(30), nullable=False) - Book_Genre = db.Column(db.String(25), nullable=False) - Price = db.Column(db.Float, nullable=False) - Book_Script = db.Column(db.String(1000)) + """ + Class model for the Books table. + + Attributes + ---------- + book_id : int + id of the book + book_name : str + name of the book + book_author : str + author of the book + book_genre : str + genre of the book + price : float + rrp of the book + book_script : str (text) + description of the book + book_image : str + image url of the book + """ + + __tablename__ = "books" + book_id = db.Column(db.Integer, primary_key=True, unique=True) + book_name = db.Column(db.String(100), unique=True, nullable=False) + book_author = db.Column(db.String(30), nullable=False) + book_genre = db.Column(db.String(25), nullable=False) + price = db.Column(db.Float, nullable=False) + book_script = db.Column(db.String(1000)) + book_image = db.Column(db.String(100), unique=True) + + def __repr__(self): + """Returns a string representation of constructed object.""" + return f"" + + def _create(self): + """Adds a new book object to the database.""" + db.session.add(self) + db.session.commit() + + def _delete(self): + """Deletes a book object from the database.""" + db.session.delete(self) + db.session.commit() -# Class for Anime table class Anime(db.Model): - __tablename__ = "Anime" - Anime_ID = db.Column(db.Integer, primary_key=True, unique=True) - Anime_Name = db.Column(db.String(50), unique=True, nullable=False) - Anime_Genre = db.Column(db.String(25), nullable=False) - Where_TW = db.Column(db.String(25)) - Anime_Script = db.Column(db.String(1000)) + """ + Class model for the Anime table. + + Attributes + ---------- + anime_id : int + id of the anime + anime_name : str + name of the anime + anime_genre : str + genre of the anime + where_tw : str + where to watch the anime + anime_script : str (text) + description of the anime + anime_image : str + image url of the anime + """ + + __tablename__ = "anime" + anime_id = db.Column(db.Integer, primary_key=True, unique=True) + anime_name = db.Column(db.String(50), unique=True, nullable=False) + anime_genre = db.Column(db.String(25), nullable=False) + where_tw = db.Column(db.String(25)) + anime_script = db.Column(db.String(1000)) + anime_image = db.Column(db.String(100), unique=True) + + def __repr__(self): + """Returns a string representation of constructed object.""" + return f"" + + def _create(self): + """Adds a new anime object to the database.""" + db.session.add(self) + db.session.commit() + + def _delete(self): + """Deletes an anime object from the database.""" + db.session.delete(self) + db.session.commit() -# Class for Games table class Games(db.Model): - __tablename__ = "Games" - Game_ID = db.Column(db.Integer, primary_key=True, unique=True) - Game_Name = db.Column(db.String(50), unique=True, nullable=False) - Game_Genre = db.Column(db.String(30), nullable=False) - W_Console = db.Column(db.String(100)) - Price = db.Column(db.Float, nullable=False) - Game_Script = db.Column(db.String(1000)) \ No newline at end of file + """ + Class model for the Games table. + + Attributes + ---------- + game_id : int + id of the game + game_name : str + name of the game + game_genre : str + genre of the game + w_console : str + which consoles the game is available on + price : float + rrp of the game + game_script : str (text) + description of the game + game_image : str + image url of the game + """ + + __tablename__ = "games" + game_id = db.Column(db.Integer, primary_key=True, unique=True) + game_name = db.Column(db.String(50), unique=True, nullable=False) + game_genre = db.Column(db.String(30), nullable=False) + w_console = db.Column(db.String(100)) + price = db.Column(db.Float, nullable=False) + game_script = db.Column(db.String(1000)) + game_image = db.Column(db.String(100), unique=True) + + def __repr__(self): + """Returns a string representation of constructed object.""" + return f"" + + def _create(self): + """Adds a new game object to the database.""" + db.session.add(self) + db.session.commit() + + def _delete(self): + """Deletes a game object from the database.""" + db.session.delete(self) + db.session.commit() diff --git a/flask-server/models/forum_models.py b/flask-server/models/forum_models.py new file mode 100644 index 0000000..977908c --- /dev/null +++ b/flask-server/models/forum_models.py @@ -0,0 +1,49 @@ +import sys +sys.path.append("..") +from exts import db +from datetime import datetime as dt, UTC + + +class Message(db.Model): + """ + Class model for the Message Board table. + + Attributes + ---------- + post_id : int + id of the message + post_content : str (text) + text content of the message + post_category : str + category of the message + post_author : str + author of the message + post_date : datetime + time and date that the message was posted + """ + + __tablename__ = "message_board" + post_id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True) + post_content = db.Column(db.Text, nullable=False) + post_category = db.Column(db.String(50), nullable=False) + post_author = db.Column(db.String(30), nullable=False) + post_date = db.Column(db.DateTime(), default=dt.now(UTC), nullable=False) + + def __repr__(self): + """Returns a string representation of constructed object.""" + return f"" + + def create(self): + """Adds a new message to the database.""" + db.session.add(self) + db.session.commit() + + def delete(self): + """Deletes a message from the database.""" + db.session.delete(self) + db.session.commit() + + def update(self, post_content): + """Updates a message in the database.""" + self.post_content = post_content + db.session.commit() diff --git a/flask-server/models/user_models.py b/flask-server/models/user_models.py index 581d172..634574b 100644 --- a/flask-server/models/user_models.py +++ b/flask-server/models/user_models.py @@ -1,39 +1,81 @@ -from flask_sqlalchemy import SQLAlchemy -from app import db +import sys +sys.path.append("..") +from exts import db from uuid import uuid4 -from datetime import datetime as dt # To make a timestamp +from datetime import datetime as dt, UTC -# Generate a unique user ID, 32 characters long def get_uuid(): + """Returns a unique user ID""" return uuid4().hex -# Class for user account table, all the important information in here class User(db.Model): + """ + Class model for the User Accounts table. + + Attributes + ---------- + user_id : str + unique identity code for the user, generated from get_uuid function + username : str + username of the user + password : str + password of the user, stored as a bcrypt hash + """ + __tablename__ = "user_accounts" - user_id = db.Column(db.String(36), primary_key=True, unique=True, default=get_uuid) # If users are inserted through MySQL the UUID will be 36 chars + user_id = db.Column(db.String(36), primary_key=True, unique=True, default=get_uuid) username = db.Column(db.String(30), unique=True, nullable=False) - password = db.Column(db.String(60), nullable=False) # Bcrypt should be 60 chars + password = db.Column(db.String(60), nullable=False) + def __repr__(self): + """Returns a string representation of constructed object.""" + return f"" + -# Class for user profile table, columns by default to be null and added by user if they wish on edit profile page class Profile(db.Model): + """ + Class model for the User Profiles table. + + Attributes + ---------- + username : str + username of the user + first_name : str + first name of the user + last_name : str + last name of the user + email : str + email address of the user + date_of_birth : date + date of birth of the user + interests : str (text) + interests of the user + date_joined : datetime + timestamp of sign up for the user + """ + __tablename__ = "user_profiles" - username = db.Column(db.String(30), primary_key=True, unique=True) # Changing from user id to username + username = db.Column(db.String(30), primary_key=True, unique=True) first_name = db.Column(db.String(50), nullable=False) last_name = db.Column(db.String(50), nullable=False) email = db.Column(db.String(254), unique=True, nullable=False) - date_of_birth = db.Column(db.Date) # This option for future functionality of calculating age and age restricting recommendations -> if null restricted by default + date_of_birth = db.Column(db.Date) # This option for future functionality of calculating age and age restricting recommendations -> change this to not null when get working interests = db.Column(db.Text) - date_joined = db.Column(db.DateTime(), default=dt.utcnow) + date_joined = db.Column(db.DateTime(), default=dt.now(UTC)) + + def __repr__(self): + """Returns a string representation of constructed object.""" + return f"" + + def update_interests(self, interests): + """Updates profile in the database.""" + self.interests = interests + db.session.commit() + def update_dob(self, dob): + """Updates profile in the database.""" + self.date_of_birth = dob + db.session.commit() -class Message(db.Model): - __tablename__ = "message_board" - post_id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True) - post_content = db.Column(db.Text, nullable=False) - post_category = db.Column(db.String(50), nullable=False) - post_author = db.Column(db.String(30), nullable=False) - post_date = db.Column(db.DateTime(), default=dt.utcnow, nullable=False) - # post_author = db.Column(db.String(30), db.ForeignKey(User.username), nullable=False) # Dropping the FK restraint for simplicity of demonstrating messages from mock users \ No newline at end of file diff --git a/flask-server/mysqlconfig.py b/flask-server/mysqlconfig.py deleted file mode 100644 index c287f1d..0000000 --- a/flask-server/mysqlconfig.py +++ /dev/null @@ -1,3 +0,0 @@ -HOST = 'localhost' -USER = 'root' -PASSWORD = 'root1234' \ No newline at end of file diff --git a/flask-server/requirements.txt b/flask-server/requirements.txt index 2539208..1ab8321 100644 --- a/flask-server/requirements.txt +++ b/flask-server/requirements.txt @@ -1,5 +1,6 @@ flask flask-sqlalchemy +flask_migrate flask-bcrypt flask-cors flask-session diff --git a/flask-server/routes.py b/flask-server/routes.py deleted file mode 100644 index f785ba4..0000000 --- a/flask-server/routes.py +++ /dev/null @@ -1,261 +0,0 @@ -# This file runs the server -from flask import current_app, jsonify, request -from app import create_app, db, bcrypt -from models.user_models import User, Profile, Message -from models.content_models import Books, Anime, Games -from flask_jwt_extended import create_access_token, create_refresh_token, unset_jwt_cookies, get_jwt_identity, jwt_required -from flask_restx import Api, Resource, fields -from email_validator import validate_email, EmailNotValidError - -# Would separate into different namespaces but don't have time unfortunately - -# Create an application instance -app = create_app() # By default uses application config -api = Api(app) # Passing our app through the Api class from Flask RestX -# api = Api(app, doc="/docs") - -# Model serialiser so can be displayed as a JSON, takes in model class and then the output format -message_model=api.model("Message", { - "post_id": fields.Integer, - "post_content": fields.String, - "post_category": fields.String, - "post_author": fields.String, - "post_date": fields.DateTime(dt_format='rfc822') -}) - -profile_model=api.model("Profile", { - "first_name": fields.String, - "last_name": fields.String, - "email": fields.String, - "date_of_birth": fields.DateTime(dt_format='rfc822'), - "interests": fields.String -}) - -books_model=api.model("Books", { - "Book_ID": fields.Integer, - "Book_Name": fields.String, - "Book_Author": fields.String, - "Book_Genre": fields.String, - "Price": fields.Float, - "Book_Script": fields.String -}) - -anime_model=api.model("Anime", { - "Anime_ID": fields.Integer, - "Anime_Name": fields.String, - "Anime_Genre": fields.String, - "Where_TW": fields.String, - "Anime_Script": fields.String -}) - -games_model=api.model("Games", { - "Game_ID": fields.Integer, - "Game_Name": fields.String, - "Game_Genre": fields.String, - "W_Console": fields.String, - "Price": fields.Float, - "Game_Script": fields.String -}) - -# Defining routes - -# Might use this function to check emails -def check_email(email): - try: - v = validate_email(email) - email = v["email"] - return True - except EmailNotValidError as e: - print(str(e)) - return False - - -@app.route("/") -def home(): - return {"message": "hello"} - - -# Register -@app.route("/register", methods=["POST"]) -def register_user(): - username = request.json["username"] # Getting each value from the json - first_name = request.json["first_name"] - last_name = request.json["last_name"] - email = request.json["email"] - password = request.json["password"] - - username_exists = User.query.filter_by(username=username).first() is not None # Checking if username is in DB - email_exists = Profile.query.filter_by(email=email).first() is not None # Checking if email is in DB - - if username_exists: - return jsonify({"error": "Username is already taken"}), 409 - - if email_exists: - return jsonify({"error": "Email is already registered"}), 409 - - # Validate input - if len(username) < 1 or len(username) > 30: - return jsonify({"error": "Username is invalid"}), 400 - if len(first_name) < 1 or len(first_name) > 50: - return jsonify({"error": "Name is invalid"}), 400 - if len(last_name) < 1 or len(last_name) > 50: - return jsonify({"error": "Name is invalid"}), 400 - # add email validation here - - # Hashing the password - hashed_password = bcrypt.generate_password_hash(password) - # Creating an instance of User class to add to user_accounts table, user_id will use default generation - new_user = User(username=username, password=hashed_password) - db.session.add(new_user) - # db.session.commit() # Need to commit the insertion of user to then grab the same user_id it generated - # Create an instance of the Profile class to add to user_profiles table - new_profile=Profile(username=username, first_name=first_name, last_name=last_name, email=email) - db.session.add(new_profile) - db.session.commit() # Commit profile - - # Creates an access and refresh token which is needed at the front end - access_token = create_access_token(identity=new_user.username) - refresh_token = create_refresh_token(identity=new_user.username) - return jsonify( - {"access_token": access_token, "refresh_token": refresh_token}) - - -# Login -@app.route("/login", methods=["POST"]) -def login_user(): - username = request.json["username"] # Gets it from the json - password = request.json["password"] - - user = User.query.filter_by(username=username).first() - - if user is None: - return jsonify({"error": "Unauthorised"}), 401 - - if not bcrypt.check_password_hash(user.password, password): - return jsonify({"error": "Unauthorised"}), 401 - - access_token = create_access_token(identity=user.username) - refresh_token = create_refresh_token(identity=user.username) - return jsonify( - {"access_token": access_token, "refresh_token": refresh_token}) - - -# Logout -@app.route("/logout", methods=["POST"]) -def logout_user(): - response = jsonify({"message": "Logout successful"}) - # Unset the JWT cookie - unset_jwt_cookies(response) - return response - -# Message board routes (get and post to any board) -@api.route("/forum") -class ForumResource(Resource): - # Get all messages, returns a list - @api.marshal_list_with(message_model) - def get(self): - messages = Message.query.all() - return messages - - # Post a message, returns a single message - @api.marshal_with(message_model) - def post(self): - data = request.get_json() - new_post = Message( - post_content = data.get("post_content"), - post_category = data.get("post_category"), - post_author = data.get("post_author"), - post_date = data.get("post_date") - ) - - db.session.add(new_post) - db.session.commit() - - return new_post, 201 - -# Message board route for filtering by category -@api.route("/forum/") -class ForumCatResource(Resource): - # Get a post by category - - # Doesn't work think doing it wrong - @api.marshal_list_with(message_model) - def get(self, search): - result = Message.query.filter_by(post_category=search).all() - - return result - - # # Doesn't work think doing it wrong - # @api.marshal_list_with(message_model) - # def get(self, post_category): - # search = Message.query.filter_by(post_category="{search}").all() - - # return search - - -# Content api -# Books -@api.route("/book_suggestions") -class BooksResource(Resource): - # Book suggestions - - @api.marshal_list_with(books_model) - def get(self): - books = Books.query.all() - return books - - -# Content api -# Anime -@api.route("/anime_suggestions") -class AnimeResource(Resource): - # Anime suggestions - - @api.marshal_list_with(anime_model) - def get(self): - anime = Anime.query.all() - return anime - -# Content api -# Games -@api.route("/games_suggestions") -class GamesResource(Resource): - # Game suggestions - - @api.marshal_list_with(games_model) - def get(self): - games = Games.query.all() - return games - - -# List of all users -@api.route("/users") -class UsersResource(Resource): - @api.marshal_list_with(profile_model) - def get(self): - users=Profile.query.all() - return users - - -@api.route("/current_user") -class UserResource(Resource): - @api.marshal_with(profile_model) - @jwt_required - def get(self, current_user): - current_user = Profile.query.filter_by(username=get_jwt_identity()).first() # Filter by username (from the JWT token) - return current_user - - -# Refresh access token, probably won't have time to implement this on the frontend though -@api.route("/refresh") -class RefreshResource(Resource): - @jwt_required(refresh=True) - def post(self): - current_user = get_jwt_identity() - new_access_token = create_access_token(identity=current_user) - - return make_response(jsonify({"access_token": new_access_token}), 200) - - -if __name__ == "__main__": - app.run(debug=True) \ No newline at end of file diff --git a/flask-server/routes/content.py b/flask-server/routes/content.py new file mode 100644 index 0000000..d661645 --- /dev/null +++ b/flask-server/routes/content.py @@ -0,0 +1,202 @@ +from flask_restx import Resource, Namespace, fields +from exts import db +from models.content_models import Books, Anime, Games + +content_ns = Namespace("content", description="A namespace for content recommendations.") + +books_model = content_ns.model("Books", { + "book_id": fields.Integer(description="ID - primary key, autoincrement from 1"), + "book_name": fields.String(description="Book title, unique"), + "book_author": fields.String(description="Author of the book"), + "book_genre": fields.String(description="Genre of the book"), + "price": fields.Float(description="Recommended retail price"), + "book_script": fields.String(description="Description of the book"), + "book_image": fields.String(description="URL of image, unique") +}) + +anime_model = content_ns.model("Anime", { + "anime_id": fields.Integer(description="ID - primary key, autoincrement from 1"), + "anime_name": fields.String(description="Anime title, unique"), + "anime_genre": fields.String(description="Genre of the anime"), + "where_tw": fields.String(description="Where to watch the anime"), + "anime_script": fields.String(description="Description of the anime"), + "anime_image": fields.String(description="URL of image, unique") +}) + +games_model = content_ns.model("Games", { + "game_id": fields.Integer(description="ID - primary key, autoincrement from 1"), + "game_name": fields.String(description="Game title, unique"), + "game_genre": fields.String(description="Genre of the game"), + "w_console": fields.String(description="Which consoles the game is available on"), + "price": fields.Float(description="Recommended retail price"), + "game_script": fields.String(description="Description of the game"), + "game_image": fields.String(description="URL of image, unique") +}) + + +@content_ns.route("/books") +class BooksAll(Resource): + + @content_ns.marshal_list_with(books_model) + def get(self): + """Get all book suggestions""" + books = Books.query.all() + return books + + +@content_ns.route("/books/id/") +class BookById(Resource): + + @content_ns.marshal_with(books_model) + def get(self, id): + """Get a book by id""" + book = Books.query.get_or_404(id) + + return book + + +@content_ns.route("/books/genre/") +class BooksByGenre(Resource): + + @content_ns.marshal_list_with(books_model) + def get(self, genre): + """Get a book by genre""" + book = Books.query.filter(Books.book_genre == genre).all() + + return book + + +@content_ns.route("/books/author/") +class BooksByAuthor(Resource): + + @content_ns.marshal_list_with(books_model) + def get(self, author): + """Get a book by author""" + book = Books.query.filter(Books.book_author.ilike(f"%{author}%")).all() + + return book + + +@content_ns.route("/books/title/") +class BooksByName(Resource): + + @content_ns.marshal_list_with(books_model) + def get(self, title): + """Get a book by title""" + book = Books.query.filter(Books.book_name.ilike(f"%{title}%")).all() + + return book + + +@content_ns.route("/anime") +class AnimeAll(Resource): + + @content_ns.marshal_list_with(anime_model) + def get(self): + """Get all anime suggestions""" + anime = Anime.query.all() + return anime + + +@content_ns.route("/anime/id/") +class AnimeById(Resource): + + @content_ns.marshal_with(anime_model) + def get(self, id): + """Get an anime by id""" + anime = Anime.query.get_or_404(id) + + return anime + +@content_ns.route("/anime/genre/") +class AnimeByGenre(Resource): + + @content_ns.marshal_list_with(anime_model) + def get(self, genre): + """Get an anime by genre""" + anime = Anime.query.filter(Anime.anime_genre == genre).all() + + return anime + + +@content_ns.route("/anime/stream/") +class AnimeByStream(Resource): + + @content_ns.marshal_list_with(anime_model) + def get(self, stream): + """Get an anime by where to watch""" + anime = Anime.query.filter(Anime.where_tw.ilike(f"%{stream}%")).all() + + return anime + + +@content_ns.route("/anime/title/") +class AnimeByName(Resource): + + @content_ns.marshal_list_with(anime_model) + def get(self, title): + """Get an anime by title""" + anime = Anime.query.filter(Anime.anime_name.ilike(f"%{title}%")).all() + + return anime + + +@content_ns.route("/games") +class GamesAll(Resource): + + @content_ns.marshal_list_with(games_model) + def get(self): + """Get all game suggestions""" + games = Games.query.all() + return games + + +@content_ns.route("/games/id/") +class GameById(Resource): + + @content_ns.marshal_with(games_model) + def get(self, id): + """Get a game by id""" + game = Games.query.get_or_404(id) + + return game + + +@content_ns.route("/games/genre/") +class GamesByGenre(Resource): + + @content_ns.marshal_list_with(games_model) + def get(self, genre): + """Get a game by genre""" + game = Games.query.filter(Games.game_genre == genre).all() + + return game + +@content_ns.route("/games/console/") +class GamesByConsole(Resource): + + @content_ns.marshal_list_with(games_model) + def get(self, console): + """Get a game by console""" + game = Games.query.filter(Games.w_console.ilike(f"%{console}%")).all() + + return game + + +@content_ns.route("/games/title/") +class GamesByName(Resource): + + @content_ns.marshal_list_with(games_model) + def get(self, title): + """Get a game by title""" + game = Games.query.filter(Games.game_name.ilike(f"%{title}%")).all() + + return game + + +@content_ns.route("/hello") +class Hello(Resource): + + def get(self): + """Basic route to test""" + return {"message": "Hello world!"} \ No newline at end of file diff --git a/flask-server/routes/forum.py b/flask-server/routes/forum.py new file mode 100644 index 0000000..211e4ae --- /dev/null +++ b/flask-server/routes/forum.py @@ -0,0 +1,108 @@ +from flask_restx import Resource, Namespace, fields +from flask import request, jsonify, make_response, abort +from models.forum_models import Message +from flask_jwt_extended import get_jwt_identity, jwt_required + +forum_ns = Namespace("forum", description="A namespace for the message board.") + +message_model = forum_ns.model("Message", { + "post_id": fields.Integer(description="ID - primary key, autoincrement from 1"), + "post_content": fields.String(description="Text content of the message"), + "post_category": fields.String(description="Category of the message"), + "post_author": fields.String(description="Author of the message"), + "post_date": fields.DateTime(description="Time and date message was posted", dt_format='rfc822') +}) + + +@forum_ns.route("/all") +class ForumAll(Resource): + @forum_ns.marshal_list_with(message_model) + def get(self): + """Get all messages""" + messages = Message.query.all() + return messages + + @forum_ns.marshal_with(message_model) + @forum_ns.expect(message_model) + @jwt_required() + def post(self): + """Create a new message""" + data = request.get_json() + new_post = Message( + post_content=data.get("post_content"), + post_category=data.get("post_category"), + post_author=data.get("post_author"), + post_date=data.get("post_date") + ) + + new_post.create() + + return new_post, 201 + + +@forum_ns.route("/id/") +class ForumById(Resource): + + @forum_ns.marshal_with(message_model) + def get(self, id): + """Get message by id""" + result = Message.query.get_or_404(id) + + return result + + @forum_ns.marshal_with(message_model) + @jwt_required() + def put(self, id): + """Update a message by id""" + original_message = Message.query.get(id) + if original_message is None: + return abort(404, "Message not found") + + username = get_jwt_identity() + message_to_edit = Message.query.filter(Message.post_id == id, Message.post_author == username).first() + if message_to_edit is None: + return abort(403, "Unauthorised: You are not the author of this message") + else: + data = request.get_json() + message_to_edit.update(data.get("post_content")) + return message_to_edit + + @forum_ns.marshal_with(message_model) + @jwt_required() + def delete(self, id): + """Delete a message by id""" + username = get_jwt_identity() + original_message = Message.query.get(id) + if original_message is None: + return abort(404, "Message not found") + + message_to_delete = Message.query.filter(Message.post_id == id, Message.post_author == username).first() + if message_to_delete is None: + return abort(403, "Unauthorised: You are not the author of this message") + + message_to_delete.delete() + + return message_to_delete + + +@forum_ns.route("/category/") +class ForumByCategory(Resource): + + @forum_ns.marshal_list_with(message_model) + def get(self, category): + """Get messages by category""" + result = Message.query.filter(Message.post_category == category).all() + + return result + + +@forum_ns.route("/author/") +class ForumByAuthor(Resource): + + @forum_ns.marshal_list_with(message_model) + def get(self, author): + """Get messages by author username""" + result = Message.query.filter(Message.post_author == author).all() + + return result + \ No newline at end of file diff --git a/flask-server/routes/user.py b/flask-server/routes/user.py new file mode 100644 index 0000000..5b16dec --- /dev/null +++ b/flask-server/routes/user.py @@ -0,0 +1,190 @@ +from flask_restx import Resource, Namespace, fields +from flask import request, jsonify, make_response, abort +from models.user_models import User, Profile +from flask_jwt_extended import create_access_token, create_refresh_token, unset_jwt_cookies, get_jwt_identity, jwt_required +from email_validator import validate_email, EmailNotValidError +from exts import db +from flask_bcrypt import Bcrypt +from datetime import datetime as dt + +bcrypt = Bcrypt() + +user_ns = Namespace("user", description="A namespace for user authentication and services.") + +profile_model = user_ns.model("Profile", { + "first_name": fields.String(description="First name"), + "last_name": fields.String(description="First name"), + "email": fields.String(description="Email, unique"), + "date_of_birth": fields.DateTime(description="Date of birth", dt_format='rfc822'), + "interests": fields.String(description="Text field for interests") +}) + +user_model = user_ns.model("User", { + "user_id": fields.String(description="ID - primary key, generated UUID"), + "username": fields.String(description="Username, unique"), + "password": fields.String(description="Password, bcrypt hash") +}) + + +def user_access_tokens(user): + """Return an access and refresh token for the user""" + access_token = create_access_token(identity=user.username) + refresh_token = create_refresh_token(identity=user.username) + return access_token, refresh_token + + +def check_email(email): + """Validate email function, return True if valid format""" + try: + validate = validate_email(email) + email = validate["email"] + return True + except EmailNotValidError as e: + print(str(e)) + return False + + +@user_ns.route("/register") +class Register(Resource): + def post(self): + """Register a new user""" + username = request.json["username"] + first_name = request.json["first_name"] + last_name = request.json["last_name"] + email = request.json["email"] + password = request.json["password"] + + username_exists = User.query.filter_by(username=username).first() is not None + email_exists = Profile.query.filter_by(email=email).first() is not None + + if username_exists: + return make_response(jsonify({"message": "Username is already taken"}), 409) + + if email_exists: + return make_response(jsonify({"message": "Email is already registered"}), 409) + + if len(username.strip()) < 1 or len(username) > 30: + return make_response(jsonify({"message": "Username must be between 1 and 30 characters"}), 400) + if len(first_name.strip()) < 1 or len(first_name) > 50: + return make_response(jsonify({"message": "First name must be between 1 and 50 characters"}), 400) + if len(last_name.strip()) < 1 or len(last_name) > 50: + return make_response(jsonify({"message": "Last name must be between 1 and 50 characters"}), 400) + if not check_email(email): + return make_response(jsonify({"message": "Email address is invalid"}), 400) + + hashed_password = bcrypt.generate_password_hash(password) + new_user = User(username=username, password=hashed_password) + db.session.add(new_user) + new_profile = Profile(username=username, first_name=first_name, last_name=last_name, email=email) + db.session.add(new_profile) + db.session.commit() + + access_token, refresh_token = user_access_tokens(new_user) + return make_response(jsonify( + {"access_token": access_token, "refresh_token": refresh_token, "user": new_user.username}), 201) + + +@user_ns.route("/login") +class Login(Resource): + def post(self): + """Login a user""" + data = request.get_json() + username = data.get("username") + password = data.get("password") + + user = User.query.filter_by(username=username).first() + + if user is None: + return make_response(jsonify({"message": "Invalid credentials"}), 401) + + if not bcrypt.check_password_hash(user.password, password): + return make_response(jsonify({"message": "Invalid credentials"}), 401) + + access_token, refresh_token = user_access_tokens(user) + return make_response(jsonify( + {"access_token": access_token, "refresh_token": refresh_token, "user": user.username}), 200) + + +@user_ns.route("/logout") +class Logout(Resource): + def post(self): + """Logout a user""" + response = jsonify({"message": "Logout successful"}) + unset_jwt_cookies(response) + return response + + +@user_ns.route("/refresh") +class RefreshToken(Resource): + @jwt_required() + def post(self): + """Refresh access token""" + current_user = get_jwt_identity() + new_access_token = create_access_token(identity=current_user) + + return make_response(jsonify({"access_token": new_access_token}), 200) + + +@user_ns.route("/current_user") +class CurrentUser(Resource): + @user_ns.marshal_with(profile_model) + @jwt_required() + def get(self): + """Get current user profile by username""" + username = get_jwt_identity() + current_user = Profile.query.filter_by(username=username).first() + if current_user is None: + return abort(401, "Unauthorised") + else: + return current_user + + @user_ns.marshal_with(profile_model) + @jwt_required() + def put(self): + """Update profile of current user, one field per request""" + username = get_jwt_identity() + current_user = Profile.query.filter_by(username=username).first() + if current_user is None: + return abort(401, "Unauthorised") + + data = request.get_json() + interests = data.get("interests") + date_of_birth = data.get("date_of_birth") + + # Will need to learn more scalable ways + if interests is not None: + current_user.update_interests(interests) + return current_user + + if date_of_birth is not None: + try: + date = dt.strptime(date_of_birth, "%Y-%m-%d").date() + current_user.update_dob(date) + return current_user + except ValueError: + return abort(400, "Invalid date format") + + return abort(400, "Nothing to update") + + +@user_ns.route("/members") +class MembersAll(Resource): + @user_ns.marshal_list_with(profile_model) + def get(self): + """List all users""" + # Might need to move these methods elsewhere, more for admins + users = Profile.query.all() + return users + + +@user_ns.route("/members/") +class MemberByUsername(Resource): + @user_ns.marshal_with(profile_model) + def get(self, member): + """List a member""" + # Turn into a search of member, also not sure something we want normal users to access + user = Profile.query.filter(Profile.username == member).first() + if user is not None: + return user + else: + return abort(404, "User not found") diff --git a/flask-server/test_routes.py b/flask-server/test_routes.py deleted file mode 100644 index 77ae960..0000000 --- a/flask-server/test_routes.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest -from unittest import TestCase -from app import create_app, db -from config import TestConfig -from routes import register_user - -class APITestCase(TestCase): - # This function will set up our test database - def setUp(self): - self.app=create_app(TestConfig) - - self.client=self.app.test_client(self) - - with self.app.app_context(): - db.init_app(self.app) - - db.create_all() - - - def test_home_response(self): - home_response = self.client.get('/') - result = home_response.json - expected = {"message":"hello"} - - self.assertEqual(expected, result) - - - def test_register_user_success(self): - pass - - - # This function will remove everything from our test database - def tearDown(self): - with self.app.app_context(): - db.session.remove() - db.drop_all() - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/flask-server/tests/mock_data.py b/flask-server/tests/mock_data.py new file mode 100644 index 0000000..05981f9 --- /dev/null +++ b/flask-server/tests/mock_data.py @@ -0,0 +1,320 @@ +books_list = [ + { + "book_id": 1, + "book_name": "Fourth Wing", + "book_author": "Rebecca Yarros", + "book_genre": "Fantasy", + "price": 9.19, + "book_script": "Twenty-year-old Violet Sorrengail was supposed to enter the Scribe Quadrant, living a quiet life among books and history. Now, the commanding general-also known as her tough-as-talons mother-has ordered Violet to join the hundreds of candidates striving to become the elite of Navarre: dragon riders.", + "book_image": "https://rb.gy/a7n2nv" + }, + { + "book_id": 2, + "book_name": "The Harry Potter Series", + "book_author": "J.K. Rowling", + "book_genre": "Fantasy", + "price": 51.65, + "book_script": "The Harry Potter books follow a young wizard named Harry as he attends Hogwarts School of Witchcraft and Wizardry. Alongside his friends Ron and Hermione, Harry faces challenges, discovers his past, and confronts the dark wizard Voldemort across seven books, filled with magic, friendship, and the battle between good and evil.", + "book_image": "https://rb.gy/mioh6n" + }, + { + "book_id": 3, + "book_name": "A Court of Thorns and Roses Series", + "book_author": "Sarah J. Maas", + "book_genre": "Fantasy", + "price": 31.74, + "book_script": "ACOTAR Follows Feyre, a huntress who accidentally kills a faerie and is taken to the faerie lands as punishment. There, she navigates faerie politics, forms relationships with powerful fae like Tamlin and Rhysand, and becomes involved in a high-stakes battle that could impact both human and faerie realms across several books filled with magic, romance, and conflicts.", + "book_image": "https://rb.gy/ydq70j" + }, + { + "book_id": 4, + "book_name": "To Kill a Kingdom", + "book_author": "Alexandra Christo", + "book_genre": "Fantasy", + "price": 4.67, + "book_script": "Princess Lira is siren royalty and the most lethal of them all. With the hearts of seventeen princes in her collection, she is revered across the sea. Until a twist of fate forces her to kill one of her own. To punish her daughter, the Sea Queen transforms Lira into the one thing they loathe most - a human. Robbed of her song, Lira has until the winter solstice to deliver Prince Elians heart to the Sea Queen or remain a human forever.", + "book_image": "https://rb.gy/09jtlo" + }, + { + "book_id": 5, + "book_name": "Elizabeth Bathory: Life and Legacy of Historys Most Prolific Female Serial Killer", + "book_author": "James Oliver", + "book_genre": "History", + "price": 6.12, + "book_script": "This book explains the life and times of this powerful woman - and how she came to be accused of so many heinous crimes. Youll gain access to a variety of historical versions, perspectives, and accounts of her life - some of which paint her as a villain and others as a victim!", + "book_image": "https://rb.gy/du98fq" + }, + { + "book_id": 6, + "book_name": "The Great Empires of the Ancient World", + "book_author": "Thomas Harrison", + "book_genre": "History", + "price": 11.63, + "book_script": "A distinguished team of internationally renowned scholars surveys the great empires from 1600 BC to AD 500, from the ancient Mediterranean to China. Exploring the very nature of empire itself, the authors show how profoundly imperialism in the distant past influenced the 19th-century powers and the modern United States.", + "book_image": "tiny.cc/2iitvz" + }, + { + "book_id": 7, + "book_name": "Landlines", + "book_author": "Raynor Winn", + "book_genre": "History", + "price": 6, + "book_script": "Embarking on a journey across the Cape Wrath Trail, over 200 miles of gruelling terrain through Scotlands remotest mountains and lochs, Raynor and Moth look to an uncertain future. Fearing that miracles dont often repeat themselves.", + "book_image": "tiny.cc/ziitvz" + }, + { + "book_id": 8, + "book_name": "Unbroken: A World War II Story of Survival", + "book_author": "Lauren Hillenbrand", + "book_genre": "History", + "price": 4.71, + "book_script": "On a May afternoon in 1943, an Army Air Forces bomber crashed into the Pacific Ocean and disappeared, leaving only a spray of debris and a slick of oil, gasoline, and blood. Then, on the ocean surface, a face appeared. It was that of a young lieutenant, the planes bombardier, who was struggling to a life raft and pulling himself aboard. So began one of the most extraordinary odysseys of the Second World War.", + "book_image": "tiny.cc/djitvz" + }, + { + "book_id": 9, + "book_name": "The Haunting of Hill House", + "book_author": "Shirley Jackson", + "book_genre": "Horror", + "price": 9.9, + "book_script": "Welcome to Hill House, an eerie mansion with a chilling past. When a group of individuals sets out to uncover its supernatural secrets, they find themselves trapped in a world where reality blurs with the terrifying unknown. Shirley Jacksons classic tale weaves a haunting narrative that explores the eerie power of a house that seems to have a mind of its own.", + "book_image": "tiny.cc/sjitvz" + }, + { + "book_id": 10, + "book_name": "Dracula", + "book_author": "Bram Stoker", + "book_genre": "Horror", + "price": 14.29, + "book_script": "When Jonathan Harker visits Transylvania to help Count Dracula with the purchase of a London house, he makes a series of horrific discoveries about his client. Soon afterwards, various bizarre incidents unfold in England: an apparently unmanned ship is wrecked off the coast of Whitby; a young woman discovers strange puncture marks on her neck; and the inmate of a lunatic asylum raves about the Master and his imminent arrival.", + "book_image": "tiny.cc/1kitvz" + }, + { + "book_id": 11, + "book_name": "The Shining", + "book_author": "Stephen King", + "book_genre": "Horror", + "price": 10.11, + "book_script": "Danny is only five years old, but in the words of old Mr Hallorann he is a shiner, aglow with psychic voltage. When his father becomes caretaker of the Overlook Hotel, Dannys visions grow out of control. As winter closes in and blizzards cut them off, the hotel seems to develop a life of its own. It is meant to be empty. So who is the lady in Room 217 and who are the masked guests going up and down in the elevator? And why do the hedges shaped like animals seem so alive? Somewhere, somehow, there is an evil force in the hotel - and that, too, is beginning to shine.", + "book_image": "tiny.cc/kkitvz" + }, + { + "book_id": 12, + "book_name": "The Exorcist", + "book_author": "William Peter Blatty", + "book_genre": "Horror", + "price": 9.19, + "book_script": "The terror begins unobtrusively. Noises in the attic. In the childs room, an odd smell, the displacement of furniture, an icy chill. At first, easy explanations are offered. Then frightening changes begin to appear in eleven-year-old Regan. Medical tests fail to shed any light on her symptoms, but it is as if a different personality has invaded her body.", + "book_image": "tiny.cc/9litvz" + } +] + +anime_list = [ + { + "anime_id": 1, + "anime_name": "Bleach", + "anime_genre": "Shonen", + "where_tw": "Disney+", + "anime_script": "Ichigo Kurosaki is a teenager from Karakura Town who can see ghosts, a talent allowing him to meet a supernatural human Rukia Kuchiki, who enters the town in search of a Hollow, a kind of monstrous lost soul who can harm both ghosts and humans.", + "anime_image": None + }, + { + "anime_id": 2, + "anime_name": "Naruto", + "anime_genre": "Shonen", + "where_tw": "Crunchyroll", + "anime_script": "The Village Hidden in the Leaves is home to the stealthiest ninja. But twelve years earlier, a fearsome Nine-tailed Fox terrorized the village before it was subdued and its spirit sealed within the body of a baby boy.", + "anime_image": None + }, + { + "anime_id": 3, + "anime_name": "Jujutsu Kaisen", + "anime_genre": "Shonen", + "where_tw": "Crunchyroll", + "anime_script": "Yuji Itadori is a boy with tremendous physical strength, though he lives a completely ordinary high school life. One day, to save a classmate who has been attacked by curses, he eats the finger of Ryomen Sukuna, taking the curse into his own soul. From then on, he shares one body with Ryomen Sukuna. Guided by the most powerful of sorcerers, Satoru Gojo, Itadori is admitted to Tokyo Jujutsu High School, an organization that fights the curses... and thus begins the heroic tale of a boy who became a curse to exorcise a curse, a life from which he could never turn back.", + "anime_image": None + }, + { + "anime_id": 4, + "anime_name": "One Piece", + "anime_genre": "Shonen", + "where_tw": "Crunchyroll", + "anime_script": "Monkey. D. Luffy refuses to let anyone or anything stand in the way of his quest to become the king of all pirates. With a course charted for the treacherous waters of the Grand Line and beyond, this is one captain who will never give up until he has claimed the greatest treasure on Earth: the Legendary One Piece!", + "anime_image": None + }, + { + "anime_id": 5, + "anime_name": "Attack on Titan", + "anime_genre": "Seinen", + "where_tw": "Crunchyroll", + "anime_script": "Known in Japan as Shingeki no Kyojin, many years ago, the last remnants of humanity were forced to retreat behind the towering walls of a fortified city to escape the massive, man-eating Titans that roamed the land outside their fortress. Only the heroic members of the Scouting Legion dared to stray beyond the safety of the walls – but even those brave warriors seldom returned alive. Those within the city clung to the illusion of a peaceful existence until the day that dream was shattered, and their slim chance at survival was reduced to one horrifying choice: kill – or be devoured!", + "anime_image": None + }, + { + "anime_id": 6, + "anime_name": "Tokyo Ghoul", + "anime_genre": "Seinen", + "where_tw": "Crunchyroll", + "anime_script": "Haise Sasaki has been tasked with teaching Qs Squad how to be outstanding investigators, but his assignment is complicated by the troublesome personalities of his students and his own uncertain grasp of his Ghoul powers. Can he pull them together as a team, or will Qs Squad first assignment be their last?", + "anime_image": None + }, + { + "anime_id": 7, + "anime_name": "Berserk", + "anime_genre": "Seinen", + "where_tw": "Crunchyroll", + "anime_script": "Spurred by the flame raging in his heart, the Black Swordsman Guts continues his seemingly endless quest for revenge. Standing in his path are heinous outlaws, delusional evil spirits, and a devout child of god.Even as it chips away at his life, Guts continues to fight his enemies, who wield repulsive and inhumane power, with nary but his body and sword—his strength as a human. What lies at the end of his travels? The answer is shrouded in the night.", + "anime_image": None + }, + { + "anime_id": 8, + "anime_name": "Death Note", + "anime_genre": "Seinen", + "where_tw": "Crunchyroll", + "anime_script": "An intelligent high school student goes on a secret crusade to eliminate criminals from the world after discovering a notebook capable of killing anyone whose name is written into it.", + "anime_image": None + }, + { + "anime_id": 9, + "anime_name": "Chainsaw Man", + "anime_genre": "Fantasy", + "where_tw": "Crunchyroll", + "anime_script": "Denji is a young boy who works as a Devil Hunter with the Chainsaw Devil Pochita. One day, as he was living his miserable life trying to pay off the debt he inherited from his parents, he got betrayed and killed. As he was losing his consciousness, he made a deal with Pochita, and got resurrected as the Chainsaw Man: the owner of the Devil’s heart.", + "anime_image": None + }, + { + "anime_id": 10, + "anime_name": "JoJos Bizarre Adventure", + "anime_genre": "Fantasy", + "where_tw": "Netflix", + "anime_script": "In ancient Mexico, people of Aztec had prospered. They had historic and strange Stone Mask. It was a miraculous mask which brings eternal life and the power of authentic ruler. But the mask suddenly disappeared. A long time after that, in late 19th centuries when the thought and life of people were suddenly changing, Jonathan Joestar met with Dio Brando. They spend time together through boyhood to youth, and the Stone Mask brings curious fate to them.", + "anime_image": None + }, + { + "anime_id": 11, + "anime_name": "Black Clover", + "anime_genre": "Fantasy", + "where_tw": "Crunchyroll", + "anime_script": "In a world where magic is everything, Asta and Yuno are both found abandoned at a church on the same day. While Yuno is gifted with exceptional magical powers, Asta is the only one in this world without any. At the age of fifteen, both receive grimoires, magic books that amplify their holder’s magic. Asta’s is a rare Grimoire of Anti-Magic that negates and repels his opponent’s spells. Being opposite but good rivals, Yuno and Asta are ready for the hardest of challenges to achieve their common dream: to be the Wizard King. Giving up is never an option!", + "anime_image": None + }, + { + "anime_id": 12, + "anime_name": "Link Click", + "anime_genre": "Fantasy", + "where_tw": "Crunchyroll", + "anime_script": "Using superpowers to enter their clientele’s photos one by one, Cheng Xiaoshi and Lu Guang take their work seriously at Time Photo Studio, a small photography shop set in the backdrop of a modern metropolis. Each job can be full of danger, but nothing is more important than fulfilling every order, no matter the scale…or peril involved!", + "anime_image": None + } +] + +games_list = [ + { + "game_id": 1, + "game_name": "Fae Farm", + "game_genre": "Cozy Games", + "w_console": "PC, NINTENDO SWITCH", + "price": 29.99, + "game_script": "Escape to the magical life of your dreams in Fae Farm, a farm sim RPG for 1-4 players. Craft, cultivate, and decorate to grow your homestead, and use spells to explore the enchanted island of Azoria!", + "game_image": None + }, + { + "game_id": 2, + "game_name": "Spellcaster University", + "game_genre": "Cozy Games", + "w_console": "PC", + "price": 19.49, + "game_script": "Develop a prestigious university of mages. Build rooms, train your students, fight orcs, slay the bureaucrats, manage your budget... a directors life is not a quiet one.", + "game_image": None + }, + { + "game_id": 3, + "game_name": "Little Witch in the Woods", + "game_genre": "Cozy Games", + "w_console": "PC, NINTENDO SWITCH", + "price": 12.39, + "game_script": " Little Witch in the Woods tells the story of Ellie, an apprentice witch. Explore the mystical forest, help the charming residents, and experience the daily life of the witch.", + "game_image": None + }, + { + "game_id": 4, + "game_name": "Cozy Grove", + "game_genre": "Cozy Games", + "w_console": "PC, NINTENDO SWITCH", + "price": 11.39, + "game_script": "Welcome to Cozy Grove, a game about camping on a haunted, ever-changing island. As a Spirit Scout, youll wander the islands forest each day, finding new hidden secrets and helping soothe the local ghosts. With a little time and a lot of crafting, youll bring color and joy back to Cozy Grove!", + "game_image": None + }, + { + "game_id": 5, + "game_name": "Hogwarts Legacy", + "game_genre": "RPG", + "w_console": "PC, XBOX, PLAYSTATION, NINTENDO SWITCH", + "price": 49.99, + "game_script": "Hogwarts Legacy is an immersive, open-world action RPG. Now you can take control of the action and be at the center of your own adventure in the wizarding world.", + "game_image": None + }, + { + "game_id": 6, + "game_name": "God of War", + "game_genre": "RPG", + "w_console": "PLAYSTATION", + "price": 39.99, + "game_script": "Against a backdrop of Norse Realms torn asunder by the fury of the Aesir, they’ve been trying their utmost to undo the end times. But despite their best efforts, Fimbulwinter presses onward. Witness the changing dynamic of the father-son relationship as they fight for survival. Atreus thirsts for knowledge to help him understand the prophecy of Loki, as Kratos struggles to break free of his past and be the father his son needs.", + "game_image": None + }, + { + "game_id": 7, + "game_name": "Lies of P", + "game_genre": "RPG", + "w_console": "PLAYSTATION, PC, XBOX", + "price": 49.99, + "game_script": "Lies of P is a thrilling soulslike that takes the story of Pinocchio, turns it on its head, and sets it against the darkly elegant backdrop of the Belle Epoque era.", + "game_image": None + }, + { + "game_id": 8, + "game_name": "Marvels Spider Man 2", + "game_genre": "RPG", + "w_console": "PLAYSTATION", + "price": 69.99, + "game_script": "Peter Parker and Miles Morales return for an exciting new adventure in the critically acclaimed Marvel’s Spider-Man franchise. Swing, jump and utilize the new Web Wings to travel across Marvel’s New York, quickly switching between Peter Parker and Miles Morales to experience different stories and epic new powers, as the iconic villain Venom threatens to destroy their lives, their city and the ones they love.", + "game_image": None + }, + { + "game_id": 9, + "game_name": "STARDEW VALLEY", + "game_genre": "SIMULATION", + "w_console": "PC, NINTENDO SWITCH, XBOX, PLAYSTATION", + "price": 10.99, + "game_script": "Youve inherited your grandfathers old farm plot in Stardew Valley. Armed with hand-me-down tools and a few coins, you set out to begin your new life. Can you learn to live off the land and turn these overgrown fields into a thriving home?", + "game_image": None + }, + { + "game_id": 10, + "game_name": "Two Point Hospital", + "game_genre": "SIMULATION", + "w_console": "PC, NINTENDO SWITCH, XBOX, PLAYSTATION", + "price": 24.99, + "game_script": "Design stunning hospitals, cure peculiar illnesses and manage troublesome staff as you spread your budding healthcare organisation across Two Point County.", + "game_image": None + }, + { + "game_id": 11, + "game_name": "GAME DEV TYCOON", + "game_genre": "SIMULATION", + "w_console": "PC", + "price": 8.5, + "game_script": "In Game Dev Tycoon you replay the history of the gaming industry by starting your own video game development company in the 80s. Create best selling games. Research new technologies and invent new game types. Become the leader of the market and gain worldwide fans.", + "game_image": None + }, + { + "game_id": 12, + "game_name": "NAHEULBEUKS DUNGEON MASTER", + "game_genre": "SIMULATION", + "w_console": "PC", + "price": 20.99, + "game_script": "A dungeon in danger ! Build, manage, and defend your tower in the satirical heroic fantasy universe of Dungeon of Naheulbeuk. From a shaky establishment to an infamous lair!", + "game_image": None + } +] \ No newline at end of file diff --git a/flask-server/tests/test_anime_api.py b/flask-server/tests/test_anime_api.py new file mode 100644 index 0000000..d154f04 --- /dev/null +++ b/flask-server/tests/test_anime_api.py @@ -0,0 +1,175 @@ +import unittest +from unittest import TestCase +import sys +sys.path.append("..") +from app import create_app, db +from config import TestConfig +from models.content_models import Anime +from mock_data import anime_list + + +def insert_anime(): + """Insert mock anime data into the database""" + for i in range(len(anime_list)): + new_anime = Anime(anime_id=anime_list[i]["anime_id"], anime_name=anime_list[i]["anime_name"], + anime_genre=anime_list[i]["anime_genre"], where_tw=anime_list[i]["where_tw"], + anime_script=anime_list[i]["anime_script"], anime_image=anime_list[i]["anime_image"]) + db.session.add(new_anime) + db.session.commit() + + +class TestAnimeAPI(TestCase): + """Test for anime related suggestions within the content namespace, inheriting directly from + TestCase (rather than TestAPI class) so can insert all mock data into tables within setUp function""" + + def setUp(self): + """Set up our test database and work within the context of our application""" + self.app = create_app(TestConfig) + + self.client = self.app.test_client(self) + + with self.app.app_context(): + + db.create_all() + insert_anime() + + def test_get_all_anime(self): + """Test get all anime""" + get_response = self.client.get("/content/anime") + status_code = get_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_anime_by_id(self): + """Test get anime by id""" + anime_id = 1 + get_response = self.client.get(f"/content/anime/id/{anime_id}") + status_code = get_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_anime_by_id_doesnt_exist(self): + """Test get anime by id that doesn't exist""" + anime_id = 42 + get_response = self.client.get(f"/content/anime/id/{anime_id}") + status_code = get_response.status_code + expected = {'message': f'The requested URL was not found on the server. If you entered the URL manually please ' + f'check your spelling and try again. You have requested this URI ' + f'[/content/anime/id/{anime_id}] but did you mean /content/anime/id/ or ' + f'/content/games/id/ or /content/anime/title/ ?'} + result = get_response.json + + self.assertEqual(status_code, 404) + self.assertEqual(expected, result) + + def test_get_anime_by_genre(self): + """Test get list of anime by a genre that exists""" + genre = "Seinen" + get_response = self.client.get(f"/content/anime/genre/{genre}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 4) + + def test_get_anime_by_genre_case_different(self): + """Test get list of anime by a genre that exists, mixed case""" + genre = "SEiNen" + get_response = self.client.get(f"/content/anime/genre/{genre}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 4) + + def test_get_anime_by_genre_doesnt_exist(self): + """Test get anime by a genre that doesn't exist, results list empty""" + genre = "Testing" + get_response = self.client.get(f"/content/anime/genre/{genre}") + status_code = get_response.status_code + expected = [] + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + self.assertEqual(expected, result) + + def test_get_anime_by_title_exact(self): + """Test get list of books by title, search exact title""" + title = "Death Note" + get_response = self.client.get(f"/content/anime/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_anime_by_title_partial(self): + """Test get list of anime by title, partial search of title""" + title = "on" + get_response = self.client.get(f"/content/anime/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 2) + + def test_get_anime_by_title_no_results(self): + """Test get list of anime by title, no match""" + title = "Sword Art Online" + get_response = self.client.get(f"/content/anime/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + def test_get_anime_by_where_to_watch_exact_only(self): + """Test get list of anime by where to watch, search that exact platform""" + stream = "Crunchyroll" + get_response = self.client.get(f"/content/anime/stream/{stream}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + print(list_length) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 10) + + def test_get_anime_by_where_to_watch_partial(self): + """Test get list of anime by where to watch, returns any that contain that streaming platform""" + stream = "ix" + get_response = self.client.get(f"/content/anime/stream/{stream}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_anime_by_where_to_watch_no_results(self): + """Test get list of anime by streaming platform, no match""" + stream = "Prime" + get_response = self.client.get(f"/content/anime/stream/{stream}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + def tearDown(self): + """Destroy all the instances created for testing, remove sessions and drop tables""" + with self.app.app_context(): + db.session.remove() + db.drop_all() + + +if __name__ == "__main__": + unittest.main() diff --git a/flask-server/tests/test_app.py b/flask-server/tests/test_app.py deleted file mode 100644 index cb39be3..0000000 --- a/flask-server/tests/test_app.py +++ /dev/null @@ -1 +0,0 @@ -# Tests on requests diff --git a/flask-server/tests/test_auth_api.py b/flask-server/tests/test_auth_api.py new file mode 100644 index 0000000..de23cd1 --- /dev/null +++ b/flask-server/tests/test_auth_api.py @@ -0,0 +1,51 @@ +import unittest +from unittest import TestCase +import sys +sys.path.append("..") +from app import create_app, db +from config import TestConfig + + +def create_user_json(username="testuser", first_name="test", last_name="user", email="testuser@test.com", + password="mytestpassword"): + """Function to create user registration json""" + json = { + "username": username, + "first_name": first_name, + "last_name": last_name, + "email": email, + "password": password + } + return json + + +default_user = create_user_json() + +expired_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcxMDAxNzcwMiwianRpI" \ + "joiMWQ0NTRmNTEtNTU5MS00Y2MzLWJkODMtMmM5M2U1MDlkMTc1IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRl" \ + "c3R1c2VyIiwibmJmIjoxNzEwMDE3NzAyLCJjc3JmIjoiODk5NzAzM2MtZTQwNi00OWNiLTlkMmMtYzVlYTM5NmQw" \ + "ZTczIiwiZXhwIjoxNzEwMDE4NjAyfQ.oYms99qrpe01vZRD9bZu8ChuE2HR-7eN9w99hlKvCqc" + + +class TestAPI(TestCase): + """Base test class for setting up test database and tables""" + + def setUp(self): + """Set up our test database and work within the context of our application""" + self.app = create_app(TestConfig) + + self.client = self.app.test_client(self) + + with self.app.app_context(): + + db.create_all() + + def tearDown(self): + """Destroy all the instances created for testing, remove sessions and drop tables""" + with self.app.app_context(): + db.session.remove() + db.drop_all() + + +if __name__ == "__main__": + unittest.main() diff --git a/flask-server/tests/test_books_api.py b/flask-server/tests/test_books_api.py new file mode 100644 index 0000000..ff5cc6c --- /dev/null +++ b/flask-server/tests/test_books_api.py @@ -0,0 +1,204 @@ +import unittest +from unittest import TestCase +import sys +sys.path.append("..") +from app import create_app, db +from config import TestConfig +from models.content_models import Books +from mock_data import books_list + + +def insert_books(): + """Insert mock book data into the database""" + for i in range(len(books_list)): + new_book = Books(book_id=books_list[i]["book_id"], book_name=books_list[i]["book_name"], + book_author=books_list[i]["book_author"], book_genre=books_list[i]["book_genre"], + price=books_list[i]["price"], book_script=books_list[i]["book_script"], + book_image=books_list[i]["book_image"]) + db.session.add(new_book) + db.session.commit() + + +class TestBooksAPI(TestCase): + """Test for book related suggestions within the content namespace, inheriting directly from + TestCase (rather than TestAPI class) so can insert all mock data into tables within setUp function""" + + def setUp(self): + """Set up our test database and work within the context of our application""" + self.app = create_app(TestConfig) + + self.client = self.app.test_client(self) + + with self.app.app_context(): + + db.create_all() + insert_books() + + def test_hello_world(self): + """Test the hello world route""" + hello_response = self.client.get("/content/hello") + status_code = hello_response.status_code + result = hello_response.json + expected = {"message": "Hello world!"} + + self.assertEqual(status_code, 200) + self.assertEqual(expected, result) + + def test_get_all_books(self): + """Test get all books""" + get_response = self.client.get("/content/books") + status_code = get_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_book_by_id(self): + """Test get book by id""" + book_id = 1 + get_response = self.client.get(f"/content/books/id/{book_id}") + status_code = get_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_book_by_diff_id(self): + """Test get book by different id""" + book_id = 12 + get_response = self.client.get(f"/content/books/id/{book_id}") + status_code = get_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_book_by_id_doesnt_exist(self): + """Test get book by id that doesn't exist""" + book_id = 42 + get_response = self.client.get(f"/content/books/id/{book_id}") + status_code = get_response.status_code + expected = {'message': f'The requested URL was not found on the server. If you entered the URL manually please ' + f'check your spelling and try again. You have requested this URI ' + f'[/content/books/id/{book_id}] but did you mean /content/books/id/ or ' + f'/content/books/title/ or /content/books ?'} + result = get_response.json + + self.assertEqual(status_code, 404) + self.assertEqual(expected, result) + + def test_get_book_by_genre(self): + """Test get list of books by a genre that exists""" + genre = "Fantasy" + get_response = self.client.get(f"/content/books/genre/{genre}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 4) + + def test_get_book_by_genre_case_different(self): + """Test get list of books by a genre that exists, mixed case""" + genre = "fanTASY" + get_response = self.client.get(f"/content/books/genre/{genre}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 4) + + def test_get_book_by_genre_doesnt_exist(self): + """Test get book by a genre that doesn't exist, results list empty""" + genre = "Testing" + get_response = self.client.get(f"/content/books/genre/{genre}") + status_code = get_response.status_code + expected = [] + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + self.assertEqual(expected, result) + + def test_get_book_by_author_full_name(self): + """Test get list of books by an author, exact name search""" + author = "J.K. Rowling" + get_response = self.client.get(f"/content/books/author/{author}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_book_by_author_surname_only(self): + """Test get list of books by an author, partial search exact surname""" + author = "Rowling" + get_response = self.client.get(f"/content/books/author/{author}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_book_by_author_partial_name(self): + """Test get list of books by an author, partial search of name""" + author = "le" + get_response = self.client.get(f"/content/books/author/{author}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 3) + + def test_get_book_by_author_no_results(self): + """Test get list of books by an author, no match""" + author = "1" + get_response = self.client.get(f"/content/books/author/{author}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + def test_get_book_by_title_exact(self): + """Test get list of books by title, search exact title""" + title = "Unbroken: A World War II Story of Survival" + get_response = self.client.get(f"/content/books/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_book_by_title_partial(self): + """Test get list of books by title, partial search of title""" + title = "The" + get_response = self.client.get(f"/content/books/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 5) + + def test_get_book_by_title_no_results(self): + """Test get list of books by title, no match""" + title = "The Very Hungry Caterpillar" + get_response = self.client.get(f"/content/books/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + def tearDown(self): + """Destroy all the instances created for testing, remove sessions and drop tables""" + with self.app.app_context(): + db.session.remove() + db.drop_all() + + +if __name__ == "__main__": + unittest.main() diff --git a/flask-server/tests/test_forum_api.py b/flask-server/tests/test_forum_api.py new file mode 100644 index 0000000..01d2c48 --- /dev/null +++ b/flask-server/tests/test_forum_api.py @@ -0,0 +1,382 @@ +import unittest +from test_auth_api import TestAPI, create_user_json, default_user + + +def create_post_json(post_content="Test content", post_category="Test", post_author="testuser"): + """Function to create post json""" + json = { + "post_content": post_content, + "post_category": post_category, + "post_author": post_author + } + return json + + +example_post = create_post_json() + + +class TestForumAPI(TestAPI): + """Tests for forum namespace routes""" + + def test_get_all_posts(self): + """Test getting all forum posts""" + forum_response = self.client.get("/forum/all") + + status_code = forum_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_post_id_not_found(self): + """Test get forum post by id route, with no data""" + post_id = 1 + response = self.client.get(f"/forum/id/{post_id}") + status_code = response.status_code + + self.assertEqual(status_code, 404) + + def test_create_post_successful(self): + """Test creating a post, login required for @jwt_required route""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = create_post_response.status_code + + self.assertEqual(status_code, 201) + + def test_get_post_id_successful(self): + """Test retrieving a forum post by id after creation""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + post_id = 1 + response = self.client.get(f"/forum/id/{post_id}") + status_code = response.status_code + + self.assertEqual(status_code, 200) + + def test_create_post_fail_no_access_token(self): + """Test creating a forum post without access token""" + create_post_response = self.client.post("/forum/all", json=example_post) + + status_code = create_post_response.status_code + expected = {'error': 'authorisation_token', 'message': 'Request does not contain a valid token'} + result = create_post_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + def test_create_post_fail_invalid_token(self): + """Test creating a forum post, invalid access token""" + invalid_token = "invalidtoken" + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {invalid_token}" + } + ) + + status_code = create_post_response.status_code + expected = {'error': 'invalid_token', 'message': 'Signature verification failed'} + result = create_post_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + def test_edit_post_successful(self): + """Test editing a forum post, login and verified same user required""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + post_id = 1 + original_get_by_id = self.client.get(f"/forum/id/{post_id}") + + update_response = self.client.put(f"/forum/id/{post_id}", + json=create_post_json(post_content="Changing the content"), + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + after_get_by_id = self.client.get(f"/forum/id/{post_id}") + original_post = original_get_by_id.json + changed_post = after_get_by_id.json + + self.assertEqual(status_code, 200) + self.assertNotEqual(original_post, changed_post) + + def test_edit_post_no_post(self): + """Test editing a post that does not exist""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + post_id = 1 + + update_response = self.client.put(f"/forum/id/{post_id}", + json=create_post_json(post_content="Changing the content"), + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + result = update_response.json + expected = {'message': f'Message not found. You have requested this URI [/forum/id/{post_id}] but did you mean' + f' /forum/id/ or /forum/all or /forum/author/ ?'} + + self.assertEqual(status_code, 404) + self.assertEqual(expected, result) + + def test_edit_post_fail_wrong_user(self): + """Test editing a post by a different user, unsuccessful""" + first_register = self.client.post("/user/register", json=default_user) + + first_access_token = first_register.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {first_access_token}" + } + ) + + post_id = 1 + original_get_by_id = self.client.get(f"/forum/id/{post_id}") + + second_register = self.client.post("/user/register", + json=create_user_json(username="differentuser", email="differentuser@test.com") + ) + + second_access_token = second_register.json["access_token"] + + update_response = self.client.put(f"/forum/id/{post_id}", + json=create_post_json(post_content="Changing the content", post_author="differentuser"), + headers={ + "Authorization": f"Bearer {second_access_token}" + } + ) + + status_code = update_response.status_code + after_get_by_id = self.client.get(f"/forum/id/{post_id}") + + original_post = original_get_by_id.json + changed_post = after_get_by_id.json + result = update_response.json + expected = {'message': 'Unauthorised: You are not the author of this message'} + + self.assertEqual(status_code, 403) + self.assertEqual(original_post, changed_post) + self.assertEqual(expected, result) + + def test_delete_post_successful(self): + """Test deleting a post, login and verified same user required""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + post_id = 1 + original_get_by_id = self.client.get(f"/forum/id/{post_id}") + + delete_response = self.client.delete(f"/forum/id/{post_id}", headers={ + "Authorization": f"Bearer {access_token}" + }) + + status_code = delete_response.status_code + before_delete_status_code = original_get_by_id.status_code + after_get_by_id = self.client.get(f"/forum/id/{post_id}") + after_delete_status_code = after_get_by_id.status_code + + self.assertEqual(status_code, 200) + self.assertEqual(after_delete_status_code, 404) + self.assertNotEqual(before_delete_status_code, after_delete_status_code) + + def test_delete_post_fail_wrong_user(self): + """Test deleting a post by a different user, unsuccessful""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + post_id = 1 + original_get_by_id = self.client.get(f"/forum/id/{post_id}") + + second_register = self.client.post("/user/register", + json=create_user_json(username="differentuser", email="differentuser@test.com") + ) + + second_access_token = second_register.json["access_token"] + + delete_response = self.client.delete(f"/forum/id/{post_id}", headers={ + "Authorization": f"Bearer {second_access_token}" + }) + + status_code = delete_response.status_code + result = delete_response.json + expected = {'message': 'Unauthorised: You are not the author of this message'} + before_delete_status_code = original_get_by_id.status_code + after_get_by_id = self.client.get(f"/forum/id/{post_id}") + after_delete_status_code = after_get_by_id.status_code + + self.assertEqual(status_code, 403) + self.assertEqual(expected, result) + self.assertEqual(before_delete_status_code, after_delete_status_code) + + def test_delete_post_no_post(self): + """Test deleting a post that does not exist""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + post_id = 1 + + delete_response = self.client.delete(f"/forum/id/{post_id}", headers={ + "Authorization": f"Bearer {access_token}" + }) + + status_code = delete_response.status_code + result = delete_response.json + expected = {'message': f'Message not found. You have requested this URI [/forum/id/{post_id}] but did you mean' + f' /forum/id/ or /forum/all or /forum/author/ ?'} + + self.assertEqual(status_code, 404) + self.assertEqual(expected, result) + + def test_delete_post_fail_no_token(self): + """Test deleting a post without an access token""" + register_response = self.client.post("/user/register", json=default_user) + + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + post_id = 1 + + delete_response = self.client.delete(f"/forum/id/{post_id}") + + status_code = delete_response.status_code + expected = {'error': 'authorisation_token', 'message': 'Request does not contain a valid token'} + result = delete_response.json + + self.assertEqual(expected, result) + self.assertEqual(status_code, 401) + + def test_get_post_by_category_results(self): + """Test get a list of forum posts by category with results""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + category = "Test" + get_response = self.client.get(f"/forum/category/{category}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_post_by_category_no_results(self): + """Test get list of forum posts in a category, no matches""" + category = "Test" + get_response = self.client.get(f"/forum/category/{category}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + def test_get_post_by_author_results(self): + """Test get a list of forum posts by a specific username with results""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + author = "testuser" + get_response = self.client.get(f"/forum/author/{author}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_post_by_author_no_results(self): + """Test get list of forum posts by a specific username, no matches""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + create_post_response = self.client.post("/forum/all", + json=example_post, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + author = "differentuser" + get_response = self.client.get(f"/forum/author/{author}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/flask-server/tests/test_games_api.py b/flask-server/tests/test_games_api.py new file mode 100644 index 0000000..d514a29 --- /dev/null +++ b/flask-server/tests/test_games_api.py @@ -0,0 +1,175 @@ +import unittest +from unittest import TestCase +import sys +sys.path.append("..") +from app import create_app, db +from config import TestConfig +from models.content_models import Games +from mock_data import games_list + + +def insert_games(): + """Insert mock game data into the database""" + for i in range(len(games_list)): + new_game = Games(game_id=games_list[i]["game_id"], game_name=games_list[i]["game_name"], + game_genre=games_list[i]["game_genre"], w_console=games_list[i]["w_console"], + price=games_list[i]["price"], game_script=games_list[i]["game_script"], + game_image=games_list[i]["game_image"]) + db.session.add(new_game) + db.session.commit() + + +class TestGamesAPI(TestCase): + """Test for game related suggestions within the content namespace, inheriting directly from + TestCase (rather than TestAPI class) so can insert all mock data into tables within setUp function""" + + def setUp(self): + """Set up our test database and work within the context of our application""" + self.app = create_app(TestConfig) + + self.client = self.app.test_client(self) + + with self.app.app_context(): + + db.create_all() + insert_games() + + def test_get_all_games(self): + """Test get all games""" + get_response = self.client.get("/content/games") + status_code = get_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_game_by_id(self): + """Test get game by id""" + game_id = 1 + get_response = self.client.get(f"/content/games/id/{game_id}") + status_code = get_response.status_code + + self.assertEqual(status_code, 200) + + def test_get_game_by_id_doesnt_exist(self): + """Test get game by id that doesn't exist""" + game_id = 42 + get_response = self.client.get(f"/content/games/id/{game_id}") + status_code = get_response.status_code + expected = {'message': f'The requested URL was not found on the server. If you entered the URL manually please ' + f'check your spelling and try again. You have requested this URI ' + f'[/content/games/id/{game_id}] but did you mean /content/games/id/ or ' + f'/content/anime/id/ or /content/games/title/ ?'} + result = get_response.json + + self.assertEqual(status_code, 404) + self.assertEqual(expected, result) + + def test_get_game_by_genre(self): + """Test get list of games by a genre that exists""" + genre = "simulation" + get_response = self.client.get(f"/content/games/genre/{genre}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 4) + + def test_get_game_by_genre_case_different(self): + """Test get list of books by a genre that exists, mixed case""" + genre = "SimuLATION" + get_response = self.client.get(f"/content/games/genre/{genre}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 4) + + def test_get_game_by_genre_doesnt_exist(self): + """Test get game by a genre that doesn't exist, results list empty""" + genre = "Testing" + get_response = self.client.get(f"/content/games/genre/{genre}") + status_code = get_response.status_code + expected = [] + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + self.assertEqual(expected, result) + + def test_get_game_by_title_exact(self): + """Test get list of games by title, search exact title""" + title = "God of War" + get_response = self.client.get(f"/content/games/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_game_by_title_partial(self): + """Test get list of games by title, partial search of title""" + title = "of" + get_response = self.client.get(f"/content/games/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 2) + + def test_get_game_by_title_no_results(self): + """Test get list of books by title, no match""" + title = "The Legend of Testing" + get_response = self.client.get(f"/content/games/title/{title}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + def test_get_game_by_console_exact_only(self): + """Test get list of games by console, search exact combination of consoles""" + console = "PC, NINTENDO SWITCH, XBOX, PLAYSTATION" + get_response = self.client.get(f"/content/games/console/{console}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 2) + + def test_get_game_by_console_partial(self): + """Test get list of games by console, partial search, returns any that contain that console""" + console = "switch" + get_response = self.client.get(f"/content/games/console/{console}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 6) + + def test_get_game_by_console_no_results(self): + """Test get list of games by console, no match""" + console = "SNES" + get_response = self.client.get(f"/content/games/console/{console}") + status_code = get_response.status_code + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + + def tearDown(self): + """Destroy all the instances created for testing, remove sessions and drop tables""" + with self.app.app_context(): + db.session.remove() + db.drop_all() + + +if __name__ == "__main__": + unittest.main() diff --git a/flask-server/tests/test_models.py b/flask-server/tests/test_models.py deleted file mode 100644 index e69de29..0000000 diff --git a/flask-server/tests/test_user_api.py b/flask-server/tests/test_user_api.py new file mode 100644 index 0000000..883ad83 --- /dev/null +++ b/flask-server/tests/test_user_api.py @@ -0,0 +1,495 @@ +import unittest +from test_auth_api import TestAPI, create_user_json, default_user, expired_token +from routes.user import check_email +from flask_jwt_extended import create_access_token + + +class TestUserAPI(TestAPI): + """Tests for user namespace routes and related functions""" + + def test_valid_email(self): + """Test a valid email address with check email""" + result = check_email("test@email.com") + self.assertTrue(result) + + def test_invalid_email(self): + """Test an invalid email address with check email""" + result = check_email("test.com") + self.assertFalse(result) + + def test_register_successful(self): + """Test successful registration of user""" + register_response = self.client.post("/user/register", json=default_user) + + status_code = register_response.status_code + + self.assertEqual(status_code, 201) + + def test_register_successful_max_length(self): + """Test successful registration of user, username, first name and last name max characters""" + register_response = self.client.post("/user/register", + json=create_user_json("thisusernameisthirtycharacters", + "Iamexactlyfiftycharacterslongexactlyexactlyexactly", + "Iamexactlyfiftycharacterslongexactlyexactlyexactly", + "testuser@test.com", "mytestpassword") + ) + + status_code = register_response.status_code + + self.assertEqual(status_code, 201) + + def test_register_fail_invalid_email(self): + """Test signing up with an invalid email address, unsuccessful registration""" + register_response = self.client.post("/user/register", + json=create_user_json(email="testuser@test") + ) + + status_code = register_response.status_code + expected = {"message": "Email address is invalid"} + result = register_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_register_fail_username_empty(self): + """Test unsuccessful registration of user due to empty username""" + register_response = self.client.post("/user/register", + json=create_user_json(username="") + ) + + status_code = register_response.status_code + expected = {"message": "Username must be between 1 and 30 characters"} + result = register_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_register_fail_first_name_long(self): + """Test unsuccessful registration of user due to too long first name""" + register_response = self.client.post("/user/register", + json=create_user_json(first_name="testtesttesttesttesttesttesttesttesttesttesttesttest") + ) + + status_code = register_response.status_code + expected = {"message": "First name must be between 1 and 50 characters"} + result = register_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_register_fail_last_name_space(self): + """Test unsuccessful registration of user due to empty last name, whitespace only""" + register_response = self.client.post("/user/register", + json=create_user_json(last_name=" ") + ) + + status_code = register_response.status_code + expected = {"message": "Last name must be between 1 and 50 characters"} + result = register_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_register_fail_username_conflict(self): + """Test unsuccessful registration of user due to username already taken""" + first_register = self.client.post("/user/register", json=default_user) + + second_register = self.client.post("/user/register", + json=create_user_json(email="differentuser@test.com") + ) + + second_status_code = second_register.status_code + expected = {"message": "Username is already taken"} + result = second_register.json + + self.assertEqual(second_status_code, 409) + self.assertEqual(expected, result) + + def test_register_fail_email_conflict(self): + """Test unsuccessful registration of user due to email already taken""" + first_register = self.client.post("/user/register", json=default_user) + + second_register = self.client.post("/user/register", + json=create_user_json(username="differentuser") + ) + + second_status_code = second_register.status_code + expected = {"message": "Email is already registered"} + result = second_register.json + + self.assertEqual(second_status_code, 409) + self.assertEqual(expected, result) + + def test_login_successful(self): + """Test creation of user and logging in successfully""" + register_response = self.client.post("/user/register", json=default_user) + + login_response = self.client.post("/user/login", + json={ + "username": "testuser", + "password": "mytestpassword" + } + ) + + status_code = login_response.status_code + json = login_response.json + result = json["user"] + expected = "testuser" + + self.assertEqual(status_code, 200) + self.assertEqual(expected, result) + + def test_login_fail_incorrect_password(self): + """Test creation of user and attempt of logging in with incorrect password""" + register_response = self.client.post("/user/register", json=default_user) + + login_response = self.client.post("/user/login", + json={ + "username": "testuser", + "password": "mywrongpassword" + } + ) + + status_code = login_response.status_code + expected = {"message": "Invalid credentials"} + result = login_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + def test_login_fail_no_username_exists(self): + """Test creation of user and attempt of logging in with unregistered username""" + register_response = self.client.post("/user/register", json=default_user) + + login_response = self.client.post("/user/login", + json={ + "username": "differentuser", + "password": "mytestpassword" + } + ) + + status_code = login_response.status_code + expected = {"message": "Invalid credentials"} + result = login_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + + def test_get_profile_successful(self): + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + get_response = self.client.get(f"/user/current_user", + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = get_response.status_code + self.assertEqual(status_code, 200) + + def test_get_profile_fail_no_user(self): + """Test get profile if user does not exist, using a token""" + with self.app.app_context(): + access_token = create_access_token(identity="Tester") + + get_response = self.client.get(f"/user/current_user", + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = get_response.status_code + expected = {'message': 'Unauthorised'} + result = get_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + def test_get_profile_fail_no_token(self): + """Test get profile without a token""" + register_response = self.client.post("/user/register", json=default_user) + + get_response = self.client.get(f"/user/current_user") + + status_code = get_response.status_code + expected = {'error': 'authorisation_token', 'message': 'Request does not contain a valid token'} + result = get_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + def test_edit_profile_interests_successful(self): + """Test edit interests in profile successfully""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + update_response = self.client.put(f"/user/current_user", + json={ + "interests": "I like testing" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + expected = {'first_name': 'test', 'last_name': 'user', 'email': 'testuser@test.com', 'date_of_birth': None, + 'interests': 'I like testing'} + result = update_response.json + + self.assertEqual(status_code, 200) + self.assertEqual(expected, result) + + def test_edit_profile_date_of_birth_successful(self): + """Test edit date of birth in profile successfully""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + update_response = self.client.put(f"/user/current_user", + json={ + "date_of_birth": "1999-12-01" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + expected = {'first_name': 'test', 'last_name': 'user', 'email': 'testuser@test.com', + 'date_of_birth': 'Wed, 01 Dec 1999 00:00:00 -0000', 'interests': None} + result = update_response.json + + self.assertEqual(status_code, 200) + self.assertEqual(expected, result) + + def test_edit_profile_date_of_birth_fail_format(self): + """Test edit date of birth in profile, wrong date format""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + update_response = self.client.put(f"/user/current_user", + json={ + "date_of_birth": "1999.12.01" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + expected = {'message': 'Invalid date format'} + result = update_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_edit_profile_date_of_birth_invalid_month(self): + """Test edit date of birth in profile, invalid month""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + update_response = self.client.put(f"/user/current_user", + json={ + "date_of_birth": "1999-20-01" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + expected = {'message': 'Invalid date format'} + result = update_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_edit_profile_date_of_birth_invalid_date(self): + """Test edit date of birth in profile, invalid date""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + update_response = self.client.put(f"/user/current_user", + json={ + "date_of_birth": "1999-04-31" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + expected = {'message': 'Invalid date format'} + result = update_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_edit_profile_interests_fail_no_user(self): + """Test edit interests in profile if user does not exist, using a token""" + with self.app.app_context(): + access_token = create_access_token(identity="Tester") + + update_response = self.client.put(f"/user/current_user", + json={ + "interests": "I like testing" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + + self.assertEqual(status_code, 401) + expected = {'message': 'Unauthorised'} + result = update_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + def test_edit_profile_fail_unsupported_field(self): + """Test trying to edit something else in profile""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + update_response = self.client.put(f"/user/current_user", + json={ + "username": "newusername" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + expected = {'message': 'Nothing to update'} + result = update_response.json + + self.assertEqual(status_code, 400) + self.assertEqual(expected, result) + + def test_edit_profile_partial_fail_two_things_same_time(self): + """Test trying to edit both valid fields in same payload, only first executed (restrict to one in frontend)""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + update_response = self.client.put(f"/user/current_user", + json={ + "interests": "I like testing", + "date_of_birth": "1999-12-01" + }, + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + status_code = update_response.status_code + expected = {'first_name': 'test', 'last_name': 'user', 'email': 'testuser@test.com', 'date_of_birth': None, + 'interests': 'I like testing'} + result = update_response.json + + self.assertEqual(status_code, 200) + self.assertEqual(expected, result) + + def test_refresh_token_successful(self): + """Come back to these later""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + refresh_response = self.client.post(f"/user/refresh", + headers={ + "Authorization": f"Bearer {access_token}" + } + ) + + result = refresh_response.json + status_code = refresh_response.status_code + + self.assertEqual(status_code, 200) + self.assertTrue(result["access_token"]) + + def test_refresh_token_fail_expired_token(self): + """Come back to these later""" + register_response = self.client.post("/user/register", json=default_user) + access_token = register_response.json["access_token"] + + refresh_response = self.client.post(f"/user/refresh", + headers={ + "Authorization": f"Bearer {expired_token}" + } + ) + + status_code = refresh_response.status_code + expected = {'error': 'token_expired', 'message': 'Token has expired'} + result = refresh_response.json + + self.assertEqual(status_code, 401) + self.assertEqual(expected, result) + + def test_logout_successful(self): + """Test logging out successfully""" + register_response = self.client.post("/user/register", json=default_user) + logout_response = self.client.post(f"/user/logout") + + status_code = logout_response.status_code + expected = {'message': 'Logout successful'} + result = logout_response.json + + self.assertEqual(status_code, 200) + self.assertEqual(expected, result) + + def test_get_all_members_results_in_list(self): + """Test getting list of all users successfully, may move this to admin space in future""" + register_response = self.client.post("/user/register", json=default_user) + member_response = self.client.get(f"/user/members") + status_code = member_response.status_code + result = member_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 1) + + def test_get_all_members_no_users(self): + """Test getting list of all users, empty list""" + get_response = self.client.get(f"/user/members") + status_code = get_response.status_code + expected = [] + result = get_response.json + list_length = len(result) + + self.assertEqual(status_code, 200) + self.assertEqual(list_length, 0) + self.assertEqual(expected, result) + + def test_search_member_successfully(self): + """Test searching a user by username, may move this to admin space in future""" + register_response = self.client.post("/user/register", json=default_user) + member = "testuser" + member_response = self.client.get(f"/user/members/{member}") + + status_code = member_response.status_code + result = member_response.json["first_name"] + expected = "test" + + self.assertEqual(status_code, 200) + self.assertEqual(expected, result) + + def test_search_member_not_found(self): + """Test searching a user by username, no user exists""" + member = "testuser" + member_response = self.client.get(f"/user/members/{member}") + + status_code = member_response.status_code + expected = {'message': f'User not found. You have requested this URI [/user/members/{member}] but did you mean ' + f'/user/members/ or /user/members or /user/register ?'} + result = member_response.json + + self.assertEqual(status_code, 404) + self.assertEqual(expected, result) + + +if __name__ == "__main__": + unittest.main() diff --git a/package-lock.json b/package-lock.json index 936b74a..134ed65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,29 @@ "name": "my-app", "version": "0.1.0", "dependencies": { + "-": "^0.0.1", + "@reduxjs/toolkit": "^2.1.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.2", + "browserify-zlib": "^0.2.0", "cors": "^2.8.5", "express": "^4.18.2", "mysql": "^2.18.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^9.1.0", "react-router-dom": "^6.18.0", "react-scripts": "5.0.1", "react-token-auth": "^2.3.8", - "web-vitals": "^2.1.4" + "redux-persist": "^6.0.0", + "redux-thunk": "^3.1.0", + "save-dev": "^0.0.1-security", + "web-vitals": "^2.1.4", + "webpack": "^5.90.3", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" }, "devDependencies": { "gh-pages": "^6.0.0", @@ -29,6 +39,11 @@ "s": "^1.0.0" } }, + "node_modules/-": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/-/-/--0.0.1.tgz", + "integrity": "sha512-3HfneK3DGAm05fpyj20sT3apkNcvPpCuccOThOPdzz8sY7GgQGe0l93XH9bt+YzibcTIgUAIMoyVJI740RtgyQ==" + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -2297,6 +2312,14 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3527,6 +3550,38 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.1.0.tgz", + "integrity": "sha512-nfJ/b4ZhzUevQ1ZPKjlDL6CMYxO4o7ZL7OSsvSOxzT/EN11LsBDgTqP7aedHtBrFSVoK7oTP1SbMWUwGb30NLg==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remix-run/router": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", @@ -4239,9 +4294,9 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.3.tgz", - "integrity": "sha512-6mfQ6iNvhSKCZJoY6sIG3m0pKkdUcweVNOLuBBKvoWGzl2yRxOJcYOTRyLKt3nxXvBLJWa6QkW//tgbIwJehmA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -4532,10 +4587,15 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.6.tgz", "integrity": "sha512-HYtNooPvUY9WAVRBr4u+4Qa9fYD1ze2IUlAD3HoA6oehn1taGwBx3Oa52U4mTslTS+GAExKpaFu39Y5xUEwfjg==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/ws": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", - "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dependencies": { "@types/node": "*" } @@ -4907,6 +4967,47 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5173,11 +5274,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" - }, "node_modules/array-includes": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", @@ -5827,12 +5923,10 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -5867,6 +5961,14 @@ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", @@ -6136,6 +6238,19 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6347,9 +6462,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.33.2.tgz", - "integrity": "sha512-a8zeCdyVk7uF2elKIGz67AjcXOxjRbwOLz8SbklEso1V+2DoW4OkAMZN9S9GBgvZIaqQi/OemFX4OiSoQEmg1Q==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.36.0.tgz", + "integrity": "sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -7044,11 +7159,6 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -7264,6 +7374,17 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/envinfo": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -8406,6 +8527,14 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -8596,6 +8725,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", @@ -9667,6 +9804,14 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -9942,6 +10087,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -10107,6 +10263,14 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -13655,6 +13819,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -15524,6 +15693,32 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-redux": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.0.tgz", + "integrity": "sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "react-native": ">=0.69", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15704,6 +15899,17 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -15727,6 +15933,27 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -15872,6 +16099,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", + "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -16203,6 +16435,11 @@ } } }, + "node_modules/save-dev": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/save-dev/-/save-dev-0.0.1-security.tgz", + "integrity": "sha512-k6knZTDNK8PKKbIqnvxiOveJinuw2LcQjqDoaorZWP9M5AR2EPsnpDeSbeoZZ0pHr5ze1uoaKdK8NBGQrJ34Uw==" + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -16456,6 +16693,17 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -17274,9 +17522,9 @@ } }, "node_modules/terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz", + "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -17291,15 +17539,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -17787,6 +18035,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -17915,18 +18171,18 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -17940,7 +18196,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -17960,6 +18216,58 @@ } } }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, "node_modules/webpack-dev-middleware": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", @@ -18139,9 +18447,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "engines": { "node": ">=10.0.0" }, @@ -18193,6 +18501,19 @@ "node": ">=10.13.0" } }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", @@ -18370,6 +18691,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 044e8a3..36e723e 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,29 @@ "homepage": "https://Angel2001-programmer.github.io/finalProject/", "private": true, "dependencies": { + "-": "^0.0.1", + "@reduxjs/toolkit": "^2.1.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.2", + "browserify-zlib": "^0.2.0", "cors": "^2.8.5", "express": "^4.18.2", "mysql": "^2.18.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^9.1.0", "react-router-dom": "^6.18.0", "react-scripts": "5.0.1", "react-token-auth": "^2.3.8", - "web-vitals": "^2.1.4" + "redux-persist": "^6.0.0", + "redux-thunk": "^3.1.0", + "save-dev": "^0.0.1-security", + "web-vitals": "^2.1.4", + "webpack": "^5.90.3", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.css b/src/App.css index 8baeba4..94707ae 100644 --- a/src/App.css +++ b/src/App.css @@ -1,10 +1,10 @@ @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@200;300;400;500;600;700&display=swap'); -body{ +body { background-color: rgb(72, 72, 72); } -li a{ +li a { text-decoration: none; color: black; } @@ -15,7 +15,7 @@ li a{ margin-left: 5px; } -.mainText{ +.mainText { font-size: 35px; margin: 0; margin-top: 190px; @@ -34,38 +34,38 @@ li a{ background-color: rgb(215, 215, 215); } -.headingTitles{ +.headingTitles { font-size: 5vh; font-weight: 400; padding: 0; margin: 0; } -.mainText{ +.mainText { color: black; margin-top: 8vh; } -.mainText2{ +.mainText2 { color: black; margin-top: 1vh; font-weight: 600; font-size: 27px; } -.card{ +.card { height: 10vh; min-width: 5%; background-color: black; } -.modal{ +.modal { background-color: purple; max-width: 500px; padding: 15px; } -#modalBG{ +#modalBG { display: flex; position: fixed; justify-content: center; @@ -76,35 +76,35 @@ li a{ z-index: 2; } -.errorMessage{ +.errorMessage { color: rgb(255, 0, 0); text-shadow: 6px 6px 10px rgba(0, 0, 0, 0.51); font-size: 20px; margin: 0; } -.missionTitle{ +.missionTitle { margin: 0; padding: 0; margin-top: 50px; text-align: center; } -.missionText{ - display: flex; - max-width: 96%; +.missionText { + /* display: flex; */ + /* max-width: 96%; */ font-size: 18px; text-align: justify; - justify-content: center; - margin: 0 auto; + /* justify-content: center; */ + /* margin: 0 auto; */ } -.dropDownMenuContainer{ +.dropDownMenuContainer { display: flex; justify-content: flex-end; } -.dropDownMenu{ +.dropDownMenu { position: absolute; display: flex; flex-direction: column; @@ -116,7 +116,7 @@ li a{ z-index: 1; } -.dropMenuItem{ +.dropMenuItem { font-size: 18px; font-weight: 700; padding: 2px; @@ -124,21 +124,21 @@ li a{ text-decoration: none; } -.dropMenuItem:hover{ +.dropMenuItem:hover { background-color: rgb(224, 224, 224); } -.dropMenuItem:last-child{ +.dropMenuItem:last-child { border-radius: 0 0 10px 10px; } -.link{ +.link { text-decoration: none; color: black; } @media all and (min-width: 800px) { - html{ + html { overflow-x: hidden; } } @@ -148,19 +148,19 @@ li a{ display: flex; flex-flow: row wrap; } - + .main section { display: flex; flex-flow: row wrap; } - section.content{ + section.content { height: 100vh; } } -@media all and (min-width: 360px){ - .mainText{ +@media all and (min-width: 360px) { + .mainText { margin-top: 12vh; } } diff --git a/src/UI/Button/button.jsx b/src/UI/Button/button.jsx index 6aa8e24..205f95c 100644 --- a/src/UI/Button/button.jsx +++ b/src/UI/Button/button.jsx @@ -9,6 +9,10 @@ const Button = props => { boxShadow: props.dropShadow, paddingLeft: props.paddingToLeft, paddingRight: props.paddingToRight, + paddingTop: props.paddingToTop, + paddingBottom: props.paddingToBottom, + height: props.height, + fontSize: props.fontSize, }}>

{props.text}

diff --git a/src/UI/Button/button.module.css b/src/UI/Button/button.module.css index 66ee1d7..730b01c 100644 --- a/src/UI/Button/button.module.css +++ b/src/UI/Button/button.module.css @@ -1,4 +1,5 @@ .button{ + display: flex; width: fit-content; background-image: linear-gradient(#D00000, #D07D00); border-radius: 10px; @@ -7,6 +8,8 @@ padding: 13px; margin-right: 3vh; box-shadow: #D1730170 5px 5px 5px; + justify-content: center; + align-items: center; } .button p { @@ -16,6 +19,6 @@ } .button:hover{ - background-image: black; + background-image: rgba(0, 0, 0, 0.425); cursor: pointer; } \ No newline at end of file diff --git a/src/UI/Card/card.jsx b/src/UI/Card/card.jsx index 5b1807d..c0f0de2 100644 --- a/src/UI/Card/card.jsx +++ b/src/UI/Card/card.jsx @@ -1,15 +1,19 @@ -import styles from "./card.module.css"; +import styles from './card.module.css'; -const Card = props => { - return( -
- {props.children} +const Card = (props) => { + return ( +
+ {props.children}
- ) -} + ); +}; -export default Card; \ No newline at end of file +export default Card; diff --git a/src/UI/Card/card.module.css b/src/UI/Card/card.module.css index 44766c8..038ccb4 100644 --- a/src/UI/Card/card.module.css +++ b/src/UI/Card/card.module.css @@ -1,12 +1,17 @@ -.card{ +.card { position: relative; display: flex; flex-direction: column; padding: 20px; gap: 20px; min-width: 55vh; - background: rgb(7,29,124); - background: linear-gradient(180deg, rgba(7,29,124,1) 0%, rgba(10,62,180,1) 45%, rgba(13,149,209,1) 100%); + background: rgb(7, 29, 124); + background: linear-gradient( + 180deg, + rgba(7, 29, 124, 1) 0%, + rgba(10, 62, 180, 1) 45%, + rgba(13, 149, 209, 1) 100% + ); border-radius: 20px; } @@ -19,4 +24,4 @@ min-width: 55vh; background-color: purple; border-radius: 20px; -} */ \ No newline at end of file +} */ diff --git a/src/UI/Login/login.jsx b/src/UI/Login/login.jsx index f3873e1..9370b80 100644 --- a/src/UI/Login/login.jsx +++ b/src/UI/Login/login.jsx @@ -6,38 +6,44 @@ import { useContext, useState } from 'react' import { UserContext, SignUpContext, NewUserContext, UserNameContext } from "../../components/FinalProject/FinalProject"; import httpClient from "../../httpClient"; import { login } from "../../auth"; +import { useDispatch } from "react-redux"; +import { setSignIn } from "../../redux/slices/userSlice" -const initialValues = { - userName: "", - password: "" -}; -const Login = props => { +function Login() { const [isOpened, setIsOpened] = useContext(UserContext); const [isSignModal, setIsSignModal] = useContext(SignUpContext); const [newUser, setNewUser] = useContext(NewUserContext); const [userName, setUserName] = useContext(UserNameContext); + const initialValues = { + userName: "", + password: "" + }; // This is just a test to see if login works with data. const [userData, setUserData] = useState(initialValues); const [errorMessage, setErrorMessage] = useState(null); let createAccount = null; - + + const dispatch = useDispatch() // Function to fetch user data from database, log in user if successful const loginUser = async () => { httpClient({ method: "POST", - url: "http://localhost:5000/login", + url: "http://localhost:5000/user/login", data: { username: userData.userName, password: userData.password } }) .then((response) => { + login(response.data.access_token) console.log(response) console.log(response.data.access_token) - login(response.data.access_token) + console.log(response.data.user) + let name = response.data.user + dispatch(setSignIn({name})) console.log(userData.userName, " has logged in") setIsOpened(false) setNewUser(true) @@ -55,27 +61,20 @@ const Login = props => { }) }; - //Set new keystroke to UserData values. const handleValues = (e) => { setUserData({ ...userData, [e.target.name]: e.target.value}); }; const handleLogin = (e) => { - //Prevents form from refreshing when Sign button is clicked. - e.preventDefault(); - if(userData.userName.trim().length === 0 || userData.password.trim().length === 0){ - setErrorMessage(

Inputs cannot be empty

) - } else { - setErrorMessage("") - // setIsOpened(false) - // setNewUser(true) - // setIsSignModal(false) - loginUser() - console.log('Login successful'); - setUserName(userData.userName) - - } + //Prevents form from refreshing when Sign button is clicked. + e.preventDefault(); + if(userData.userName.trim().length === 0 || userData.password.trim().length === 0){ + setErrorMessage(

Inputs cannot be empty

) + } else { + setErrorMessage("") + loginUser() + } } //Check if Modal is opened. diff --git a/src/UI/signup/signup.jsx b/src/UI/signup/signup.jsx index 956bce6..b59cf62 100644 --- a/src/UI/signup/signup.jsx +++ b/src/UI/signup/signup.jsx @@ -6,6 +6,8 @@ import { useState, useContext } from "react"; import { SignUpContext, UserContext, NewUserContext } from "../../components/FinalProject/FinalProject"; import httpClient from "../../httpClient"; import { login } from "../../auth"; +import { useDispatch } from "react-redux"; +import { setSignIn } from "../../redux/slices/userSlice" const SignUp = () => { const initialValues = { @@ -17,24 +19,23 @@ const SignUp = () => { confirmPSW: "" }; - + // Are there any contexts or states that we don't need anymore? let SignedUp = null; const [isSignUp, setIsSignUp] = useContext(SignUpContext); - const [isSignModal, setIsSignModal] = useContext(SignUpContext); - const [isOpened, setIsOpened] = useContext(UserContext); const [newUser, setNewUser] = useContext(NewUserContext); const [count, setCount] = useState(5); const [newUserData, setNewUserData] = useState(initialValues); const [errorMessage, setErrorMessage] = useState(""); + const dispatch = useDispatch() // Function to register a new user, post the data to backend if legit, and log user in afterwards const registerUser = async () => { httpClient({ method: "POST", - url: "http://127.0.0.1:5000/register", + url: "http://127.0.0.1:5000/user/register", data: { username: newUserData.userName, first_name: newUserData.firstName, @@ -46,7 +47,10 @@ const SignUp = () => { .then((response) => { console.log(response) console.log(response.data.access_token) + console.log(response.data.user) login(response.data.access_token) + let name = response.data.user + dispatch(setSignIn({name})) console.log(newUserData.userName, " has signed up") setNewUser(true) }).catch((error) => { @@ -61,18 +65,11 @@ const SignUp = () => { }) }; - - - const handleValues = (e) => { setNewUserData({ ...newUserData, [e.target.name]: e.target.value}); }; - - - // Added in the error checks that were missing (VARCHAR 50 for each name, 30 for username, 254 for email, 8 for password if it isn't there?) - // Might want to convert it to a switch case or rewrite it in a nicer JS looking way - // Add check for number in password + // @TODO Need to refactor this! Make the error handling more efficient const handleForm = (e) => { e.preventDefault(); @@ -87,28 +84,29 @@ const SignUp = () => { } else if (newUserData.password !== newUserData.confirmPSW){ setErrorMessage("Password does not match."); setNewUser(false); - } else if (newUserData.userName.length >= 30){ + } else if (newUserData.userName.length > 30){ setErrorMessage("Username must not be more than 30 characters."); setNewUser(false); - } else if (newUserData.firstName.length >= 50){ + } else if (newUserData.firstName.length > 50){ setErrorMessage("First name must be no more than 50 characters."); setNewUser(false); - } else if (newUserData.lastName.length >= 50){ + } else if (newUserData.lastName.length > 50){ setErrorMessage("Last name must be no more than 50 characters."); setNewUser(false); - } else if (newUserData.email.length >= 254){ + } else if (newUserData.email.length > 254){ setErrorMessage("Email address is too long."); setNewUser(false); - } else if (newUserData.password.length >= 72 || newUserData.password.length < 8){ - setErrorMessage("Password is too short or long."); // Don't give details on max length + } else if (newUserData.password.length > 72 || newUserData.password.length < 8){ + setErrorMessage("Password length is invalid."); setNewUser(false); } else { setErrorMessage(""); - // setNewUser(true); registerUser(); } } - console.log(newUserData.userName.length); + + // We can delete this right? The constant counting + // console.log(newUserData.userName.length); if (newUser === true){ setTimeout(() => { diff --git a/src/assets/icons/comments.png b/src/assets/icons/comments.png new file mode 100644 index 0000000..b2a0cbd Binary files /dev/null and b/src/assets/icons/comments.png differ diff --git a/src/assets/icons/message.png b/src/assets/icons/message.png new file mode 100644 index 0000000..724af34 Binary files /dev/null and b/src/assets/icons/message.png differ diff --git a/src/assets/img/TeamMembers/haiying.jpg b/src/assets/img/TeamMembers/haiying.jpg deleted file mode 100644 index 505ab59..0000000 Binary files a/src/assets/img/TeamMembers/haiying.jpg and /dev/null differ diff --git a/src/components/DropDownMenu/dropDownMenu.jsx b/src/components/DropDownMenu/dropDownMenu.jsx index d969f2c..001a9ce 100644 --- a/src/components/DropDownMenu/dropDownMenu.jsx +++ b/src/components/DropDownMenu/dropDownMenu.jsx @@ -3,20 +3,25 @@ import { NewUserContext } from "../../components/FinalProject/FinalProject" import { useContext } from 'react'; import { useAuth, logout } from "../../auth"; import httpClient from "../../httpClient"; +import { useDispatch } from "react-redux"; +import { setSignOut } from "../../redux/slices/userSlice" // set log out functionality here? const DropDownMenu = props => { const [newUser, setNewUser] = useContext(NewUserContext); + const dispatch = useDispatch() + // Function to log out the user function logMeOut() { httpClient({ method: "POST", - url: "http://localhost:5000/logout" + url: "http://localhost:5000/user/logout" }) .then((response) => { logout() + dispatch(setSignOut()) alert("You have successfully logged out") }).catch((error) => { if (error.response) { diff --git a/src/components/EditBanner/EditBanner.jsx b/src/components/EditBanner/EditBanner.jsx index 39a0771..ceb8539 100644 --- a/src/components/EditBanner/EditBanner.jsx +++ b/src/components/EditBanner/EditBanner.jsx @@ -2,17 +2,24 @@ import styles from "./EditBanner.module.css"; import profile from '../../assets/images/logos/user.png'; import { Fragment, useContext } from "react"; import { UserNameContext } from "../../components/FinalProject/FinalProject"; +import { useSelector } from "react-redux" +import { selectCurrentUser } from "../../redux/slices/userSlice" const EditBanner = () => { const [UserName, setUserName] = useContext(UserNameContext); + const user = useSelector(selectCurrentUser) + + console.log(user) + + const welcome = user ? `${user.name}` : "Guest" return (
profile. -

User2

+

{welcome}

) diff --git a/src/components/EditInput/EditInput.jsx b/src/components/EditInput/EditInput.jsx new file mode 100644 index 0000000..0ca35fc --- /dev/null +++ b/src/components/EditInput/EditInput.jsx @@ -0,0 +1,30 @@ +import styles from '../EditProfileDetails/EditProfileDetails.module.css'; +import { useState } from 'react'; + +const EditInput = ({ placeholder, value, editProfile, type, onChange }) => { + const [inputValid, setInputValid] = useState(false); + return ( + <> + + edit Password. { + setInputValid(!inputValid); + console.log(inputValid); + }} + > + + ); +}; + +export default EditInput; diff --git a/src/components/EditPosts/EditPosts.jsx b/src/components/EditPosts/EditPosts.jsx index 662adca..0c4b4db 100644 --- a/src/components/EditPosts/EditPosts.jsx +++ b/src/components/EditPosts/EditPosts.jsx @@ -1,33 +1,55 @@ -import styles from "./EditPosts.module.css"; +import styles from './EditPosts.module.css'; import profile from '../../assets/images/logos/user.png'; -const EditPosts = props => { - return ( -
-

Posts

- {props.data.map((post) => ( -
-
-
- profile. -
-
-
{post.post_author}
-
{post.post_category}
-
-
+const EditPosts = (props) => { + return ( +
+

Posts

+ {props.data.map((post) => ( +
+
+
+ profile. +
+
+
{post.post_author}
+
{post.post_category}
+
+
-

- {post.post_content} -

+

{post.post_content}

-

- {post.post_date} -

-
- ))} -
- ) -} +

+ {new Date(post.post_date).toLocaleDateString('en-GB', { + weekday: 'short', + day: 'numeric', + month: 'short', + year: 'numeric', + })} +

+

+ {new Date(post.post_date).toLocaleTimeString('en-GB', { + hour12: 'true', + })} +

+
+ comments +

0

+
+
+ ))} +
+ ); +}; export default EditPosts; diff --git a/src/components/EditPosts/EditPosts.module.css b/src/components/EditPosts/EditPosts.module.css index 7bd109d..5552d65 100644 --- a/src/components/EditPosts/EditPosts.module.css +++ b/src/components/EditPosts/EditPosts.module.css @@ -63,3 +63,10 @@ margin: 10px auto 30px; padding: 0 10px; } + +.commentContainer{ + display: flex; + flex-direction: row; + gap: 5px; + align-items: center; +} diff --git a/src/components/EditProfileDetails/EditProfileDetails.jsx b/src/components/EditProfileDetails/EditProfileDetails.jsx index 5097dde..3839c7d 100644 --- a/src/components/EditProfileDetails/EditProfileDetails.jsx +++ b/src/components/EditProfileDetails/EditProfileDetails.jsx @@ -1,86 +1,150 @@ -import styles from "./EditProfileDetails.module.css"; +import styles from './EditProfileDetails.module.css'; import editProfile from '../../assets/images/editProfile.svg'; +import React, { useEffect, useState } from 'react'; +import httpClient from '../../httpClient'; +import { useSelector } from 'react-redux'; +import { selectCurrentUser } from '../../redux/slices/userSlice'; +import EditInput from '../EditInput/EditInput'; + +// Have added an API call ish but no idea how to get it into the profile box or how to convert the profile box to view rather than edit, tried to copy recommendations.js +// Think would be cool to have the component as view only initially with an "edit" button, that then allows you to make a put or post request to edit certain fields const EditPosts = () => { - return ( -
-
- - - edit Password. -
-
- - - edit Password. -
-
- - - edit Password. -
-
- - - edit Password. -
-
- - - edit Password. -
-
- ) -} + const user = useSelector(selectCurrentUser); + const [userDetails, setUserDetails] = useState({ + first_name: null, + last_name: null, + email: null, + date_of_birth: null, + interests: null, + }); + + // let editable = true; + const [userValues, setUserValues] = useState({ + first_name: 'ANGEL', + last_name: '', + username: '', + email: '', + password: '' + }); + + const [List, setList] = useState([]); + const token = localStorage.getItem('REACT_TOKEN_AUTH_KEY') + console.log(token) + const [profile, setProfile] = useState(null); + + useEffect(() => { + const getAPI = async () => { + try { + const response = await httpClient.get("http://localhost:5000/user/current_user", {headers: {"Authorization": `Bearer ${JSON.parse(token)}`}}); + // console.log(response.data) + setUserDetails((ud) => (ud = response.data)); + // console.log(userDetails); + } catch(error) { + console.log(error) + } + }; + getAPI() + }, [user.name, token]); + + // useEffect(() => { + // const getAPI = async () => { + // try { + // const response = await httpClient.get( + // 'http://localhost:5000/user/members/' + user.name + // ); + // setUserDetails((ud) => (ud = response.data)); + // console.log(userDetails); + // } catch (error) { + // console.log(error); + // } + // }; + // getAPI(); + // }, [user.name]); + + return ( +
{ + e.preventDefault(); + alert('Changes submitted!'); + + return setUserValues({ + first_name: '', + last_name: '', + username: '', + email: '', + password: '', + }); + }} + onKeyDown={(e) => { + if (e.key === 'ENTER') return null; + }} + > +
+ + + setUserValues({ ...userValues, first_name: e.target.value }) + } + /> +
+
+ + + setUserValues({ ...userValues, last_name: e.target.value }) + } + /> +
+
+ + + setUserValues({ ...userValues, username: e.target.value }) + } + /> +
+
+ + + setUserValues({ ...userValues, email: e.target.value }) + } + /> +
+
+ + + setUserValues({ ...userValues, password: e.target.value }) + } + /> +
+ +
+ ); +}; + export default EditPosts; diff --git a/src/components/EditProfileDetails/EditProfileDetails.module.css b/src/components/EditProfileDetails/EditProfileDetails.module.css index e290732..5fd6329 100644 --- a/src/components/EditProfileDetails/EditProfileDetails.module.css +++ b/src/components/EditProfileDetails/EditProfileDetails.module.css @@ -1,74 +1,83 @@ .EditAccountform { - display: flex; - flex-direction: column; - gap: 10px; - padding: 25px; - min-width: 750px; - justify-content: center; - align-items: center; - background-color: #d9d9d9; - border-radius: 15px; - margin: 0 auto; + display: flex; + flex-direction: column; + gap: 10px; + padding: 25px; + min-width: 750px; + justify-content: center; + align-items: center; + background-color: #d9d9d9; + border-radius: 15px; + margin: 0 auto; } - .editInput { - display: flex; - padding: 8px; - border-radius: 10px; - text-align: center; - width: 60vh; - gap: 10px; + display: flex; + padding: 8px; + border-radius: 10px; + text-align: center; + width: 60vh; + gap: 10px; +} + +.editInputActive { + display: flex; + padding: 8px; + border-radius: 10px; + text-align: center; + width: 60vh; + gap: 10px; + border-color: rgb(0, 106, 255); } .formContainer { - display: flex; - flex-direction: column; - align-items: center; - margin: 0 auto; - padding-bottom: 30px; + display: flex; + flex-direction: column; + align-items: center; + margin: 0 auto; + padding-bottom: 30px; } .row { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 20px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 20px; } .imageButton { - background-color: none; - width: 5vh; + background-color: none; + width: 5vh; } .row label { - width: 120px; + width: 120px; } @media all and (max-width: 820px) { - .EditAccountform { - width: 93.6vw; - } + .EditAccountform { + width: 93.6vw; + } - .postsContainer { - margin: 90px auto 10px; - } + .postsContainer { + margin: 90px auto 10px; + } - .row label { - width: 180px; - } + .row label { + width: 180px; + } } -@media all and (min-width: 360px){ - .EditAccountform { - width: 80vw; - } +@media all and (min-width: 360px) { + .EditAccountform { + width: 80vw; + } } -@media all and (max-width: 360px){ - .EditAccountform { - width: 80vw; - margin-left: 10px; - } -} \ No newline at end of file +@media all and (max-width: 360px) { + .EditAccountform { + width: 80vw; + margin-left: 10px; + } +} diff --git a/src/components/FinalProject/FinalProject.jsx b/src/components/FinalProject/FinalProject.jsx index 62596ed..55f1e88 100644 --- a/src/components/FinalProject/FinalProject.jsx +++ b/src/components/FinalProject/FinalProject.jsx @@ -1,13 +1,15 @@ -import { Fragment, createContext, useState } from 'react'; +import { Fragment, createContext, useEffect, useState } from 'react'; import NavGraph from '../../navigation/NavGraph'; import AccountCreation from '../../components/accountCreation/accountCreation'; -import Recommendations from '../../pages/recommendations/recommendations'; +import styles from './FinalProject.module.css'; +import Card from '../../UI/Card/card'; +import Button from '../../UI/Button/button'; +// import { response } from 'express'; +import httpClient from '../../httpClient'; // export const UserContext = createContext(); export const SignUpContext = createContext(); export const NewUserContext = createContext(); -export const MobileNavContext = createContext(); -export const StyleMobileNavContext = createContext(); export const UserContext = createContext(); export const UserNameContext = createContext(); @@ -15,29 +17,144 @@ const FinalProject = () => { const [isOpened, setIsOpened] = useState(false); const [isSignModal, setIsSignModal] = useState(false); const [newUser, setNewUser] = useState(false); - const [isMobileClicked, setIsMobileClicked] = useState(false); - let [userName, setUserName] = useState("User"); - - let style2 = null - - return( - - - - - - - - - - - - - - + const [isChatRoom, setIsChatRoom] = useState(false); + const [users, setIsUsers] = useState([]); + const [otherUser, setOtherUser] = useState('UserName'); + const [userInput, setUserInput] = useState(); + const [messageInput, setMessageInput] = useState(); + const [usersList, setUserList] = useState([]); + let [userName, setUserName] = useState('User'); + let style2 = null; + const onChatRoom = () => { + setIsChatRoom(!isChatRoom); + }; + + useEffect(() => { + const getMembers = async () => { + try { + httpClient({ + method: 'GET', + url: 'http://localhost:5000/user/members', + }).then((response) => { + setUserList(response.data); + }); + } catch (error) { + console.log(error); + } + }; + getMembers(); + }, []); + + const handleSearchUser = (e) => { + console.log(e.target.value); + setUserInput(e.target.value); + + // Filter method not working will return to this later. + if (e.target.value.trim().length > 0) { + setIsUsers(usersList); + + const filtered = usersList.filter( + (user) => user.first_name.toLowerCase() === e.target.value.toLowerCase() + ); + setIsUsers(filtered); + } else { + setIsUsers(usersList); + } + }; + + return ( + + + + + + + + + + - - ) -} +
+ {isChatRoom ? ( + + +
    + {users.map((name) => ( +
  • { + setOtherUser(name.first_name); + setIsUsers([]); + setUserName(''); + setUserInput(''); + setMessageInput(''); + }} + > + {name.first_name} +
  • + ))} +
+

{otherUser}

+
+ +

Hello There!

+
+
+
+ +

Hi User hows it going?

+
+
+
+ { + setMessageInput(e.target.value); + }} + > +
+
+ ) : null} +
onChatRoom()} + > + message users +
+
+
+ ); +}; -export default FinalProject; \ No newline at end of file +export default FinalProject; diff --git a/src/components/FinalProject/FinalProject.module.css b/src/components/FinalProject/FinalProject.module.css new file mode 100644 index 0000000..cbe4d72 --- /dev/null +++ b/src/components/FinalProject/FinalProject.module.css @@ -0,0 +1,85 @@ +.chatBubble { + display: flex; + position: fixed; + height: 60px; + width: 60px; + background-color: rgb(135, 225, 0); + border-radius: 130px; + bottom: 30px; + right: 30px; + justify-content: center; + align-items: center; + box-shadow: 2px 5px 10px 5px rgba(0, 0, 0, 0.413); + border-color: rgb(0, 201, 0); + border-width: 3px; + border-style: solid; + cursor: pointer; +} + +.chatBubble:active { + background-color: rgb(0, 201, 0); +} + +.messageContainer { + position: fixed; + display: flex; + bottom: 30px; + right: 120px; + z-index: 1; +} + +.profileName { + margin: 0; + font-weight: 600; + font-size: 25px; +} + +.user1 { + color: #ffffff; + font-weight: 500; + margin-right: 50%; +} + +.user2 { + display: flex; + color: #ffffff; + font-weight: 500; + margin-left: 50%; +} + +.messageText { + margin: 0; + padding: 0; + text-align: left; + max-width: fit-content; +} + +.inputField { + padding: 10px; + border-radius: 7px; + width: 90%; +} + +.sendMessageContainer { + display: flex; + gap: 15px; + min-width: 100%; + justify-content: flex-start; +} + +.listUser { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; +} + +.userItem { + margin: 0; + padding: 10px; +} + +.userItem:hover { + background-color: rgba(0, 0, 0, 0.178); + cursor: pointer; +} diff --git a/src/components/ForumItem/ForumItem.jsx b/src/components/ForumItem/ForumItem.jsx index 1ca6942..b008a44 100644 --- a/src/components/ForumItem/ForumItem.jsx +++ b/src/components/ForumItem/ForumItem.jsx @@ -1,22 +1,55 @@ -import styles from "./ForumItem.module.css"; +import styles from './ForumItem.module.css'; -const ForumItem = props => { - let profile = null; - if (props.icon !== ""){ - profile = Icon - } else { - profile = Icon - } +const ForumItem = (props) => { + let profile = null; + let comments = null; + if (props.icon !== '') { + profile = ( + Icon + ); + } else { + profile = ( + Icon + ); + } - return( -
- {profile} -
-

{props.title}

-

{props.userName}

-
-
- ) -} + if (!props.isComments) { + comments = null; + } else { + comments = ( +
+ comments +

0

+
+ ); + } -export default ForumItem; \ No newline at end of file + return ( +
+ {profile} +
+

{props.title}

+

{props.userName}

+ {comments} +

{props.date}

+
+
+ ); +}; + +export default ForumItem; diff --git a/src/components/ForumItem/ForumItem.module.css b/src/components/ForumItem/ForumItem.module.css index 143d8c5..78dbbc6 100644 --- a/src/components/ForumItem/ForumItem.module.css +++ b/src/components/ForumItem/ForumItem.module.css @@ -7,6 +7,7 @@ border-radius: 10px; align-items: center; flex-direction: row; + cursor: pointer; } .Textcolumn{ @@ -53,16 +54,27 @@ width: 60px; background-color: grey; border-radius: 10px; - /* margin-right: 25px; */ border: 4px #4C4B4B solid; } .profilePicture{ display: flex; - /* width: 50px; */ height: 50px; } +.commentContainer{ + display: flex; + flex-direction: row; + gap: 5px; + align-items: center; +} + +.comment{ + width: 35px; + height: 35px; + margin: 0; +} + @media all and (min-width: 360px) { .row{ width: 96%; diff --git a/src/components/MobileNav/MobileNav.jsx b/src/components/MobileNav/MobileNav.jsx deleted file mode 100644 index 1b53ba3..0000000 --- a/src/components/MobileNav/MobileNav.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import styles from "./MobileNav.module.css"; -import { Link } from "react-router-dom"; -import { NewUserContext, MobileNavContext, StyleMobileNavContext, UserContext} from "../../components/FinalProject/FinalProject"; -import { useContext, useState } from "react"; - - -// Angel, can you update this one to match the login requirements of the other navbar too? Although I don't think mobile functionality is a prio for submission -const MobileNav = () => { - const [newUser, setNewUser] = useContext(NewUserContext); - const [isMobileClicked, setIsMobileClicked] = useContext(MobileNavContext); - const [isOpened, setIsOpened] = useContext(UserContext); - - let style2 = StyleMobileNavContext; - - if (!isMobileClicked){ - style2 = {display: 'none'}; - } else { - style2 = {display: 'block'}; - } - - return( -
- {newUser ? -
-
- setIsMobileClicked(false)}>Home -
-
- setIsMobileClicked(false)}>Recommendations -
-
- setIsMobileClicked(false)}>About -
-
- setIsMobileClicked(false)}>Forums -
-
- setIsMobileClicked(false)}>Profile -
-
- { - setNewUser(false) - setIsMobileClicked(false)}} - >Sign Out -
-
: -
-
- setIsMobileClicked(false)}>Home -
-
- setIsMobileClicked(false)}>About -
- {/* Will implement tomorrow. */} -
- { - setIsOpened(true); - setIsMobileClicked(false)}} - >Sign in -
-
- } -
- ) -} - -export default MobileNav \ No newline at end of file diff --git a/src/components/MobileNav/MobileNav.module.css b/src/components/MobileNav/MobileNav.module.css deleted file mode 100644 index d1bb57e..0000000 --- a/src/components/MobileNav/MobileNav.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.MobileNav{ - display: none; -} - -@media all and (max-width: 875px){ - .MobileNav{ - position: absolute; - z-index: 1; - top: 21.3vh; - justify-content: center; - width: 100%; - } - .navItemsMobile{ - display: flex; - flex-direction: column; - background-color: #353be552; - width: 100%; - } - - .navItemMobile{ - font-size: 20px; - padding-top: 5px; - padding-bottom: 5px; - font-weight: 500; - width: 100%; - text-align: center; - margin: 0 auto; - } - - .navItemMobile:hover{ - background-color: #2d32c052; - } - - .navLink{ - color: white; - text-decoration: none; - } -} \ No newline at end of file diff --git a/src/components/NavBar/nav.module.css b/src/components/NavBar/nav.module.css index 986430c..4c18efe 100644 --- a/src/components/NavBar/nav.module.css +++ b/src/components/NavBar/nav.module.css @@ -1,176 +1,175 @@ -.navbar{ - display: flex; - position: absolute; - background-color: #353be552; - padding-top: 15px; - padding-bottom: 15px; - margin: 0; - width: 100%; - justify-content: space-between; - align-items: center; - z-index: 1; +.navbar { + display: flex; + position: absolute; + background-color: #353be552; + padding-top: 15px; + padding-bottom: 15px; + margin: 0; + width: 100%; + justify-content: space-between; + align-items: center; + z-index: 1; } .navbar h1 { - display: flex; - align-items: center; - margin-left: 5px; - color: white; - text-shadow: 4px 4px 4px #0094FF56; - font-weight: 800; - font-size: 55px; + display: flex; + align-items: center; + margin-left: 5px; + color: white; + text-shadow: 4px 4px 4px #0094ff56; + font-weight: 800; + font-size: 55px; } -.navItems{ - display: flex; - gap: 10px; - justify-content: flex-end; - /* width: 71%; */ - margin: 0; - padding: 0; +.navItems { + display: flex; + gap: 10px; + justify-content: flex-end; + /* width: 71%; */ + margin: 0; + padding: 0; +} + +.navItem { + font-size: 25px; + color: white; + font-weight: 400; + /* padding: 5px; */ } -.navItem{ - font-size: 25px; - color: white; - font-weight: 400; - /* padding: 5px; */ +.navItem:hover { + border: white 2px solid; } -.navItem:hover{ - border: white 2px solid; +.MobileMenu { + display: none; } -.MobileMenu{ +.profile { + display: flex; + justify-content: center; + align-items: center; + height: 60px; + width: 60px; + background-color: grey; + border-radius: 10px; + /* margin-right: 25px; */ + border: 4px #4c4b4b solid; +} + +.profilePicture { + display: flex; + /* width: 50px; */ + height: 50px; +} + +.profileRow { + position: relative; + display: flex; + flex-direction: row; + /* margin-right: 5px; */ + align-items: center; + justify-content: center; +} + +.navHeadingTitles { + font-size: 5vh; + font-weight: 400; + padding: 0; + margin: 0; +} + +.link { + text-decoration: none; +} + +.logo { + height: 150px; + border-radius: 90px; + margin-left: 20px; +} +@media all and (max-width: 900px) { + .MobileMenu { display: none; + height: 50px; + width: 50px; + } + + .navItems { + overflow-x: hidden; + width: 65%; + } + + .navbar h1 { + margin-left: 0; + font-size: 35px; + } } -.profile{ +@media all and (max-width: 872px) { + .MobileMenu { + display: block; + } + + .menu { display: flex; - justify-content: center; + flex-direction: row; align-items: center; - height: 60px; - width: 60px; - background-color: grey; - border-radius: 10px; - /* margin-right: 25px; */ - border: 4px #4C4B4B solid; + gap: 65vw; + } + + .navItems { + display: none; + overflow-x: hidden; + } + + .navbar h1 { + margin-left: 15px; + font-size: 35px; + } } -.profilePicture{ +@media all and (max-width: 800px) { + .MobileMenu { + display: block; + } + + .menu { display: flex; - /* width: 50px; */ - height: 50px; -} + flex-direction: row; + gap: 65vw; + } + + .navItems { + display: none; + overflow-x: hidden; + } -.profileRow{ - position: relative; + .navbar h1 { + margin-left: 15px; + font-size: 35px; + } +} +@media all and (max-width: 700px) { + .menu { display: flex; flex-direction: row; - /* margin-right: 5px; */ - align-items: center; - justify-content: center; + gap: 50vw; + } } -.navHeadingTitles{ - font-size: 5vh; - font-weight: 400; - padding: 0; - margin: 0; +@media all and (max-width: 500px) { + .menu { + display: flex; + flex-direction: row; + gap: 38vw; } +} -.link{ - text-decoration: none; -} - -.logo{ - height: 150px; - border-radius: 90px; - margin-left: 20px; -} -@media all and (max-width: 900px){ - .MobileMenu{ - display: none; - height: 50px; - width: 50px; - } - - .navItems{ - overflow-x: hidden; - width: 65%; - } - - .navbar h1 { - margin-left: 0; - font-size: 35px; - } -} - -@media all and (max-width: 872px){ - .MobileMenu{ - display: block; - } - - .menu{ - display: flex; - flex-direction: row; - align-items: center; - gap: 65vw; - } - - .navItems{ - display: none; - overflow-x: hidden; - } - - .navbar h1 { - margin-left: 15px; - font-size: 35px; - } -} - -@media all and (max-width: 800px){ - .MobileMenu{ - display: block; - } - - .menu{ - display: flex; - flex-direction: row; - gap: 65vw; - } - - .navItems{ - display: none; - overflow-x: hidden; - } - - .navbar h1 { - margin-left: 15px; - font-size: 35px; - } -} - -@media all and (max-width: 700px){ - .menu{ - display: flex; - flex-direction: row; - gap: 50vw; - } -} - -@media all and (max-width: 500px){ - .menu{ - display: flex; - flex-direction: row; - gap: 38vw; - } -} - -@media all and (max-width: 360px){ - .menu{ - display: flex; - flex-direction: row; - gap: 30vw; - } -} \ No newline at end of file +@media all and (max-width: 360px) { + .menu { + display: flex; + flex-direction: row; + gap: 30vw; + } +} diff --git a/src/components/NavBar/navbar.jsx b/src/components/NavBar/navbar.jsx index 131b364..9d2a569 100644 --- a/src/components/NavBar/navbar.jsx +++ b/src/components/NavBar/navbar.jsx @@ -1,90 +1,131 @@ -import styles from "./nav.module.css"; -import Button from "../../UI/Button/button"; -import profile from "../../assets/images/logos/user.png"; -import profileDropArrow from "../../assets/images/profileArrow.svg"; -import { Link } from "react-router-dom"; -import logo from "../../logo.png"; - -import Menu from "../../assets/images/logos/menu.svg"; -import { useAuth } from "../../auth"; -import { UserContext, NewUserContext, SignUpContext, MobileNavContext } from "../../components/FinalProject/FinalProject"; -import { useContext, useState, useEffect } from "react"; - - +import styles from './nav.module.css'; +import Button from '../../UI/Button/button'; +import profile from '../../assets/images/logos/user.png'; +import profileDropArrow from '../../assets/images/profileArrow.svg'; +import { Link } from 'react-router-dom'; +import logo from '../../logo.png'; +import Menu from '../../assets/images/logos/menu.svg'; +import { useAuth } from '../../auth'; +import { + UserContext, + NewUserContext, + SignUpContext, +} from '../../components/FinalProject/FinalProject'; +import { useContext } from 'react'; function NavBar(props) { - const [logged]=useAuth(); - const [isOpened, setIsOpened] = useContext(UserContext); + const [logged] = useAuth(); + const [isOpened, setIsOpened] = useContext(UserContext); // const [newUser, setNewUser] = useContext(NewUserContext); // const [isSignUp, setIsSignUp] = useContext(SignUpContext); - const [isMobileClicked, setIsMobileClicked] = useContext(MobileNavContext); - // Links that logged in user sees const LoggedInLinks = () => { return ( - <> -
-

Home

-

Recommendations

-

Forums

-

About

- {/*

Sign Out

*/} -
+ <> +
+ +

Home

+ + +

Recommendations

+ + +

Forums

+ + +

About

+ + {/*

Sign Out

*/} +
- profile. -
- drop arrow. props.onChangePressed(!props.isPressed)}/> + profile.
-
- - - ) - } + drop arrow. props.onChangePressed(!props.isPressed)} + /> +
+
+ + ); + }; // Links that logged out user sees const LoggedOutLinks = () => { return ( <>
-

Home

-

About

- + +

Home

+ + +

About

+ +
- ) - } + ); + }; // Rendering the nav bar depending on if user is logged in or not - return( + return ( -) + ); } export default NavBar; - - // } - - - - - // const NavBar = () => { // useEffect(() => { // // skip initial render @@ -111,25 +152,24 @@ export default NavBar; // } // export default NavBar; -// /* +// /* // /*

IntroVerse

*/ // /* { -// // isSignedin +// // isSignedin // ? //
//

Home

//

About

//

Forums

-//
+//
// : //
//

Home

//

About

//
- -// } */ +// } */ // // With drop down menu // // const LoggedInLinks = props => { @@ -144,14 +184,14 @@ export default NavBar; // //
// // profile. // //
-// // drop arrow. props.onChangePressed(!props.isPressed)}/> // // // // // // // // ) -// // } \ No newline at end of file +// // } diff --git a/src/components/landing/Landing.module.css b/src/components/landing/Landing.module.css index b23b3c6..20e506b 100644 --- a/src/components/landing/Landing.module.css +++ b/src/components/landing/Landing.module.css @@ -1,184 +1,184 @@ -.main{ - display: flex; - min-height: 50.9vw; - background-image: url("../../assets/images/backgrounds/background.svg"); - background-size: cover; - background-position: center; - background-repeat: no-repeat; +.main { + display: flex; + min-height: 50.9vw; + background-image: url('../../assets/images/backgrounds/background.svg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; } -.rowHome{ - display: flex; - position: relative; - bottom: 30px; - flex-direction: row; - align-items: flex-end; - gap: 30vh; +.rowHome { + display: flex; + position: relative; + bottom: 30px; + flex-direction: row; + justify-content: space-between; + align-items: flex-end; + width: 50%; } -.containerHeading{ - display: flex; - flex-direction: column; - color: white; +.containerHeading { + display: flex; + flex-direction: column; + color: white; } -.textHeading{ - position: relative; - margin-left: 30px; - width: 10px; - text-shadow: 5px 5px 5px rgba(0, 0, 0, 0.572); - z-index: 1; +.textHeading { + position: relative; + margin-left: 30px; + width: 10px; + text-shadow: 5px 5px 5px rgba(0, 0, 0, 0.572); + z-index: 1; } -.dropArrow{ - height: 40px; - margin-left: 60vh; - margin-bottom: 0; - animation-name: drop-Arrow; - animation-duration: 1.3s; - animation-iteration-count: infinite; +.dropArrow { + display: flex; + height: 40px; + animation-name: drop-Arrow; + animation-duration: 1.3s; + animation-iteration-count: infinite; } @keyframes drop-Arrow { - from {margin-bottom: 15px;} - to {margin-bottom: 0;} + from { + margin-bottom: 15px; + } + to { + margin-bottom: 0; + } } @media all and (min-width: 1000px) { - .main{ - height: 100vh; + .main { + height: 100vh; + } - } - - .rowHome{ - gap: 20vh; - } + .rowHome { + gap: 20vh; + } } @media all and (min-width: 900px) { - .main{ - height: 100vh; - - } - - .textHeading{ - font-size: 7vh; - } - - .rowHome{ - gap: 40vh; - } - - .dropArrow{ - margin-left: 0; - } - - .container0{ - min-height: 30vh; - gap: 1vh; - } - - .container1{ - min-height: 30vh; - gap: 1vh; - } + .main { + height: 100vh; + } + + .textHeading { + font-size: 7vh; + } + + .rowHome { + gap: 40vh; + } + /* + .dropArrow { + margin-left: 0; + } */ + + .container0 { + min-height: 30vh; + gap: 1vh; + } + + .container1 { + min-height: 30vh; + gap: 1vh; + } } - @media all and (min-width: 800px) { - .main{ - height: 100vh; - - } - - .textHeading{ - font-size: 7vh; - } - - .rowHome{ - gap: 40vh; - } - - .dropArrow{ - margin-left: 0; - } - - .container0{ - min-height: 30vh; - gap: 1vh; - padding-bottom: 15px; - } - - .container1{ - min-height: 30vh; - gap: 1vh; - padding-bottom: 15px; - } + .main { + height: 100vh; + } + + .textHeading { + font-size: 7vh; + } + + .rowHome { + gap: 40vh; + } + + .dropArrow { + margin-left: 0; + } + + .container0 { + min-height: 30vh; + gap: 1vh; + padding-bottom: 15px; + } + + .container1 { + min-height: 30vh; + gap: 1vh; + padding-bottom: 15px; + } } @media all and (max-width: 500px) { - .main{ - height: 100vh; - } + .main { + height: 100vh; + } - .textHeading{ - font-size: 6vh; - margin-left: 10px; - } + .textHeading { + font-size: 6vh; + margin-left: 10px; + } - .rowHome{ - gap: 35vh; - } + .rowHome { + gap: 35vh; + } - .dropArrow{ - margin-left: 0; - } + /* .dropArrow { + margin-left: 0; + } */ - .container0{ - flex-direction: row; - min-height: 30vh; - gap: 1vh; - } + .container0 { + flex-direction: row; + min-height: 30vh; + gap: 1vh; + } - .container1{ - flex-direction: column; - min-height: 30vh; - gap: 1vh; - } + .container1 { + flex-direction: column; + min-height: 30vh; + gap: 1vh; + } } @media all and (min-width: 400px) { - .main{ - height: 100vh; - } + .main { + height: 100vh; + } - .textHeading{ - font-size: 6vh; - } + .textHeading { + font-size: 6vh; + } - .rowHome{ - gap: 31vh; - } + .rowHome { + gap: 31vh; + } - .dropArrow{ - margin-left: 0; - } + /* .dropArrow { + margin-left: 0; + } */ - .container0{ - flex-direction: column; - min-height: 30vh; - gap: 1vh; - } + .container0 { + flex-direction: column; + min-height: 30vh; + gap: 1vh; + } - .container1{ - flex-direction: column; - min-height: 30vh; - gap: 1vh; - } + .container1 { + flex-direction: column; + min-height: 30vh; + gap: 1vh; + } - .MobileNav{ - display: none; - } + .MobileNav { + display: none; + } } /* @media all and (max-width: 800px) { @@ -217,29 +217,29 @@ } */ @media all and (max-width: 360px) { - .main{ - height: 100vh; - } - - .textHeading{ - font-size: 5vh; - } - - .rowHome{ - gap: 28vh; - } - - .dropArrow{ - margin-left: 0; - } - - .container0{ - min-height: 30vh; - gap: 1vh; - } - - .container1{ - min-height: 30vh; - gap: 1vh; - } -} \ No newline at end of file + .main { + height: 100vh; + } + + .textHeading { + font-size: 5vh; + } + + .rowHome { + gap: 28vh; + } + /* + .dropArrow { + margin-left: 0; + } */ + + .container0 { + min-height: 30vh; + gap: 1vh; + } + + .container1 { + min-height: 30vh; + gap: 1vh; + } +} diff --git a/src/components/landing/landing.jsx b/src/components/landing/landing.jsx index b975659..0360ec5 100644 --- a/src/components/landing/landing.jsx +++ b/src/components/landing/landing.jsx @@ -1,17 +1,21 @@ -import styles from "./Landing.module.css" -import dropArrow from "../../assets/images/logos/drop_Icon.svg" +import styles from './Landing.module.css'; +import dropArrow from '../../assets/images/logos/drop_Icon.svg'; const Landing = () => { - return( + return (
-
+
-

Find your community

-
- Scroll down for more information +

Find your community

+ Scroll down for more information +
- ) -} + ); +}; -export default Landing; \ No newline at end of file +export default Landing; diff --git a/src/index.js b/src/index.js index 17d0e53..9e16e20 100644 --- a/src/index.js +++ b/src/index.js @@ -2,13 +2,22 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; +import store from "./redux/store"; +import { Provider } from 'react-redux'; +import { PersistGate } from "redux-persist/integration/react"; +import { persistStore } from 'redux-persist'; import reportWebVitals from './reportWebVitals'; +let persistor = persistStore(store); const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + + + + + ); diff --git a/src/pages/about/about.jsx b/src/pages/about/about.jsx index 0ff5834..b395e20 100644 --- a/src/pages/about/about.jsx +++ b/src/pages/about/about.jsx @@ -2,70 +2,83 @@ import { Fragment, useState } from 'react'; import styles from './about.module.css'; import Angel from '../../assets/img/TeamMembers/angel.jpg'; import Katherine from '../../assets/img/TeamMembers/katherine.png'; -import haiying from '../../assets/img/TeamMembers/haiying.jpg'; import Katalin from '../../assets/img/TeamMembers/katalin.png'; import Abbie from '../../assets/img/TeamMembers/abbie.jpg'; import TeamMemeber from '../../components/TeamMember/teamMember'; import NavBar from '../../components/NavBar/navbar'; import DropDownMenu from '../../components/DropDownMenu/dropDownMenu'; -import MobileNav from '../../components/MobileNav/MobileNav'; import Charities from '../../components/Charities/Charities'; export default function About() { - const [isPressed, setIsPressed] = useState(false); + const [isPressed, setIsPressed] = useState(false); - return ( - - - - -
-
-

About

-

Our Mission

-

At Introverse, we are dedicated to nurturing the mental well-being of introverts who cherish the worlds of anime, gaming and literature. We understand that finding a community where you can truly belong and express your passions can be a transformative experience. Our mission is to create an inviting digital haven where introverts can connect, share, and engage in meaningful conversations about anime, games and reading. -We strive to offer a platform that resonates with the unique needs and preferences of our audience, providing a space that feels like home. Here, every voice is valued, every interest is celebrated, and every individual is encouraged to embrace their love for anime, literature and gaming. Our commitment extends beyond creating a community; it's about fostering an environment where introverts can flourish, find like-minded friends, and feel empowered to explore their passions without any reservations. -In this journey, we are not just building a website; we are crafting a sanctuary where the beauty of solitude meets the warmth of togetherness, all centred around the shared love for anime, games and books.

-

The Team!

-
- + return ( + + + +
+
+

About

+

Our Mission

+

+ At Introverse, we are dedicated to nurturing the mental well-being + of introverts who cherish the worlds of anime, gaming and + literature. We understand that finding a community where you can + truly belong and express your passions can be a transformative + experience. Our mission is to create an inviting digital haven where + introverts can connect, share, and engage in meaningful + conversations about anime, games and reading. We strive to offer a + platform that resonates with the unique needs and preferences of our + audience, providing a space that feels like home. Here, every voice + is valued, every interest is celebrated, and every individual is + encouraged to embrace their love for anime, literature and gaming. + Our commitment extends beyond creating a community; it's about + fostering an environment where introverts can flourish, find + like-minded friends, and feel empowered to explore their passions + without any reservations. In this journey, we are not just building + a website; we are crafting a sanctuary where the beauty of solitude + meets the warmth of togetherness, all centred around the shared love + for anime, games and books. +

+

The Team!

+
+ - + - + - - - -
- -
-
-
- ); + +
+ +
+
+
+ ); } diff --git a/src/pages/about/about.module.css b/src/pages/about/about.module.css index d301dc0..c7f17a7 100644 --- a/src/pages/about/about.module.css +++ b/src/pages/about/about.module.css @@ -1,18 +1,17 @@ .about { - display: flex; - flex-wrap: wrap; - gap: 20px; - margin: 50px 0; - justify-content: center; - align-items: start; + display: flex; + flex-wrap: wrap; + gap: 20px; + justify-content: center; + align-items: start; } .content { - margin: 0 auto; - padding: 20px; - padding-top: 110px; + margin: 0 auto; + padding: 20px; + padding-top: 50px; } .paragraph { - text-align: left; + text-align: left; } diff --git a/src/pages/editAccount/editAccount.jsx b/src/pages/editAccount/editAccount.jsx index 48bffb8..96cd1d2 100644 --- a/src/pages/editAccount/editAccount.jsx +++ b/src/pages/editAccount/editAccount.jsx @@ -2,68 +2,86 @@ import { Fragment, useState, React, useEffect, useContext } from 'react'; import styles from './editAccount.module.css'; import NavBar from '../../components/NavBar/navbar'; import DropDownMenu from '../../components/DropDownMenu/dropDownMenu'; -import MobileNav from '../../components/MobileNav/MobileNav'; -import EditPosts from "../../components/EditPosts/EditPosts"; -import EditBanner from "../../components/EditBanner/EditBanner"; -import EditDetailsProfile from "../../components/EditProfileDetails/EditProfileDetails"; -import { UserNameContext } from "../../components/FinalProject/FinalProject"; +import EditPosts from '../../components/EditPosts/EditPosts'; +import EditBanner from '../../components/EditBanner/EditBanner'; +import EditDetailsProfile from '../../components/EditProfileDetails/EditProfileDetails'; +import httpClient from '../../httpClient'; +import { UserNameContext } from '../../components/FinalProject/FinalProject'; +import { useSelector } from 'react-redux'; +import { selectCurrentUser } from '../../redux/slices/userSlice'; -import axios from 'axios'; const EditAccount = () => { - const [isPressed, setIsPressed] = useState(false); - const [posts, setPosts] = useState(null); - const [filteredposts, setfilteredposts] = useState(null); - const [UserName, setUserName] = useContext(UserNameContext); + const [isPressed, setIsPressed] = useState(false); + const [posts, setPosts] = useState(null); + const [filteredposts, setfilteredposts] = useState(null); - console.log(UserName) + // Think we can replace the useContext with the useSelector + const [UserName, setUserName] = useContext(UserNameContext); - let error = null; - let user = "BlaxeXD" - let filteredList = null; + const user = useSelector(selectCurrentUser); - useEffect(() => { - const getForms = async () => { - const res = await axios.get("http://localhost:5000/forum") - .then(res => { - // APIres = res.data - setPosts(res.data) - filteredList = res.data.filter((list) => list.post_author === "User2") - console.log(filteredList); - setfilteredposts(filteredList); - if(filteredList.length === 0){ - error =

No Posts Yet.

- setPosts(null) - } - }) - .catch(err => { - setPosts(null); - // APIres = null; - console.log(err); - error =

No Posts Yet.

}); - } - getForms(); - // getAPI(); - }, []) + console.log(UserName); + let error = null; + let filteredList = null; - return ( - - - - -
- - {posts !== null? - - : -
-

No Posts Yet.

-
- } - {/* */} -
-
- ); + /* + To do + - Change filtered list to use the user's username in the url, doesn't work on frontend the moment not sure why, if it is because of the filtering + - ("http://localhost:5000/forum/author/" + user.name) + - Have uncommented EditDetailsProfile component, will add tasks related to that there + */ + + useEffect(() => { + const getForms = async () => { + const res = await httpClient + .get('http://localhost:5000/forum/all') + .then((res) => { + setPosts(res.data); + filteredList = res.data.filter( + (list) => list.post_author === user.name + ); + console.log(filteredList); + setfilteredposts(filteredList); + if (filteredList.length === 0) { + error =

No Posts Yet.

; + setPosts(null); + } + }) + .catch((err) => { + setPosts(null); + console.log(err); + error =

No Posts Yet.

; + }); + }; + getForms(); + }, []); + + console.log(filteredList); + + return ( + + + +
+ + {posts !== null ? ( + + ) : ( +
+

No Posts Yet.

+
+ )} + +
+
+ ); }; export default EditAccount; diff --git a/src/pages/editAccount/editAccount.module.css b/src/pages/editAccount/editAccount.module.css index d5630ae..f90f29f 100644 --- a/src/pages/editAccount/editAccount.module.css +++ b/src/pages/editAccount/editAccount.module.css @@ -1,13 +1,13 @@ .main { - background-color: #f0f0f0; - position: relative; - height: 100%; + background-color: #f0f0f0; + position: relative; + min-height: 100vh; } -.NoPosts{ - display: flex; - margin-top: 70px; - /* height: 200px; */ - justify-content: center; - align-items: center; -} \ No newline at end of file +.NoPosts { + display: flex; + margin-top: 70px; + /* height: 200px; */ + justify-content: center; + align-items: center; +} diff --git a/src/pages/forum/forum.jsx b/src/pages/forum/forum.jsx index 609802f..f179f02 100644 --- a/src/pages/forum/forum.jsx +++ b/src/pages/forum/forum.jsx @@ -1,89 +1,107 @@ -import { Fragment, useState, useEffect } from 'react' +import { Fragment, useState, useEffect } from 'react'; import NavBar from '../../components/NavBar/navbar'; -import styles from "./forum.module.css"; -import Card from "../../UI/Card/card" -import ForumItem from "../../components/ForumItem/ForumItem"; -import introduce from "../../assets/images/logos/introduce.png" -import anime from "../../assets/images/logos/anime.png" -import gaming from "../../assets/images/logos/joystick.png" -import books from "../../assets/images/logos/books_3771417.png" -import manga from "../../assets/images/logos/manga.png" -import Button from "../../UI/Button/button"; +import styles from './forum.module.css'; +import Card from '../../UI/Card/card'; +import ForumItem from '../../components/ForumItem/ForumItem'; +import introduce from '../../assets/images/logos/introduce.png'; +import anime from '../../assets/images/logos/anime.png'; +import gaming from '../../assets/images/logos/joystick.png'; +import books from '../../assets/images/logos/books_3771417.png'; +import manga from '../../assets/images/logos/manga.png'; +import Button from '../../UI/Button/button'; import { Link } from 'react-router-dom'; -import DropDownMenu from "../../components/DropDownMenu/dropDownMenu"; -import MobileNav from '../../components/MobileNav/MobileNav'; +import DropDownMenu from '../../components/DropDownMenu/dropDownMenu'; import httpClient from '../../httpClient'; // import api from "../../jsonAPI/posts.json"; -import API from '../../httpClient'; import axios from 'axios'; -import { create } from 'react-test-renderer'; +import { useSelector } from 'react-redux'; +import { selectCurrentUser } from '../../redux/slices/userSlice'; -export default function Forum() { - const [isClicked, setIsClicked] = useState(false); - const [postContent, setPostContent] = useState(""); - const [isPost, setIsPost] = useState(false); - const [title, setTitle] = useState(""); - const [loaded, setIsLoaded] = useState(false); - const [newPost, setIsNewPost] = useState(); - const [isPressed, setIsPressed] = useState(false); - const [posts, setPosts] = useState(""); - const [filteredposts, setfilteredposts] = useState([]); - const currDate = new Date().toISOString().slice(0, 10); - let postID = null; +export default function Forum() { + const [isClicked, setIsClicked] = useState(false); + const [postContent, setPostContent] = useState(''); + const [isPost, setIsPost] = useState(false); + const [title, setTitle] = useState(''); + const [loaded, setIsLoaded] = useState(false); + const [newPost, setIsNewPost] = useState(); + const [endpoint, setEndPoint] = useState(''); + const [isPressed, setIsPressed] = useState(false); + const [posts, setPosts] = useState([]); + // const [filteredposts, setfilteredposts] = useState([]); + // const currDate = new Date().toLocaleString('en-UK', { hour12: true }); Date and Time. + // const currDate = new Date().toLocaleDateString('en-GB'); + // let endpoint = ''; + const user = useSelector(selectCurrentUser); - let error = ''; - let APIres = []; + let postID = null; - const list = [ - {icon: introduce, title: "Introduce"}, - {icon: anime, title: "Anime"}, - {icon: gaming, title: "Games"}, - {icon: books, title: "Books"}, - {icon: manga, title: "Manga"} - ] + let error = ''; + // let APIres = []; - const forumHandler = (category) => { - setTitle(category.title) - setIsClicked(true) - console.log("Clicked"); - console.log(category) - filterArray(category.title); - } + let timeStamp = new Date().toISOString().slice(0, 19).replace('T', ' '); - const submitPost = async () => { - httpClient({ - method: "POST", - url: "http://127.0.0.1:5000/forum", - data: { - post_id: Number(postID + 1), - post_content: postContent, - post_category: title, - post_author: "User2", - post_date: currDate - } - }) + const list = [ + { icon: introduce, title: 'Introduce' }, + { icon: anime, title: 'Anime' }, + { icon: gaming, title: 'Games' }, + { icon: books, title: 'Books' }, + { icon: manga, title: 'Manga' }, + ]; + + const forumHandler = (category) => { + setTitle(category.title); + setIsClicked(true); + // console.log("Clicked"); + // console.log(category); + setEndPoint(category.title); + console.log('endpoint: ' + endpoint); + // filterArray(category.title); + }; + + const token = localStorage.getItem('REACT_TOKEN_AUTH_KEY'); + console.log(token); + + const submitPost = async () => { + httpClient({ + method: 'POST', + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${JSON.parse(token)}`, + }, + url: 'http://127.0.0.1:5000/forum/all', + data: { + post_id: Number(postID + 1), + post_content: postContent, + post_category: title, + post_author: user.name, + post_date: timeStamp, + }, + }) .then((response) => { - console.log(response) - setIsNewPost(!newPost) - setfilteredposts([...filteredposts, response]) - filterArray(title); - console.log(filteredposts) - alert('Post Added!, \n please refresh browser.') - }).catch((error) => { + console.log(response); + setIsNewPost(!newPost); + // setfilteredposts([...filteredposts, response]); + // filterArray(title); + // console.log(filteredposts) + alert('Post Added!'); + }) + .catch((error) => { if (error.response) { - console.log(error.response) - console.log(error.response.status) - console.log(error.response.headers) - setIsNewPost(false) + // console.log(error.response) + // console.log(error.response.status) + // console.log(error.response.headers) + setIsNewPost(false); if (error.response.status === 401) { - alert("Invalid Post"); + alert('Invalid Post'); } } - }) - } + }); + }; + + console.log('date: ' + timeStamp); + + // console.log(posts) - console.log(posts) - // Post request needs to be implemented const createPost = () => { setIsPost(true); @@ -91,142 +109,183 @@ export default function Forum() { const postHandler = (e) => { setPostContent(e.target.value); - } - + }; - useEffect(() => { - console.log(newPost); - console.log(posts); - const getForms = async () => { - const res = await axios.get("http://localhost:5000/forum") - .then(res => { - APIres = res.data - setPosts(res.data) - if (res.data.post_id < postID){ - filterArray(title) - } + useEffect(() => { + // console.log(newPost); + // console.log(posts); + const getForms = async (ep) => { + const res = await httpClient + .get(`http://localhost:5000/forum/category/${ep}`) + .then((res) => { + // APIres = res.data; + setPosts(res.data); + console.log('api response: ' + posts); }) - .catch(err => { - setPosts(null); + .catch((err) => { + setPosts([]); console.log(err); - error =

No Posts Yet.

}); + error =

No Posts Yet.

; + }); + }; + if (endpoint !== '') { + getForms(endpoint); } - getForms(); - },[newPost]) + }, [endpoint, newPost]); - const filterArray = (category) => { - console.log(category); - postID = (posts[posts.length - 1].post_id); - let filterList = posts.filter((list) => list.post_category === category) - setfilteredposts(filterList); - console.log(posts); - } + // const filterArray = (category) => { + // console.log(category); + // postID = posts[posts.length - 1].post_id; + // let filterList = posts.filter((list) => list.post_category === category); + // setfilteredposts(filterList); + // console.log(posts); + // }; return ( - {isPost?
- -
-

Create Post

-

{title}

-