Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ jobs:

- name: Install dependencies
run: cd Frontend && npm install --force # This should install all dependencies, regardless of the workspace

- name: Write shared .env file
run: echo "${{ github.ref == 'refs/heads/deployment' && secrets.SHARED_PROD_ENV_FILE || secrets.SHARED_DEV_ENV_FILE }}" > Frontend/shared/.env

- name: Write .env file
- name: Write Frontend .env file
run: echo "${{ github.ref == 'refs/heads/deployment' && secrets.FRONTEND_PROD_ENV_FILE || secrets.FRONTEND_DEV_ENV_FILE }}" > Frontend/speedcart-react/.env

- name: Build front end
Expand Down
71 changes: 70 additions & 1 deletion Frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "my-monorepo",
"type": "module",
"private": true,
"workspaces": [
"shared",
Expand Down Expand Up @@ -35,6 +36,7 @@
"process": "^0.11.10",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^6.0.0",
"react-icons": "^5.2.1",
"react-intersection-observer": "^9.5.3",
"react-native": "0.74.5",
Expand Down
2 changes: 0 additions & 2 deletions Frontend/shared/.env

This file was deleted.

3 changes: 2 additions & 1 deletion Frontend/shared/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
dist/
src/constants/config.json
src/constants/config.json
.env
12 changes: 8 additions & 4 deletions Frontend/shared/generate-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ const dotenv = require('dotenv');
dotenv.config();

const config = {
API_DOMAIN: process.env.API_DOMAIN,
API_PORT: process.env.API_PORT,
TESTING_MODE: process.env.TESTING_MODE,
API_DOMAIN: process.env.API_DOMAIN || 'localhost',
API_PORT: process.env.API_PORT || '', // ensure it always exists
TESTING_MODE: process.env.TESTING_MODE || 'false',
};

// Write the config to a file in the shared directory
fs.writeFileSync(path.resolve(__dirname, './src/constants/config.json'), JSON.stringify(config, null, 2));
fs.writeFileSync(
path.resolve(__dirname, './src/constants/config.json'),
JSON.stringify(config, null, 2)
);

console.log('Config file generated successfully.');
5 changes: 4 additions & 1 deletion Frontend/shared/src/constants/BaseUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import config from './config.json';

const API_DOMAIN = config.API_DOMAIN;
const API_PORT = config.API_PORT;

// Protocol depends on if we're using dev or production
const API_PROTOCOL = API_DOMAIN === 'localhost' ? 'http' : 'https';

// This can be reused for all backend interactions
export const BASE_URL: string = `${API_PROTOCOL}://${API_DOMAIN}`;
export const BASE_URL: string = API_PORT
? `${API_PROTOCOL}://${API_DOMAIN}:${API_PORT}`
: `${API_PROTOCOL}://${API_DOMAIN}`;
export const TESTING_MODE: boolean = config.TESTING_MODE === 'true';
3 changes: 2 additions & 1 deletion Frontend/speedcart-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
]
},
"dependencies": {
"vite": "^6.3.3"
"vite": "^6.3.3",
"vite-tsconfig-paths": "^5.1.4"
}
}
45 changes: 31 additions & 14 deletions Frontend/speedcart-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
// App.js
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';

import PageLayout from '@components/PageLayout';
import Navigation from '@components/Navigation';
import Footer from '@components/Footer';
import Modal from '@components/Modal';
import SitePolicies from '@components/SitePolicies';

import { AppRoute } from '@constants/routes';

import Home from '@pages/Home';
import ShoppingListShare from '@pages/ShoppingListShare';
import Login from '@pages/Login';
import Dashboard from '@pages/Dashboard';
import NewShoppingListWithProvider from '@pages/NewShoppingListWithProvider';
import ShoppingListDetailWithProvider from '@pages/ShoppingListDetailWithProvider';
const Home = React.lazy(() => import('@pages/Home'));
const ShoppingListShare = React.lazy(() => import('@pages/ShoppingListShare'));
const Login = React.lazy(() => import('@pages/Login'));
const Dashboard = React.lazy(() => import('@pages/Dashboard'));
const NewShoppingListWithProvider = React.lazy(() => import('@pages/NewShoppingListWithProvider'));
const ShoppingListDetailWithProvider = React.lazy(() => import('@pages/ShoppingListDetailWithProvider'));

import './App.css';

function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}

