Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,31 @@ logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Dependency directories
node_modules

# Optional npm cache directory
.npm

# Images (now accessed from cloud)
images
# Optional REPL history
.node_repl_history

# Custom
private
build
dist
.env.local
.env.development.local
.env.test.local
.env.production.local
75 changes: 75 additions & 0 deletions Changed/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { createContext, useEffect, useState } from "react";
import "./App.css";
import { getCategories, getLatestRecipes, login } from "./ApiClient";
import { CategoryList } from "./components/CategoryList";
import { Hero } from "./components/Hero";
import { Navbar } from "./components/Navbar";
import { CategoryPage } from "./components/CategoryPage";
import { RecipeDetailsPage } from "./components/recipedetailspage/RecipeDetailsPage";
import { RecipeList } from "./components/RecipeList";
import { Routes, Route } from "react-router";
import { Profile } from "./components/Profile";
import { SearchResultPage } from "./components/SearchResultPage";

export const AuthContext = createContext(null);
function App() {
const [categories, setCategories] = useState([]);
const [latest, setLatest] = useState([]);

const [currentUser, setCurrentUser] = useState({});

useEffect(() => {
getCategories()
.then((data) => setCategories(data))
.catch((e) => console.log(e));
}, []);

useEffect(() => {
getLatestRecipes()
.then((data) => setLatest(data))
.catch((e) => console.log(e));
}, []);

useEffect(() => {
login({ email: "zappe.thomson@test.com", password: "Test123!" })
.then((data) => setCurrentUser(data))
.catch((e) => console.log(e));
}, []);

return (
<>
<AuthContext.Provider value={currentUser}>
<Navbar />
<main className="bg-lightbeige">
<Routes>
<Route
path="/"
element={
<>
<Hero />
<div className="mx-8 py-4">
<CategoryList
title={"Recipe Categories"}
listItems={categories}
/>
<hr className=" my-4 text-center h-[0.0625rem] bg-deepbrown border-0" />
<RecipeList title={"New Added Recipes"} recipes={latest} />
</div>
</>
}
/>
<Route path="/recipe/:recipeId" element={<RecipeDetailsPage />} />
<Route
path="/recipes/category/:category"
element={<CategoryPage />}
/>
<Route path="/profile" element={<Profile />} />
<Route path="/search" element={<SearchResultPage />} />
</Routes>
</main>
</AuthContext.Provider>
</>
);
}

export default App;
30 changes: 30 additions & 0 deletions Changed/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import "./App.css";
import { AuthContex } from "./context/AuthContext";
import { useFetchData } from "./hooks/useFetchData";
import { Navbar } from "./components/Navbar";
import { AppRoutes } from "./AppRoutes";

function App() {
const { categories, latestRecipes, currentUser } = useFetchData();

return (
<>
<AuthContex.Provider value={currentUser}>
<Navbar />
<main className="bg-lightbeige">
<AppRoutes categories={categories} latestRecipes={latestRecipes} />
</main>
</AuthContex.Provider>
</>
);
}

export default App;

//*| The logic for fetching data (categories, recipes, and user login) |
//*| via useEffect will be moved into a custom hook named useFetchData.|

//*| All routes inside <Routes> will be moved to a separate component |
//*| called AppRoutes to make App.jsx more focused on managing global |
//*| state and context. |
98 changes: 94 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,76 @@
# Cooksphere
<div align="center">
<img src="./client/public/logo.png" width="150" alt="StackTally logo">
<h1> Cooksphere </h1>
<h3><i>"Cooksphere creates a space where users can discover,<br/> save or share recipes."</i></h3>


