diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..7d2db8445
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+.env
+package-lock.json
+.vite/
+uploads/
+dist/
+*.log
diff --git a/README.md b/README.md
index 0e1211217..b056d8a8f 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# [your app name here]
+# StickerStory
CodePath WEB103 Final Project
-Designed and developed by: [your names here]
+Designed and developed by: Kelly Chan, Arsheen Taj Syed
π Link to deployed app:
@@ -10,39 +10,55 @@ Designed and developed by: [your names here]
### Description and Purpose
-[text goes here]
+StickerStory is a digital memory journal that lets users creatively capture and preserve their favorite moments. Users can create multiple journals and fill each journal with scrapbook-style pages. On each page, users can upload photos, add decorative stickers, doodles, and text, and attach labels, descriptions, locations, and dates. To make pages more personal, users have the option of responding to random prompts and even attach a Spotify song to capture the mood of the moment.
-### Inspiration
+The purpose of StickerStory is to give users a fun, creative, and interactive way to document memories and reflect on their experiences. It combines journaling, scrapbooking, and music in one app, making it both personal and shareable for users who want to save and revisit their favorite moments over time.
-[text goes here]
+### Inspiration
+Our inspiration stems from the nostalgia of physical scrapbooking combined with the modern convenience of digital journaling apps. We aimed to create an experience that transcends simple text-based logs, providing a rich, visual, and highly personal way to document life. The inclusion of stickers and doodling mimics the tactile joy of paper scrapbooks, while features like Spotify integration and prompt generation add a unique digital twist to the memory-capturing experience.
## Tech Stack
-Frontend:
-
-Backend:
+Frontend: HTML, CSS, JavaScript
+Backend: Express, Node.js, PostgreSQL
## Features
-### [Name of Feature 1]
+### β
Journal & Page Creation (Baseline)
+Users can create, name, and manage multiple distinct journals and add new blank pages to any journal.
-[short description goes here]
+
-[gif goes here]
+https://imgur.com/a/jx9wb8L
-### [Name of Feature 2]
+### β
Multimedia Page Editor (Baseline)
+Allows users to upload a photo, add a text description, and attach a date/location to a page.
-[short description goes here]
+
+https://imgur.com/a/nWyYdsu
-[gif goes here]
+### β
Sticker & Doodle Toolkit (Custom)
+Users can select from a library of digital stickers and use a doodling tool to personalize their page design.
+
+
+https://imgur.com/a/FHzCOxQ
+
+### β
Spotify Song Link (Custom)
+Users can link a Spotify song URL to a page to capture the mood, storing the song data in a one-to-one relationship.
+
+
+https://imgur.com/a/IzWDR47
-### [Name of Feature 3]
+### β
Memory Prompt Generator
+Users can opt to respond to a random, inspiring prompt (e.g., "What was the most surprising thing that happened today?") to help spark a memory.
-[short description goes here]
+
+https://imgur.com/a/UmzoKx1
+### Tagging and Filtering
+Users can tag pages with labels (e.g., βvacation,β βfamilyβ) and filter pages by these tags.
[gif goes here]
-### [ADDITIONAL FEATURES GO HERE - ADD ALL FEATURES HERE IN THE FORMAT ABOVE; you will check these off and add gifs as you complete them]
## Installation Instructions
diff --git a/milestones/milestone1.md b/milestones/milestone1.md
index 52b9b0038..6e4d26157 100644
--- a/milestones/milestone1.md
+++ b/milestones/milestone1.md
@@ -6,27 +6,27 @@ This document should be completed and submitted during **Unit 5** of this course
This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-- [ ] Read and understand all required features
- - [ ] Understand you **must** implement **all** baseline features and **two** custom features
-- [ ] In `readme.md`: update app name to your app's name
-- [ ] In `readme.md`: add all group members' names
-- [ ] In `readme.md`: complete the **Description and Purpose** section
-- [ ] In `readme.md`: complete the **Inspiration** section
-- [ ] In `readme.md`: list a name and description for all features (minimum 6 for full points) you intend to include in your app (in future units, you will check off features as you complete them and add GIFs demonstrating the features)
-- [ ] In `planning/user_stories.md`: add all user stories (minimum 10 for full points)
-- [ ] In `planning/user_stories.md`: use 1-3 unique user roles in your user stories
-- [ ] In this document, complete all thre questions in the **Reflection** section below
+- [X] Read and understand all required features
+ - [X] Understand you **must** implement **all** baseline features and **two** custom features
+- [X] In `readme.md`: update app name to your app's name
+- [X] In `readme.md`: add all group members' names
+- [X] In `readme.md`: complete the **Description and Purpose** section
+- [X] In `readme.md`: complete the **Inspiration** section
+- [X] In `readme.md`: list a name and description for all features (minimum 6 for full points) you intend to include in your app (in future units, you will check off features as you complete them and add GIFs demonstrating the features)
+- [X] In `planning/user_stories.md`: add all user stories (minimum 10 for full points)
+- [X] In `planning/user_stories.md`: use 1-3 unique user roles in your user stories
+- [X] In this document, complete all three questions in the **Reflection** section below
## Reflection
### 1. What went well during this unit?
-[ππΎππΎππΎ your answer here]
+Planning and thinking about the capstone project.
### 2. What were some challenges your group faced in this unit?
-[ππΎππΎππΎ your answer here]
+Coordinating the timings and distributing the work among all.
### 3. What additional support will you need in upcoming units as you continue to work on your final project?
-[ππΎππΎππΎ your answer here]
+Guiding with resources and feedback on the idea and implementation plan.
diff --git a/milestones/milestone2.md b/milestones/milestone2.md
index e3178cd81..5d51d44df 100644
--- a/milestones/milestone2.md
+++ b/milestones/milestone2.md
@@ -6,24 +6,24 @@ This document should be completed and submitted during **Unit 6** of this course
This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-- [ ] In `planning/wireframes.md`: add wireframes for at least three pages in your web app.
- - [ ] Include a list of pages in your app
-- [ ] In `planning/entity_relationship_diagram.md`: add the entity relationship diagram you developed for your database.
- - [ ] Your entity relationship diagram should include the tables in your database.
-- [ ] Prepare your three-minute pitch presentation, to be presented during Unit 7 (the next unit).
- - [ ] You do **not** need to submit any materials in advance of your pitch.
-- [ ] In this document, complete all three questions in the **Reflection** section below
+- [X] In `planning/wireframes.md`: add wireframes for at least three pages in your web app.
+ - [X] Include a list of pages in your app
+- [X] In `planning/entity_relationship_diagram.md`: add the entity relationship diagram you developed for your database.
+ - [X] Your entity relationship diagram should include the tables in your database.
+- [X] Prepare your three-minute pitch presentation, to be presented during Unit 7 (the next unit).
+ - [X] You do **not** need to submit any materials in advance of your pitch.
+- [X] In this document, complete all three questions in the **Reflection** section below
## Reflection
### 1. What went well during this unit?
-[ππΎππΎππΎ your answer here]
+Designing the low-fidelity wireframes for our project went really well. Our group had similar ideas about how we wanted StickerStory to look and function, so the process felt smooth and collaborative. It was fun seeing our ideas come to life visually and realizing we were all on the same page about the appβs layout and features.
### 2. What were some challenges your group faced in this unit?
-[ππΎππΎππΎ your answer here]
+One challenge we faced was narrowing down all of our ideas into something thatβs realistic for our timeline. We had a lot of creative concepts, like music integration and sticker customization, but had to decide which ones were essential for the first version of our app. It was also a bit tricky coordinating what parts we should do, so communication took some extra effort.
### 3. What additional support will you need in upcoming units as you continue to work on your final project?
-[ππΎππΎππΎ your answer here]
+As we move forward, weβll need more support with the technical side of implementation, especially setting up the database relationships (like many-to-many tables) and integrating external APIs such as Spotify. Weβd also appreciate guidance on front-end design tools or techniques to help us make the sticker and collage features both functional and visually appealing.
diff --git a/milestones/milestone3.md b/milestones/milestone3.md
index 571ce7651..5a589054b 100644
--- a/milestones/milestone3.md
+++ b/milestones/milestone3.md
@@ -8,34 +8,35 @@ This unit, be sure to complete all tasks listed below. To complete a task, place
You will need to reference the GitHub Project Management guide in the course portal for more information about how to complete each of these steps.
-- [ ] In your repo, create a project board.
+- [X] In your repo, create a project board.
- *Please be sure to share your project board with the grading team's GitHub **codepathreview**. This is separate from your repository's sharing settings.*
-- [ ] In your repo, create at least 5 issues from the features on your feature list.
-- [ ] In your repo, update the status of issues in your project board.
-- [ ] In your repo, create a GitHub Milestone for each final project unit, corresponding to each of the 5 milestones in your `milestones/` directory.
- - [ ] Set the completion percentage of each milestone. The GitHub Milestone for this unit (Milestone 3 - Unit 7) should be 100% completed when you submit for full points.
-- [ ] In `readme.md`, check off the features you have completed in this unit by adding a β
emoji in front of the feature's name.
- - [ ] Under each feature you have completed, include a GIF showing feature functionality.
-- [ ] In this documents, complete all five questions in the **Reflection** section below.
+- [X] In your repo, create at least 5 issues from the features on your feature list.
+- [X] In your repo, update the status of issues in your project board.
+- [X] In your repo, create a GitHub Milestone for each final project unit, corresponding to each of the 5 milestones in your `milestones/` directory.
+ - [X] Set the completion percentage of each milestone. The GitHub Milestone for this unit (Milestone 3 - Unit 7) should be 100% completed when you submit for full points.
+- [X] In `readme.md`, check off the features you have completed in this unit by adding a β
emoji in front of the feature's name.
+ - [X] Under each feature you have completed, include a GIF showing feature functionality.
+- [X] In this documents, complete all five questions in the **Reflection** section below.
## Reflection
### 1. What went well during this unit?
-[ππΎππΎππΎ your answer here]
+We were able to organize and divide the work effectively and complete the implementations easily, and there have been no merge issues so far.
### 2. What were some challenges your group faced in this unit?
-[ππΎππΎππΎ your answer here]
+The initial setup and how to set up the file structures were something we had to think about.
### Did you finish all of your tasks in your sprint plan for this week? If you did not finish all of the planned tasks, how would you prioritize the remaining tasks on your list?
-[ππΎππΎππΎ your answer here]
+Yes, we were able to implement the features we wanted and have the journal pages and setup we wanted for this milestone.
### Which features and user stories would you consider βat riskβ? How will you change your plan if those items remain βat riskβ?
-[ππΎππΎππΎ your answer here]
+The Auto-Generated Starter Pages feature, I think it's "at risk" because this feature was introduced as a stretch goal rather than a core requirement. Our current focus is on delivering a solid journaling and scrapbooking experience. Auto-generating starter pages involves additional complexity, such as template creation and logic for personalization, which may divert resources from the primary goal.
+
### 5. What additional support will you need in upcoming units as you continue to work on your final project?
-[ππΎππΎππΎ your answer here]
+Perhaps show how to pull from the main branch without conflicts and test the code written by others more efficiently; that would be helpful.
diff --git a/milestones/milestone5.md b/milestones/milestone5.md
index 139284283..401e678e7 100644
--- a/milestones/milestone5.md
+++ b/milestones/milestone5.md
@@ -6,13 +6,13 @@ This document should be completed and submitted during **Unit 9** of this course
This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-- [ ] Deploy your project on Render
- - [ ] In `readme.md`, add the link to your deployed project
-- [ ] Update the status of issues in your project board as you complete them
-- [ ] In `readme.md`, check off the features you have completed in this unit by adding a β
emoji in front of their title
- - [ ] Under each feature you have completed, **include a GIF** showing feature functionality
-- [ ] In this document, complete the **Reflection** section below
-- [ ] π©π©π©**Complete the Final Project Feature Checklist section below**, detailing each feature you completed in the project (ONLY include features you implemented, not features you planned)
+- [x] Deploy your project on Render
+ - [x] In `readme.md`, add the link to your deployed project
+- [x] Update the status of issues in your project board as you complete them
+- [x] In `readme.md`, check off the features you have completed in this unit by adding a β
emoji in front of their title
+ - [x] Under each feature you have completed, **include a GIF** showing feature functionality
+- [x] In this document, complete the **Reflection** section below
+- [x] π©π©π©**Complete the Final Project Feature Checklist section below**, detailing each feature you completed in the project (ONLY include features you implemented, not features you planned)
- [ ] π©π©π©**Record a GIF showing a complete run-through of your app** that displays all the components included in the **Final Project Feature Checklist** below
- [ ] Include this GIF in the **Final Demo GIF** section below
@@ -24,39 +24,39 @@ Complete the checklist below detailing each baseline, custom, and stretch featur
ππΎππΎππΎ Check off each completed feature below.
-- [ ] The project includes an Express backend app and a React frontend app
-- [ ] The project includes these backend-specific features:
- - [ ] At least one of each of the following database relationships in Postgres
- - [ ] one-to-many
+- [X] The project includes an Express backend app and a React frontend app
+- [X] The project includes these backend-specific features:
+ - [X] At least one of each of the following database relationships in Postgres
+ - [X] one-to-many
- [ ] many-to-many with a join table
- - [ ] A well-designed RESTful API that:
- - [ ] supports all four main request types for a single entity (ex. tasks in a to-do list app): GET, POST, PATCH, and DELETE
- - [ ] the user can **view** items, such as tasks
- - [ ] the user can **create** a new item, such as a task
- - [ ] the user can **update** an existing item by changing some or all of its values, such as changing the title of task
- - [ ] the user can **delete** an existing item, such as a task
- - [ ] Routes follow proper naming conventions
- - [ ] The web app includes the ability to reset the database to its default state
-- [ ] The project includes these frontend-specific features:
- - [ ] At least one redirection, where users are able to navigate to a new page with a new URL within the app
- - [ ] At least one interaction that the user can initiate and complete on the same page without navigating to a new page
- - [ ] Dynamic frontend routes created with React Router
- - [ ] Hierarchically designed React components
- - [ ] Components broken down into categories, including Page and Component types
- - [ ] Corresponding container components and presenter components as appropriate
-- [ ] The project includes dynamic routes for both frontend and backend apps
-- [ ] The project is deployed on Render with all pages and features that are visible to the user are working as intended
+ - [X] A well-designed RESTful API that:
+ - [X] supports all four main request types for a single entity (ex. tasks in a to-do list app): GET, POST, PATCH, and DELETE
+ - [X] the user can **view** items, such as tasks
+ - [X] the user can **create** a new item, such as a task
+ - [X] the user can **update** an existing item by changing some or all of its values, such as changing the title of task
+ - [X] the user can **delete** an existing item, such as a task
+ - [X] Routes follow proper naming conventions
+ - [X] The web app includes the ability to reset the database to its default state
+- [X] The project includes these frontend-specific features:
+ - [X] At least one redirection, where users are able to navigate to a new page with a new URL within the app
+ - [X] At least one interaction that the user can initiate and complete on the same page without navigating to a new page
+ - [X] Dynamic frontend routes created with React Router
+ - [X] Hierarchically designed React components
+ - [X] Components broken down into categories, including Page and Component types
+ - [X] Corresponding container components and presenter components as appropriate
+- [X] The project includes dynamic routes for both frontend and backend apps
+- [X] The project is deployed on Render with all pages and features that are visible to the user are working as intended
### Custom Features
ππΎππΎππΎ Check off each completed feature below.
-- [ ] The project gracefully handles errors
-- [ ] The project includes a one-to-one database relationship
+- [X] The project gracefully handles errors
+- [X] The project includes a one-to-one database relationship
- [ ] The project includes a slide-out pane or modal as appropriate for your use case that pops up and covers the page content without navigating away from the current page
- [ ] The project includes a unique field within the join table
- [ ] The project includes a custom non-RESTful route with corresponding controller actions
-- [ ] The user can filter or sort items based on particular criteria as appropriate for your use case
+- [X] The user can filter or sort items based on particular criteria as appropriate for your use case
- [ ] Data is automatically generated in response to a certain event or user action. Examples include generating a default inventory for a new user starting a game or creating a starter set of tasks for a user creating a new task app account
- [ ] Data submitted via a POST or PATCH request is validated before the database is updated (e.g. validating that an event is in the future before allowing a new event to be created)
- [ ] *To receive full credit, please be sure to demonstrate in your walkthrough that for certain inputs, the item will NOT be successfully created or updated.*
@@ -65,14 +65,14 @@ Complete the checklist below detailing each baseline, custom, and stretch featur
ππΎππΎππΎ Check off each completed feature below.
-- [ ] A subset of pages require the user to log in before accessing the content
+- [X] A subset of pages require the user to log in before accessing the content
- [ ] Users can log in and log out via GitHub OAuth with Passport.js
- [ ] Restrict available user options dynamically, such as restricting available purchases based on a user's currency
- [ ] Show a spinner while a page or page element is loading
- [ ] Disable buttons and inputs during the form submission process
- [ ] Disable buttons after they have been clicked
- *At least 75% of buttons in your app must exhibit this behavior to receive full credit*
-- [ ] Users can upload images to the app and have them be stored on a cloud service
+- [X] Users can upload images to the app and have them be stored on a cloud service
- *A user profile picture does **NOT** count for this rubric item **only if** the app also includes "Login via GitHub" functionality.*
- *Adding a photo via a URL does **NOT** count for this rubric item (for example, if the user provides a URL with an image to attach it to the post).*
- *Selecting a photo from a list of provided photos does **NOT** count for this rubric item.*
@@ -86,20 +86,20 @@ Complete the checklist below detailing each baseline, custom, and stretch featur
### 1. What went well during this unit?
-[ππΎππΎππΎ your answer here]
+We were able to plan and execute the website as envisioned and incorporate everything we learned in class.
### 2. What were some challenges your group faced in this unit?
-[ππΎππΎππΎ your answer here]
+Merging and pulling code from each other.
### 3. What were some of the highlights or achievements that you are most proud of in this project?
-[ππΎππΎππΎ your answer here]
+The website is aesthetically pleasing and functionally incorporates authentication, a database, a user interface, and all other necessary elements into the project.
### 4. Reflecting on your web development journey so far, how have you grown since the beginning of the course?
-[ππΎππΎππΎ your answer here]
+We made something as we wanted! We were able to plan something we were passionate about and bring it to life.
### 5. Looking ahead, what are your goals related to web development, and what steps do you plan to take to achieve them?
-[ππΎππΎππΎ your answer here]
+Learn more advanced web development concepts and keep growing by learning, reflecting, and practising.
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..a34e0680f
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "web103_finalproject",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}
diff --git a/planning/entity_relationship_diagram.md b/planning/entity_relationship_diagram.md
index 12c25f62c..438612ab5 100644
--- a/planning/entity_relationship_diagram.md
+++ b/planning/entity_relationship_diagram.md
@@ -4,14 +4,15 @@ Reference the Creating an Entity Relationship Diagram final project guide in the
## Create the List of Tables
-[ππΎππΎππΎ List each table in your diagram]
+1. **users**: Stores user authentication and profile information.
+2. **journals**: Stores collections of pages (e.g., "My Travels Journal"). This has a **one-to-many** relationship with users.
+3. **pages**: Stores the individual memory entries or scrapbook pages, including text, photo URLs, dates, and locations. This has a **one-to-many** relationship with journals.
+4. **stickers**: Stores the reference library of digital stickers available for use on pages.
+5. **pagestickers**: This is the **join table** handling the **many-to-many** relationship between pages and stickers. It also includes custom fields for sticker position and rotation.
+6. **moodsongs**: Stores the Spotify song metadata linked to a page. This is the **one-to-one** relationship with pages.
## Add the Entity Relationship Diagram
-[ππΎππΎππΎ Include an image or images of the diagram below. You may also wish to use the following markdown syntax to outline each table, as per your preference.]
+
+
-| Column Name | Type | Description |
-|-------------|------|-------------|
-| id | integer | primary key |
-| name | text | name of the shoe model |
-| ... | ... | ... |
diff --git a/planning/user_stories.md b/planning/user_stories.md
index 1e55ecbcd..0c7831785 100644
--- a/planning/user_stories.md
+++ b/planning/user_stories.md
@@ -3,11 +3,22 @@
Reference the Writing User Stories final project guide in the course portal for more information about how to complete each of the sections below.
## Outline User Roles
+- Journaler (primary user)
+- Designer (user-focused on visual customization)
+- Music Lover (user who wants to attach music to memories)
-[ππΎππΎππΎ Include at least at least 1, but no more than 3, user roles.]
## Draft User Stories
-[ππΎππΎππΎ Include at least at least 10 user stories in this format:]
+1. As a Journaler, I want to create multiple journals so I can organize my memories by theme.
+2. As a Journaler, I want to add pages to a journal so I can document specific events.
+3. As a Designer, I want to add stickers and doodles to a page so I can personalize it visually.
+4. As a Journaler, I want to write text and captions on each page to describe my memories.
+5. As a Music Lover, I want to attach a Spotify song to a page so I can capture the mood.
+6. As a Journaler, I want to receive random prompts so I can get inspiration when Iβm stuck.
+7. As a Journaler, I want to tag pages with labels so I can filter and find them later.
+8. As a Designer, I want to preview a page in a modal so I donβt lose my place in the journal.
+9. As a Journaler, I want the app to validate my inputs so I donβt lose data or make mistakes.
+10. As a Journaler, I want a starter page to be created when I make a new journal so I can get started quickly.
+
-1. As a [user role], I want to [what], so that [why].
diff --git a/planning/wireframe-images/Add-Page.png b/planning/wireframe-images/Add-Page.png
new file mode 100644
index 000000000..4e4d97183
Binary files /dev/null and b/planning/wireframe-images/Add-Page.png differ
diff --git a/planning/wireframe-images/All-Journals.png b/planning/wireframe-images/All-Journals.png
new file mode 100644
index 000000000..4a3003e0e
Binary files /dev/null and b/planning/wireframe-images/All-Journals.png differ
diff --git a/planning/wireframe-images/Create-Journal.png b/planning/wireframe-images/Create-Journal.png
new file mode 100644
index 000000000..109c52e19
Binary files /dev/null and b/planning/wireframe-images/Create-Journal.png differ
diff --git a/planning/wireframe-images/Journal-Details.png b/planning/wireframe-images/Journal-Details.png
new file mode 100644
index 000000000..80632fbfd
Binary files /dev/null and b/planning/wireframe-images/Journal-Details.png differ
diff --git a/planning/wireframe-images/Journal-Page-View.png b/planning/wireframe-images/Journal-Page-View.png
new file mode 100644
index 000000000..2b36387ed
Binary files /dev/null and b/planning/wireframe-images/Journal-Page-View.png differ
diff --git a/planning/wireframe-images/Landing.png b/planning/wireframe-images/Landing.png
new file mode 100644
index 000000000..1f3f0ec28
Binary files /dev/null and b/planning/wireframe-images/Landing.png differ
diff --git a/planning/wireframe-images/Login.png b/planning/wireframe-images/Login.png
new file mode 100644
index 000000000..dabfae1f2
Binary files /dev/null and b/planning/wireframe-images/Login.png differ
diff --git a/planning/wireframe-images/Page-Details.png b/planning/wireframe-images/Page-Details.png
new file mode 100644
index 000000000..8b0b762fc
Binary files /dev/null and b/planning/wireframe-images/Page-Details.png differ
diff --git a/planning/wireframe-images/Page-Preview.png b/planning/wireframe-images/Page-Preview.png
new file mode 100644
index 000000000..7d323bdda
Binary files /dev/null and b/planning/wireframe-images/Page-Preview.png differ
diff --git a/planning/wireframes.md b/planning/wireframes.md
index fbcd15a0c..f987ec47f 100644
--- a/planning/wireframes.md
+++ b/planning/wireframes.md
@@ -3,19 +3,51 @@
Reference the Creating an Entity Relationship Diagram final project guide in the course portal for more information about how to complete this deliverable.
## List of Pages
+- Landing Page β
+- Login Page β
+- Sign Up Page
+- View All Journals β
+- Create New Journal β
+- Journal All Page Views β
+- Journal Single Page Views β
+- Add New Page to Journal β
+- Add Page Details β
+- Add Page Preview β
[ππΎππΎππΎ List the pages you expect to have in your app, with a β next to pages you have wireframed]
-## Wireframe 1: [page title]
+## Wireframe 1: Landing Page
-[ππΎππΎππΎ include wireframe 1]
+
-## Wireframe 2: [page title]
+## Wireframe 2: Login Page
-[ππΎππΎππΎ include wireframe 2]
+
-## Wireframe 3: [page title]
+## Wireframe 3: View All Journals
-[ππΎππΎππΎ include wireframe 3]
+
-[ππΎππΎππΎ include more wireframes as desired]
+## Wireframe 4: Create Journal
+
+
+
+## Wireframe 5: Journal All Page View
+
+
+
+## Wireframe 6: Journal Single Page View
+
+
+
+## Wireframe 7: Add Page View
+
+
+
+## Wireframe 8: Add Page Details View
+
+
+
+## Wireframe 9: Add Page Preview View
+
+
diff --git a/project/client/.DS_Store b/project/client/.DS_Store
new file mode 100644
index 000000000..0fb3caa99
Binary files /dev/null and b/project/client/.DS_Store differ
diff --git a/project/client/index.html b/project/client/index.html
new file mode 100644
index 000000000..8014d693a
--- /dev/null
+++ b/project/client/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ StickerStory
+
+
+
+
+
+
diff --git a/project/client/public/covers/bluecover.jpg b/project/client/public/covers/bluecover.jpg
new file mode 100644
index 000000000..fbfed54f5
Binary files /dev/null and b/project/client/public/covers/bluecover.jpg differ
diff --git a/project/client/public/covers/graycover.jpg b/project/client/public/covers/graycover.jpg
new file mode 100644
index 000000000..855faa475
Binary files /dev/null and b/project/client/public/covers/graycover.jpg differ
diff --git a/project/client/public/covers/greencover.jpg b/project/client/public/covers/greencover.jpg
new file mode 100644
index 000000000..2592a3c84
Binary files /dev/null and b/project/client/public/covers/greencover.jpg differ
diff --git a/project/client/public/covers/pinkcover.jpg b/project/client/public/covers/pinkcover.jpg
new file mode 100644
index 000000000..b84d50a5c
Binary files /dev/null and b/project/client/public/covers/pinkcover.jpg differ
diff --git a/project/client/public/covers/yellowcover.jpg b/project/client/public/covers/yellowcover.jpg
new file mode 100644
index 000000000..d70bbaa84
Binary files /dev/null and b/project/client/public/covers/yellowcover.jpg differ
diff --git a/project/client/public/logo.png b/project/client/public/logo.png
new file mode 100644
index 000000000..f913ac476
Binary files /dev/null and b/project/client/public/logo.png differ
diff --git a/project/client/public/pages/black.jpg b/project/client/public/pages/black.jpg
new file mode 100644
index 000000000..aed5939f1
Binary files /dev/null and b/project/client/public/pages/black.jpg differ
diff --git a/project/client/public/pages/dotted.jpg b/project/client/public/pages/dotted.jpg
new file mode 100644
index 000000000..329d5d7f8
Binary files /dev/null and b/project/client/public/pages/dotted.jpg differ
diff --git a/project/client/public/pages/lines_white.jpg b/project/client/public/pages/lines_white.jpg
new file mode 100644
index 000000000..05f764ddc
Binary files /dev/null and b/project/client/public/pages/lines_white.jpg differ
diff --git a/project/client/public/pages/note.jpg b/project/client/public/pages/note.jpg
new file mode 100644
index 000000000..50644a304
Binary files /dev/null and b/project/client/public/pages/note.jpg differ
diff --git a/project/client/public/pages/notebook_lines.jpg b/project/client/public/pages/notebook_lines.jpg
new file mode 100644
index 000000000..996950319
Binary files /dev/null and b/project/client/public/pages/notebook_lines.jpg differ
diff --git a/project/client/public/pages/pink.jpg b/project/client/public/pages/pink.jpg
new file mode 100644
index 000000000..430ec6234
Binary files /dev/null and b/project/client/public/pages/pink.jpg differ
diff --git a/project/client/public/pages/square.jpg b/project/client/public/pages/square.jpg
new file mode 100644
index 000000000..ab27589a8
Binary files /dev/null and b/project/client/public/pages/square.jpg differ
diff --git a/project/client/public/pages/white.jpg b/project/client/public/pages/white.jpg
new file mode 100644
index 000000000..ff181c4cd
Binary files /dev/null and b/project/client/public/pages/white.jpg differ
diff --git a/project/client/public/stickers/Meow.jpg b/project/client/public/stickers/Meow.jpg
new file mode 100644
index 000000000..031825528
Binary files /dev/null and b/project/client/public/stickers/Meow.jpg differ
diff --git a/project/client/public/stickers/circle.jpg b/project/client/public/stickers/circle.jpg
new file mode 100644
index 000000000..78eabea6d
Binary files /dev/null and b/project/client/public/stickers/circle.jpg differ
diff --git a/project/client/public/stickers/collage.jpg b/project/client/public/stickers/collage.jpg
new file mode 100644
index 000000000..81645fe9d
Binary files /dev/null and b/project/client/public/stickers/collage.jpg differ
diff --git a/project/client/public/stickers/flowers.jpg b/project/client/public/stickers/flowers.jpg
new file mode 100644
index 000000000..08a0fab70
Binary files /dev/null and b/project/client/public/stickers/flowers.jpg differ
diff --git a/project/client/public/stickers/hearts.jpg b/project/client/public/stickers/hearts.jpg
new file mode 100644
index 000000000..a68756841
Binary files /dev/null and b/project/client/public/stickers/hearts.jpg differ
diff --git a/project/client/public/stickers/moon.jpg b/project/client/public/stickers/moon.jpg
new file mode 100644
index 000000000..30b202b34
Binary files /dev/null and b/project/client/public/stickers/moon.jpg differ
diff --git a/project/client/public/stickers/stars.jpg b/project/client/public/stickers/stars.jpg
new file mode 100644
index 000000000..fd4789ede
Binary files /dev/null and b/project/client/public/stickers/stars.jpg differ
diff --git a/project/client/src/App.css b/project/client/src/App.css
new file mode 100644
index 000000000..6d74d09fd
--- /dev/null
+++ b/project/client/src/App.css
@@ -0,0 +1,4 @@
+.app {
+ min-height: 100vh;
+ background-color: transparent;
+}
diff --git a/project/client/src/App.jsx b/project/client/src/App.jsx
new file mode 100644
index 000000000..ad352f042
--- /dev/null
+++ b/project/client/src/App.jsx
@@ -0,0 +1,58 @@
+import React, { useState, useEffect } from 'react';
+import { BrowserRouter as Router, useLocation, useRoutes } from "react-router-dom";
+import Navigation from './components/Navigation';
+
+import Home from './pages/Home';
+import AllJournals from './pages/AllJournals';
+import CreateJournal from './pages/CreateJournal';
+import EditJournal from './pages/EditJournal';
+import AllPages from './pages/AllPages';
+import PageDetails from './pages/PageDetails';
+import AddPage from './pages/AddPage';
+import PreviewPage from './pages/PreviewPage';
+
+import './App.css';
+
+const App = () => {
+ const location = useLocation();
+ const [user, setUser] = useState(null);
+
+ useEffect(() => {
+ fetch('http://localhost:3000/auth/me', { credentials: 'include' }) // Change this
+ .then(res => res.json())
+ .then(data => {
+ console.log('π€ User data received:', data); // Add this debug log
+ setUser(data);
+ })
+ .catch(err => console.log('Error fetching user:', err));
+ }, []);
+
+ const handleLogout = () => {
+ window.location.href = 'http://localhost:3000/auth/logout';
+ };
+
+ const element = useRoutes([
+ { path: '/', element: },
+ { path: '/journals', element: },
+ { path: '/journals/create', element: },
+ { path: '/journals/:journalId/edit', element: },
+ { path: '/journals/:journalId', element: },
+ { path: '/journals/:journalId/pages/add', element: },
+ { path: '/journals/:journalId/pages/:pageId', element: },
+ { path: '/journals/:journalId/pages/:pageId/preview', element: },
+ ]);
+
+ return (
+
+ {location.pathname !== '/' && (
+
+ )}
+ {element}
+
+ );
+};
+
+export default App;
diff --git a/project/client/src/components/EditorSidebar.jsx b/project/client/src/components/EditorSidebar.jsx
new file mode 100644
index 000000000..97ae49695
--- /dev/null
+++ b/project/client/src/components/EditorSidebar.jsx
@@ -0,0 +1,275 @@
+import React, { useState, useRef } from 'react';
+import '../css/AddPage.css';
+
+const EditorSidebar = ({
+ pageData,
+ updatePageBackground,
+ pageBackgrounds,
+ stickerLibrary,
+ handleStickerClick,
+ expandedSections,
+ toggleSection,
+ isDoodling,
+ startDoodling,
+ stopDoodling,
+ clearDoodle,
+ brushColor,
+ setBrushColor,
+ brushSize,
+ setBrushSize,
+ doodleCanvasRef,
+ textInput,
+ setTextInput,
+ textSize,
+ setTextSize,
+ textColor,
+ setTextColor,
+ handleAddText,
+ handleImageUpload
+}) => {
+ return (
+
+ );
+};
+
+export default EditorSidebar;
+
diff --git a/project/client/src/components/Navigation.jsx b/project/client/src/components/Navigation.jsx
new file mode 100644
index 000000000..a742da837
--- /dev/null
+++ b/project/client/src/components/Navigation.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Link } from "react-router-dom";
+import '../App.css';
+import '../css/Navigation.css';
+
+const Navigation = ({ userName = '', onLogout }) => {
+ return (
+
+ );
+};
+
+export default Navigation;
diff --git a/project/client/src/components/PageLayout.jsx b/project/client/src/components/PageLayout.jsx
new file mode 100644
index 000000000..2473d059d
--- /dev/null
+++ b/project/client/src/components/PageLayout.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import '../css/PageLayout.css';
+
+const PageLayout = ({
+ title,
+ pagePreview,
+ formFields,
+ footerActions,
+ mode = 'details' // 'details' or 'preview'
+}) => {
+ return (
+
+
+
{title}
+
+
+
+ {/* Left Section - Page Preview */}
+
+
+ {/* Right Section - Form Fields */}
+
+
+
+ {/* Footer Actions */}
+ {footerActions && (
+
+ {footerActions}
+
+ )}
+
+ );
+};
+
+export default PageLayout;
+
diff --git a/project/client/src/css/AddPage.css b/project/client/src/css/AddPage.css
new file mode 100644
index 000000000..728d80b6f
--- /dev/null
+++ b/project/client/src/css/AddPage.css
@@ -0,0 +1,976 @@
+/* Reset and Base Styles */
+.add-page-container {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background-color: #fff5e7; /* Page background color */
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Main Container - Three-Column Layout */
+.main-container {
+ display: grid;
+ grid-template-columns: 250px 1fr 80px; /* Left sidebar, center, right sidebar */
+ flex: 1;
+ gap: 0;
+ height: calc(100vh - 120px);
+}
+
+/* Left Sidebar - Editor Tools */
+.editor-sidebar {
+ background-color: white;
+ padding: 2rem 1.5rem;
+ overflow-y: auto;
+ border-right: 1px solid #e0d9c7;
+}
+
+/* Editor Section Styles */
+.editor-section {
+ margin-bottom: 2rem;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.section-header.clickable {
+ cursor: pointer;
+ user-select: none;
+ transition: opacity 0.2s;
+}
+
+.section-header.clickable:hover {
+ opacity: 0.7;
+}
+
+.star-icon {
+ font-size: 1rem;
+}
+
+.section-title {
+ font-size: 1rem;
+ font-weight: 500;
+ color: #333;
+ margin: 0;
+ flex: 1;
+}
+
+.chevron-icon {
+ font-size: 0.8rem;
+ color: #666;
+ transition: transform 0.3s ease;
+ margin-left: auto;
+}
+
+.chevron-icon.rotated {
+ transform: rotate(180deg);
+}
+
+.section-content {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.3s ease, padding 0.3s ease;
+ padding: 0;
+}
+
+.section-content.expanded {
+ max-height: 1000px;
+ padding: 1rem 0;
+}
+
+.section-content.collapsed {
+ max-height: 0;
+ padding: 0;
+}
+
+/* Page Background Section */
+.page-background-swatches {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.page-background-swatch {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ border: 2px solid #e0d9c7;
+ cursor: pointer;
+ padding: 0;
+ background: transparent;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ position: relative;
+ overflow: hidden;
+}
+
+.page-background-swatch:hover {
+ transform: scale(1.1);
+ border-color: #3d2817;
+}
+
+.page-background-swatch.selected {
+ border-width: 3px;
+ border-color: #3d2817;
+ box-shadow: 0 0 0 2px rgba(61, 40, 23, 0.2);
+}
+
+.background-swatch-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+/* Step 6: Stickers Section */
+.stickers-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.5rem;
+ padding: 0.5rem 0;
+}
+
+.sticker-item {
+ aspect-ratio: 1;
+ border: 2px solid #e0d9c7;
+ border-radius: 6px;
+ background-color: #fafafa;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ padding: 0.25rem;
+ overflow: hidden;
+}
+
+.sticker-item:hover {
+ border-color: #3d2817;
+ transform: scale(1.05);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.sticker-item .sticker-emoji {
+ font-size: 1.5rem;
+}
+
+.sticker-item .sticker-image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ display: block;
+}
+
+/* Stickers on Canvas */
+.canvas-sticker {
+ position: absolute;
+ cursor: move;
+ user-select: none;
+ z-index: 10; /* Stickers layer - above doodles, below text */
+ transition: transform 0.1s ease;
+ padding: 5px;
+ min-width: 50px;
+ min-height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.canvas-sticker:hover {
+ transform: scale(1.05);
+ background-color: rgba(61, 40, 23, 0.05);
+ border-radius: 8px;
+}
+
+.canvas-sticker.dragging {
+ transform: scale(1.1);
+ opacity: 0.9;
+ z-index: 100;
+ background-color: rgba(61, 40, 23, 0.1);
+ border-radius: 8px;
+}
+
+.canvas-sticker-emoji {
+ pointer-events: none;
+ display: block;
+}
+
+.canvas-sticker-image {
+ pointer-events: none;
+ display: block;
+ user-select: none;
+}
+
+/* Step 7: Doodles Section */
+.doodle-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.doodle-btn {
+ background-color: #3d2817;
+ color: white;
+ border: none;
+ padding: 0.75rem 1rem;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ width: 100%;
+}
+
+.doodle-btn:hover {
+ background-color: #2d1f12;
+}
+
+.doodle-options {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.doodle-color-picker {
+ width: 100%;
+ height: 40px;
+ border: 2px solid #e0d9c7;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.doodle-brush-slider {
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: #e0d9c7;
+ outline: none;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.doodle-brush-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: #3d2817;
+ cursor: pointer;
+}
+
+.doodle-brush-slider::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: #3d2817;
+ cursor: pointer;
+ border: none;
+}
+
+.brush-size-value {
+ font-size: 0.9rem;
+ color: #666;
+ text-align: center;
+}
+
+.button-group {
+ flex-direction: row;
+ gap: 0.5rem;
+}
+
+.button-group .doodle-btn {
+ flex: 1;
+}
+
+/* Doodle Canvas Overlay */
+.doodle-canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 8; /* Doodles layer - above images, below stickers */
+ touch-action: none;
+}
+
+.doodle-canvas.active {
+ pointer-events: all;
+ cursor: crosshair;
+}
+
+/* When doodling is active, disable dragging on other elements */
+.page-canvas:has(.doodle-canvas.active) .canvas-sticker,
+.page-canvas:has(.doodle-canvas.active) .canvas-text,
+.page-canvas:has(.doodle-canvas.active) .canvas-image {
+ pointer-events: none;
+}
+
+/* Step 8: Text Section */
+.text-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.text-input {
+ width: 100%;
+ padding: 0.5rem;
+ border: 2px solid #e0d9c7;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ font-family: inherit;
+ resize: vertical;
+}
+
+.text-input:focus {
+ outline: none;
+ border-color: #3d2817;
+}
+
+.text-size-slider {
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: #e0d9c7;
+ outline: none;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.text-size-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: #3d2817;
+ cursor: pointer;
+}
+
+.text-size-slider::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: #3d2817;
+ cursor: pointer;
+ border: none;
+}
+
+.text-size-value {
+ font-size: 0.9rem;
+ color: #666;
+ text-align: center;
+}
+
+.text-color-picker {
+ width: 100%;
+ height: 40px;
+ border: 2px solid #e0d9c7;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.text-btn {
+ background-color: #3d2817;
+ color: white;
+ border: none;
+ padding: 0.75rem 1rem;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ width: 100%;
+}
+
+.text-btn:hover {
+ background-color: #2d1f12;
+}
+
+/* Text Elements on Canvas */
+.canvas-text {
+ position: absolute;
+ cursor: move;
+ user-select: none;
+ z-index: 15; /* Text layer - top layer */
+ padding: 0.5rem;
+ border: 2px dashed transparent;
+ border-radius: 4px;
+ min-width: 50px;
+ min-height: 30px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.1s ease, border-color 0.2s ease, background-color 0.2s ease;
+}
+
+.canvas-text:hover {
+ border-color: #3d2817;
+ transform: scale(1.02);
+ background-color: rgba(61, 40, 23, 0.05);
+}
+
+.canvas-text.dragging {
+ transform: scale(1.05);
+ opacity: 0.9;
+ border-color: #3d2817;
+ background-color: rgba(61, 40, 23, 0.1);
+ z-index: 100;
+}
+
+.canvas-text-content {
+ pointer-events: none;
+ white-space: nowrap;
+ display: block;
+}
+
+/* Step 9: Upload Images Section */
+.upload-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.upload-area {
+ border: 2px dashed #e0d9c7;
+ border-radius: 8px;
+ padding: 2rem 1rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s;
+ background-color: #fafafa;
+}
+
+.upload-area:hover {
+ border-color: #3d2817;
+ background-color: #f5f5f5;
+}
+
+.upload-area.drag-over {
+ border-color: #3d2817;
+ background-color: #f0f0f0;
+ border-style: solid;
+}
+
+.upload-icon {
+ font-size: 2.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.upload-text {
+ font-size: 0.9rem;
+ color: #333;
+ margin: 0.25rem 0;
+ font-weight: 500;
+}
+
+.upload-hint {
+ font-size: 0.75rem;
+ color: #666;
+ margin: 0.25rem 0 0 0;
+}
+
+/* Images on Canvas */
+.canvas-image {
+ position: absolute;
+ cursor: move;
+ user-select: none;
+ z-index: 5; /* Images layer - above background, below doodles */
+ border: 2px dashed transparent;
+ border-radius: 4px;
+ transition: transform 0.1s ease, border-color 0.2s ease, background-color 0.2s ease;
+ padding: 5px;
+ background-color: transparent;
+}
+
+.canvas-image:hover {
+ border-color: #3d2817;
+ transform: scale(1.02);
+ background-color: rgba(61, 40, 23, 0.05);
+}
+
+.canvas-image.dragging {
+ transform: scale(1.05);
+ opacity: 0.9;
+ border-color: #3d2817;
+ background-color: rgba(61, 40, 23, 0.1);
+ z-index: 100;
+}
+
+.canvas-image-img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ border-radius: 4px;
+ pointer-events: none;
+ display: block;
+}
+
+/* Center Section - Page Canvas */
+.page-section {
+ background-color: #fff5e7; /* Page background color */
+ padding: 2rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ overflow-y: auto;
+}
+
+.page-title {
+ font-family: 'Georgia', 'Times New Roman', serif;
+ font-size: 2.5rem;
+ font-weight: bold;
+ color: #3d2817; /* Dark brown */
+ margin-bottom: 2rem;
+}
+
+.page-alert {
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ margin-bottom: 1rem;
+ font-size: 0.9rem;
+ text-align: center;
+ width: 100%;
+ max-width: 800px;
+}
+
+.page-alert.error {
+ background-color: #fee;
+ color: #c00;
+ border: 1px solid #fcc;
+}
+
+.page-alert.success {
+ background-color: #efe;
+ color: #060;
+ border: 1px solid #cfc;
+ text-align: center;
+}
+
+.page-canvas {
+ background-color: white;
+ width: 100%;
+ max-width: 800px;
+ min-height: 600px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Subtle gray shadow */
+ border-radius: 4px;
+ position: relative;
+ padding: 2rem;
+}
+
+/* Right Sidebar - Action Buttons */
+.action-buttons {
+ background-color: transparent;
+ padding: 2rem 1rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1.5rem;
+ border-left: 1px solid #e0d9c7;
+ min-width: 80px;
+}
+
+/* Step 11: Action Buttons Styling */
+.action-btn {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ border: 2px solid #3d2817; /* Black outline */
+ background-color: transparent;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0;
+ position: relative;
+}
+
+.action-btn:hover {
+ background-color: rgba(61, 40, 23, 0.1);
+ transform: scale(1.1);
+ border-color: #2d1f12;
+}
+
+.action-btn:active {
+ transform: scale(0.95);
+}
+
+.action-icon {
+ font-size: 1.5rem;
+ display: block;
+ line-height: 1;
+}
+
+
+/* Footer Section */
+.add-page-footer {
+ background-color: #fff5e7; /* Page background color */
+ padding: 1.5rem 2rem;
+ display: flex;
+ justify-content: flex-end;
+ border-top: 1px solid #e0d9c7;
+}
+
+.next-btn {
+ background-color: #3d2817; /* Dark brown */
+ color: white;
+ border: none;
+ padding: 0.75rem 2rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.next-btn:hover {
+ background-color: #2d1f12;
+}
+
+/* Prompt Banner */
+.prompt-banner {
+ width: 100%;
+ max-width: 800px;
+ background-color: #fff;
+ border: 2px solid #3d2817;
+ border-radius: 12px;
+ padding: 1rem 1.5rem;
+ margin-bottom: 1.5rem;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+}
+
+.prompt-banner-label {
+ display: block;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 0.5rem;
+}
+
+.prompt-banner-text {
+ font-size: 1.1rem;
+ color: #3d2817;
+ margin: 0;
+ font-weight: 500;
+ line-height: 1.5;
+}
+
+/* Spotify Banner */
+.spotify-banner {
+ width: 100%;
+ max-width: 800px;
+ background-color: #f7f4ef;
+ border: 2px solid #3d2817;
+ border-radius: 12px;
+ padding: 1rem;
+ margin-bottom: 1.5rem;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+}
+
+.spotify-banner-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.75rem;
+}
+
+.spotify-banner-label {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.spotify-banner .spotify-remove-btn {
+ background: transparent;
+ border: 1px solid #3d2817;
+ color: #3d2817;
+ padding: 0.25rem 0.75rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.spotify-banner .spotify-remove-btn:hover {
+ background-color: #3d2817;
+ color: white;
+}
+
+/* Prompt Modal */
+.prompt-modal-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: rgba(0, 0, 0, 0.45);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+ z-index: 999;
+}
+
+.prompt-modal {
+ background-color: #fff;
+ border-radius: 16px;
+ padding: 2rem;
+ max-width: 420px;
+ width: 100%;
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
+ position: relative;
+ text-align: center;
+}
+
+.prompt-close-btn {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ background: transparent;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: #3d2817;
+ line-height: 1;
+ padding: 0.25rem;
+ width: 2rem;
+ height: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: background-color 0.2s;
+}
+
+.prompt-close-btn:hover {
+ background-color: rgba(61, 40, 23, 0.1);
+}
+
+.prompt-modal h3 {
+ font-size: 1.5rem;
+ color: #3d2817;
+ margin: 0 0 1rem 0;
+}
+
+.prompt-text {
+ font-size: 1.1rem;
+ color: #3d2817;
+ margin: 1rem 0 1.5rem;
+ font-weight: 500;
+ line-height: 1.6;
+ min-height: 3rem;
+}
+
+.prompt-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.prompt-action-btn {
+ background-color: #3d2817;
+ color: white;
+ border: none;
+ padding: 0.75rem 1.25rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.prompt-action-btn.secondary {
+ background-color: transparent;
+ color: #3d2817;
+ border: 2px solid #3d2817;
+}
+
+.prompt-action-btn:hover {
+ background-color: #2d1f12;
+}
+
+.prompt-action-btn.secondary:hover {
+ background-color: rgba(61, 40, 23, 0.05);
+}
+
+/* Spotify Modal */
+.spotify-modal-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: rgba(0, 0, 0, 0.45);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+ z-index: 999;
+}
+
+.spotify-modal {
+ background-color: #fff;
+ border-radius: 16px;
+ padding: 2rem;
+ max-width: 500px;
+ width: 100%;
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
+ position: relative;
+}
+
+.spotify-close-btn {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ background: transparent;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: #3d2817;
+ line-height: 1;
+ padding: 0.25rem;
+ width: 2rem;
+ height: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: background-color 0.2s;
+}
+
+.spotify-close-btn:hover {
+ background-color: rgba(61, 40, 23, 0.1);
+}
+
+.spotify-modal h3 {
+ font-size: 1.5rem;
+ color: #3d2817;
+ margin: 0 0 1.5rem 0;
+}
+
+.spotify-input-label {
+ display: block;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #3d2817;
+ margin-bottom: 0.5rem;
+}
+
+.spotify-input {
+ width: 100%;
+ padding: 0.75rem;
+ border: 2px solid #e0d9c7;
+ border-radius: 8px;
+ font-size: 1rem;
+ margin-bottom: 1rem;
+ box-sizing: border-box;
+}
+
+.spotify-input:focus {
+ outline: none;
+ border-color: #3d2817;
+}
+
+.spotify-error {
+ color: #b3261e;
+ font-size: 0.875rem;
+ margin: -0.75rem 0 1rem 0;
+}
+
+.spotify-preview-area {
+ margin: 1.5rem 0;
+ min-height: 152px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #f7f4ef;
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.spotify-preview-placeholder {
+ color: #666;
+ font-size: 0.9rem;
+ text-align: center;
+ margin: 0;
+}
+
+.spotify-modal-actions {
+ display: flex;
+ gap: 0.75rem;
+ margin-top: 1.5rem;
+}
+
+.spotify-save-btn {
+ flex: 1;
+ background-color: #3d2817;
+ color: white;
+ border: none;
+ padding: 0.75rem 1.25rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.spotify-save-btn:hover {
+ background-color: #2d1f12;
+}
+
+.spotify-remove-btn.secondary {
+ flex: 1;
+ background-color: transparent;
+ color: #3d2817;
+ border: 2px solid #3d2817;
+ padding: 0.75rem 1.25rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.spotify-remove-btn.secondary:hover {
+ background-color: rgba(61, 40, 23, 0.05);
+}
+
+/* Responsive adjustments */
+@media (max-width: 1024px) {
+ .main-container {
+ grid-template-columns: 200px 1fr 60px;
+ }
+}
+
+@media (max-width: 768px) {
+ .main-container {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto 1fr auto;
+ }
+
+ .editor-sidebar {
+ border-right: none;
+ border-bottom: 1px solid #e0d9c7;
+ }
+
+ .action-buttons {
+ border-left: none;
+ border-top: 1px solid #e0d9c7;
+ flex-direction: row;
+ justify-content: center;
+ padding: 1rem;
+ gap: 1rem;
+ }
+
+ .action-btn {
+ width: 45px;
+ height: 45px;
+ }
+
+ .action-icon {
+ font-size: 1.25rem;
+ }
+}
+
diff --git a/project/client/src/css/AllJournals.css b/project/client/src/css/AllJournals.css
new file mode 100644
index 000000000..fb91e3eeb
--- /dev/null
+++ b/project/client/src/css/AllJournals.css
@@ -0,0 +1,536 @@
+/* src/css/AllJournals.css */
+
+.all-journals-page {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 2rem;
+ position: relative;
+}
+
+/* Search and Create Button Row */
+.search-create-row {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ padding-bottom: 1.5rem;
+ border-bottom: 2px solid #e5e7eb;
+}
+
+.search-create-row .btn-filled {
+ padding: 0.75rem 1.25rem; /* smaller width */
+ flex: 0 0 auto;
+ width: auto;
+ white-space: nowrap;
+}
+
+/* Search Container */
+.search-container {
+ position: relative;
+ max-width: 800px;
+ width: 100%;
+}
+
+.search-icon {
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #9ca3af;
+ font-size: 20px;
+ pointer-events: none;
+}
+
+.search-container input[type="text"] {
+ width: 100%;
+ padding: 0.875rem 1.25rem 0.875rem 2.75rem;
+ font-size: 1rem;
+ border: 2px solid #e5e7eb;
+ border-radius: 12px;
+ outline: none;
+ transition: all 0.2s ease;
+ background-color: #ffffff;
+}
+
+.search-container input[type="text"]:focus {
+ border-color: #493000;
+ box-shadow: 0 0 0 3px rgba(73, 48, 0, 0.1);
+}
+
+.search-container input[type="text"]::placeholder {
+ color: #9ca3af;
+}
+
+/* Create Button - uses global btn-filled class */
+.search-create-row .btn-filled {
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+/* Filters */
+.filters {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 1.5rem;
+ padding: 1.5rem;
+ background-color: #f9fafb;
+ border-radius: 12px;
+ margin-bottom: 2rem;
+ border: 1px solid #e5e7eb;
+}
+
+.filters-label {
+ font-weight: 600;
+ color: #374151;
+ font-size: 0.95rem;
+ letter-spacing: 0.025em;
+}
+
+.filter-group {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.filter-group label {
+ font-weight: 500;
+ color: #4b5563;
+ font-size: 0.9rem;
+ white-space: nowrap;
+}
+
+.filter-group input[type="month"],
+.filter-group select {
+ padding: 0.625rem 1rem;
+ border: 2px solid #e5e7eb;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ outline: none;
+ background-color: #ffffff;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 150px;
+}
+
+.filter-group input[type="month"]:focus,
+.filter-group select:focus {
+ border-color: #493000;
+ box-shadow: 0 0 0 3px rgba(73, 48, 0, 0.1);
+}
+
+.filter-group select {
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ background-size: 1.25rem;
+ padding-right: 2.5rem;
+}
+
+/* Tags Filter */
+.filter-group.tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.filter-group.tags button {
+ padding: 0.5rem 1rem;
+ border: 2px solid #e5e7eb;
+ border-radius: 20px;
+ background-color: #ffffff;
+ color: #6b7280;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ outline: none;
+}
+
+.filter-group.tags button:hover {
+ border-color: #493000;
+ color: #493000;
+ transform: translateY(-1px);
+}
+
+.filter-group.tags button.selected {
+ background-color: #493000;
+ border-color: #493000;
+ color: #ffffff;
+}
+
+/* Journal Cards Grid */
+.journal-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 2rem;
+ margin-top: 2rem;
+}
+
+/* Individual Journal Card */
+.journal-card {
+ background-color: #ffffff;
+ border-radius: 20px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ cursor: pointer;
+ border: 1px solid #e5e7eb;
+ position: relative;
+}
+
+.journal-card:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15);
+}
+
+/* Journal Cover */
+.journal-cover {
+ width: 100%;
+ aspect-ratio: 3 / 4;
+ position: relative;
+ overflow: hidden;
+ background-color: #f3f4f6;
+}
+
+.journal-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.journal-cover .color-cover {
+ width: 100%;
+ height: 100%;
+}
+
+/* Three Dots Menu */
+.menu-container {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ z-index: 10;
+}
+
+.menu-button {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background-color: rgba(255, 255, 255, 0.95);
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.menu-button:hover {
+ background-color: #ffffff;
+ transform: scale(1.1);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.menu-button i {
+ color: #1f2937;
+ font-size: 1rem;
+}
+
+/* Dropdown Menu */
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 0.5rem;
+ background-color: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ border: 1px solid #e5e7eb;
+ min-width: 140px;
+ overflow: hidden;
+ z-index: 20;
+}
+
+.dropdown-menu button {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: none;
+ background: none;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 0.9rem;
+ color: #374151;
+ font-weight: 500;
+}
+
+.dropdown-menu button:hover {
+ background-color: #f9fafb;
+}
+
+.dropdown-menu button.delete-option {
+ color: #ef4444;
+}
+
+.dropdown-menu button.delete-option:hover {
+ background-color: #fef2f2;
+}
+
+.dropdown-menu button i {
+ font-size: 0.85rem;
+ width: 16px;
+}
+
+/* Journal Info */
+.journal-info {
+ padding: 1.25rem;
+}
+
+.journal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0.5rem;
+ gap: 0.75rem;
+}
+
+.journal-header h2 {
+ margin: 0;
+ color: #1f2937;
+ font-size: 1.125rem;
+ font-weight: 600;
+ flex: 1;
+ line-height: 1.4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+/* Add Page Button */
+.add-page-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.add-page-button i {
+ font-size: 1.75rem;
+ color: #493000;
+}
+
+.add-page-button:hover i {
+ color: #000000;
+ transform: scale(1.15);
+}
+
+.add-page-button:active i {
+ transform: scale(0.95);
+}
+
+/* Journal Description */
+.journal-description {
+ margin: 0 0 0.75rem 0;
+ color: #6b7280;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ font-style: italic;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+/* Journal Meta */
+.journal-meta {
+ display: flex;
+ align-items: center;
+ margin-top: 0.75rem;
+}
+
+.journal-meta .page-count {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.85rem;
+ color: #9ca3af;
+ font-weight: 500;
+}
+
+.journal-meta i {
+ font-size: 0.8rem;
+}
+
+/* Empty State */
+.empty-state {
+ text-align: center;
+ padding: 4rem 2rem;
+ max-width: 500px;
+ margin: 2rem auto;
+}
+
+.empty-state i {
+ font-size: 4rem;
+ color: #d1d5db;
+ margin-bottom: 1rem;
+}
+
+.empty-state h3 {
+ color: #374151;
+ margin: 0 0 0.5rem 0;
+ font-size: 1.5rem;
+}
+
+.empty-state p {
+ color: #6b7280;
+ margin: 0 0 1.5rem 0;
+ line-height: 1.6;
+}
+
+/* Loading State */
+.loading-container {
+ text-align: center;
+ padding: 4rem 2rem;
+}
+
+.spinner {
+ width: 50px;
+ height: 50px;
+ margin: 0 auto 1rem;
+ border: 4px solid #e5e7eb;
+ border-top-color: #493000;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.loading-container p {
+ color: #6b7280;
+ font-size: 1rem;
+}
+
+/* Responsive Design */
+@media (max-width: 1024px) {
+ .journal-cards {
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ }
+}
+
+@media (max-width: 768px) {
+ .all-journals-page {
+ padding: 1rem;
+ }
+
+ .page-title {
+ font-size: 2rem;
+ }
+
+ .search-create-row {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .search-container {
+ width: 100%;
+ }
+
+ .search-create-row{
+ width: 100%;
+ justify-content: center;
+ }
+
+ .btn-filled {
+ max-width: fit-content;
+ }
+
+ .filters {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .filter-group {
+ width: 100%;
+ }
+
+ .filter-group input[type="month"],
+ .filter-group select {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .journal-cards {
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1.25rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .journal-cards {
+ grid-template-columns: 1fr;
+ }
+
+ .add-page-button i {
+ font-size: 1.5rem;
+ }
+}
+
+/* Smooth Transitions */
+* {
+ box-sizing: border-box;
+}
+
+button {
+ font-family: inherit;
+}
+
+input,
+select {
+ font-family: inherit;
+}
+
+.back-button {
+ position: absolute;
+ top: 2rem;
+ left: 2rem;
+ font-size: 1.5rem;
+ color: #6b7280;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0.5rem;
+ border-radius: 8px;
+}
+
+.back-button:hover {
+ color: #493000;
+ transform: translateX(-4px);
+}
+
+.journal-description-subtitle {
+ font-style: italic;
+ color: #6b7280;
+ margin-top: -1rem;
+ margin-bottom: 1.5rem;
+ text-align: center;
+ font-size: 0.95rem;
+}
\ No newline at end of file
diff --git a/project/client/src/css/CreateJournal.css b/project/client/src/css/CreateJournal.css
new file mode 100644
index 000000000..aff4c9cb7
--- /dev/null
+++ b/project/client/src/css/CreateJournal.css
@@ -0,0 +1,387 @@
+/* src/css/CreateJournal.css */
+
+.create-journal-page {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+ position: relative;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
+}
+
+/* Back Button */
+.back-button {
+ position: absolute;
+ top: 2rem;
+ left: 2rem;
+ font-size: 1.5rem;
+ color: #6b7280;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0.5rem;
+ border-radius: 8px;
+}
+
+.back-button:hover {
+ color: #493000;
+ transform: translateX(-4px);
+}
+
+/* Page Title */
+.create-journal-page h1 {
+ text-align: center;
+ color: #1f2937;
+ margin: 3rem 0 2.5rem 0;
+ font-weight: 700;
+ font-size: 50px;
+}
+
+/* Main Container */
+.create-journal-container {
+ display: grid;
+ grid-template-columns: 400px 1fr;
+ gap: 3rem;
+ margin-top: 2rem;
+}
+
+/* Left Side - Cover Preview Section */
+.cover-preview-section {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.cover-preview {
+ width: 100%;
+ aspect-ratio: 4/5;
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
+ background: #ffffff;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.cover-preview:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2);
+}
+
+.cover-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.color-cover {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid #e5e7eb;
+}
+
+.blank-cover-text {
+ color: #9ca3af;
+ font-size: 1.25rem;
+ font-weight: 500;
+ text-align: center;
+}
+
+/* Upload Section */
+.upload-section {
+ margin-bottom: 0.5rem;
+}
+
+.upload-label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.875rem 1.5rem;
+ background-color: #f9fafb;
+ border: 2px dashed #d1d5db;
+ border-radius: 10px;
+ color: #6b7280;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 0.95rem;
+}
+
+.upload-label:hover {
+ background-color: #f3f4f6;
+ border-color: #493000;
+ color: #493000;
+}
+
+.upload-label i {
+ font-size: 1rem;
+}
+
+/* Color Options */
+.color-options {
+ text-align: center;
+}
+
+.color-options-label {
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #4b5563;
+ margin-bottom: 1rem;
+}
+
+.color-circles {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.color-circle {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ border: 3px solid transparent;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.color-circle:hover {
+ transform: scale(1.15);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
+}
+
+.color-circle.selected {
+ border-color: #1f2937;
+ box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.2);
+ transform: scale(1.1);
+}
+
+.color-circle i {
+ font-size: 1.2rem;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.color-circle:focus {
+ outline: none;
+ box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.3);
+}
+
+/* Right Side - Form */
+.journal-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ background-color: #ffffff;
+ padding: 2rem;
+ border-radius: 16px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border: 1px solid #e5e7eb;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ position: relative;
+}
+
+.form-group label {
+ font-weight: 600;
+ color: #374151;
+ font-size: 0.95rem;
+}
+
+.form-group input[type="text"],
+.form-group textarea {
+ padding: 0.875rem 1rem;
+ border: 2px solid #e5e7eb;
+ border-radius: 10px;
+ font-size: 1rem;
+ outline: none;
+ transition: all 0.2s ease;
+ background-color: #ffffff;
+ font-family: inherit;
+}
+
+.form-group input[type="text"]:focus,
+.form-group textarea:focus {
+ border-color: #493000;
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
+}
+
+.form-group input[type="text"]::placeholder,
+.form-group textarea::placeholder {
+ color: #9ca3af;
+}
+
+.form-group textarea {
+ resize: vertical;
+ min-height: 100px;
+}
+
+/* Character Count */
+.char-count {
+ font-size: 0.8rem;
+ color: #9ca3af;
+ text-align: right;
+ margin-top: -0.25rem;
+}
+
+/* Form Actions */
+.form-actions {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.btn-outline,
+.btn-filled {
+ flex: 1;
+ padding: 0.875rem 1.5rem;
+ border-radius: 10px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border: none;
+ outline: none;
+}
+
+.btn-outline {
+ background-color: transparent;
+ border: 2px solid #e5e7eb;
+ color: #6b7280;
+}
+
+.btn-outline:hover {
+ border-color: #493000;
+ color: #493000;
+ background-color: #f9fafb;
+}
+
+.btn-filled:active,
+.btn-outline:active {
+ transform: translateY(0);
+}
+
+.btn-filled:disabled {
+ background: #d1d5db;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+
+/* Responsive Design */
+@media (max-width: 1024px) {
+ .create-journal-container {
+ grid-template-columns: 350px 1fr;
+ gap: 2rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .create-journal-page {
+ padding: 1rem;
+ }
+
+ .back-button {
+ position: static;
+ margin-bottom: 1rem;
+ }
+
+ .create-journal-page h1 {
+ margin: 1rem 0 1.5rem 0;
+ font-size: 1.75rem;
+ }
+
+ .create-journal-container {
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ }
+
+ .cover-preview-section {
+ max-width: 400px;
+ margin: 0 auto;
+ }
+
+ .form-actions {
+ flex-direction: column-reverse;
+ }
+
+ .btn-outline,
+ .btn-filled {
+ width: 100%;
+ }
+}
+
+@media (max-width: 480px) {
+ .create-journal-page h1 {
+ font-size: 1.5rem;
+ }
+
+ .cover-preview {
+ max-width: 300px;
+ margin: 0 auto;
+ }
+
+ .color-circle {
+ width: 45px;
+ height: 45px;
+ }
+
+ .journal-form {
+ padding: 1.5rem;
+ }
+}
+
+/* Loading State */
+.journal-form.loading {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+/* Success Animation */
+@keyframes successPulse {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+}
+
+.journal-form.success {
+ animation: successPulse 0.5s ease;
+}
+
+/* Error State */
+.form-group.error input,
+.form-group.error textarea {
+ border-color: #ef4444;
+}
+
+.form-group .error-message {
+ color: #ef4444;
+ font-size: 0.85rem;
+ margin-top: 0.25rem;
+}
+
+/* Accessibility */
+.color-circle:focus-visible {
+ outline: 3px solid #493000;
+ outline-offset: 2px;
+}
+
+button:focus-visible {
+ outline: 3px solid #493000;
+ outline-offset: 2px;
+}
\ No newline at end of file
diff --git a/project/client/src/css/Home.css b/project/client/src/css/Home.css
new file mode 100644
index 000000000..45f28937e
--- /dev/null
+++ b/project/client/src/css/Home.css
@@ -0,0 +1,22 @@
+.home-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ text-align: center;
+}
+
+.home-container h1 {
+ margin-bottom: 0em;
+}
+
+.home-container p {
+ font-size: 20px;
+ font-style: italic;
+}
+
+.home-buttons {
+ display: flex;
+ gap: 1.5rem;
+}
\ No newline at end of file
diff --git a/project/client/src/css/Navigation.css b/project/client/src/css/Navigation.css
new file mode 100644
index 000000000..2b1439010
--- /dev/null
+++ b/project/client/src/css/Navigation.css
@@ -0,0 +1,35 @@
+.navbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #FEE2BB;
+ padding: 1.2rem 1rem;
+ font-family: 'Rubik', sans-serif;
+}
+
+.navbar-left h1 {
+ margin: 0;
+ font-size: 2.2rem;
+}
+
+.navbar-right {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.user-box {
+ background-color: #493000;
+ color: white;
+ padding: 0.8rem 1.5rem;
+ border-radius: 8px;
+ font-weight: 400;
+ font-size: 18px;
+}
+
+.logout-icon {
+ color: black;
+ font-size: 2rem;
+ cursor: pointer;
+ transition: transform 0.2s;
+}
diff --git a/project/client/src/css/PageDetails.css b/project/client/src/css/PageDetails.css
new file mode 100644
index 000000000..ab900e51c
--- /dev/null
+++ b/project/client/src/css/PageDetails.css
@@ -0,0 +1,280 @@
+/* Page Preview Styles */
+.page-preview-loading,
+.page-preview-empty {
+ text-align: center;
+ padding: 2rem;
+ color: #666;
+}
+
+.page-preview-canvas {
+ position: relative;
+ overflow: hidden;
+ border-radius: 8px;
+}
+
+.page-preview-prompt {
+ padding: 0.75rem 1rem;
+ background-color: rgba(255, 255, 255, 0.9);
+ border-radius: 8px;
+ margin: 0.5rem;
+ font-size: 0.9rem;
+ color: #3d2817;
+}
+
+.page-preview-spotify {
+ margin: 0.5rem;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.page-preview-image {
+ pointer-events: none;
+ user-select: none;
+}
+
+.page-preview-doodle {
+ pointer-events: none;
+ user-select: none;
+}
+
+.page-preview-sticker {
+ pointer-events: none;
+ user-select: none;
+}
+
+.page-preview-text {
+ pointer-events: none;
+ user-select: none;
+ white-space: nowrap;
+}
+
+/* Read-only form fields */
+.page-form-readonly {
+ padding: 0.75rem;
+ border: 1px solid #e0d9c7;
+ border-radius: 8px;
+ font-size: 1rem;
+ background-color: #f7f4ef;
+ color: #3d2817;
+ min-height: 2.5rem;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+/* Tags Section */
+.tags-input-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.tags-display {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ min-height: 2.5rem;
+ padding: 0.5rem;
+ border: 1px solid #e0d9c7;
+ border-radius: 8px;
+ background-color: #ffffff;
+ align-items: flex-start;
+}
+
+.tag-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.875rem;
+ background: linear-gradient(135deg, #fff5e7 0%, #f7f4ef 100%);
+ border: 1.5px solid #d4c5a9;
+ border-radius: 20px;
+ font-size: 0.875rem;
+ color: #3d2817;
+ font-weight: 500;
+ box-shadow: 0 2px 4px rgba(61, 40, 23, 0.1);
+ transition: all 0.2s ease;
+}
+
+.tag-chip:hover {
+ background: linear-gradient(135deg, #f7f4ef 0%, #e8e0d3 100%);
+ box-shadow: 0 3px 6px rgba(61, 40, 23, 0.15);
+ transform: translateY(-1px);
+}
+
+.tag-remove {
+ background: none;
+ border: none;
+ color: #8b6f47;
+ font-size: 1.2rem;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0;
+ margin-left: 0.25rem;
+ width: 18px;
+ height: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.2s;
+}
+
+.tag-remove:hover {
+ background-color: #d4c5a9;
+ color: #3d2817;
+}
+
+.tags-input {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #e0d9c7;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-family: inherit;
+ background-color: #ffffff;
+ color: #3d2817;
+ box-sizing: border-box;
+}
+
+.tags-input:focus {
+ outline: none;
+ border-color: #3d2817;
+ box-shadow: 0 0 0 2px rgba(61, 40, 23, 0.1);
+}
+
+.tags-input::placeholder {
+ color: #999;
+}
+
+/* Read-only tags display */
+.tags-display-readonly {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ min-height: 2.5rem;
+ padding: 0.5rem;
+ border: 1px solid #e0d9c7;
+ border-radius: 8px;
+ background-color: #f7f4ef;
+}
+
+.tag-chip-readonly {
+ display: inline-block;
+ padding: 0.5rem 0.875rem;
+ background: linear-gradient(135deg, #fff5e7 0%, #f7f4ef 100%);
+ border: 1.5px solid #d4c5a9;
+ border-radius: 20px;
+ font-size: 0.875rem;
+ color: #3d2817;
+ font-weight: 500;
+ box-shadow: 0 2px 4px rgba(61, 40, 23, 0.1);
+}
+
+.no-tags {
+ color: #999;
+ font-style: italic;
+}
+
+/* Prompt and Spotify Display in Form */
+.page-form-prompt-display {
+ padding: 0.75rem 1rem;
+ background-color: #f7f4ef;
+ border: 1px solid #d4c5a9;
+ border-radius: 8px;
+ font-size: 0.95rem;
+ color: #3d2817;
+ line-height: 1.5;
+ font-style: italic;
+}
+
+.page-form-spotify-display {
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid #e0d9c7;
+}
+
+/* Success Message Overlay */
+.page-save-success-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(61, 40, 23, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.page-save-success-message {
+ background: linear-gradient(135deg, #fff5e7 0%, #f7f4ef 100%);
+ border: 2px solid #d4c5a9;
+ border-radius: 16px;
+ padding: 2.5rem 3rem;
+ text-align: center;
+ box-shadow: 0 8px 24px rgba(61, 40, 23, 0.3);
+ max-width: 400px;
+ animation: slideUp 0.4s ease;
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+.page-save-success-message .success-icon {
+ width: 64px;
+ height: 64px;
+ background: linear-gradient(135deg, #8b6f47 0%, #6b5433 100%);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 1.5rem;
+ font-size: 2rem;
+ color: #fff5e7;
+ font-weight: bold;
+ box-shadow: 0 4px 12px rgba(139, 111, 71, 0.4);
+ animation: scaleIn 0.5s ease 0.2s both;
+}
+
+@keyframes scaleIn {
+ from {
+ transform: scale(0);
+ }
+ to {
+ transform: scale(1);
+ }
+}
+
+.page-save-success-message h3 {
+ margin: 0 0 0.75rem 0;
+ font-size: 1.5rem;
+ color: #3d2817;
+ font-weight: 600;
+}
+
+.page-save-success-message p {
+ margin: 0;
+ font-size: 1rem;
+ color: #6b5433;
+ line-height: 1.5;
+}
+
diff --git a/project/client/src/css/PageLayout.css b/project/client/src/css/PageLayout.css
new file mode 100644
index 000000000..577f72e87
--- /dev/null
+++ b/project/client/src/css/PageLayout.css
@@ -0,0 +1,191 @@
+/* Page Layout Container */
+.page-layout-container {
+ min-height: calc(100vh - 80px);
+ padding: 2rem;
+ background-color: #fff5e7;
+}
+
+/* Page Layout Header */
+.page-layout-header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.page-layout-title {
+ font-size: 2.5rem;
+ font-weight: bold;
+ color: #3d2817;
+ margin: 0;
+ font-family: 'Arial Rounded MT Bold', 'Arial', sans-serif;
+}
+
+/* Page Layout Content */
+.page-layout-content {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+/* Left Section - Page Preview */
+.page-layout-preview {
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+}
+
+.page-preview-container {
+ width: 100%;
+ max-width: 600px;
+ background-color: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 2rem;
+ min-height: 600px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Right Section - Form */
+.page-layout-form {
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+}
+
+.page-form-container {
+ width: 100%;
+ max-width: 500px;
+}
+
+/* Form Fields */
+.page-form-field {
+ margin-bottom: 1.5rem;
+}
+
+.page-form-label {
+ display: block;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #3d2817;
+ margin-bottom: 0.5rem;
+}
+
+.page-form-input {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #e0d9c7;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-family: inherit;
+ background-color: #ffffff;
+ color: #3d2817;
+ box-sizing: border-box;
+}
+
+.page-form-input:focus {
+ outline: none;
+ border-color: #3d2817;
+ box-shadow: 0 0 0 2px rgba(61, 40, 23, 0.1);
+}
+
+.page-form-textarea {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #e0d9c7;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-family: inherit;
+ background-color: #ffffff;
+ color: #3d2817;
+ resize: vertical;
+ min-height: 100px;
+ box-sizing: border-box;
+}
+
+.page-form-textarea:focus {
+ outline: none;
+ border-color: #3d2817;
+ box-shadow: 0 0 0 2px rgba(61, 40, 23, 0.1);
+}
+
+/* Footer Actions */
+.page-layout-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 2rem;
+ padding-top: 2rem;
+ max-width: 1400px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.page-footer-btn {
+ padding: 0.75rem 2rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ border: none;
+}
+
+.page-footer-btn.back {
+ background-color: #fff5e7;
+ color: #3d2817;
+ border: 1px solid #3d2817;
+}
+
+.page-footer-btn.back:hover {
+ background-color: #f7f4ef;
+}
+
+.page-footer-btn.primary {
+ background-color: #3d2817;
+ color: #ffffff;
+}
+
+.page-footer-btn.primary:hover {
+ background-color: #2d1f12;
+}
+
+/* Responsive Design */
+@media (max-width: 1024px) {
+ .page-layout-content {
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ }
+
+ .page-layout-preview,
+ .page-layout-form {
+ justify-content: center;
+ }
+
+ .page-preview-container,
+ .page-form-container {
+ max-width: 100%;
+ }
+}
+
+@media (max-width: 768px) {
+ .page-layout-container {
+ padding: 1rem;
+ }
+
+ .page-layout-title {
+ font-size: 2rem;
+ }
+
+ .page-layout-footer {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .page-footer-btn {
+ width: 100%;
+ }
+}
+
diff --git a/project/client/src/data/prompts.js b/project/client/src/data/prompts.js
new file mode 100644
index 000000000..3acb41d6c
--- /dev/null
+++ b/project/client/src/data/prompts.js
@@ -0,0 +1,15 @@
+const prompts = [
+ 'What was the most surprising thing that happened today?',
+ 'Describe a moment that made you smile recently.',
+ 'What is one small win you are proud of right now?',
+ 'Who inspired you this week and why?',
+ 'What challenge are you currently facing, and how are you handling it?',
+ 'Write about a place that makes you feel calm.',
+ 'What lesson did you learn from a recent mistake?',
+ 'Describe the last conversation that energized you.',
+ 'What is a goal you are excited to work toward?',
+ 'If today had a theme song, what would it be and why?'
+];
+
+export default prompts;
+
diff --git a/project/client/src/index.css b/project/client/src/index.css
new file mode 100644
index 000000000..8b1a00521
--- /dev/null
+++ b/project/client/src/index.css
@@ -0,0 +1,75 @@
+body {
+ font-family: 'Roboto', sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: #FFF5E7;
+ color: black;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: 'Caprasimo', cursive;
+ font-weight: 400;
+ font-style: normal;
+ color: black;
+}
+
+h1 {
+ font-size: 100px;
+}
+
+h2 {
+ font-size: 60px;
+ text-align: center;
+}
+
+/* SHARED BUTTON STYLES */
+.btn-filled,
+.btn-stroke {
+ padding: 0.75rem 3rem;
+ font-size: 1.2rem;
+ font-weight: 600;
+ border-radius: 10px;
+ cursor: pointer;
+ transition: 0.3s ease;
+}
+
+/* FILLED BUTTON */
+.btn-filled {
+ background-color: #493000;
+ color: white;
+ border: 2px solid #493000;
+}
+.btn-filled:hover {
+ background-color: #0a0600;
+ border-color: #0a0600;
+}
+
+/* STROKE BUTTON */
+.btn-stroke {
+ background-color: transparent;
+ color: #493000;
+ border: 3px solid #493000;
+}
+.btn-stroke:hover {
+ background-color: #0a0600;
+ color: white;
+ border-color: #0a0600;
+}
+
+/* BACK BUTTON */
+.back-button {
+ position: absolute;
+ top: 2rem;
+ left: 2rem;
+ font-size: 1.5rem;
+ color: #6b7280;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0.5rem;
+ border-radius: 8px;
+}
+
+.back-button:hover {
+ color: #493000;
+ transform: translateX(-4px);
+}
\ No newline at end of file
diff --git a/project/client/src/main.jsx b/project/client/src/main.jsx
new file mode 100644
index 000000000..a2ca76165
--- /dev/null
+++ b/project/client/src/main.jsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import App from './App'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+)
\ No newline at end of file
diff --git a/project/client/src/pages/AddPage.jsx b/project/client/src/pages/AddPage.jsx
new file mode 100644
index 000000000..c8e51a330
--- /dev/null
+++ b/project/client/src/pages/AddPage.jsx
@@ -0,0 +1,1039 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import EditorSidebar from '../components/EditorSidebar';
+import prompts from '../data/prompts';
+import '../css/AddPage.css';
+
+const AddPage = () => {
+ const navigate = useNavigate();
+ const { journalId } = useParams();
+
+ // Step 6: Sticker Library
+ const emojiStickers = [
+ { id: 1, emoji: 'β', name: 'Star', type: 'emoji' },
+ { id: 2, emoji: 'β€οΈ', name: 'Heart', type: 'emoji' },
+ { id: 3, emoji: 'π', name: 'Party', type: 'emoji' },
+ { id: 4, emoji: 'π', name: 'Sparkle', type: 'emoji' },
+ { id: 5, emoji: 'π«', name: 'Dizzy', type: 'emoji' },
+ { id: 6, emoji: 'β¨', name: 'Sparkles', type: 'emoji' },
+ { id: 7, emoji: 'π', name: 'Balloon', type: 'emoji' },
+ { id: 8, emoji: 'π', name: 'Confetti', type: 'emoji' },
+ { id: 9, emoji: 'π', name: 'Gift Heart', type: 'emoji' },
+ { id: 10, emoji: 'π', name: 'Gift', type: 'emoji' },
+ { id: 11, emoji: 'πΈ', name: 'Cherry Blossom', type: 'emoji' },
+ { id: 12, emoji: 'πΊ', name: 'Hibiscus', type: 'emoji' }
+ ];
+
+ // Image Stickers from public/stickers folder
+ const imageStickers = [
+ { id: 101, image: '/stickers/circle.jpg', name: 'Circle', type: 'image' },
+ { id: 102, image: '/stickers/collage.jpg', name: 'Collage', type: 'image' },
+ { id: 103, image: '/stickers/flowers.jpg', name: 'Flowers', type: 'image' },
+ { id: 104, image: '/stickers/hearts.jpg', name: 'Hearts', type: 'image' },
+ { id: 105, image: '/logo.png', name: 'Logo', type: 'image' },
+ { id: 106, image: '/stickers/Meow.jpg', name: 'Meow', type: 'image' },
+ { id: 107, image: '/stickers/moon.jpg', name: 'Moon', type: 'image' },
+ { id: 108, image: '/stickers/stars.jpg', name: 'Stars', type: 'image' }
+ ];
+
+ // Combined sticker library
+ const stickerLibrary = [...emojiStickers, ...imageStickers];
+
+ // Page Background Options
+ const pageBackgrounds = [
+ { id: 'white', name: 'White', image: '/pages/white.jpg' },
+ { id: 'black', name: 'Black', image: '/pages/black.jpg' },
+ { id: 'dotted', name: 'Dotted', image: '/pages/dotted.jpg' },
+ { id: 'lines_white', name: 'Lined', image: '/pages/lines_white.jpg' },
+ { id: 'notebook_lines', name: 'Notebook', image: '/pages/notebook_lines.jpg' },
+ { id: 'pink', name: 'Pink', image: '/pages/pink.jpg' },
+ { id: 'square', name: 'Grid', image: '/pages/square.jpg' },
+ { id: 'note', name: 'Note', image: '/pages/note.jpg' }
+ ];
+
+ // Step 3: State Management
+ const [pageData, setPageData] = useState({
+ pageColor: 'white',
+ pageBackgroundImage: null,
+ images: [],
+ stickers: [],
+ textElements: [],
+ doodle: null,
+ spotifyUrl: null,
+ prompt: null
+ });
+
+ const [expandedSections, setExpandedSections] = useState({
+ stickers: false,
+ doodles: false,
+ text: false,
+ upload: false
+ });
+
+ // Step 7: Doodle State
+ const [isDoodling, setIsDoodling] = useState(false);
+ const [brushColor, setBrushColor] = useState('#000000');
+ const [brushSize, setBrushSize] = useState(5);
+ const doodleCanvasRef = useRef(null);
+ const pageCanvasRef = useRef(null);
+ const isDrawingRef = useRef(false);
+ const lastPosRef = useRef({ x: 0, y: 0 });
+
+ // Step 8: Text State
+ const [textInput, setTextInput] = useState('');
+ const [textSize, setTextSize] = useState(24);
+ const [textColor, setTextColor] = useState('#000000');
+
+ // Prompt and Spotify Modal State
+ const [showPromptModal, setShowPromptModal] = useState(false);
+ const [currentPrompt, setCurrentPrompt] = useState(null);
+ const [showSpotifyModal, setShowSpotifyModal] = useState(false);
+ const [spotifyInput, setSpotifyInput] = useState('');
+ const [spotifyError, setSpotifyError] = useState('');
+ const [spotifyPreviewId, setSpotifyPreviewId] = useState(null);
+
+ // Save State
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState('');
+ const [saveSuccess, setSaveSuccess] = useState('');
+
+ // Helper function to update page background
+ const updatePageBackground = (backgroundId, backgroundImage = null) => {
+ setPageData(prev => ({
+ ...prev,
+ pageColor: backgroundId,
+ pageBackgroundImage: backgroundImage
+ }));
+ };
+
+ // Helper function to toggle sections
+ const toggleSection = (sectionName) => {
+ setExpandedSections(prev => ({
+ ...prev,
+ [sectionName]: !prev[sectionName]
+ }));
+ };
+
+ const getCanvasBackgroundStyle = () => {
+ if (pageData.pageBackgroundImage) {
+ return {
+ backgroundImage: `url(${pageData.pageBackgroundImage})`,
+ backgroundSize: 'cover',
+ backgroundPosition: 'center',
+ backgroundRepeat: 'no-repeat'
+ };
+ }
+ const colorMap = {
+ 'white': '#ffffff',
+ 'gray': '#808080',
+ 'black': '#000000'
+ };
+ return {
+ backgroundColor: colorMap[pageData.pageColor] || '#ffffff'
+ };
+ };
+
+ const getCanvasTextColor = () => {
+ // Use black text for light backgrounds, white for dark
+ if (pageData.pageBackgroundImage) {
+ // For images, default to black text (can be adjusted per image if needed)
+ const darkBackgrounds = ['black.jpg'];
+ const isDark = darkBackgrounds.some(bg => pageData.pageBackgroundImage.includes(bg));
+ return isDark ? '#ffffff' : '#000000';
+ }
+ return pageData.pageColor === 'black' ? '#ffffff' : '#000000';
+ };
+
+ const getRandomPrompt = () => {
+ if (!prompts.length) return null;
+ const randomPrompt = prompts[Math.floor(Math.random() * prompts.length)];
+ setCurrentPrompt(randomPrompt);
+ return randomPrompt;
+ };
+
+ const extractSpotifyTrackId = (url) => {
+ if (!url) return null;
+ const trackMatch = url.match(/spotify\.com\/track\/([a-zA-Z0-9]+)/);
+ if (trackMatch && trackMatch[1]) return trackMatch[1];
+ const uriMatch = url.match(/spotify:track:([a-zA-Z0-9]+)/);
+ if (uriMatch && uriMatch[1]) return uriMatch[1];
+ return null;
+ };
+
+ const isValidSpotifyUrl = (url) => {
+ if (!url) return false;
+ return url.includes('spotify.com') || url.startsWith('spotify:track:');
+ };
+
+ const getSpotifyEmbedUrl = (url) => {
+ const trackId = extractSpotifyTrackId(url);
+ if (!trackId) return null;
+ return `https://open.spotify.com/embed/track/${trackId}?utm_source=generator`;
+ };
+
+ // Step 6: Sticker Functions
+ const handleStickerClick = (sticker) => {
+ const stickerId = `sticker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ const canvasRect = pageCanvasRef.current?.getBoundingClientRect();
+ const centerX = canvasRect ? canvasRect.width / 2 - 40 : 400;
+ const centerY = canvasRect ? canvasRect.height / 2 - 40 : 300;
+
+ const stickerData = {
+ id: stickerId,
+ stickerId: sticker.id,
+ type: sticker.type || 'emoji',
+ emoji: sticker.emoji || null,
+ image: sticker.image || null,
+ name: sticker.name,
+ x: centerX,
+ y: centerY,
+ rotation: 0
+ };
+
+ setPageData(prev => ({
+ ...prev,
+ stickers: [...prev.stickers, stickerData]
+ }));
+ };
+
+ const updateStickerPosition = (stickerId, newX, newY) => {
+ setPageData(prev => ({
+ ...prev,
+ stickers: prev.stickers.map(sticker =>
+ sticker.id === stickerId ? { ...sticker, x: newX, y: newY } : sticker
+ )
+ }));
+ };
+
+ // Step 7: Doodle Functions
+ const setupDoodleCanvas = () => {
+ const canvas = doodleCanvasRef.current;
+ const container = pageCanvasRef.current;
+ if (!canvas || !container) return;
+
+ const rect = container.getBoundingClientRect();
+ canvas.width = rect.width;
+ canvas.height = rect.height;
+
+ const ctx = canvas.getContext('2d');
+ ctx.strokeStyle = brushColor;
+ ctx.lineWidth = brushSize;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ };
+
+ const startDoodling = () => {
+ setIsDoodling(true);
+ setupDoodleCanvas();
+ };
+
+ const stopDoodling = () => {
+ setIsDoodling(false);
+ isDrawingRef.current = false;
+ saveDoodle();
+ };
+
+ const saveDoodle = () => {
+ const canvas = doodleCanvasRef.current;
+ if (!canvas) return;
+ const imageData = canvas.toDataURL('image/png');
+ setPageData(prev => ({ ...prev, doodle: imageData }));
+ };
+
+ const clearDoodle = () => {
+ const canvas = doodleCanvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ };
+
+ const getCanvasCoordinates = (e) => {
+ const canvas = doodleCanvasRef.current;
+ if (!canvas) return { x: 0, y: 0 };
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top
+ };
+ };
+
+ const startDrawing = (e) => {
+ if (!isDoodling) return;
+ isDrawingRef.current = true;
+ const coords = getCanvasCoordinates(e);
+ lastPosRef.current = coords;
+ };
+
+ const draw = (e) => {
+ if (!isDoodling || !isDrawingRef.current) return;
+ const canvas = doodleCanvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ const coords = getCanvasCoordinates(e);
+
+ ctx.beginPath();
+ ctx.moveTo(lastPosRef.current.x, lastPosRef.current.y);
+ ctx.lineTo(coords.x, coords.y);
+ ctx.stroke();
+
+ lastPosRef.current = coords;
+ };
+
+ const stopDrawing = () => {
+ isDrawingRef.current = false;
+ };
+
+ // Step 8: Text Functions
+ const handleAddText = () => {
+ if (!textInput.trim()) {
+ alert('Please enter some text');
+ return;
+ }
+
+ const textId = `text-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ const canvasRect = pageCanvasRef.current?.getBoundingClientRect();
+ const centerX = canvasRect ? canvasRect.width / 2 : 400;
+ const centerY = canvasRect ? canvasRect.height / 2 : 300;
+
+ const textData = {
+ id: textId,
+ content: textInput,
+ x: centerX,
+ y: centerY,
+ fontSize: textSize,
+ color: textColor
+ };
+
+ setPageData(prev => ({
+ ...prev,
+ textElements: [...prev.textElements, textData]
+ }));
+
+ setTextInput('');
+ };
+
+ const updateTextPosition = (textId, newX, newY) => {
+ setPageData(prev => ({
+ ...prev,
+ textElements: prev.textElements.map(text =>
+ text.id === textId ? { ...text, x: newX, y: newY } : text
+ )
+ }));
+ };
+
+ // Step 11: Right Sidebar Action Button Handlers
+ const handleMusicClick = () => {
+ setSpotifyError('');
+ const existingUrl = pageData.spotifyUrl || '';
+ setSpotifyInput(existingUrl);
+ setSpotifyPreviewId(extractSpotifyTrackId(existingUrl));
+ setShowSpotifyModal(true);
+ };
+
+ const handleChatBubbleClick = () => {
+ setShowPromptModal(true);
+ getRandomPrompt();
+ };
+
+ const handleClosePromptModal = () => {
+ setShowPromptModal(false);
+ };
+
+ const handleGetNewPrompt = () => {
+ getRandomPrompt();
+ };
+
+ const handleUsePrompt = () => {
+ if (!currentPrompt) {
+ getRandomPrompt();
+ }
+ setPageData(prev => ({ ...prev, prompt: currentPrompt || prompts[0] || '' }));
+ setShowPromptModal(false);
+ };
+
+ const handleSpotifyInputChange = (value) => {
+ setSpotifyInput(value);
+ setSpotifyError('');
+ const trackId = extractSpotifyTrackId(value);
+ setSpotifyPreviewId(trackId);
+ };
+
+ const handleSaveSpotifyUrl = () => {
+ if (!spotifyInput.trim()) {
+ setSpotifyError('Please paste a Spotify track link.');
+ return;
+ }
+ if (!isValidSpotifyUrl(spotifyInput) || !extractSpotifyTrackId(spotifyInput)) {
+ setSpotifyError('Enter a valid Spotify track URL.');
+ return;
+ }
+ setPageData(prev => ({ ...prev, spotifyUrl: spotifyInput.trim() }));
+ setShowSpotifyModal(false);
+ };
+
+ const handleClearSpotifyUrl = () => {
+ setSpotifyInput('');
+ setSpotifyPreviewId(null);
+ setSpotifyError('');
+ setPageData(prev => ({ ...prev, spotifyUrl: null }));
+ setShowSpotifyModal(false);
+ };
+
+ const buildPagePayload = () => {
+ const sanitizedImages = Array.isArray(pageData.images)
+ ? pageData.images.map((image) => ({
+ id: image.id,
+ fileName: image.fileName,
+ data: image.data,
+ x: image.x,
+ y: image.y,
+ width: image.width,
+ height: image.height
+ }))
+ : [];
+
+ return {
+ pageColor: pageData.pageColor,
+ pageBackgroundImage: pageData.pageBackgroundImage || null,
+ prompt: pageData.prompt?.trim() || '',
+ images: sanitizedImages,
+ stickers: Array.isArray(pageData.stickers) ? pageData.stickers : [],
+ textElements: Array.isArray(pageData.textElements) ? pageData.textElements : [],
+ doodle: pageData.doodle || null,
+ spotifyUrl: pageData.spotifyUrl || null,
+ metadata: {
+ stickerCount: pageData.stickers.length,
+ imageCount: pageData.images.length,
+ textCount: pageData.textElements.length,
+ brushColor,
+ brushSize,
+ textColor,
+ textSize,
+ savedFrom: 'add-page',
+ timestamp: new Date().toISOString()
+ }
+ };
+ };
+
+ const handleNext = async () => {
+ setSaveError('');
+ setSaveSuccess('');
+
+ const hasContent =
+ pageData.textElements.length > 0 ||
+ pageData.images.length > 0 ||
+ pageData.stickers.length > 0 ||
+ !!pageData.doodle ||
+ !!pageData.spotifyUrl ||
+ !!(pageData.prompt && pageData.prompt.trim());
+
+ if (!hasContent) {
+ alert('Add at least one element to your page before continuing.');
+ return;
+ }
+
+ const payload = buildPagePayload();
+
+ try {
+ setIsSaving(true);
+
+ const response = await fetch(`http://localhost:3000/api/journals/${journalId}/pages`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'include',
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ let message = 'Failed to save page.';
+ const contentType = response.headers.get('content-type');
+
+ if (contentType && contentType.includes('application/json')) {
+ try {
+ const errorData = await response.json();
+ if (errorData?.message) {
+ message = errorData.message;
+ } else if (errorData?.error) {
+ message = `${errorData.error}: ${errorData.message || 'Unknown error'}`;
+ }
+ console.error('Server error response:', errorData);
+ } catch (parseError) {
+ console.error('Failed to parse error response:', parseError);
+ message = `Server error (${response.status}): Failed to parse response`;
+ }
+ } else {
+ const errorText = await response.text();
+ console.error('Raw error response:', errorText);
+ message = `Server error (${response.status}): ${errorText || 'Unknown error'}`;
+ }
+ throw new Error(message);
+ }
+
+ const savedPage = await response.json();
+ setSaveSuccess('Page saved successfully!');
+
+ // Navigate to PageDetails with the saved page data
+ navigate(`/journals/${journalId}/pages/${savedPage.id || savedPage.page_id || 'new'}`, {
+ state: { pageData: savedPage }
+ });
+ } catch (error) {
+ console.error('Failed to save page', error);
+ setSaveError(error.message || 'Failed to save page.');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ // Step 9: Image Upload Functions
+ const validateImageFile = (file) => {
+ const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
+ if (!validTypes.includes(file.type)) {
+ alert('Invalid file type. Please upload a JPG, PNG, GIF, or WebP image.');
+ return false;
+ }
+ const maxSize = 10 * 1024 * 1024;
+ if (file.size > maxSize) {
+ alert('File size too large. Please upload an image smaller than 10MB.');
+ return false;
+ }
+ return true;
+ };
+
+ const handleImageUpload = (file) => {
+ if (!validateImageFile(file)) return;
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const imageData = e.target.result;
+ const imageId = `image-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ const canvasRect = pageCanvasRef.current?.getBoundingClientRect();
+ const centerX = canvasRect ? canvasRect.width / 2 - 150 : 400;
+ const centerY = canvasRect ? canvasRect.height / 2 - 150 : 300;
+
+ const imageDataObj = {
+ id: imageId,
+ data: imageData,
+ fileName: file.name,
+ x: centerX,
+ y: centerY,
+ width: 300,
+ height: 300
+ };
+
+ setPageData(prev => ({
+ ...prev,
+ images: [...prev.images, imageDataObj]
+ }));
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const updateImagePosition = (imageId, newX, newY) => {
+ setPageData(prev => ({
+ ...prev,
+ images: prev.images.map(img =>
+ img.id === imageId ? { ...img, x: newX, y: newY } : img
+ )
+ }));
+ };
+
+ useEffect(() => {
+ if (isDoodling) {
+ setupDoodleCanvas();
+ }
+ }, [isDoodling, brushColor, brushSize]);
+
+ useEffect(() => {
+ if (pageData.doodle && doodleCanvasRef.current && !isDoodling) {
+ const img = new Image();
+ img.onload = () => {
+ setupDoodleCanvas();
+ const ctx = doodleCanvasRef.current.getContext('2d');
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = pageData.doodle;
+ }
+ }, [pageData.doodle, isDoodling]);
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (isDoodling && doodleCanvasRef.current) {
+ setupDoodleCanvas();
+ if (pageData.doodle) {
+ const img = new Image();
+ img.onload = () => {
+ const ctx = doodleCanvasRef.current.getContext('2d');
+ ctx.drawImage(img, 0, 0);
+ };
+ img.src = pageData.doodle;
+ }
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, [isDoodling, pageData.doodle]);
+
+ // Draggable Element Component
+ const DraggableElement = ({ id, x, y, width, height, className = '', onPositionChange, children }) => {
+ const [isDragging, setIsDragging] = useState(false);
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
+ const elementRef = useRef(null);
+ const dragStartRef = useRef({ mouseX: 0, mouseY: 0, elementX: 0, elementY: 0 });
+ const currentOffsetRef = useRef({ x: 0, y: 0 });
+
+ const handleMouseDown = (e) => {
+ // Don't start drag if doodling is active
+ if (isDoodling) return;
+
+ e.stopPropagation();
+ e.preventDefault();
+
+ const canvas = pageCanvasRef.current;
+ if (!canvas || !elementRef.current) return;
+
+ const canvasRect = canvas.getBoundingClientRect();
+ const elementRect = elementRef.current.getBoundingClientRect();
+
+ // Calculate offset from mouse click to element top-left
+ const offsetX = e.clientX - elementRect.left;
+ const offsetY = e.clientY - elementRect.top;
+
+ // Store initial positions
+ dragStartRef.current = {
+ mouseX: e.clientX,
+ mouseY: e.clientY,
+ elementX: x,
+ elementY: y,
+ offsetX: offsetX,
+ offsetY: offsetY
+ };
+
+ setIsDragging(true);
+ setDragOffset({ x: 0, y: 0 });
+ };
+
+ useEffect(() => {
+ if (!isDragging) return;
+
+ const handleMouseMove = (e) => {
+ e.preventDefault();
+
+ const canvas = pageCanvasRef.current;
+ if (!canvas) return;
+
+ // Calculate mouse movement
+ const deltaX = e.clientX - dragStartRef.current.mouseX;
+ const deltaY = e.clientY - dragStartRef.current.mouseY;
+
+ // Calculate new position
+ let newX = dragStartRef.current.elementX + deltaX;
+ let newY = dragStartRef.current.elementY + deltaY;
+
+ // Get element dimensions
+ const elementWidth = width || 80;
+ const elementHeight = height || 80;
+
+ // Constrain to canvas bounds
+ const maxX = canvas.offsetWidth - elementWidth;
+ const maxY = canvas.offsetHeight - elementHeight;
+ const constrainedX = Math.max(0, Math.min(newX, maxX));
+ const constrainedY = Math.max(0, Math.min(newY, maxY));
+
+ // Update visual position immediately for smooth dragging
+ const offsetX = constrainedX - dragStartRef.current.elementX;
+ const offsetY = constrainedY - dragStartRef.current.elementY;
+
+ // Store in ref for immediate access
+ currentOffsetRef.current = { x: offsetX, y: offsetY };
+
+ // Use requestAnimationFrame for smooth updates
+ requestAnimationFrame(() => {
+ setDragOffset({ x: offsetX, y: offsetY });
+ });
+ };
+
+ const handleMouseUp = (e) => {
+ e.preventDefault();
+
+ // Calculate final position using current offset from ref
+ const finalX = dragStartRef.current.elementX + currentOffsetRef.current.x;
+ const finalY = dragStartRef.current.elementY + currentOffsetRef.current.y;
+
+ // Update position in parent state only on mouse up
+ onPositionChange(finalX, finalY);
+
+ setIsDragging(false);
+ setDragOffset({ x: 0, y: 0 });
+ currentOffsetRef.current = { x: 0, y: 0 };
+ };
+
+ document.addEventListener('mousemove', handleMouseMove, { passive: false });
+ document.addEventListener('mouseup', handleMouseUp);
+ document.body.style.userSelect = 'none';
+ document.body.style.cursor = 'grabbing';
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.body.style.userSelect = '';
+ document.body.style.cursor = '';
+ };
+ }, [isDragging, width, height, onPositionChange]);
+
+ return (
+
+ {children}
+
+ );
+ };
+
+ return (
+
+
+ {/* Main Container with Three Sections */}
+
+ {/* Left Sidebar - Editor Tools */}
+
+
+ {/* Center Section - Page Canvas */}
+ {/* Step 10: Page Canvas Rendering System */}
+
+ Add Page
+ {saveError && (
+
+ {saveError}
+
+ )}
+ {saveSuccess && (
+
+ {saveSuccess}
+
+ )}
+ {pageData.prompt && (
+
+
Prompt
+
{pageData.prompt}
+
+ )}
+ {getSpotifyEmbedUrl(pageData.spotifyUrl) && (
+
+
+ Spotify track
+
+
+
+
+ )}
+
+ {/* Step 10: Render all page elements in correct layering order */}
+ {/* Layer 1: Images (z-index: 5) */}
+ {/* Render Images */}
+ {pageData.images.map(image => (
+
updateImagePosition(image.id, newX, newY)}
+ >
+
+
+ ))}
+
+ {/* Layer 2: Doodles (z-index: 8) - rendered as canvas overlay */}
+ {/* Doodle Canvas Overlay */}
+
+
+
+ {/* Right Sidebar - Action Buttons */}
+ {/* Step 11: Music and Chat Bubble Buttons */}
+
+
+
+ {showPromptModal && (
+
+
+
+
Need a memory prompt?
+
+ {currentPrompt || 'Click the button to get inspired!'}
+
+
+
+
+
+
+
+ )}
+
+ {showSpotifyModal && (
+
+
+
+
Link a Spotify song
+
+
handleSpotifyInputChange(e.target.value)}
+ />
+ {spotifyError &&
{spotifyError}
}
+
+ {spotifyPreviewId ? (
+
+ ) : (
+
+ Paste a Spotify track link to see a preview.
+
+ )}
+
+
+
+
+
+
+
+ )}
+
+ {/* Footer/Bottom Navigation */}
+
+
+ );
+};
+
+export default AddPage;
diff --git a/project/client/src/pages/AllJournals.jsx b/project/client/src/pages/AllJournals.jsx
new file mode 100644
index 000000000..1dda3d986
--- /dev/null
+++ b/project/client/src/pages/AllJournals.jsx
@@ -0,0 +1,232 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import '../css/AllJournals.css';
+import axios from 'axios';
+
+const AllJournals = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const [journals, setJournals] = useState([]);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filterDate, setFilterDate] = useState('');
+ const [filterLocation, setFilterLocation] = useState('');
+ const [filterTags, setFilterTags] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [openMenuId, setOpenMenuId] = useState(null);
+
+ const locations = ['New York', 'Los Angeles', 'Chicago', 'Other'];
+ const tags = ['Travel', 'Work', 'Personal', 'School'];
+
+ useEffect(() => {
+ fetchJournals();
+ }, [location]);
+
+ useEffect(() => {
+ const handleClickOutside = (e) => {
+ if (!e.target.closest('.menu-container')) {
+ setOpenMenuId(null);
+ }
+ };
+ document.addEventListener('click', handleClickOutside);
+ return () => document.removeEventListener('click', handleClickOutside);
+ }, []);
+
+ const fetchJournals = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get('http://localhost:3000/api/journals', {
+ withCredentials: true,
+ });
+ setJournals(response.data);
+ } catch (err) {
+ console.error('Error fetching journals:', err);
+ if (err.response?.status === 401) {
+ navigate('/');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const filteredJournals = journals
+ .filter(j => j.name.toLowerCase().includes(searchTerm.toLowerCase()))
+ .filter(j => filterDate ? j.createdAt?.includes(filterDate) : true)
+ .filter(j => filterLocation ? j.location === filterLocation : true)
+ .filter(j => filterTags.length > 0 ? filterTags.every(tag => j.tags?.includes(tag)) : true);
+
+ const handleAddPage = (e, journalId) => {
+ e.stopPropagation();
+ navigate(`/journals/${journalId}/pages/add`);
+ };
+
+ const handleViewJournal = (journalId) => {
+ navigate(`/journals/${journalId}`);
+ };
+
+ const toggleMenu = (e, journalId) => {
+ e.stopPropagation();
+ setOpenMenuId(openMenuId === journalId ? null : journalId);
+ };
+
+ const handleEdit = (e, journalId) => {
+ e.stopPropagation();
+ navigate(`/journals/${journalId}/edit`);
+ };
+
+ const handleDelete = async (e, journalId) => {
+ e.stopPropagation();
+
+ if (!window.confirm('Are you sure you want to delete this journal? This action cannot be undone.')) {
+ return;
+ }
+
+ try {
+ await axios.delete(`http://localhost:3000/api/journals/${journalId}`, {
+ withCredentials: true,
+ });
+
+ setJournals(journals.filter(j => j.id !== journalId));
+ setOpenMenuId(null);
+ } catch (err) {
+ console.error('Error deleting journal:', err);
+ alert('Failed to delete journal. Please try again.');
+ }
+ };
+
+ const toggleTag = (tag) => {
+ setFilterTags(prev =>
+ prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]
+ );
+ };
+
+ const handleCreateJournal = () => {
+ navigate('/journals/create');
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading your journals...
+
+
+ );
+ }
+
+ return (
+
+ {/* Centered Title */}
+
All Journals
+
+ {/* Search and Create Button Row */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+ {filteredJournals.length === 0 ? (
+
+
+
No journals found
+
+ {journals.length === 0
+ ? "Start journaling your memories by creating your first journal!"
+ : "Try adjusting your search or filters."
+ }
+
+ {journals.length === 0 && (
+
+ )}
+
+ ) : (
+
+ {filteredJournals.map(journal => (
+
handleViewJournal(journal.id)}>
+
+ {journal.coverImage ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ {openMenuId === journal.id && (
+
+
+
+
+ )}
+
+
+
+
+
+
{journal.name}
+
+
+
+ {journal.description && (
+
{journal.description}
+ )}
+
+
+
+
+ {journal.pageCount || 0} pages
+
+
+
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default AllJournals;
\ No newline at end of file
diff --git a/project/client/src/pages/AllPages.jsx b/project/client/src/pages/AllPages.jsx
new file mode 100644
index 000000000..dc65779d0
--- /dev/null
+++ b/project/client/src/pages/AllPages.jsx
@@ -0,0 +1,255 @@
+// src/pages/AllPages.jsx
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import '../css/AllJournals.css'; // reuse styles
+import axios from 'axios';
+
+const AllPages = () => {
+ const navigate = useNavigate();
+ const { journalId } = useParams();
+
+ const [journal, setJournal] = useState(null);
+ const [pages, setPages] = useState([]);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filterDate, setFilterDate] = useState('');
+ const [filterLocation, setFilterLocation] = useState('');
+ const [filterTags, setFilterTags] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Extract dynamic filter values from existing pages
+ const uniqueTags = Array.from(
+ new Set(
+ pages.flatMap(p => {
+ let tags = p.metadata?.tags || [];
+ if (typeof tags === 'string') {
+ tags = tags.split(',').map(t => t.trim());
+ }
+ return tags;
+ })
+ )
+ );
+
+ const uniqueLocations = Array.from(
+ new Set(
+ pages
+ .map(p => p.metadata?.location)
+ .filter(loc => loc && loc.trim() !== '')
+ )
+ );
+
+ const uniqueDates = Array.from(
+ new Set(
+ pages
+ .map(p => p.metadata?.date)
+ .filter(d => d && d.trim() !== '')
+ .map(d => {
+ // Convert "November 18, 2025" β "2025-11"
+ const parsed = new Date(d);
+ return !isNaN(parsed) ? parsed.toISOString().slice(0, 7) : null;
+ })
+ .filter(Boolean)
+ )
+ );
+
+ useEffect(() => {
+ fetchJournalAndPages();
+ }, [journalId]);
+
+ const fetchJournalAndPages = async () => {
+ try {
+ setLoading(true);
+
+ // fetch journal info
+ const journalRes = await axios.get(`http://localhost:3000/api/journals/${journalId}`, {
+ withCredentials: true,
+ });
+ setJournal(journalRes.data);
+
+ // fetch pages for this journal
+ const pagesRes = await axios.get(`http://localhost:3000/api/journals/${journalId}/pages`, {
+ withCredentials: true,
+ });
+ setPages(pagesRes.data);
+
+ } catch (err) {
+ console.error('Error fetching pages:', err);
+ if (err.response?.status === 401) {
+ navigate('/');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleBack = () => navigate('/journals');
+
+ const filteredPages = pages
+ .filter(p => p.title.toLowerCase().includes(searchTerm.toLowerCase()))
+ .filter(p => {
+ if (!filterDate) return true;
+ const d = p.metadata?.date;
+ if (!d) return false;
+ const parsed = new Date(d);
+ const yearMonth = !isNaN(parsed) ? parsed.toISOString().slice(0, 7) : '';
+ return yearMonth === filterDate;
+ })
+
+ .filter(p => {
+ if (!filterLocation) return true;
+ return p.metadata?.location === filterLocation;
+ })
+
+ .filter(p => {
+ if (filterTags.length === 0) return true;
+ let tags = p.metadata?.tags || [];
+ if (typeof tags === 'string') {
+ tags = tags.split(',').map(t => t.trim());
+ }
+ return filterTags.every(tag => tags.includes(tag));
+ });
+
+ const handleViewPage = (pageId) => {
+ navigate(`/journals/${journalId}/view/${pageId}`);
+ };
+
+ const handleAddPage = () => {
+ navigate(`/journals/${journalId}/pages/add`);
+ };
+
+ const toggleTag = (tag) => {
+ setFilterTags(prev =>
+ prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]
+ );
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
{journal?.name || 'Pages'}
+
+ {journal?.description && (
+
{journal.description}
+ )}
+
+ {/* Search and Add Page Button Row */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+ {/* Filters */}
+
+
Filters:
+
+
+
+
+
+
+
+
+ {uniqueTags.map(tag => (
+
+ ))}
+
+
+
+
+ {uniqueTags.map(tag => (
+
+ ))}
+
+
+
+ {/* Page Cards */}
+ {filteredPages.length === 0 ? (
+
+
+
No pages found
+
+ {pages.length === 0
+ ? "Add your first page to this journal!"
+ : "Try adjusting your search or filters."
+ }
+
+ {pages.length === 0 && (
+
+ )}
+
+ ) : (
+
+ {filteredPages.map(page => (
+
handleViewPage(page.id)}>
+
+ {/* Placeholder color cover for pages */}
+
+
+
+
+
+
{page.title}
+
+
+
+
+
+ {new Date(page.createdAt).toLocaleDateString()}
+
+ {page.location && | {page.location}}
+
+
+
+ ))}
+
+ )}
+
+ );
+};
+export default AllPages;
diff --git a/project/client/src/pages/CreateJournal.jsx b/project/client/src/pages/CreateJournal.jsx
new file mode 100644
index 000000000..febd60ebe
--- /dev/null
+++ b/project/client/src/pages/CreateJournal.jsx
@@ -0,0 +1,241 @@
+// src/pages/CreateJournal.jsx
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import '../css/CreateJournal.css';
+import axios from 'axios';
+
+// Cover options - white removed from selectable options
+const coverOptions = [
+ { color: '#FFB6D9', name: 'pink', image: '/covers/pinkcover.jpg' },
+ { color: '#9ED9CC', name: 'green', image: '/covers/greencover.jpg' },
+ { color: '#A0CED9', name: 'blue', image: '/covers/bluecover.jpg' },
+ { color: '#FFD580', name: 'yellow', image: '/covers/yellowcover.jpg' },
+ { color: '#B8B8B8', name: 'gray', image: '/covers/graycover.jpg' },
+];
+
+const CreateJournal = () => {
+ const navigate = useNavigate();
+
+ const [journalName, setJournalName] = useState('');
+ const [description, setDescription] = useState('');
+ const [selectedCover, setSelectedCover] = useState(null); // Start with null (no selection)
+ const [customImage, setCustomImage] = useState(null);
+ const [customImagePreview, setCustomImagePreview] = useState(null);
+
+ const handleBack = () => navigate('/journals');
+
+ const handleCoverSelect = (cover) => {
+ setSelectedCover(cover);
+ setCustomImage(null);
+ setCustomImagePreview(null);
+ };
+
+ const handleImageUpload = (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ if (!file.type.startsWith('image/')) {
+ alert('Please upload an image file.');
+ return;
+ }
+
+ if (file.size > 5 * 1024 * 1024) {
+ alert('Image size should be less than 5MB.');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setCustomImagePreview(reader.result);
+ setCustomImage(file);
+ setSelectedCover(null);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!journalName.trim()) {
+ alert('Please enter a journal name.');
+ return;
+ }
+
+ // Validate that user selected a cover or uploaded an image
+ if (!selectedCover && !customImage) {
+ alert('Please choose a cover or upload an image.');
+ return;
+ }
+
+ if (customImage) {
+ const formData = new FormData();
+ formData.append('name', journalName.trim());
+ formData.append('description', description.trim());
+ formData.append('coverImage', customImage);
+ formData.append('coverColor', '#FFFFFF');
+ formData.append('coverName', 'custom');
+
+ try {
+ const response = await axios.post('http://localhost:3000/api/journals', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ withCredentials: true,
+ });
+
+ console.log('Journal created:', response.data);
+ navigate('/journals');
+ } catch (err) {
+ console.error('Error creating journal:', err);
+ if (err.response?.status === 401) {
+ alert('Please log in to create a journal.');
+ navigate('/');
+ } else {
+ alert('Failed to create journal. Please try again.');
+ }
+ }
+ } else {
+ const journalData = {
+ name: journalName.trim(),
+ description: description.trim(),
+ coverImage: selectedCover.image,
+ coverColor: selectedCover.color,
+ coverName: selectedCover.name,
+ createdAt: new Date().toISOString(),
+ };
+
+ try {
+ const response = await axios.post('http://localhost:3000/api/journals', journalData, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ withCredentials: true,
+ });
+
+ console.log('Journal created:', response.data);
+ navigate('/journals');
+ } catch (err) {
+ console.error('Error creating journal:', err);
+ if (err.response?.status === 401) {
+ alert('Please log in to create a journal.');
+ navigate('/');
+ } else {
+ alert('Failed to create journal. Please try again.');
+ }
+ }
+ }
+ };
+
+ return (
+
+
+
Create New Journal
+
+
+
+
+ {customImagePreview ? (
+

+ ) : selectedCover?.image ? (
+

+ ) : (
+
+
+ Choose a cover below
+
+
+ )}
+
+
+
+
+
+
+
+
+
Or choose a preset:
+
+ {coverOptions.map((cover) => (
+
handleCoverSelect(cover)}
+ title={`${cover.name} cover`}
+ role="button"
+ tabIndex={0}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ handleCoverSelect(cover);
+ }
+ }}
+ >
+ {selectedCover?.name === cover.name && !customImagePreview && (
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default CreateJournal;
\ No newline at end of file
diff --git a/project/client/src/pages/EditJournal.jsx b/project/client/src/pages/EditJournal.jsx
new file mode 100644
index 000000000..43935ada9
--- /dev/null
+++ b/project/client/src/pages/EditJournal.jsx
@@ -0,0 +1,347 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import '../css/CreateJournal.css';
+import axios from 'axios';
+
+const coverOptions = [
+ { color: '#FFB6D9', name: 'pink', image: '/covers/pinkcover.jpg' },
+ { color: '#9ED9CC', name: 'green', image: '/covers/greencover.jpg' },
+ { color: '#A0CED9', name: 'blue', image: '/covers/bluecover.jpg' },
+ { color: '#FFD580', name: 'yellow', image: '/covers/yellowcover.jpg' },
+ { color: '#B8B8B8', name: 'gray', image: '/covers/graycover.jpg' },
+];
+
+const EditJournal = () => {
+ const navigate = useNavigate();
+ const { journalId: id } = useParams();
+
+ const [journalName, setJournalName] = useState('');
+ const [description, setDescription] = useState('');
+ const [selectedCover, setSelectedCover] = useState(null);
+ const [currentCoverImage, setCurrentCoverImage] = useState(null);
+ const [customImage, setCustomImage] = useState(null);
+ const [customImagePreview, setCustomImagePreview] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [originalData, setOriginalData] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ console.log('π EditJournal mounted');
+ console.log('π Journal ID from URL:', id);
+ console.log('π Journal ID type:', typeof id);
+ fetchJournal();
+ }, [id]);
+
+ const fetchJournal = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ console.log('π‘ Fetching journal:', id);
+ console.log('π‘ Full URL:', `http://localhost:3000/api/journals/${id}`);
+
+ const response = await axios.get(`http://localhost:3000/api/journals/${id}`, {
+ withCredentials: true,
+ });
+
+ console.log('β
Journal fetched successfully:', response.data);
+
+ const journal = response.data;
+ setJournalName(journal.name);
+ setDescription(journal.description || '');
+ setOriginalData(journal);
+
+ // Determine cover type
+ if (journal.coverImage) {
+ const preset = coverOptions.find(c => c.image === journal.coverImage);
+ if (preset) {
+ console.log('π¨ Preset cover found:', preset.name);
+ setSelectedCover(preset);
+ } else {
+ console.log('πΌοΈ Custom cover image:', journal.coverImage);
+ setCurrentCoverImage(journal.coverImage);
+ setCustomImagePreview(journal.coverImage);
+ }
+ }
+
+ setLoading(false);
+ } catch (err) {
+ console.error('β Error fetching journal:', err);
+ console.error('β Error response:', err.response?.data);
+ console.error('β Error status:', err.response?.status);
+
+ setError(err.response?.data?.message || 'Failed to load journal');
+
+ if (err.response?.status === 401) {
+ alert('Please log in to edit journals.');
+ navigate('/');
+ } else if (err.response?.status === 404) {
+ console.error('β Journal not found. ID:', id);
+ setTimeout(() => {
+ alert('Journal not found. Redirecting to journals list.');
+ navigate('/journals');
+ }, 100);
+ }
+ setLoading(false);
+ }
+ };
+
+ const handleBack = () => navigate('/journals');
+
+ const handleCoverSelect = (cover) => {
+ setSelectedCover(cover);
+ setCustomImage(null);
+ setCustomImagePreview(null);
+ setCurrentCoverImage(null);
+ };
+
+ const handleImageUpload = (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ if (!file.type.startsWith('image/')) {
+ alert('Please upload an image file.');
+ return;
+ }
+
+ if (file.size > 5 * 1024 * 1024) {
+ alert('Image size should be less than 5MB.');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setCustomImagePreview(reader.result);
+ setCustomImage(file);
+ setSelectedCover(null);
+ setCurrentCoverImage(null);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const hasChanges = () => {
+ if (!originalData) return false;
+
+ if (journalName.trim() !== originalData.name) return true;
+ if (description.trim() !== (originalData.description || '')) return true;
+ if (customImage) return true;
+ if (selectedCover && selectedCover.image !== originalData.coverImage) return true;
+
+ return false;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!journalName.trim()) {
+ alert('Please enter a journal name.');
+ return;
+ }
+
+ if (!hasChanges()) {
+ alert('No changes detected.');
+ return;
+ }
+
+ console.log('πΎ Saving changes to journal:', id);
+
+ if (customImage) {
+ const formData = new FormData();
+ formData.append('name', journalName.trim());
+ formData.append('description', description.trim());
+ formData.append('coverImage', customImage);
+ formData.append('coverColor', '#FFFFFF');
+ formData.append('coverName', 'custom');
+
+ try {
+ const response = await axios.put(`http://localhost:3000/api/journals/${id}`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ withCredentials: true,
+ });
+
+ console.log('β
Journal updated:', response.data);
+ navigate('/journals');
+ } catch (err) {
+ console.error('β Error updating journal:', err);
+ alert('Failed to update journal. Please try again.');
+ }
+ } else {
+ const journalData = {
+ name: journalName.trim(),
+ description: description.trim(),
+ coverImage: selectedCover?.image || currentCoverImage,
+ coverColor: selectedCover?.color || originalData.coverColor,
+ coverName: selectedCover?.name || originalData.coverName,
+ };
+
+ console.log('π€ Sending update:', journalData);
+
+ try {
+ const response = await axios.put(`http://localhost:3000/api/journals/${id}`, journalData, {
+ headers: { 'Content-Type': 'application/json' },
+ withCredentials: true,
+ });
+
+ console.log('β
Journal updated:', response.data);
+ navigate('/journals');
+ } catch (err) {
+ console.error('β Error updating journal:', err);
+ alert('Failed to update journal. Please try again.');
+ }
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
Error Loading Journal
+
{error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
Edit Journal
+
+
+
+
+ {customImagePreview ? (
+

+ ) : selectedCover?.image ? (
+

+ ) : currentCoverImage ? (
+

+ ) : (
+
+
+
+ Choose a cover below
+
+
+ )}
+
+
+
+
+
+
+
+
+
Or choose a preset:
+
+ {coverOptions.map((cover) => (
+
handleCoverSelect(cover)}
+ title={`${cover.name} cover`}
+ role="button"
+ tabIndex={0}
+ >
+ {selectedCover?.name === cover.name && (
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default EditJournal;
\ No newline at end of file
diff --git a/project/client/src/pages/Home.jsx b/project/client/src/pages/Home.jsx
new file mode 100644
index 000000000..2e78d5e8e
--- /dev/null
+++ b/project/client/src/pages/Home.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import "../css/Home.css";
+
+const Home = () => {
+
+ const handleGoogleLogin = () => {
+ window.location.href = "http://localhost:3000/auth/google";
+ };
+
+ return (
+
+
+ Welcome to
StickerStory
+
+
+ Your digital memory journal
+
+
+
+
+
+ );
+};
+
+export default Home;
diff --git a/project/client/src/pages/PageDetails.jsx b/project/client/src/pages/PageDetails.jsx
new file mode 100644
index 000000000..05c3eb861
--- /dev/null
+++ b/project/client/src/pages/PageDetails.jsx
@@ -0,0 +1,422 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useParams, useLocation } from 'react-router-dom';
+import PageLayout from '../components/PageLayout';
+import '../css/PageDetails.css';
+
+const PageDetails = () => {
+ const navigate = useNavigate();
+ const { journalId, pageId } = useParams();
+ const location = useLocation();
+
+ // Get today's date in a readable format
+ const getTodayDate = () => {
+ const today = new Date();
+ const options = { year: 'numeric', month: 'long', day: 'numeric' };
+ return today.toLocaleDateString('en-US', options);
+ };
+
+ // Get page data from navigation state or fetch from API
+ const [pageData, setPageData] = useState(location.state?.pageData || null);
+ const [formData, setFormData] = useState({
+ title: '',
+ date: getTodayDate(),
+ location: '',
+ description: '',
+ tags: []
+ });
+ const [tagInput, setTagInput] = useState('');
+ const [isLoading, setIsLoading] = useState(!pageData);
+ const [isSaving, setIsSaving] = useState(false);
+
+ // Fetch page data if not provided via navigation
+ useEffect(() => {
+ if (!pageData && pageId) {
+ fetch(`http://localhost:3000/api/journals/${journalId}/pages/${pageId}`, {
+ credentials: 'include'
+ })
+ .then(res => res.json())
+ .then(data => {
+ setPageData(data);
+ if (data.metadata) {
+ let tags = data.metadata.tags || [];
+ if (typeof tags === 'string') {
+ tags = tags.split(',').map(t => t.trim()).filter(t => t.length > 0);
+ }
+ setFormData({
+ title: data.metadata.title || '',
+ date: data.metadata.date || getTodayDate(),
+ location: data.metadata.location || '',
+ description: data.metadata.description || '',
+ tags: Array.isArray(tags) ? tags : []
+ });
+ } else {
+ // Set default date if no metadata
+ setFormData(prev => ({ ...prev, date: getTodayDate() }));
+ }
+ setIsLoading(false);
+ })
+ .catch(err => {
+ console.error('Failed to fetch page:', err);
+ setIsLoading(false);
+ });
+ } else if (pageData && pageData.metadata) {
+ let tags = pageData.metadata.tags || [];
+ if (typeof tags === 'string') {
+ tags = tags.split(',').map(t => t.trim()).filter(t => t.length > 0);
+ }
+ setFormData({
+ title: pageData.metadata.title || '',
+ date: pageData.metadata.date || getTodayDate(),
+ location: pageData.metadata.location || '',
+ description: pageData.metadata.description || '',
+ tags: Array.isArray(tags) ? tags : []
+ });
+ setIsLoading(false);
+ } else if (pageData) {
+ // Set default date even if no metadata
+ setFormData(prev => ({ ...prev, date: getTodayDate() }));
+ setIsLoading(false);
+ }
+ }, [pageId, journalId, pageData]);
+
+ const handleInputChange = (field, value) => {
+ setFormData(prev => ({
+ ...prev,
+ [field]: value
+ }));
+ };
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ try {
+ const payload = {
+ ...pageData,
+ metadata: {
+ ...pageData.metadata,
+ ...formData,
+ tags: Array.isArray(formData.tags) ? formData.tags : formData.tags
+ }
+ };
+
+ const url = pageId
+ ? `http://localhost:3000/api/journals/${journalId}/pages/${pageId}`
+ : `http://localhost:3000/api/journals/${journalId}/pages`;
+
+ const method = pageId ? 'PUT' : 'POST';
+
+ const response = await fetch(url, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'include',
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to save page details');
+ }
+
+ const savedPage = await response.json();
+ navigate(`/journals/${journalId}/pages/${savedPage.id}/preview`);
+ } catch (error) {
+ console.error('Failed to save:', error);
+ alert('Failed to save page details. Please try again.');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleBack = () => {
+ navigate(`/journals/${journalId}/pages/add`);
+ };
+
+ // Render page preview
+ const renderPagePreview = () => {
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ if (!pageData) {
+ return No page data available
;
+ }
+
+ const getBackgroundStyle = () => {
+ if (pageData.pageBackgroundImage) {
+ return {
+ backgroundImage: `url(${pageData.pageBackgroundImage})`,
+ backgroundSize: 'cover',
+ backgroundPosition: 'center',
+ backgroundRepeat: 'no-repeat'
+ };
+ }
+ const colorMap = {
+ 'white': '#ffffff',
+ 'gray': '#808080',
+ 'black': '#000000'
+ };
+ return {
+ backgroundColor: colorMap[pageData.pageColor] || '#ffffff'
+ };
+ };
+
+ const getTextColor = () => {
+ if (pageData.pageBackgroundImage) {
+ const darkBackgrounds = ['black.jpg'];
+ const isDark = darkBackgrounds.some(bg => pageData.pageBackgroundImage.includes(bg));
+ return isDark ? '#ffffff' : '#000000';
+ }
+ return pageData.pageColor === 'black' ? '#ffffff' : '#000000';
+ };
+
+
+ return (
+
+ {/* Images */}
+ {pageData.images && pageData.images.map(image => (
+
+

+
+ ))}
+
+ {/* Doodle */}
+ {pageData.doodle && (
+

+ )}
+
+ {/* Stickers */}
+ {pageData.stickers && pageData.stickers.map(sticker => (
+
+ {sticker.type === 'image' && sticker.image ? (
+

+ ) : (
+
{sticker.emoji}
+ )}
+
+ ))}
+
+ {/* Text Elements */}
+ {pageData.textElements && pageData.textElements.map(text => (
+
+ {text.content}
+
+ ))}
+
+ );
+ };
+
+ // Helper function to get Spotify embed URL
+ const getSpotifyEmbedUrl = (url) => {
+ if (!url) return null;
+ const trackMatch = url.match(/spotify\.com\/track\/([a-zA-Z0-9]+)/);
+ if (trackMatch && trackMatch[1]) {
+ return `https://open.spotify.com/embed/track/${trackMatch[1]}?utm_source=generator`;
+ }
+ return null;
+ };
+
+ // Render form fields
+ const renderFormFields = () => (
+ <>
+
+
+ handleInputChange('title', e.target.value)}
+ />
+
+
+ {/* Prompt Field */}
+ {pageData?.prompt && (
+
+
+
+ {pageData.prompt}
+
+
+ )}
+
+ {/* Spotify Song Field */}
+ {getSpotifyEmbedUrl(pageData?.spotifyUrl) && (
+
+ )}
+
+
+
+ handleInputChange('date', e.target.value)}
+ />
+
+
+
+
+ handleInputChange('location', e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ {formData.tags.map((tag, index) => (
+
+ {tag}
+
+
+ ))}
+
+
setTagInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && tagInput.trim()) {
+ e.preventDefault();
+ const newTag = tagInput.trim();
+ if (!formData.tags.includes(newTag)) {
+ setFormData(prev => ({
+ ...prev,
+ tags: [...prev.tags, newTag]
+ }));
+ }
+ setTagInput('');
+ }
+ }}
+ />
+
+
+ >
+ );
+
+ // Render footer actions
+ const renderFooterActions = () => (
+ <>
+
+
+ >
+ );
+
+ return (
+
+ );
+};
+
+export default PageDetails;
diff --git a/project/client/src/pages/PreviewPage.jsx b/project/client/src/pages/PreviewPage.jsx
new file mode 100644
index 000000000..50e310802
--- /dev/null
+++ b/project/client/src/pages/PreviewPage.jsx
@@ -0,0 +1,333 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import PageLayout from '../components/PageLayout';
+import '../css/PageDetails.css';
+
+const PreviewPage = () => {
+ const navigate = useNavigate();
+ const { journalId, pageId } = useParams();
+
+ const [pageData, setPageData] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [showSuccessMessage, setShowSuccessMessage] = useState(false);
+
+ useEffect(() => {
+ if (pageId) {
+ fetch(`http://localhost:3000/api/journals/${journalId}/pages/${pageId}`, {
+ credentials: 'include'
+ })
+ .then(res => res.json())
+ .then(data => {
+ setPageData(data);
+ setIsLoading(false);
+ })
+ .catch(err => {
+ console.error('Failed to fetch page:', err);
+ setIsLoading(false);
+ });
+ }
+ }, [pageId, journalId]);
+
+ const handleBack = () => {
+ navigate(`/journals/${journalId}/pages/${pageId}`);
+ };
+
+ const handleSave = async () => {
+ if (!pageData) {
+ alert('No page data to save.');
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ const url = `http://localhost:3000/api/journals/${journalId}/pages/${pageId}`;
+
+ const response = await fetch(url, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'include',
+ body: JSON.stringify(pageData)
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to save page');
+ }
+
+ const savedPage = await response.json();
+ setShowSuccessMessage(true);
+ // Navigate to journals page after showing success message
+ setTimeout(() => {
+ navigate('/journals');
+ }, 1500);
+ } catch (error) {
+ console.error('Failed to save:', error);
+ alert('Failed to save page. Please try again.');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ // Render page preview (same as PageDetails)
+ const renderPagePreview = () => {
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ if (!pageData) {
+ return No page data available
;
+ }
+
+ const getBackgroundStyle = () => {
+ if (pageData.pageBackgroundImage) {
+ return {
+ backgroundImage: `url(${pageData.pageBackgroundImage})`,
+ backgroundSize: 'cover',
+ backgroundPosition: 'center',
+ backgroundRepeat: 'no-repeat'
+ };
+ }
+ const colorMap = {
+ 'white': '#ffffff',
+ 'gray': '#808080',
+ 'black': '#000000'
+ };
+ return {
+ backgroundColor: colorMap[pageData.pageColor] || '#ffffff'
+ };
+ };
+
+ const getTextColor = () => {
+ if (pageData.pageBackgroundImage) {
+ const darkBackgrounds = ['black.jpg'];
+ const isDark = darkBackgrounds.some(bg => pageData.pageBackgroundImage.includes(bg));
+ return isDark ? '#ffffff' : '#000000';
+ }
+ return pageData.pageColor === 'black' ? '#ffffff' : '#000000';
+ };
+
+ const getSpotifyEmbedUrl = (url) => {
+ if (!url) return null;
+ const trackMatch = url.match(/spotify\.com\/track\/([a-zA-Z0-9]+)/);
+ if (trackMatch && trackMatch[1]) {
+ return `https://open.spotify.com/embed/track/${trackMatch[1]}?utm_source=generator`;
+ }
+ return null;
+ };
+
+ return (
+
+ {/* Images */}
+ {pageData.images && pageData.images.map(image => (
+
+

+
+ ))}
+
+ {/* Doodle */}
+ {pageData.doodle && (
+

+ )}
+
+ {/* Stickers */}
+ {pageData.stickers && pageData.stickers.map(sticker => (
+
+ {sticker.type === 'image' && sticker.image ? (
+

+ ) : (
+
{sticker.emoji}
+ )}
+
+ ))}
+
+ {/* Text Elements */}
+ {pageData.textElements && pageData.textElements.map(text => (
+
+ {text.content}
+
+ ))}
+
+ );
+ };
+
+ // Render form fields (read-only in preview)
+ const renderFormFields = () => {
+ if (!pageData || !pageData.metadata) {
+ return No metadata available
;
+ }
+
+ const getSpotifyEmbedUrl = (url) => {
+ if (!url) return null;
+ const trackMatch = url.match(/spotify\.com\/track\/([a-zA-Z0-9]+)/);
+ if (trackMatch && trackMatch[1]) {
+ return `https://open.spotify.com/embed/track/${trackMatch[1]}?utm_source=generator`;
+ }
+ return null;
+ };
+
+ return (
+ <>
+
+
+
{pageData.metadata.title || 'N/A'}
+
+
+ {/* Prompt Field */}
+ {pageData.prompt && (
+
+
+
+ {pageData.prompt}
+
+
+ )}
+
+ {/* Spotify Song Field */}
+ {getSpotifyEmbedUrl(pageData.spotifyUrl) && (
+
+ )}
+
+
+
+
{pageData.metadata.date || 'N/A'}
+
+
+
+
+
{pageData.metadata.location || 'N/A'}
+
+
+
+
+
{pageData.metadata.description || 'N/A'}
+
+
+
+
+
+ {(() => {
+ let tags = pageData.metadata.tags || [];
+ if (typeof tags === 'string') {
+ tags = tags.split(',').map(t => t.trim()).filter(t => t.length > 0);
+ }
+ if (Array.isArray(tags) && tags.length > 0) {
+ return tags.map((tag, index) => (
+
+ {tag}
+
+ ));
+ }
+ return No tags;
+ })()}
+
+
+ >
+ );
+ };
+
+ // Render footer actions
+ const renderFooterActions = () => (
+ <>
+
+
+ >
+ );
+
+ return (
+ <>
+ {showSuccessMessage && (
+
+
+
β
+
Page Saved Successfully!
+
Your page has been saved to your journal.
+
+
+ )}
+
+ >
+ );
+};
+
+export default PreviewPage;
diff --git a/project/client/src/services/API b/project/client/src/services/API
new file mode 100644
index 000000000..e69de29bb
diff --git a/project/client/vite.config.js b/project/client/vite.config.js
new file mode 100644
index 000000000..ee3258062
--- /dev/null
+++ b/project/client/vite.config.js
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: '../server/public',
+ emptyOutDir: true
+ },
+ resolve: {
+ alias: {
+ 'picocss': path.resolve(__dirname, '../node_modules/@picocss/pico/css')
+ }
+ },
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000'
+ }
+ }
+ }
+})
diff --git a/project/package.json b/project/package.json
new file mode 100644
index 000000000..fe6b7e28f
--- /dev/null
+++ b/project/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "boltbucket",
+ "type": "module",
+ "scripts": {
+ "dev": "concurrently \"cd client && vite\" \"cd server && nodemon server\"",
+ "start": "cd server && node server",
+ "build": "cd client && vite build"
+ },
+ "dependencies": {
+ "@picocss/pico": "^1.5.7",
+ "axios": "^1.13.2",
+ "concurrently": "^7.6.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.6.1",
+ "express": "^4.21.2",
+ "express-session": "^1.18.2",
+ "html2canvas": "^1.4.1",
+ "multer": "^2.0.2",
+ "nodemon": "^3.1.11",
+ "passport": "^0.7.0",
+ "passport-google-oauth20": "^2.0.0",
+ "pg": "^8.10.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-modal": "^3.16.1",
+ "react-router-dom": "^6.9.0",
+ "serve-favicon": "^2.5.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.28",
+ "@types/react-dom": "^18.0.11",
+ "@vitejs/plugin-react": "^5.1.1",
+ "vite": "^7.2.2"
+ }
+}
diff --git a/project/server/config/database.js b/project/server/config/database.js
new file mode 100644
index 000000000..c4216326c
--- /dev/null
+++ b/project/server/config/database.js
@@ -0,0 +1,14 @@
+import pg from 'pg'
+
+const config = {
+ user: process.env.PGUSER,
+ password: process.env.PGPASSWORD,
+ host: process.env.PGHOST,
+ port: process.env.PGPORT,
+ database: process.env.PGDATABASE,
+ ssl: {
+ rejectUnauthorized: false
+ }
+}
+
+export const pool = new pg.Pool(config)
\ No newline at end of file
diff --git a/project/server/config/reset.js b/project/server/config/reset.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/project/server/data/journals.js b/project/server/data/journals.js
new file mode 100644
index 000000000..a09e21125
--- /dev/null
+++ b/project/server/data/journals.js
@@ -0,0 +1,16 @@
+// Temporary in-memory storage for journals.
+// This resets every time the server restarts.
+
+let journals = [];
+
+export function getJournals() {
+ return journals;
+}
+
+export function addJournal(journal) {
+ journals.push(journal);
+}
+
+export function getJournalById(id) {
+ return journals.find((j) => j.id === id);
+}
diff --git a/project/server/data/pages.js b/project/server/data/pages.js
new file mode 100644
index 000000000..fe762c1e6
--- /dev/null
+++ b/project/server/data/pages.js
@@ -0,0 +1,20 @@
+// Temporary in-memory storage for pages.
+// This resets every time the server restarts.
+
+let pages = [];
+
+export function getPages() {
+ return pages;
+}
+
+export function addPage(page) {
+ pages.push(page);
+}
+
+export function getPagesByJournalId(journalId) {
+ return pages.filter((p) => p.journalId === journalId);
+}
+
+export function getPageById(id) {
+ return pages.find((p) => p.id === id);
+}
diff --git a/project/server/passport.js b/project/server/passport.js
new file mode 100644
index 000000000..797e07b68
--- /dev/null
+++ b/project/server/passport.js
@@ -0,0 +1,26 @@
+import dotenv from 'dotenv';
+dotenv.config();
+
+import passport from 'passport';
+import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
+
+passport.use(
+ new GoogleStrategy(
+ {
+ clientID: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ callbackURL:
+ process.env.NODE_ENV === 'production'
+ ? `${process.env.FRONTEND_URL_PROD}/auth/google/callback`
+ : 'http://localhost:3000/auth/google/callback',
+ },
+ (accessToken, refreshToken, profile, done) => {
+ return done(null, profile);
+ }
+ )
+);
+
+passport.serializeUser((user, done) => done(null, user));
+passport.deserializeUser((user, done) => done(null, user));
+
+export default passport;
diff --git a/project/server/routes/auth.js b/project/server/routes/auth.js
new file mode 100644
index 000000000..eec1708d1
--- /dev/null
+++ b/project/server/routes/auth.js
@@ -0,0 +1,36 @@
+import express from 'express';
+import passport from 'passport';
+
+const router = express.Router();
+
+const FRONTEND_URL =
+ process.env.NODE_ENV === 'production'
+ ? process.env.FRONTEND_URL_PROD || 'https://your-deployed-frontend.com'
+ : process.env.FRONTEND_URL_DEV || 'http://localhost:5173';
+
+router.get(
+ '/google',
+ passport.authenticate('google', { scope: ['profile', 'email'] })
+);
+
+router.get(
+ '/google/callback',
+ passport.authenticate('google', { failureRedirect: '/' }),
+ (req, res) => {
+ res.redirect(`${FRONTEND_URL}/journals`);
+ }
+);
+
+router.get('/logout', (req, res, next) => {
+ req.logout((err) => {
+ if (err) return next(err);
+ res.redirect(FRONTEND_URL);
+ });
+});
+
+router.get('/current-user', (req, res) => {
+ res.json(req.user || null);
+});
+
+
+export default router;
diff --git a/project/server/routes/pages.js b/project/server/routes/pages.js
new file mode 100644
index 000000000..f640cb663
--- /dev/null
+++ b/project/server/routes/pages.js
@@ -0,0 +1,260 @@
+import express from 'express';
+
+// In-memory storage for pages (will be shared across requests)
+let pages = [];
+
+// Create pages router with access to journals array
+export default function createPagesRouter(journals, requireAuth) {
+ const router = express.Router();
+
+ // POST create page
+ router.post('/api/journals/:journalId/pages', requireAuth, (req, res) => {
+ try {
+ const journalId = parseInt(req.params.journalId);
+ const userId = req.user?.id || req.user?.googleId;
+
+ console.log('π POST /api/journals/:journalId/pages');
+ console.log('π Journal ID:', journalId);
+ console.log('π€ User ID:', userId);
+
+ // Verify journal exists and belongs to user
+ const journal = journals.find(j => j.id === journalId && j.userId === userId);
+ if (!journal) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Journal not found'
+ });
+ }
+
+ const {
+ pageColor,
+ pageBackgroundImage,
+ prompt,
+ images,
+ stickers,
+ textElements,
+ doodle,
+ spotifyUrl,
+ metadata
+ } = req.body;
+
+ const newPage = {
+ id: Date.now(),
+ journalId: journalId,
+ pageColor: pageColor || 'white',
+ pageBackgroundImage: pageBackgroundImage || null,
+ prompt: prompt || null,
+ images: images || [],
+ stickers: stickers || [],
+ textElements: textElements || [],
+ doodle: doodle || null,
+ spotifyUrl: spotifyUrl || null,
+ metadata: metadata || {},
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ };
+
+ pages.push(newPage);
+
+ // Update journal page count
+ journal.pageCount = (journal.pageCount || 0) + 1;
+
+ console.log('β
Page created:', newPage.id);
+ console.log('π Total pages now:', pages.length);
+
+ res.status(201).json(newPage);
+ } catch (error) {
+ console.error('β Error creating page:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: 'Failed to create page'
+ });
+ }
+ });
+
+ // GET all pages for a journal
+ router.get('/api/journals/:journalId/pages', requireAuth, (req, res) => {
+ try {
+ const journalId = parseInt(req.params.journalId);
+ const userId = req.user?.id || req.user?.googleId;
+
+ console.log('π GET /api/journals/:journalId/pages');
+ console.log('π Journal ID:', journalId);
+
+ // Verify journal exists and belongs to user
+ const journal = journals.find(j => j.id === journalId && j.userId === userId);
+ if (!journal) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Journal not found'
+ });
+ }
+
+ const journalPages = pages.filter(p => p.journalId === journalId);
+
+ console.log('β
Found pages:', journalPages.length);
+ res.json(journalPages);
+ } catch (error) {
+ console.error('β Error fetching pages:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: 'Failed to fetch pages'
+ });
+ }
+ });
+
+ // GET single page by ID
+ router.get('/api/journals/:journalId/pages/:pageId', requireAuth, (req, res) => {
+ try {
+ const journalId = parseInt(req.params.journalId);
+ const pageId = parseInt(req.params.pageId);
+ const userId = req.user?.id || req.user?.googleId;
+
+ console.log('π GET /api/journals/:journalId/pages/:pageId');
+ console.log('π Journal ID:', journalId);
+ console.log('π Page ID:', pageId);
+
+ // Verify journal exists and belongs to user
+ const journal = journals.find(j => j.id === journalId && j.userId === userId);
+ if (!journal) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Journal not found'
+ });
+ }
+
+ const page = pages.find(p => p.id === pageId && p.journalId === journalId);
+
+ if (!page) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Page not found'
+ });
+ }
+
+ console.log('β
Found page:', page.id);
+ res.json(page);
+ } catch (error) {
+ console.error('β Error fetching page:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: 'Failed to fetch page'
+ });
+ }
+ });
+
+ // PUT update page
+ router.put('/api/journals/:journalId/pages/:pageId', requireAuth, (req, res) => {
+ try {
+ const journalId = parseInt(req.params.journalId);
+ const pageId = parseInt(req.params.pageId);
+ const userId = req.user?.id || req.user?.googleId;
+
+ console.log('βοΈ PUT /api/journals/:journalId/pages/:pageId');
+ console.log('π Journal ID:', journalId);
+ console.log('π Page ID:', pageId);
+
+ // Verify journal exists and belongs to user
+ const journal = journals.find(j => j.id === journalId && j.userId === userId);
+ if (!journal) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Journal not found'
+ });
+ }
+
+ const pageIndex = pages.findIndex(p => p.id === pageId && p.journalId === journalId);
+
+ if (pageIndex === -1) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Page not found'
+ });
+ }
+
+ const {
+ pageColor,
+ pageBackgroundImage,
+ prompt,
+ images,
+ stickers,
+ textElements,
+ doodle,
+ spotifyUrl,
+ metadata
+ } = req.body;
+
+ pages[pageIndex] = {
+ ...pages[pageIndex],
+ pageColor: pageColor || pages[pageIndex].pageColor,
+ pageBackgroundImage: pageBackgroundImage !== undefined ? pageBackgroundImage : pages[pageIndex].pageBackgroundImage,
+ prompt: prompt !== undefined ? prompt : pages[pageIndex].prompt,
+ images: images !== undefined ? images : pages[pageIndex].images,
+ stickers: stickers !== undefined ? stickers : pages[pageIndex].stickers,
+ textElements: textElements !== undefined ? textElements : pages[pageIndex].textElements,
+ doodle: doodle !== undefined ? doodle : pages[pageIndex].doodle,
+ spotifyUrl: spotifyUrl !== undefined ? spotifyUrl : pages[pageIndex].spotifyUrl,
+ metadata: metadata ? { ...pages[pageIndex].metadata, ...metadata } : pages[pageIndex].metadata,
+ updatedAt: new Date().toISOString()
+ };
+
+ console.log('β
Page updated:', pages[pageIndex].id);
+ res.json(pages[pageIndex]);
+ } catch (error) {
+ console.error('β Error updating page:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: 'Failed to update page'
+ });
+ }
+ });
+
+ // DELETE page
+ router.delete('/api/journals/:journalId/pages/:pageId', requireAuth, (req, res) => {
+ try {
+ const journalId = parseInt(req.params.journalId);
+ const pageId = parseInt(req.params.pageId);
+ const userId = req.user?.id || req.user?.googleId;
+
+ console.log('ποΈ DELETE /api/journals/:journalId/pages/:pageId');
+ console.log('π Journal ID:', journalId);
+ console.log('π Page ID:', pageId);
+
+ // Verify journal exists and belongs to user
+ const journal = journals.find(j => j.id === journalId && j.userId === userId);
+ if (!journal) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Journal not found'
+ });
+ }
+
+ const pageIndex = pages.findIndex(p => p.id === pageId && p.journalId === journalId);
+
+ if (pageIndex === -1) {
+ return res.status(404).json({
+ error: 'Not found',
+ message: 'Page not found'
+ });
+ }
+
+ const deletedPage = pages[pageIndex];
+ pages.splice(pageIndex, 1);
+
+ // Update journal page count
+ journal.pageCount = Math.max(0, (journal.pageCount || 1) - 1);
+
+ console.log('β
Page deleted:', deletedPage.id);
+ res.json({ message: 'Page deleted successfully' });
+ } catch (error) {
+ console.error('β Error deleting page:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: 'Failed to delete page'
+ });
+ }
+ });
+
+ return router;
+}
+
diff --git a/project/server/server.js b/project/server/server.js
new file mode 100644
index 000000000..beaae14a3
--- /dev/null
+++ b/project/server/server.js
@@ -0,0 +1,289 @@
+import dotenv from 'dotenv';
+dotenv.config();
+
+import express from "express";
+import session from "express-session";
+import cors from "cors";
+import passport from "./passport.js";
+import multer from "multer";
+import path from "path";
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import authRoutes from './routes/auth.js';
+import fs from "fs";
+import { getPages, getPageById, getPagesByJournalId } from "./data/pages.js";
+import { getJournals, addJournal, getJournalById } from './data/journals.js';
+
+let journals = [];
+let pages = [];
+
+const app = express();
+const PORT = 3000;
+
+// --------------------------------------------------
+// Middleware
+// --------------------------------------------------
+app.use(cors({
+ origin: "http://localhost:5173",
+ credentials: true,
+}));
+
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+
+app.use(
+ session({
+ secret: "SECRET123",
+ resave: false,
+ saveUninitialized: false,
+ })
+);
+
+app.use(passport.initialize());
+app.use(passport.session());
+
+app.use("/auth", authRoutes);
+
+// --------------------------------------------------
+// Multer Upload Setup
+// --------------------------------------------------
+const uploadDir = path.join(process.cwd(), "uploads");
+if (!fs.existsSync(uploadDir)) {
+ fs.mkdirSync(uploadDir);
+}
+
+const storage = multer.diskStorage({
+ destination: (_, __, cb) => cb(null, uploadDir),
+ filename: (_, file, cb) => {
+ const unique = Date.now() + path.extname(file.originalname);
+ cb(null, unique);
+ }
+});
+
+const upload = multer({ storage });
+
+// --------------------------------------------------
+// Auth Middleware
+// --------------------------------------------------
+const requireAuth = (req, res, next) => {
+ if (!req.isAuthenticated()) {
+ return res.status(401).json({ error: "Not authenticated" });
+ }
+ next();
+};
+
+// --------------------------------------------------
+// Auth Routes
+// --------------------------------------------------
+app.get("/auth/google",
+ passport.authenticate("google", { scope: ["profile", "email"] })
+);
+
+app.get(
+ "/auth/google/callback",
+ passport.authenticate("google", {
+ successRedirect: "http://localhost:5173/journals",
+ failureRedirect: "http://localhost:5173",
+ })
+);
+
+app.get("/auth/me", (req, res) => {
+ if (!req.user) return res.json(null);
+ res.json(req.user);
+});
+
+// --------------------------------------------------
+// JOURNAL ROUTES
+// --------------------------------------------------
+
+/**
+ * CREATE A JOURNAL
+ * Supports both:
+ * - multipart/form-data (with image upload)
+ * - application/json (preset covers)
+ */
+
+app.post("/api/journals", requireAuth, upload.single("coverImage"), (req, res) => {
+ try {
+ const { name, description, coverColor, coverName, coverImage } = req.body;
+
+ if (!name?.trim()) {
+ return res.status(400).json({ error: "Journal name is required." });
+ }
+
+ const userId = req.user.id || req.user.googleId;
+
+ const newJournal = {
+ id: Date.now(),
+ name: name.trim(),
+ description: description?.trim() || "",
+ coverColor: coverColor || null,
+ coverName: coverName || null,
+
+ // Uploaded image OR preset image
+ coverImage: req.file
+ ? `/uploads/${req.file.filename}`
+ : coverImage || null,
+
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ userId,
+
+ pageCount: 0,
+ tags: [],
+ location: ""
+ };
+
+ journals.push(newJournal);
+ res.status(201).json(newJournal);
+ } catch (err) {
+ console.error("Error creating journal:", err);
+ res.status(500).json({ error: "Failed to create journal" });
+ }
+});
+
+// Get all journals for the logged-in user
+app.get("/api/journals", requireAuth, (req, res) => {
+ const userId = req.user.id || req.user.googleId;
+ const userJournals = journals.filter(j => j.userId === userId);
+ res.json(userJournals);
+});
+
+// --------------------------------------------------
+// PAGE ROUTES
+// --------------------------------------------------
+
+// Create page
+app.post("/api/journals/:journalId/pages", requireAuth, (req, res) => {
+ const journalId = req.params.journalId;
+ const journal = journals.find(j => j.id == journalId);
+
+ if (!journal) return res.status(404).json({ error: "Journal not found" });
+
+ const { title, content, tags, location } = req.body;
+
+ const newPage = {
+ id: Date.now(),
+ journalId,
+ title,
+ content,
+ tags: tags || [],
+ location: location || "",
+ createdAt: new Date().toISOString(),
+ };
+
+ pages.push(newPage);
+
+ journal.pageCount = pages.filter(p => p.journalId == journalId).length;
+
+ res.status(201).json(newPage);
+});
+
+// Get single page
+app.get("/api/journals/:journalId/pages/:pageId", requireAuth, (req, res) => {
+ const page = pages.find(p => p.id == req.params.pageId);
+ if (!page) return res.status(404).json({ error: "Page not found" });
+ res.json(page);
+});
+
+// Get all pages for a journal
+app.get("/api/journals/:journalId/pages", requireAuth, (req, res) => {
+ const journalId = parseInt(req.params.journalId, 10);
+ const journalPages = pages.filter(p => p.journalId === journalId);
+ res.json(journalPages);
+});
+
+// Delete a page
+app.delete("/api/journals/:journalId/pages/:pageId", requireAuth, (req, res) => {
+ const index = pages.findIndex(p => p.id == req.params.pageId);
+ if (index === -1) return res.status(404).json({ error: "Page not found" });
+
+ pages.splice(index, 1);
+ res.json({ message: "Page deleted" });
+});
+
+// --------------------------------------------------
+// JOURNAL :id ROUTES (these must come LAST)
+// --------------------------------------------------
+
+// Get a single journal
+app.get("/api/journals/:id", requireAuth, (req, res) => {
+ const journalId = parseInt(req.params.id, 10);
+ const userId = req.user.id || req.user.googleId;
+
+ console.log('\n=== GET JOURNAL REQUEST ===');
+ console.log('π URL:', req.url);
+ console.log('π’ Requested ID (raw):', req.params.id, typeof req.params.id);
+ console.log('π’ Parsed ID:', journalId, typeof journalId);
+ console.log('π€ User ID:', userId);
+ console.log('π Total journals in memory:', journals.length);
+ console.log('π All journals:', JSON.stringify(journals.map(j => ({
+ id: j.id,
+ type: typeof j.id,
+ name: j.name,
+ userId: j.userId
+ })), null, 2));
+
+ const journal = journals.find(j => {
+ console.log(` Comparing: j.id (${j.id}, ${typeof j.id}) === journalId (${journalId}, ${typeof journalId}) = ${j.id === journalId}`);
+ return j.id === journalId;
+ });
+
+ if (!journal) {
+ console.log('β Journal NOT FOUND');
+ console.log('=========================\n');
+ return res.status(404).json({ error: "Journal not found" });
+ }
+
+ console.log('β
Journal FOUND:', journal.name);
+ console.log('=========================\n');
+ res.json(journal);
+});
+
+// Update a journal
+app.put("/api/journals/:id", requireAuth, upload.single("coverImage"), (req, res) => {
+ const journalId = parseInt(req.params.id, 10);
+ const journal = journals.find(j => j.id === journalId);
+
+ if (!journal) return res.status(404).json({ error: "Journal not found" });
+
+ const { name, description, coverColor, coverName, coverImage } = req.body;
+
+ journal.name = name?.trim() || journal.name;
+ journal.description = description?.trim() || journal.description;
+ journal.coverColor = coverColor || journal.coverColor;
+ journal.coverName = coverName || journal.coverName;
+
+ if (req.file) {
+ journal.coverImage = `/uploads/${req.file.filename}`;
+ } else if (coverImage) {
+ journal.coverImage = coverImage;
+ }
+
+ journal.updatedAt = new Date().toISOString();
+
+ res.json(journal);
+});
+
+// Delete a journal
+app.delete("/api/journals/:id", requireAuth, (req, res) => {
+ const journalId = parseInt(req.params.id, 10);
+ const index = journals.findIndex(j => j.id === journalId);
+
+ if (index === -1) return res.status(404).json({ error: "Journal not found" });
+
+ journals.splice(index, 1);
+ res.json({ message: "Journal deleted" });
+});
+
+// --------------------------------------------------
+// STATIC FILE SERVING FOR UPLOADS
+// --------------------------------------------------
+app.use("/uploads", express.static(path.join(process.cwd(), "uploads")));
+
+// --------------------------------------------------
+// Start Server
+// --------------------------------------------------
+app.listen(PORT, () => {
+ console.log(`Server running on http://localhost:${PORT}`);
+});
diff --git a/project/server/uploads/1763254847804.jpg b/project/server/uploads/1763254847804.jpg
new file mode 100644
index 000000000..fbfed54f5
Binary files /dev/null and b/project/server/uploads/1763254847804.jpg differ
diff --git a/project/server/uploads/1763255008020.jpg b/project/server/uploads/1763255008020.jpg
new file mode 100644
index 000000000..fbfed54f5
Binary files /dev/null and b/project/server/uploads/1763255008020.jpg differ
diff --git a/project/server/uploads/1763481875421.jpg b/project/server/uploads/1763481875421.jpg
new file mode 100644
index 000000000..fbfed54f5
Binary files /dev/null and b/project/server/uploads/1763481875421.jpg differ
diff --git a/project/server/uploads/1763510018171.jpg b/project/server/uploads/1763510018171.jpg
new file mode 100644
index 000000000..b84d50a5c
Binary files /dev/null and b/project/server/uploads/1763510018171.jpg differ
diff --git a/project/server/uploads/1763510067193.jpg b/project/server/uploads/1763510067193.jpg
new file mode 100644
index 000000000..2592a3c84
Binary files /dev/null and b/project/server/uploads/1763510067193.jpg differ
diff --git a/project/server/uploads/1763513168988.jpg b/project/server/uploads/1763513168988.jpg
new file mode 100644
index 000000000..eb00a9e4c
Binary files /dev/null and b/project/server/uploads/1763513168988.jpg differ