function App() {
const [showSitePolicies, setShowSitePolicies] = useState(true);

Expand All @@ -38,14 +51,18 @@ function App() {
<Modal isOpen={showSitePolicies} isCloseable={false} >
<SitePolicies onAccept={handleSitePoliciesAccept} />
</Modal>
<Routes>
<Route path={AppRoute.HOME} element={<Home id="HomePage"/>} />
<Route path={AppRoute.DASHBOARD} element={<Dashboard/>} />
<Route path={AppRoute.NEW_SHOPPING_LIST} element={<NewShoppingListWithProvider/>} />
<Route path={AppRoute.LOGIN} element={<Login />} />
<Route path={`${AppRoute.SHOPPING_LIST_DETAIL}/:id`} element={<ShoppingListDetailWithProvider />} />
<Route path={`${AppRoute.SHOPPING_LIST_SHARE}/:token`} element={<ShoppingListShare />} />
</Routes>
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { /* reset state if needed */ }}>
<React.Suspense fallback={<PageLayout>Loading...</PageLayout>}>
<Routes>
<Route path={AppRoute.HOME} element={<Home id="HomePage"/>} />
<Route path={AppRoute.DASHBOARD} element={<Dashboard/>} />
<Route path={AppRoute.NEW_SHOPPING_LIST} element={<NewShoppingListWithProvider/>} />
<Route path={AppRoute.LOGIN} element={<Login />} />
<Route path={`${AppRoute.SHOPPING_LIST_DETAIL}/:id`} element={<ShoppingListDetailWithProvider />} />
<Route path={`${AppRoute.SHOPPING_LIST_SHARE}/:token`} element={<ShoppingListShare />} />
</Routes>
</React.Suspense>
</ErrorBoundary>
<Footer id="policyFooter" />
</Router>
);
Expand Down
Binary file modified Frontend/speedcart-react/src/assets/images/freshProduce.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Frontend/speedcart-react/src/assets/images/phoneAndLaptop.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { AuthContextType, createGroceryItem, createShoppingList, deleteGroceryIt

const ShoppingListContext = createContext(null);

// Defined here so it doesn't get recreated on every render
function partition<T>(arr: T[], predicate: (val: T) => boolean): [T[], T[]] {
const pass: T[] = [], fail: T[] = [];
for (const el of arr) (predicate(el) ? pass : fail).push(el);
return [pass, fail];
}

// This component handles all state-related work for any pages
// that deal with saving shopping lists
export const ShoppingListProvider = ({ children }) => {
Expand All @@ -18,6 +25,7 @@ export const ShoppingListProvider = ({ children }) => {
const [deletedItems, setDeletedItems] = useState([]); // Any items deleted in the front end should obviously be removed from the database on the back end
const [newItems, setNewItems] = useState([]); // Any new items added in the front end should be added to the database on the back end
const [crudMode, setCrudMode] = useState<CrudMode>(CrudMode.READ);
const [error, setError] = useState<string>(''); // Stores any kind of error that occurred with the most recent batch of remote requests
// These state variables are necessary if the user changes from editing mode to view mode
const [originalShoppingList, setOriginalShoppingList] = useState(null);
const [originalGroceryItems, setOriginalGroceryItems] = useState([]);
Expand Down Expand Up @@ -126,36 +134,42 @@ export const ShoppingListProvider = ({ children }) => {
currentListID = listID;
}

if (existingItems.length > 0) {
// Update each existing grocery item
const itemPromises = existingItems.map(item => callBackendAPI(updateGroceryItem, {
item: item
}));
// Pool all item-related promises into one array without evaluating them yet (i.e. lazy evaluation)
const allItemPromises: (() => Promise<Response>)[] = [];

await Promise.all(itemPromises);
// Collect all lazy API calls
for (const item of existingItems) {
allItemPromises.push(() => callBackendAPI(updateGroceryItem, { item }));
}

if (deletedItems.length > 0) {
// Remove each grocery item that the user wants to delete
const itemDeletePromises = deletedItems.map(item => callBackendAPI(deleteGroceryItem, {
item: item
}));
for (const item of deletedItems) {
allItemPromises.push(() => callBackendAPI(deleteGroceryItem, { item }));
}

await Promise.all(itemDeletePromises);
for (const item of newItems) {
allItemPromises.push(() =>
callBackendAPI(createGroceryItem, {
item: {
...item,
shopping_list_id: currentListID
}
})
);
}

if (newItems.length > 0) {
// Add each new item the user wants to add
const itemCreationPromises = newItems.map(item => callBackendAPI(createGroceryItem, {
item: {
...item,
shopping_list_id: currentListID
}
}));
// Start all the calls at once, lazily
const settled = await Promise.allSettled(allItemPromises);

const [fulfilled, rejected] = partition(settled, r => r.status === "fulfilled");

await Promise.all(itemCreationPromises);
if (rejected.length > 0) {
console.error("Some operations failed:", rejected);
setError("Some items failed to save. Please try again.");
return;
}


console.log("All operations succeeded!");
setError(''); // Clear any previous error
};

return (
Expand Down
Loading
Loading