[![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
[![Express](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB)](https://expressjs.com/)
[![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)](https://reactjs.org/)
[![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Context API](https://img.shields.io/badge/contextapi-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)](https://reactjs.org/docs/context.html)
![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white)
[![Cypress](https://img.shields.io/badge/cypress-%2317202C.svg?style=for-the-badge&logo=cypress&logoColor=white)](https://www.cypress.io/)
[![Mongoose](https://img.shields.io/badge/mongoose-%238A4C39.svg?style=for-the-badge&logo=mongoose&logoColor=white)](https://mongoosejs.com/)
[![Mocha](https://img.shields.io/badge/mocha-%23D8B545.svg?style=for-the-badge&logo=mocha&logoColor=white)](https://mochajs.org/)
[![Chai](https://img.shields.io/badge/chai-%23A30000.svg?style=for-the-badge&logo=chai&logoColor=white)](https://www.chaijs.com/)

</div>



## Recap

During this refactoring, we improved the project's structure and performance by updating key components located in the ./RecipeDetailsPage/

* RecipeDetailsPage.tsx,
* GeneralCard.tsx,
* Ingredients.tsx,
* Instructions.tsx
* Reviews.tsx

By simplifying these components, centralising logic.

## Key Benefits

* Increased safety and clarity thanks to TypeScript.
* Cleaner and more maintainable code with centralised logic and route reorganisation.
* Better user experience with error handling and visual feedback.
* Ready to scale with a modular structure and robust typing.


## Key Changes

1. Migration to TypeScript:
* We migrated the project from JavaScript to TypeScript, adding type safety and improving code clarity.
* Defined interfaces for key data structures (categories, recipes, users) in types.d.ts.
* Added types to states and functions in App.tsx and other components.

2. Centralised Data Logic:
* Created a custom hook called useFetchData to handle data fetching (categories, recipes, and user login).
* This simplified App.tsx and centralised the fetching logic in one place.

3. Improved Route Organisation:
* Moved all routes to a separate component called AppRoutes, allowing App.tsx to focus on global state and context management.

4. Authentication Handling with AuthContext:
* Created an AuthContext to manage the authenticated user’s state and avoid "prop drilling".
* Added default values and safe handling of null values using optional chaining (?.) and fallbacks.

5. Project Structure Improvements:
* Reorganised files for better separation of concerns (e.g., moved AuthContext to a dedicated folder).
* Gradually migrated components to TypeScript (e.g., Navbar.jsx to Navbar.tsx).

6. Refactoring of Key Components in components/RecipeDetailsPage:
* RecipeDetailsPage.tsx: Simplified the component by breaking it into smaller, reusable parts.
* GeneralCard.tsx: Improved reusability and type safety.
* Ingredients.tsx: Enhanced readability and maintainability.
* Instructions.tsx: Streamlined the logic and improved error handling.
* Reviews.tsx: Added better validation and reusable logic for user reviews.# Cooksphere

Cooksphere creates a space where users can discover, save or share recipes.

# Frontend
## Frontend
The frontend shows categories of recipes. The user can either look into those or search for recipes. Both the search result page and the category page show a list of recipes which can be filtered and sorted. On the recipe details page users see information about the recipe. At the bottom of the page are reviews of other users listed and a form to post a review and rate the recipe. In the profile are the user's favorite and uploaded recipes listed. Furthermore he can upload a recipe by filling the form. The frontend was built with React and TailwindCSS. Uploaded recipe images are sent to Cloudinary.

Assets like category images are taken from Unsplash and the profile avatar or logo are AI generated.
Expand Down Expand Up @@ -30,12 +98,34 @@ npm install
npm run server
```

Once both the frontend and the backend are running open http://localhost:5173 in your browser. Have fun!
Once both the frontend and the backend are running open [localhost:5173](http://localhost:5173) in your browser. Have fun!

## Known Issues and Next Todos
- User updates are not reflected immediately, so when adding a recipe as a favorite or uploading a new one, you have to refresh the browser page manually to fetch the updated user
- Form validation is not complete yet
- Authentication is missing (frontend and backend), you are currently logged in as one user (s. in App.jsx)
- Use env variables for PORTs, URLs etc
- Split router (backend) and apiclient (frontend) to multiple files to separate recipe and user related functions
- Create more common components like button or headings
- Create more common components like button or headings

##
<h3>Solo Project by</h3>
<a href="https://github.com/b-rak">
<img src="https://avatars.githubusercontent.com/u/153173804?v=4" width="60px" hspace="10" style="border-radius: 100px; outline: solid 1px gray;outline-offset: -0.5px;">
</a>

##
<h3>Legacy Project Team</h3>


<a href="https://github.com/j7sus">
<img src="https://github.com/j7sus.png" width="60px" hspace="10" style="border-radius: 100px; outline: solid 1px gray;outline-offset: -0.5px;">
</a>
<a href="hhttps://github.com/lmleg9">
<img src="https://avatars.githubusercontent.com/u/166398249?v=4" width="60px" hspace="10" style="border-radius: 100px; outline: solid 1px gray;outline-offset: -0.5px;">
</a>

##
:) <div align="center">
![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)
</div>
8 changes: 8 additions & 0 deletions client/.vite/deps/_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"hash": "bd8b89b5",
"configHash": "6c6f5b0c",
"lockfileHash": "e3b0c442",
"browserHash": "5a3aece1",
"optimized": {},
"chunks": {}
}
3 changes: 3 additions & 0 deletions client/.vite/deps/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
9 changes: 9 additions & 0 deletions client/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "cypress";

export default defineConfig({
e2e: {

baseUrl: "http://localhost:3000",

},
});
13 changes: 13 additions & 0 deletions client/cypress/e2e/spec.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
describe('cypress cooksphere', () => {
it('renders the default elements on the screen', () => {
cy.visit('http://localhost:5173/');
cy.get('[data-testid="cypress-title"]').should('exist')
.should('have.text', 'Cooksphere');
});

it('renders the categories on the screen', () => {
cy.visit('http://localhost:5173/');
cy.get('[data-testid="listItem-1737462017379"]').should('exist');

});
});
5 changes: 5 additions & 0 deletions client/cypress/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
37 changes: 37 additions & 0 deletions client/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
17 changes: 17 additions & 0 deletions client/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'
1 change: 1 addition & 0 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default [
'react-refresh': reactRefresh,
},
rules: {
"react/prop-types": "off",
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
Expand Down
Loading