Compare commits
24 Commits
api-with-j
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2754fe6c09 | ||
|
|
8a939e6a81 | ||
|
|
63d0049450 | ||
|
|
47360518ce | ||
|
|
949762f3a0 | ||
|
|
6d4ebd7757 | ||
|
|
e7a27d7fe9 | ||
|
|
e89067d942 | ||
|
|
1d4763333b | ||
|
|
a7f3fd6e10 | ||
|
|
9e146f0825 | ||
|
|
8ae6cf4ab0 | ||
|
|
46454a84c2 | ||
|
|
8f1cfa0ad9 | ||
|
|
1e85a714dc | ||
|
|
99829533fd | ||
|
|
1daf3418ce | ||
|
|
a30960a1b4 | ||
|
|
28c4747aba | ||
|
|
9945ebadb4 | ||
|
|
fc1046bad5 | ||
|
|
7bd29e4dba | ||
|
|
bd282ce2bb | ||
|
|
1b32ac38d1 |
@@ -9,12 +9,16 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/material": "^5.11.9",
|
||||
"@tinymce/tinymce-react": "^4.2.0",
|
||||
"axios": "^1.2.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-select": "^5.7.0",
|
||||
"sass": "^1.56.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { useAuthContext } from './context/AuthContext';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import API from './util/API';
|
||||
|
||||
// pages, ui, components, styles
|
||||
import Subscriptions from './components/pages/Subscriptions/Subscriptions';
|
||||
@@ -19,9 +17,10 @@ import CollectionBrowser from './components/pages/CollectionBrowser';
|
||||
import { Navbar } from './components/ui';
|
||||
import GroceryList from './components/pages/GroceryList';
|
||||
import GroceryListCollection from './components/pages/GroceryListCollection';
|
||||
import { TokenType } from './util/types';
|
||||
import './sass/App.scss';
|
||||
import handleToken from './util/handleToken';
|
||||
import AddFriends from './components/pages/AddFriends';
|
||||
import Sandbox from './components/Sandbox';
|
||||
import './sass/App.scss';
|
||||
|
||||
function App() {
|
||||
const { setUser, token, setToken } = useAuthContext();
|
||||
@@ -48,20 +47,26 @@ function App() {
|
||||
<div className="App">
|
||||
<Navbar />
|
||||
<Routes>
|
||||
{/* Base access privileges */}
|
||||
<Route path="/" element={<Welcome />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* Protected routes */}
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/collections" element={<CollectionBrowser />} />
|
||||
<Route path="/collections/:id" element={<Collection />} />
|
||||
<Route path="/add-friends" element={<AddFriends />} />
|
||||
<Route path="/explore" element={<Browser header="" searchFunction={() => {}} />} />
|
||||
<Route path="/recipe/:id" element={<Recipe />} />
|
||||
<Route path="/subscriptions" element={<Subscriptions />} />
|
||||
<Route path="/subscriptions/:id" element={<Collection />} />
|
||||
|
||||
<Route path="/add-recipe" element={<AddRecipe />} />
|
||||
<Route path="/grocery-list" element={<GroceryListCollection />} />
|
||||
<Route path="/grocery-list/:id" element={<GroceryList />} />
|
||||
|
||||
{/* For dev use */}
|
||||
<Route path="/sandbox" element={<Sandbox />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
||||
93
client/src/components/Sandbox.tsx
Normal file
93
client/src/components/Sandbox.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import Select, { MultiValue } from "react-select";
|
||||
import Creatable, { useCreatable } from "react-select/creatable";
|
||||
import { useEffect, useState } from "react"
|
||||
import { useAuthContext } from "../context/AuthContext";
|
||||
import { ICollection, IIngredient } from "../schemas";
|
||||
import { Button, Page } from "./ui";
|
||||
import API from "../util/API";
|
||||
import { OptionType } from "../util/types";
|
||||
|
||||
export default function Sandbox() {
|
||||
const [ingredients, setIngredients] = useState<Array<IIngredient>>([]);
|
||||
const [collections, setCollections] = useState<Array<OptionType>>([]);
|
||||
const [newEntries, setNewEntries] = useState<Array<OptionType>>([]);
|
||||
const [selections, setSelections] = useState<Array<OptionType>>([]);
|
||||
const { user, token } = useAuthContext();
|
||||
|
||||
function handleNewIngredient(name: string) {
|
||||
if (!user || !user.id) return;
|
||||
|
||||
let maxID = 0;
|
||||
ingredients.forEach((entry: IIngredient) => {
|
||||
if (entry.id! > maxID) {
|
||||
maxID = entry.id!;
|
||||
}
|
||||
});
|
||||
|
||||
const newEntry: OptionType = {
|
||||
label: name,
|
||||
value: maxID + 1
|
||||
}
|
||||
|
||||
const newIngredient: IIngredient = {
|
||||
id: maxID + 1,
|
||||
name: name,
|
||||
createdbyid: user.id
|
||||
}
|
||||
|
||||
setIngredients(prev => [...prev, newIngredient]);
|
||||
setNewEntries(prev => [...prev, newEntry]);
|
||||
}
|
||||
|
||||
function handleChange(event: MultiValue<OptionType>) {
|
||||
setSelections(prev => [...prev, ...event])
|
||||
}
|
||||
|
||||
async function bulkInsertIngredients() {
|
||||
if (!user || !token) return;
|
||||
|
||||
console.log(newEntries.length - ingredients.length);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
token && (async() => {
|
||||
const ingredients = new API.Ingredient(token);
|
||||
const collections = new API.Collection(token);
|
||||
|
||||
const allIngredients = await ingredients.getAll();
|
||||
if (allIngredients) {
|
||||
setNewEntries(prev => [...prev, ...allIngredients]);
|
||||
setIngredients(allIngredients.map((each: IIngredient) => {
|
||||
return { label: each.name, value: each.id }
|
||||
}));
|
||||
}
|
||||
|
||||
const myCollections = await collections.getAllAuthored();
|
||||
if (myCollections) setCollections(myCollections.map((each: ICollection) => {
|
||||
return { label: each.name, value: each.id }
|
||||
}))
|
||||
})();
|
||||
}, [token])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(newEntries);
|
||||
}, [newEntries])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Sandbox</h1>
|
||||
|
||||
<p>Ingredients:</p>
|
||||
|
||||
{ ingredients
|
||||
? <Creatable isMulti value={selections} onChange={(value: MultiValue<OptionType>) => handleChange(value)} onCreateOption={handleNewIngredient} options={newEntries} />
|
||||
: <p>Loading...</p>
|
||||
}
|
||||
|
||||
<p>My collections:</p>
|
||||
{ collections ? <Select options={collections} /> : <p>Loading...</p> }
|
||||
|
||||
<Button onClick={bulkInsertIngredients}>Go</Button>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
47
client/src/components/derived/CollectionList.tsx
Normal file
47
client/src/components/derived/CollectionList.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
import { useAuthContext } from "../../context/AuthContext"
|
||||
import useDateFormat from "../../hooks/useDateFormat";
|
||||
import { ICollection } from "../../schemas";
|
||||
import API from "../../util/API";
|
||||
import { Card } from "../ui"
|
||||
|
||||
type CollectionListType = FC<{ targetID?: number | string }>
|
||||
|
||||
const CollectionList: CollectionListType = ({ targetID = null }) => {
|
||||
const [collections, setCollections] = useState<ICollection[]>();
|
||||
const [author, setAuthor] = useState<string>();
|
||||
const { user, token } = useAuthContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (user && token) {
|
||||
if (targetID) {
|
||||
(async() => {
|
||||
const Collections = new API.Collection(token);
|
||||
const result = await Collections.getAllAuthored(targetID);
|
||||
setCollections(result);
|
||||
})();
|
||||
} else {
|
||||
(async() => {
|
||||
const Collections = new API.Collection(token);
|
||||
const result = await Collections.getAllAuthored();
|
||||
setCollections(result);
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [user])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{ collections && collections.map(each =>
|
||||
<div className="collection-item" key={v4()}>
|
||||
<h2>{each.name}</h2>
|
||||
{ targetID && <p>Created by {author}</p>}
|
||||
<p>Created {useDateFormat(each.datecreated)}</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollectionList
|
||||
@@ -1,58 +1,71 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { getAllUsers, getFriendships, getPendingFriendRequests, getUserByID } from "../../util/apiUtils";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
import API from "../../util/API";
|
||||
import UserCard from "../ui/UserCard";
|
||||
import { IUser, IFriendship } from "../../schemas";
|
||||
import { Card, Divider, Panel } from "../ui";
|
||||
import FriendSearchWidget from "../ui/Widgets/FriendSearchWidget";
|
||||
|
||||
export default function Friends() {
|
||||
const Friends: FC<{ targetUser?: IUser }> = ({ targetUser }) => {
|
||||
const [friends, setFriends] = useState<IFriendship[]>();
|
||||
const [userList, setUserList] = useState(new Array<IUser>());
|
||||
const { user } = useAuthContext();
|
||||
const { user, token } = useAuthContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
if (!user || !token) return;
|
||||
(async function() {
|
||||
try {
|
||||
const rawResult = await getFriendships();
|
||||
const Friends = new API.Friendship(token);
|
||||
const result: IFriendship[] | null = await Friends.getAll();
|
||||
|
||||
if (rawResult.length) {
|
||||
const result = rawResult.filter((item: IFriendship) => (item.senderid == user.id) && !(item.pending));
|
||||
if (result?.length) {
|
||||
setFriends(result);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})()
|
||||
}, [user])
|
||||
})();
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
friends && friends.map(async (friend: IFriendship) => {
|
||||
const userData = await getUserByID(friend.targetid);
|
||||
if (!token || !friends) return;
|
||||
|
||||
friends.map(async (friend: IFriendship) => {
|
||||
const User = new API.User(token);
|
||||
const userData = await User.getByID(friend.targetid as string);
|
||||
if (userData) setUserList((prev: IUser[]) => {
|
||||
return [...prev, userData]
|
||||
if (prev.includes(userData)) {
|
||||
return prev;
|
||||
} else {
|
||||
return [...prev, userData]
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [friends]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(userList);
|
||||
}, [setUserList])
|
||||
|
||||
return (
|
||||
<>
|
||||
{ userList.length ?
|
||||
(
|
||||
<Panel extraStyles="flex-row">
|
||||
<h2>Your friendships:</h2>
|
||||
<Card extraClasses="flex-row">
|
||||
<h2>Friends ({ userList?.length ?? "0" }):</h2>
|
||||
|
||||
<div className="friends-list">
|
||||
{
|
||||
userList.map((user: IUser) => {
|
||||
return <UserCard key={v4()} user={user} />
|
||||
return <UserCard key={v4()} targetUser={user} />
|
||||
})
|
||||
}
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<p>Looking for someone else?</p>
|
||||
<p>You can search for more friends <a href="/add-friends">here!</a></p>
|
||||
</aside>
|
||||
</Card>
|
||||
) :
|
||||
(
|
||||
<Card>
|
||||
@@ -68,3 +81,5 @@ export default function Friends() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Friends
|
||||
195
client/src/components/derived/IngredientSelector.tsx
Normal file
195
client/src/components/derived/IngredientSelector.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Autocomplete, TextField } from "@mui/material"
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { DropdownData, IIngredient, RecipeIngredient } from "../../schemas";
|
||||
import { IngredientFieldData } from "../../util/types";
|
||||
import { Button } from "../ui";
|
||||
|
||||
interface IngredientSelectorProps {
|
||||
position: number
|
||||
ingredients: IIngredient[]
|
||||
units: DropdownData[]
|
||||
getRowState: (input: IngredientFieldData) => void
|
||||
destroy: (position: number) => void
|
||||
}
|
||||
|
||||
const createIngredient = (name: string, userid: string | number) => {
|
||||
return {
|
||||
name: name,
|
||||
createdbyid: userid
|
||||
}
|
||||
}
|
||||
|
||||
const quantityOptions: readonly any[] = [
|
||||
{ name: "1/8" , value: 0.125 },
|
||||
{ name: "1/4" , value: 0.250 },
|
||||
{ name: "1/3" , value: 0.333 },
|
||||
{ name: "3/8" , value: 0.375 },
|
||||
{ name: "1/2" , value: 0.500 },
|
||||
{ name: "5/8" , value: 0.625 },
|
||||
{ name: "2/3" , value: 0.666 },
|
||||
{ name: "3/4" , value: 0.750 },
|
||||
{ name: "7/8" , value: 0.875 },
|
||||
{ name: "1 1/4" , value: 1.250 },
|
||||
{ name: "1 1/3" , value: 1.333 },
|
||||
{ name: "1 1/2" , value: 1.500 },
|
||||
{ name: "1 2/3" , value: 1.666 },
|
||||
{ name: "1 3/4" , value: 1.750 },
|
||||
{ name: "2 1/4" , value: 2.250 },
|
||||
{ name: "2 1/3" , value: 2.333 },
|
||||
{ name: "2 1/2" , value: 2.500 },
|
||||
{ name: "2 3/4" , value: 2.750 },
|
||||
{ name: "3 1/4" , value: 3.250 },
|
||||
{ name: "3 1/2" , value: 3.500 },
|
||||
{ name: "3 3/4" , value: 3.750 },
|
||||
{ name: "4 1/4" , value: 4.250 },
|
||||
{ name: "4 1/2" , value: 4.500 },
|
||||
{ name: "4 3/4" , value: 4.750 },
|
||||
{ name: "5 1/4" , value: 5.250 },
|
||||
{ name: "5 1/2" , value: 5.500 },
|
||||
{ name: "5 3/4" , value: 5.750 },
|
||||
]
|
||||
|
||||
function IngredientSelector({ position, ingredients, units, getRowState, destroy }: IngredientSelectorProps) {
|
||||
const { user } = useAuthContext();
|
||||
|
||||
const [ingredientOptions, setIngredientOptions] = useState(ingredients.map(each => each.name));
|
||||
const [rowState, setRowState] = useState<IngredientFieldData>({
|
||||
quantity: undefined,
|
||||
rowPosition: position,
|
||||
measurement: undefined,
|
||||
ingredientSelection: undefined,
|
||||
ingredients: ingredients
|
||||
})
|
||||
|
||||
const [quantityError, setQuantityError] = useState<null | string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getRowState(rowState);
|
||||
}, [rowState, setRowState])
|
||||
|
||||
function validateQuantity(input: any) {
|
||||
const value = new Number(input).valueOf();
|
||||
|
||||
if (Number.isNaN(value)) {
|
||||
console.log('is nan');
|
||||
setQuantityError("Please provide a valid input (number)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
console.log('is null');
|
||||
setRowState({ ...rowState, quantity: undefined });
|
||||
setQuantityError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setQuantityError(null);
|
||||
setRowState({ ...rowState, quantity: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="ingredient-widget"><tbody>
|
||||
<tr>
|
||||
<td className="quantity-of-unit">
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
autoHighlight
|
||||
options={quantityOptions.map(each => each.name)}
|
||||
className="ui-creatable-component"
|
||||
onChange={(event, value) => {
|
||||
console.log(value);
|
||||
validateQuantity(quantityOptions.filter(option => (option.name) == value)[0]['value'] as number);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
variant="outlined"
|
||||
color={(rowState.quantity == null) ? (quantityError ? "error" : "info") : (quantityError ? "error" : "success")}
|
||||
label={quantityError ?? "Quantity"}
|
||||
onChange={(e) => validateQuantity(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="ingredient-unit">
|
||||
<Autocomplete
|
||||
autoHighlight
|
||||
options={units.map(each => each.name)}
|
||||
className="ui-creatable-component"
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
variant="outlined"
|
||||
label="Unit"
|
||||
/>
|
||||
)}
|
||||
onChange={(event, value) => {
|
||||
if (value) {
|
||||
setRowState({ ...rowState, measurement: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="ingredient-name">
|
||||
<Autocomplete
|
||||
autoHighlight
|
||||
options={ingredientOptions}
|
||||
freeSolo
|
||||
className="ui-creatable-component"
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
variant="outlined"
|
||||
label="Ingredient Name"
|
||||
/>
|
||||
)}
|
||||
onChange={(event, newValue) => {
|
||||
if (!user) return;
|
||||
|
||||
if (typeof newValue == 'string') {
|
||||
const newIngredient = createIngredient(newValue, user.id!);
|
||||
|
||||
setIngredientOptions((prev) => {
|
||||
let shouldInsert = true;
|
||||
for (let each of prev) {
|
||||
if (each == newValue) shouldInsert = false;
|
||||
}
|
||||
|
||||
return (shouldInsert ? [...prev, newValue] : prev);
|
||||
});
|
||||
|
||||
setRowState((prev) => {
|
||||
let shouldInsert = true;
|
||||
for (let each of prev.ingredients) {
|
||||
if (each.name == newValue) shouldInsert = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
ingredients: (shouldInsert ? [...prev.ingredients, newIngredient] : [...prev.ingredients]),
|
||||
ingredientSelection: newValue
|
||||
}
|
||||
})
|
||||
} else if (newValue == null) {
|
||||
setRowState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
ingredients: ingredients,
|
||||
ingredientSelection: undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button onClick={() => destroy(position)}>Close</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
)
|
||||
}
|
||||
|
||||
export default IngredientSelector
|
||||
22
client/src/components/pages/AddFriends.tsx
Normal file
22
client/src/components/pages/AddFriends.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import Protect from "../../util/Protect"
|
||||
import { Divider, Panel } from "../ui"
|
||||
import FriendSearchWidget from "../ui/Widgets/NewFriendWidget"
|
||||
|
||||
const AddFriends = () => {
|
||||
return (
|
||||
<Protect redirect="/add-friends">
|
||||
<h1>Search for New Friends</h1>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel>
|
||||
<h2>Use the widget below to search for new friends!</h2>
|
||||
<Divider />
|
||||
|
||||
<FriendSearchWidget />
|
||||
</Panel>
|
||||
</Protect>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddFriends
|
||||
@@ -1,60 +1,337 @@
|
||||
// library/framework
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Autocomplete, TextField } from "@mui/material";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
// util/api
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { IRecipe } from "../../schemas";
|
||||
import { Button, Divider, Form, Page, Panel } from "../ui"
|
||||
import { DropdownData, IIngredient, IRecipe, RecipeIngredient } from "../../schemas";
|
||||
import { IngredientFieldData } from "../../util/types";
|
||||
import Protect from "../../util/Protect";
|
||||
import API from "../../util/API";
|
||||
|
||||
const AddRecipe = () => {
|
||||
const authContext = useAuthContext();
|
||||
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '', ingredients: [] })
|
||||
const [form, setForm] = useState<JSX.Element>();
|
||||
// ui/components
|
||||
import { Button, Card, Divider, Panel, RichText, Toast } from "../ui"
|
||||
import IngredientSelector from "../derived/IngredientSelector";
|
||||
|
||||
const getFormState = useCallback((data: IRecipe) => {
|
||||
setInput(data);
|
||||
}, [input])
|
||||
export default function AddRecipe() {
|
||||
/**********************************
|
||||
* STATE AND CONTEXT
|
||||
*********************************/
|
||||
const { user, token } = useAuthContext();
|
||||
|
||||
const handleCreate = () => {
|
||||
// received recipe data
|
||||
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '' })
|
||||
|
||||
// UI state handling
|
||||
const [ingredients, setIngredients] = useState<IIngredient[]>([]);
|
||||
const [measurements, setMeasurements] = useState<DropdownData[]>([]);
|
||||
const [courseData, setCourseData] = useState<DropdownData[]>([]);
|
||||
const [ingredientFields, setIngredientFields] = useState<Array<JSX.Element>>([]);
|
||||
const [ingredientFieldData, setIngredientFieldData] = useState<Array<IngredientFieldData>>([]);
|
||||
const [optionCount, setOptionCount] = useState(0);
|
||||
|
||||
// status reporting
|
||||
const [toast, setToast] = useState(<></>)
|
||||
|
||||
/**********************************
|
||||
* CALLBACKS FOR CHILD COMPONENTS
|
||||
*********************************/
|
||||
// callback to retrieve state from ingredient rows
|
||||
const getRowState = useCallback((value: IngredientFieldData) => {
|
||||
setIngredientFieldData((prev) => {
|
||||
const newState = prev;
|
||||
newState[value.rowPosition] = value;
|
||||
return newState;
|
||||
});
|
||||
}, [ingredientFieldData])
|
||||
|
||||
// callback passed to each ingredient row to enable the row to be closed
|
||||
const destroySelector = useCallback((position: number) => {
|
||||
setIngredientFieldData((prev) => {
|
||||
return [...prev.filter(each => each.rowPosition !== position)];
|
||||
})
|
||||
|
||||
setIngredientFields((prev) => {
|
||||
const newState = new Array<JSX.Element>();
|
||||
|
||||
for (let i = 0; i < prev.length; i++) {
|
||||
if (i === position) {
|
||||
continue;
|
||||
} else {
|
||||
newState.push(prev[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
})
|
||||
}, [ingredientFields]);
|
||||
|
||||
/**********************************
|
||||
* PAGE MOUNT BEHAVIOR AND SIDE EFFECTS
|
||||
*********************************/
|
||||
// store all ingredients on page mount
|
||||
useEffect(() => {
|
||||
token && (async() => {
|
||||
const ingredients = new API.Ingredient(token);
|
||||
const _dropdowns = new API.Dropdowns(token);
|
||||
const ingredientList = await ingredients.getAll();
|
||||
const measurementList = await _dropdowns.getAllMeasurements();
|
||||
const courseList = await _dropdowns.getAllCourses();
|
||||
|
||||
function filterDuplicateEntries(receivedList: any[], previousState: any) {
|
||||
let newEntries = [];
|
||||
for (let each of receivedList) {
|
||||
if (previousState.includes(each)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newEntries.push(each);
|
||||
}
|
||||
|
||||
return newEntries;
|
||||
}
|
||||
|
||||
if (ingredientList) {
|
||||
setIngredients((prev) => {
|
||||
let newEntries = filterDuplicateEntries(ingredientList, prev);
|
||||
return [...prev, ...newEntries];
|
||||
});
|
||||
}
|
||||
|
||||
if (measurementList) {
|
||||
setMeasurements((prev) => {
|
||||
let newEntries = filterDuplicateEntries(measurementList, prev);
|
||||
return [...prev, ...newEntries];
|
||||
});
|
||||
}
|
||||
|
||||
if (courseList && !courseData.length) {
|
||||
setCourseData((prev) => {
|
||||
let newEntries = filterDuplicateEntries(courseList, prev);
|
||||
return [...prev, ...newEntries];
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [token])
|
||||
|
||||
// mount the ingredient selection section once conditions met:
|
||||
// 1. ingredients have been fetched
|
||||
// 2. measurements have been fetched
|
||||
// 3. ingredient fields have not already been initialized
|
||||
useEffect(() => {
|
||||
const conditionsMet = (ingredients.length && measurements.length) && (!ingredientFields.length);
|
||||
|
||||
if (conditionsMet) {
|
||||
setIngredientFields([<IngredientSelector key={v4()} position={optionCount} ingredients={ingredients} units={measurements} getRowState={getRowState} destroy={destroySelector} />]);
|
||||
}
|
||||
}, [ingredients, measurements])
|
||||
|
||||
/**********************************
|
||||
* PAGE SPECIFIC FUNCTIONS
|
||||
*********************************/
|
||||
// submit handler
|
||||
async function handleCreate() {
|
||||
if (!user || !token) return;
|
||||
|
||||
let recipeID;
|
||||
let recipeName;
|
||||
|
||||
// initialize API handlers
|
||||
const recipeAPI = new API.Recipe(token);
|
||||
const ingredientAPI = new API.Ingredient(token);
|
||||
|
||||
// array to aggregate error/success messages
|
||||
let messages = new Array<string>();
|
||||
|
||||
// inject current user id into recipe entry
|
||||
setInput({ ...input, authoruserid: user.id! });
|
||||
|
||||
// verify all required fields are set
|
||||
for (let field of Object.keys(input)) {
|
||||
if (!input[field as keyof IRecipe]) return;
|
||||
// account for an edge case where this state may not have been set yet
|
||||
if (field == 'authoruserid' as keyof IRecipe) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!input[field as keyof IRecipe]) {
|
||||
messages.push("Missing required field " + field);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('good to go!')
|
||||
// post recipe entry
|
||||
const result = await recipeAPI.post(input);
|
||||
|
||||
if (result) {
|
||||
recipeID = result.recipe.id;
|
||||
recipeName = result.recipe.name;
|
||||
}
|
||||
|
||||
let preparedIngredientData = new Array<RecipeIngredient>();
|
||||
let newIngredientCount = 0;
|
||||
|
||||
// check ingredient row for null values; normalize each row's data and insert into array above
|
||||
for (let row of ingredientFieldData) {
|
||||
if (!row) continue;
|
||||
console.log(row);
|
||||
|
||||
if (row.ingredientSelection === undefined) {
|
||||
messages.push("Please ensure you have included ingredient selections for all rows.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!row.quantity) {
|
||||
messages.push("Please provide a quantity for ingredient " + row.ingredientSelection);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!row.measurement) {
|
||||
messages.push(row.ingredientSelection + " missing required unit of measurement");
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* TO DO:
|
||||
*
|
||||
* this inner row isn't working correctly just yet
|
||||
* once the inputs for each row have been validated:
|
||||
*
|
||||
* 1. create new ingredient entries for new ingredients
|
||||
* 2. create ingredient recipe links for all recipes
|
||||
*/
|
||||
|
||||
for (let ing of row.ingredients) {
|
||||
// filter out recipes that already exist
|
||||
if (!ingredients.filter(x => x.name == ing.name).includes(ing)) {
|
||||
console.log(ing.name);
|
||||
|
||||
// post the new ingredient to the database
|
||||
const newEntry = await ingredientAPI.post(ing);
|
||||
const newID = newEntry.id;
|
||||
|
||||
const newIngredientData: RecipeIngredient = {
|
||||
ingredientid: newID,
|
||||
recipeid: recipeID ?? null,
|
||||
name: row.ingredientSelection as string,
|
||||
quantity: row.quantity,
|
||||
unit: row.measurement
|
||||
}
|
||||
|
||||
preparedIngredientData.push(newIngredientData);
|
||||
|
||||
messages.push(`Successfully created new ingredient: ${ing.name}!`);
|
||||
console.log(newEntry);
|
||||
newIngredientCount++;
|
||||
} else {
|
||||
const newIngredientData: RecipeIngredient = {
|
||||
ingredientid: (ingredients.filter(x => x.name == ing.name)[0].id as number),
|
||||
recipeid: recipeID ?? null,
|
||||
name: row.ingredientSelection as string,
|
||||
quantity: row.quantity,
|
||||
unit: row.measurement
|
||||
}
|
||||
|
||||
preparedIngredientData.push(newIngredientData);
|
||||
}
|
||||
|
||||
// update the ingredient list
|
||||
setIngredients((prev) => [...prev, ing]);
|
||||
}
|
||||
}
|
||||
|
||||
// handle recipe post resolve/reject
|
||||
if (result) {
|
||||
let recipeIngredientCount = 0;
|
||||
|
||||
for (let ing of preparedIngredientData) {
|
||||
const ok = await recipeAPI.addIngredientToRecipe(ing, recipeID);
|
||||
if (ok) recipeIngredientCount++;
|
||||
}
|
||||
|
||||
messages.push(`Created recipe ${recipeName} with ${recipeIngredientCount} total ingredients!`)
|
||||
if (newIngredientCount > 0) {
|
||||
messages.push(`Successfully created ${newIngredientCount} new ingredients! Thanks for helping us grow.`);
|
||||
}
|
||||
|
||||
setToast(
|
||||
<Toast>
|
||||
<p>Created recipe {recipeName} successfully!</p>
|
||||
<p>View your new recipe <a href={`/recipe/${recipeID}`}>here!</a></p>
|
||||
</Toast>
|
||||
)
|
||||
} else {
|
||||
setToast(
|
||||
<Toast variant="fail">
|
||||
<p>Error creating your recipe</p>
|
||||
<p>Please refresh the browser window and try again.</p>
|
||||
</Toast>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
authContext.user && setInput((prev: IRecipe) => {
|
||||
return {
|
||||
...prev,
|
||||
authoruserid: authContext.user!.id!
|
||||
}
|
||||
})
|
||||
}, [authContext])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(input);
|
||||
}, [input])
|
||||
// logic for inserting a new ingredient row
|
||||
function handleNewOption() {
|
||||
setIngredientFields((prev) => [...prev, <IngredientSelector position={optionCount + 1} key={v4()} ingredients={ingredients} units={measurements} getRowState={getRowState} destroy={destroySelector} />])
|
||||
setOptionCount(prev => prev + 1);
|
||||
}
|
||||
|
||||
/**********************************
|
||||
* RENDER
|
||||
*********************************/
|
||||
return (
|
||||
<Page>
|
||||
<Protect redirect="/add-recipe">
|
||||
<h1>Add a New Recipe</h1>
|
||||
<Divider />
|
||||
|
||||
<Panel>
|
||||
<Form parent={input} _config={{
|
||||
parent: "AddRecipe",
|
||||
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
|
||||
labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"],
|
||||
dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'custom picker', 'TINYMCE'],
|
||||
initialState: input,
|
||||
getState: getFormState,
|
||||
richTextInitialValue: "<p>Enter recipe details here!</p>"
|
||||
}} />
|
||||
<Panel id="create-recipe-panel" extraClasses="ui-form-component width-80">
|
||||
<div className="form-row">
|
||||
<label>Recipe Name:</label>
|
||||
<TextField variant="outlined" label="Recipe Name" onChange={(e) => setInput({ ...input, name: e.target.value })}/>
|
||||
</div>
|
||||
|
||||
{ form || <h2>Loading...</h2> }
|
||||
<div className="form-row">
|
||||
<label>Prep Time:</label>
|
||||
<TextField variant="outlined" label="Prep Time" onChange={(e) => setInput({ ...input, preptime: e.target.value })}/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label>Course:</label>
|
||||
{ courseData.length &&
|
||||
<Autocomplete
|
||||
autoHighlight
|
||||
options={courseData}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} variant="outlined" label="Course" />
|
||||
)}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>}
|
||||
</div>
|
||||
<Button onClick={() => console.log(ingredientFieldData)}>Ingredient Field Data</Button>
|
||||
|
||||
{ ingredients && (
|
||||
<>
|
||||
<Card extraClasses="form-row flex-row ingredient-card">
|
||||
<label id="ingredients-label">Ingredients:</label>
|
||||
<div className="ingredient-container">
|
||||
{ ingredientFields }
|
||||
<Button id="add-ingredient-button" onClick={handleNewOption}>Add Ingredient</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="form-row">
|
||||
<label id="description-label">Description:</label>
|
||||
<RichText id="add-ingredient-description" getState={(text) => setInput({ ...input, description: text })} />
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCreate}>Create Recipe!</Button>
|
||||
|
||||
<div id="toast">{ toast }</div>
|
||||
</Panel>
|
||||
</Page>
|
||||
</Protect>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddRecipe;
|
||||
@@ -1,38 +1,69 @@
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import Protect from "../../util/Protect";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import API from "../../util/API";
|
||||
import { ICollection, IRecipe, IUser } from "../../schemas";
|
||||
import { v4 } from "uuid";
|
||||
import { Panel } from "../ui";
|
||||
|
||||
const Collection = () => {
|
||||
const [isDefault, setIsDefault] = useState(true);
|
||||
const { user } = useAuthContext();
|
||||
const [data, setData] = useState<ICollection>();
|
||||
const [owner, setOwner] = useState<IUser>();
|
||||
const [recipes, setRecipes] = useState<IRecipe[]>();
|
||||
const [content, setContent] = useState(<></>);
|
||||
const { user, token } = useAuthContext();
|
||||
const { id } = useParams();
|
||||
|
||||
if (id) {
|
||||
setIsDefault(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
token && (async() => {
|
||||
const collections = new API.Collection(token);
|
||||
const users = new API.User(token);
|
||||
|
||||
const result: ICollection = await collections.getByID(id);
|
||||
setData(result);
|
||||
|
||||
const allRecipes = await collections.getRecipesFromOne(id);
|
||||
setRecipes(allRecipes);
|
||||
|
||||
const actualUser = await users.getByID(result.ownerid as string);
|
||||
setOwner(actualUser);
|
||||
})();
|
||||
}, [token])
|
||||
|
||||
useEffect(() => {
|
||||
if (user && data && recipes) {
|
||||
setContent(
|
||||
<>
|
||||
<div className="section-header">
|
||||
<h1>COLLECTION: {data.name}</h1>
|
||||
{ <p>Collection by: {owner ? `${owner.firstname} ${owner.lastname}` : `${user.firstname} ${user.lastname}`}</p> }
|
||||
<p>{recipes.length || 0} recipe{recipes.length != 1 && "s" }</p>
|
||||
{ data.ismaincollection && (
|
||||
owner ? <p>(This is {owner.firstname}'s main collection)</p> : <p>(This is your main collection)</p>
|
||||
)}
|
||||
|
||||
<Panel>
|
||||
{
|
||||
recipes && recipes.map((each: IRecipe) =>
|
||||
<div className="recipe-card" key={v4()}>
|
||||
<h2>{each.name}</h2>
|
||||
{ each.description && <div dangerouslySetInnerHTML={{ __html: each.description }}></div> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [data, recipes])
|
||||
|
||||
return (
|
||||
<Protect>
|
||||
{ isDefault ?
|
||||
|
||||
<>
|
||||
<h1>Mikayla's collection</h1>
|
||||
<p>37 recipes</p>
|
||||
<p>71 ingredients</p>
|
||||
<p>11 types of cuisine</p>
|
||||
</>
|
||||
|
||||
:
|
||||
|
||||
<>
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
|
||||
{/* recipes */}
|
||||
<Protect redirect={`/collections/${id}`}>
|
||||
{ content }
|
||||
</Protect>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,69 @@
|
||||
import { Page } from "../ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { ICollection } from "../../schemas";
|
||||
import API from "../../util/API";
|
||||
import Protect from "../../util/Protect";
|
||||
import { Page, Panel } from "../ui";
|
||||
|
||||
const CollectionBrowser = () => {
|
||||
const [list, setList] = useState<ICollection[]>();
|
||||
const { token } = useAuthContext();
|
||||
|
||||
async function getRecipeCount(collection: ICollection) {
|
||||
if (!token) return [];
|
||||
const collections = new API.Collection(token);
|
||||
const result = await collections.getRecipesFromOne(collection.id);
|
||||
if (result) return result;
|
||||
return [];
|
||||
}
|
||||
|
||||
async function mapRecipes() {
|
||||
if (!list) return;
|
||||
|
||||
return list.map(async (each) => {
|
||||
const count = await getRecipeCount(each);
|
||||
|
||||
return (
|
||||
<Panel key={v4()}>
|
||||
<h2>{each.name}</h2>
|
||||
<p>{count.length} recipes</p>
|
||||
<a href={`/collections/${each.id}`}>Link to details</a>
|
||||
</Panel>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
(async() => {
|
||||
const collections = new API.Collection(token);
|
||||
const allRecipes = await collections.getAllAuthored();
|
||||
if (allRecipes) setList(allRecipes);
|
||||
})();
|
||||
}, [token])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [list])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Browsing your {2} collections:</h1>
|
||||
</Page>
|
||||
<Protect redirect="/collections">
|
||||
{ list && (
|
||||
<>
|
||||
<h1>Browsing your {list.length} collection{ (list.length !== 1) && "s" }:</h1>
|
||||
|
||||
{ list.map(each => {
|
||||
return (
|
||||
<Panel key={v4()}>
|
||||
<h2>{each.name}</h2>
|
||||
<a href={`/collections/${each.id}`}>Link to details</a>
|
||||
</Panel>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Protect>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AuthContext, useAuthContext } from "../../context/AuthContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { IUser, IUserAuth } from "../../schemas";
|
||||
import { Button, Form, Page, Panel } from "../ui";
|
||||
import { FormConfig } from "../ui/Form";
|
||||
import { FormConfig } from "../ui/Form/Form";
|
||||
import API from "../../util/API";
|
||||
|
||||
export default function Login() {
|
||||
@@ -29,7 +29,7 @@ export default function Login() {
|
||||
setToken(result.token);
|
||||
|
||||
// if there is a redirect, go there, else go home
|
||||
navigate(`/${redirect ?? ''}`);
|
||||
navigate(redirect ?? '/');
|
||||
}
|
||||
|
||||
// check for logged in user and mount form
|
||||
@@ -41,9 +41,9 @@ export default function Login() {
|
||||
<Page>
|
||||
<h1>Hello! Nice to see you again.</h1>
|
||||
|
||||
<Panel extraStyles="form-panel">
|
||||
<Panel extraClasses="form-panel">
|
||||
|
||||
<Form parent={input} _config={{
|
||||
<Form<IUserAuth> _config={{
|
||||
parent: 'login',
|
||||
keys: Object.keys(input),
|
||||
labels: ["Email", "Password"],
|
||||
|
||||
@@ -1,22 +1,196 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { IUser } from "../../schemas";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AuthContext, useAuthContext } from "../../context/AuthContext";
|
||||
import { Button, Page } from "../ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AxiosError } from "axios";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import API from "../../util/API";
|
||||
import Protect from "../../util/Protect";
|
||||
import { ICollection, IUser } from "../../schemas";
|
||||
import Friends from "../derived/Friends";
|
||||
import CollectionList from "../derived/CollectionList";
|
||||
import { Button, Divider, Page, Panel } from "../ui";
|
||||
import useDateFormat from "../../hooks/useDateFormat";
|
||||
|
||||
interface ProfileMetadata {
|
||||
targetID?: string | number
|
||||
targetUser?: IUser | undefined
|
||||
formattedDate: string
|
||||
collections: ICollection[]
|
||||
friends: IUser[]
|
||||
isSet: boolean
|
||||
}
|
||||
|
||||
export default function Profile() {
|
||||
const [message, setMessage] = useState<JSX.Element>();
|
||||
const { user } = useAuthContext();
|
||||
// globals and router utils
|
||||
const { user, token } = useAuthContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Protect redirect="profile">
|
||||
<div className="profile-authenticated">
|
||||
<h1>{user && user.firstname}'s Profile</h1>
|
||||
<Friends />
|
||||
</div>
|
||||
</Protect>
|
||||
)
|
||||
// UI state info
|
||||
const [contents, setContents] = useState<JSX.Element>(<></>);
|
||||
|
||||
// master state for this page
|
||||
const [metadata, setMetadata] = useState<ProfileMetadata>({
|
||||
targetID: undefined,
|
||||
targetUser: undefined,
|
||||
formattedDate: "",
|
||||
collections: [],
|
||||
friends: [],
|
||||
isSet: false
|
||||
});
|
||||
|
||||
// STEP 1: FETCH METADATA (requires token)
|
||||
useEffect(() => {
|
||||
if (!token || !user) return;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const targetID = params.get('id');
|
||||
|
||||
// if a target is specified in the url
|
||||
if (targetID) {
|
||||
setMetadata((prev: ProfileMetadata) => {
|
||||
return { ...prev, targetID: targetID }
|
||||
});
|
||||
|
||||
// fetch and store user data with associated user id
|
||||
(async() => {
|
||||
try {
|
||||
const User = new API.User(token);
|
||||
const result = await User.getByID(targetID);
|
||||
if (result) {
|
||||
setMetadata((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
targetUser: result,
|
||||
formattedDate: useDateFormat(result.datecreated)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error?.response?.status == 404) {
|
||||
// to do: replace with customizable 404 page
|
||||
setContents(
|
||||
<Page>
|
||||
<h1>404: Not found</h1>
|
||||
<Divider />
|
||||
<Panel>
|
||||
<p>No user found with ID {targetID}</p>
|
||||
<Button onClick={() => navigate('/')}>Home</Button>
|
||||
</Panel>
|
||||
</Page>
|
||||
)
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// do the same for this user's collections and friends
|
||||
(async() => {
|
||||
const Collections = new API.Collection(token);
|
||||
const result = await Collections.getAllAuthored(metadata.targetID);
|
||||
if (result) {
|
||||
setMetadata((prev: ProfileMetadata) => {
|
||||
return {
|
||||
...prev, collections: result
|
||||
}
|
||||
})
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
// otherwise, this is the current user's profile and should load some slightly different info
|
||||
(async() => {
|
||||
const Collections = new API.Collection(token);
|
||||
const result = await Collections.getAllAuthored();
|
||||
if (result) {
|
||||
setMetadata((prev: ProfileMetadata) => {
|
||||
return {
|
||||
...prev, collections: result
|
||||
}
|
||||
})
|
||||
}
|
||||
})();
|
||||
|
||||
setMetadata((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
formattedDate: useDateFormat(user.datecreated)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setMetadata((prev) => {
|
||||
return { ...prev, isSet: true }
|
||||
})
|
||||
}, [token]);
|
||||
|
||||
// STEP 2: set up page UI based on profile config above
|
||||
useEffect(() => {
|
||||
if (metadata.isSet) {
|
||||
|
||||
// if this is another user's profile
|
||||
if (metadata.targetUser) {
|
||||
setContents(
|
||||
<Protect redirect="/">
|
||||
<div className="profile-authenticated">
|
||||
<h1>{metadata.targetUser.firstname}'s Profile</h1>
|
||||
|
||||
<div className="profile-grid">
|
||||
<Panel>
|
||||
<h2>About {metadata.targetUser.firstname} {metadata.targetUser.lastname}:</h2>
|
||||
<p>Recipin Member since: {metadata.formattedDate}</p>
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<h2>{metadata.targetUser.firstname}'s collections ({ metadata.collections.length ?? "0" }):</h2>
|
||||
<CollectionList targetID={metadata.targetUser.id} />
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<h2>{metadata.targetUser.firstname}'s friends:</h2>
|
||||
<Friends />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</Protect>
|
||||
)
|
||||
} else {
|
||||
|
||||
// if this is the current user's profile
|
||||
setContents(
|
||||
<Protect redirect="/profile">
|
||||
<div className="profile-authenticated">
|
||||
<h1>{user!.firstname}'s Profile</h1>
|
||||
|
||||
<div className="profile-grid">
|
||||
<Panel>
|
||||
<h2>About me:</h2>
|
||||
<p>{user!.firstname} {user!.lastname}</p>
|
||||
<p>Recipin Member since: {metadata.formattedDate}</p>
|
||||
<Divider />
|
||||
<p>30 recipes</p>
|
||||
<p>2 collections</p>
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
{/* include number of collections */}
|
||||
<h2><a href="/collections">My collections</a> ({ metadata.collections.length || 0 }):</h2>
|
||||
<CollectionList />
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<Friends />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</Protect>
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [metadata])
|
||||
|
||||
// STEP 3: mount the UI
|
||||
return contents
|
||||
}
|
||||
@@ -1,36 +1,109 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Page, Panel } from "../ui";
|
||||
import { IRecipe } from "../../util/types";
|
||||
import { Divider, Page, Panel } from "../ui";
|
||||
import { IRecipe, IUser, IIngredient, RecipeIngredient } from "../../schemas";
|
||||
import { getRecipeByID } from "../../util/apiUtils";
|
||||
import Protect from "../../util/Protect";
|
||||
import API from "../../util/API";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import ResourceNotFound from "./StatusPages/404";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export default function Recipe() {
|
||||
const [recipe, setRecipe] = useState<IRecipe>();
|
||||
const { user, token } = useAuthContext();
|
||||
const { id } = useParams();
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
<Page>
|
||||
<h1>404 | Not Found</h1>
|
||||
<p>There's no content here! Technically, you shouldn't have even been able to get here.</p>
|
||||
<p>So, kudos, I guess!</p>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
const [recipe, setRecipe] = useState<IRecipe | "no recipe">();
|
||||
const [userData, setUserData] = useState<IUser>();
|
||||
const [ingredientData, setIngredientData] = useState<RecipeIngredient[]>([]);
|
||||
const [view, setView] = useState<JSX.Element>(<h1>Loading...</h1>);
|
||||
|
||||
useEffect(() => {
|
||||
getRecipeByID(id);
|
||||
}, [])
|
||||
if (token && id) {
|
||||
(async() => {
|
||||
const recipeAPI = new API.Recipe(token);
|
||||
const result = await recipeAPI.getByID(id);
|
||||
if (result) {
|
||||
setRecipe(result);
|
||||
} else {
|
||||
setRecipe("no recipe");
|
||||
}
|
||||
})()
|
||||
}
|
||||
}, [token])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{ recipe && (
|
||||
<Panel>
|
||||
useEffect(() => {
|
||||
if (recipe && id) {
|
||||
if (recipe === "no recipe") {
|
||||
setView(<ResourceNotFound><h2>We couldn't find a recipe with the ID {id}.</h2></ResourceNotFound>);
|
||||
} else {
|
||||
if (!user || !token) return;
|
||||
|
||||
// while ingredient data has not yet been mapped,
|
||||
// get ingredients and map them
|
||||
(!ingredientData.length) && (async() => {
|
||||
const ingredientAPI = new API.Ingredient(token);
|
||||
const result = await ingredientAPI.getAllForRecipe(id);
|
||||
setIngredientData(result);
|
||||
})();
|
||||
|
||||
const selfAuthored = (recipe.authoruserid == user.id!);
|
||||
if (selfAuthored) {
|
||||
setUserData(user);
|
||||
} else {
|
||||
(async() => {
|
||||
const userAPI = new API.User(token);
|
||||
const foundUser = await userAPI.getByID(recipe.authoruserid as string);
|
||||
setUserData(foundUser);
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [recipe, id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!userData) return;
|
||||
|
||||
if (recipe && ingredientData && recipe !== "no recipe") {
|
||||
setView(
|
||||
<Protect redirect={`/recipe/${id}`}>
|
||||
<h1>{recipe.name}</h1>
|
||||
<p>{recipe.description}</p>
|
||||
<p>{recipe.preptime}</p>
|
||||
</Panel>
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
<h2>Provided courtesy of {userData.firstname} {userData.lastname}</h2>
|
||||
|
||||
<Divider />
|
||||
|
||||
<p>Prep time: {recipe.preptime}</p>
|
||||
|
||||
<Divider />
|
||||
|
||||
<h2>Ingredients:</h2>
|
||||
{ ingredientData.length
|
||||
? ingredientData.map((each: RecipeIngredient) => (
|
||||
<div key={v4()}>
|
||||
<p>{each.quantity} {each.quantity == 1 ? each.unit : (each.unit + "s")} {each.name}</p>
|
||||
</div>
|
||||
)) : <p>No ingredients for this recipe</p>
|
||||
}
|
||||
|
||||
<Divider />
|
||||
|
||||
<div dangerouslySetInnerHTML={{ __html: (recipe.description || "")}}></div>
|
||||
|
||||
</Protect>
|
||||
);
|
||||
}
|
||||
}, [userData, recipe, ingredientData, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ingredientData.length || !token) return;
|
||||
for (let each of ingredientData) {
|
||||
(async() => {
|
||||
const ingredientAPI = new API.Ingredient(token);
|
||||
const result = await ingredientAPI.getByID(each.ingredientid!.toString());
|
||||
console.log(result);
|
||||
})();
|
||||
}
|
||||
}, [ingredientData, token])
|
||||
|
||||
return view
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { v4 } from "uuid";
|
||||
import { RegisterVariantType, VariantLabel } from ".";
|
||||
import { useAuthContext } from "../../../context/AuthContext";
|
||||
import { IUser, IUserAuth } from "../../../schemas";
|
||||
import { attemptLogin, attemptRegister } from "../../../util/apiUtils";
|
||||
import { IUser } from "../../../schemas";
|
||||
import API from "../../../util/API";
|
||||
import { Button, Page, Panel } from "../../ui";
|
||||
import Divider from "../../ui/Divider";
|
||||
import Form from "../../ui/Form";
|
||||
import Form from "../../ui/Form/Form";
|
||||
|
||||
const blankUser: IUser = {
|
||||
firstname: '',
|
||||
@@ -54,9 +52,9 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
|
||||
|
||||
<h2>Tell us a bit about yourself:</h2>
|
||||
|
||||
<Panel extraStyles="form-panel two-columns">
|
||||
<Panel extraClasses="form-panel two-columns">
|
||||
|
||||
<Form parent={input} _config={{
|
||||
<Form<IUser> _config={{
|
||||
parent: "register",
|
||||
keys: ['firstname', 'lastname', 'handle', 'email', 'password'],
|
||||
initialState: input,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RegisterVariantType, VariantLabel } from ".";
|
||||
import { useNow } from "../../../hooks/useNow";
|
||||
import { ICollection, IUser, IUserAuth } from "../../../schemas";
|
||||
import { attemptLogin, createNewCollection } from "../../../util/apiUtils";
|
||||
import { ICollection } from "../../../schemas";
|
||||
import API from "../../../util/API";
|
||||
import { Button, Divider, Page, Panel } from "../../ui";
|
||||
import TextField from "../../ui/TextField";
|
||||
import { useAuthContext } from "../../../context/AuthContext";
|
||||
|
||||
const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) => {
|
||||
@@ -28,10 +26,7 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) =>
|
||||
datemodified: now
|
||||
}
|
||||
|
||||
console.log(collection);
|
||||
|
||||
const result = await collectionAPI.post(collection);
|
||||
console.log(result);
|
||||
if (result) transitionDisplay(VariantLabel.AddFriends);
|
||||
}
|
||||
|
||||
@@ -50,7 +45,7 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) =>
|
||||
<h3>What would you like to call your main collection?</h3>
|
||||
|
||||
{/* <TextField onChange={(e: ChangeEvent<HTMLInputElement>) => setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'} /> */}
|
||||
<input type="text" onChange={(e) => setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'}></input>
|
||||
<input type="text" onChange={(e) => setCollectionName(e.target.value)} placeholder={user.firstname + '\'s Collection'}></input>
|
||||
</Panel>
|
||||
|
||||
<Button onClick={handleClick}>Next</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { useAuthContext } from "../../../context/AuthContext";
|
||||
import { IUser } from "../../../schemas";
|
||||
import AboutYou from "./aboutyou";
|
||||
|
||||
16
client/src/components/pages/StatusPages/403.tsx
Normal file
16
client/src/components/pages/StatusPages/403.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Divider, Page } from "../../ui";
|
||||
|
||||
export default function AccessForbidden({ children = <></> }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>403: Unauthorized</h1>
|
||||
{ children }
|
||||
|
||||
<Divider />
|
||||
<Button onClick={() => navigate('/')}>Home</Button>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
16
client/src/components/pages/StatusPages/404.tsx
Normal file
16
client/src/components/pages/StatusPages/404.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Divider, Page } from "../../ui";
|
||||
|
||||
export default function ResourceNotFound({ children = <></> }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>404: We didn't find what you are looking for</h1>
|
||||
{ children }
|
||||
|
||||
<Divider />
|
||||
<Button onClick={() => navigate('/')}>Home</Button>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { attemptLogout, checkCredientials } from "../../util/apiUtils";
|
||||
import { Button, Page, Panel } from "../ui"
|
||||
import Divider from "../ui/Divider";
|
||||
|
||||
const Welcome = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, setUser } = useAuthContext();
|
||||
const { user } = useAuthContext();
|
||||
|
||||
const authUserActions = (
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase flexrow">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase flexrow">
|
||||
<Button onClick={() => navigate('/explore')}>Browse Recipes</Button>
|
||||
<Button onClick={() => navigate('/subscriptions')}>Subscriptions</Button>
|
||||
<Button onClick={() => navigate('/grocery-list')}>Grocery Lists</Button>
|
||||
@@ -18,30 +16,28 @@ const Welcome = () => {
|
||||
)
|
||||
|
||||
const callToRegister = (
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||
<h2>Ready to get started?</h2>
|
||||
<Button onClick={() => navigate('/register')}>Register</Button>
|
||||
<Button onClick={attemptLogout}>Log Out</Button>
|
||||
</Panel>
|
||||
)
|
||||
|
||||
return (
|
||||
<Page extraStyles="narrow-dividers">
|
||||
<Panel extraStyles='inherit-background c-papyrus uppercase'>
|
||||
<Page extraClasses="narrow-dividers">
|
||||
<Panel extraClasses='inherit-background c-papyrus uppercase'>
|
||||
<h1>Welcome to Recipin</h1>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||
<h2>Simple Recipe Management and Sharing for the Home</h2>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||
<h2>Build Shopping Lists Directly from Your Recipes</h2>
|
||||
<button onClick={checkCredientials}></button>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import Protect from "../../util/Protect";
|
||||
import Form from "./Form";
|
||||
import Form from "./Form/Form";
|
||||
|
||||
interface BrowserProps {
|
||||
children?: JSX.Element[]
|
||||
@@ -9,6 +10,8 @@ interface BrowserProps {
|
||||
}
|
||||
|
||||
const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => {
|
||||
const { user, token } = useAuthContext();
|
||||
|
||||
const [form, setForm] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -16,7 +19,7 @@ const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Protect>
|
||||
<Protect redirect="/explore">
|
||||
<h1>{header}</h1>
|
||||
</Protect>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { ButtonComponent } from "../../util/types"
|
||||
import "/src/sass/components/Button.scss";
|
||||
|
||||
const Button: ButtonComponent = ({ onClick = (() => {}), children, extraStyles, disabled = false, disabledText = null }) => {
|
||||
const Button: ButtonComponent = ({ onClick = (() => {}), children, extraClasses, id = null, disabled = false, disabledText = null }) => {
|
||||
if (id?.length) {
|
||||
return (
|
||||
<button id={id} onClick={onClick} disabled={disabled} className={`ui-button ${extraClasses || ''}`}>
|
||||
{ disabled ? (disabledText || children || "Button") : (children || "Button") }
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={onClick} disabled={disabled} className={`ui-button ${extraStyles || ''}`}>
|
||||
<button onClick={onClick} disabled={disabled} className={`ui-button ${extraClasses || ''}`}>
|
||||
{ disabled ? (disabledText || children || "Button") : (children || "Button") }
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC } from "react"
|
||||
import { MultiChildPortal } from "../../util/types"
|
||||
import { FC } from "react";
|
||||
|
||||
const Card: FC<MultiChildPortal> = ({ children = <></>, extraStyles = ""}) => {
|
||||
|
||||
const Card: FC<{ children?: JSX.Element | JSX.Element[], extraClasses?: string }> = ({ children = <></>, extraClasses = ""}) => {
|
||||
return (
|
||||
<div className={`ui-card ${extraStyles}`}>
|
||||
{ children }
|
||||
<div className={`ui-card ${extraClasses}`}>
|
||||
{ Array.isArray(children) ? <>{children}</> : children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { PortalBase } from "../../util/types";
|
||||
import "/src/sass/components/Dropdown.scss";
|
||||
|
||||
// expects to receive buttons as children
|
||||
const Dropdown: FC<PortalBase> = ({ children, extraStyles = null }) => {
|
||||
const Dropdown: FC<PortalBase> = ({ children, extraClasses = null }) => {
|
||||
return (
|
||||
<Panel extraStyles={`ui-dropdown ${extraStyles}`}>
|
||||
<Panel extraClasses={`ui-dropdown ${extraClasses}`}>
|
||||
{ children }
|
||||
</Panel>
|
||||
)
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import { v4 } from "uuid"
|
||||
import RichText from "./RichText"
|
||||
|
||||
export interface FormConfig<T> {
|
||||
parent: string
|
||||
keys: string[]
|
||||
initialState: T
|
||||
getState: (received: T) => void
|
||||
labels?: string[]
|
||||
dataTypes?: string[]
|
||||
richTextInitialValue?: string
|
||||
extraStyles?: string
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
parent: any
|
||||
_config: FormConfig<any>
|
||||
}
|
||||
|
||||
const Form: FC<FormProps> = ({ parent, _config }) => {
|
||||
type T = typeof parent;
|
||||
const { getState } = _config;
|
||||
|
||||
const [config, setConfig] = useState<FormConfig<T>>();
|
||||
const [state, setState] = useState<T>();
|
||||
const [contents, setContents] = useState<JSX.Element[]>();
|
||||
|
||||
// initial setup
|
||||
useEffect(() => {
|
||||
if (!config) setConfig({
|
||||
..._config,
|
||||
labels: _config.labels ?? _config.keys,
|
||||
dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"),
|
||||
});
|
||||
|
||||
if (!state) setState(_config.initialState);
|
||||
}, [])
|
||||
|
||||
// usecallback handling
|
||||
useEffect(() => {
|
||||
state && getState(state);
|
||||
}, [state]);
|
||||
|
||||
// update methods
|
||||
function updateRichText(txt: string, idx: number) {
|
||||
if (!config) return;
|
||||
|
||||
setState((prev: T) => {
|
||||
return {
|
||||
...prev,
|
||||
[config.keys[idx]]: txt
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function update(e: ChangeEvent<HTMLElement>, idx: number) {
|
||||
if (!config) return;
|
||||
|
||||
setState((prev: T) => {
|
||||
return {
|
||||
...prev,
|
||||
[config.keys[idx]]: e.target['value' as keyof EventTarget]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// mount the form once config has been loaded
|
||||
useEffect(() => {
|
||||
if (state && config) {
|
||||
const result = config.keys.map((each: string, i: number) => {
|
||||
|
||||
if (config.dataTypes![i] == 'TINYMCE') {
|
||||
return (
|
||||
<div id={`${config.parent}-row-${i}`} key={v4()}>
|
||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||
<RichText id={`${config.parent}-${each}`} initialValue={config.richTextInitialValue} getState={(txt) => updateRichText(txt, i)} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div id={`${config.parent}-row-${i}`} key={v4()}>
|
||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||
<input
|
||||
type={config.dataTypes![i]}
|
||||
id={`${config.parent}-${each}`}
|
||||
onChange={(e) => update(e, i)}
|
||||
value={state[i as keyof T] as string}>
|
||||
</input>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
setContents(result);
|
||||
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<div className={`ui-form-component ${_config.extraStyles}`}>
|
||||
{ contents }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form;
|
||||
141
client/src/components/ui/Form/Form.tsx
Normal file
141
client/src/components/ui/Form/Form.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import { v4 } from "uuid"
|
||||
import { useAuthContext } from "../../../context/AuthContext";
|
||||
import { useSelectorContext } from "../../../context/SelectorContext";
|
||||
import SelectorProvider from "../../../context/SelectorProvider";
|
||||
import { IIngredient, IUser } from "../../../schemas";
|
||||
import API from "../../../util/API";
|
||||
import RichText from "../RichText"
|
||||
import Selector from "../Selector";
|
||||
import "/src/sass/components/Form.scss";
|
||||
|
||||
export interface FormConfig<T> {
|
||||
parent: string
|
||||
keys: string[]
|
||||
initialState: T
|
||||
getState: (received: T) => void
|
||||
labels?: string[]
|
||||
dataTypes?: string[]
|
||||
richTextInitialValue?: string
|
||||
extraClasses?: string
|
||||
selectorInstance?: JSX.Element
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
_config: FormConfig<any>
|
||||
}
|
||||
|
||||
function Form<T>({ _config }: FormProps) {
|
||||
const { getState } = _config;
|
||||
|
||||
const [config, setConfig] = useState<FormConfig<T>>();
|
||||
const [state, setState] = useState<T>(_config.initialState);
|
||||
const [contents, setContents] = useState<JSX.Element[]>();
|
||||
|
||||
const { token } = useAuthContext();
|
||||
|
||||
// initial setup
|
||||
useEffect(() => {
|
||||
setConfig({
|
||||
..._config,
|
||||
labels: _config.labels ?? _config.keys,
|
||||
dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"),
|
||||
});
|
||||
|
||||
if (!state) setState(_config.initialState);
|
||||
}, [])
|
||||
|
||||
// usecallback handling
|
||||
useEffect(() => {
|
||||
state && getState(state);
|
||||
}, [state]);
|
||||
|
||||
// update methods
|
||||
function updateRichText(txt: string, idx: number) {
|
||||
if (!config) return;
|
||||
|
||||
setState((prev: T) => {
|
||||
return {
|
||||
...prev,
|
||||
[config.keys[idx]]: txt
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function update(e: ChangeEvent<HTMLElement>, idx: number) {
|
||||
if (!config) return;
|
||||
|
||||
setState((prev: T) => {
|
||||
return {
|
||||
...prev,
|
||||
[config.keys[idx]]: e.target['value' as keyof EventTarget]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function populateSelector(key: string): Promise<any[] | null> {
|
||||
if (!token) return null;
|
||||
|
||||
switch (key) {
|
||||
case "ingredient":
|
||||
const ingredients = new API.Ingredient(token);
|
||||
const result = await ingredients.getAll();
|
||||
if (result) return result;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// mount the form once config has been loaded
|
||||
useEffect(() => {
|
||||
if (state && config) {
|
||||
(async() => {
|
||||
const result = config.keys.map(async (each: string, i: number) => {
|
||||
if (config.dataTypes![i] == 'TINYMCE') {
|
||||
return (
|
||||
<div className="form-row-editor" id={`${config.parent}-row-${i}`} key={v4()}>
|
||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||
<RichText id={`${config.parent}-${each}`} initialValue={config.richTextInitialValue} getState={(txt) => updateRichText(txt, i)} />
|
||||
</div>
|
||||
)
|
||||
} else if (config.dataTypes![i] == 'SELECTOR') {
|
||||
if (!config.selectorInstance) throw new Error("Dropdown was not provided to form component.")
|
||||
|
||||
return (
|
||||
<div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}>
|
||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||
{ config.selectorInstance }
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}>
|
||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||
<input
|
||||
type={config.dataTypes![i]}
|
||||
id={`${config.parent}-${each}`}
|
||||
onChange={(e) => update(e, i)}
|
||||
value={state[i as keyof T] as string}>
|
||||
</input>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
const mappedContents = await Promise.all(result);
|
||||
mappedContents && setContents(mappedContents);
|
||||
})();
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<div className={`ui-form-component ${_config.extraClasses ?? ""}`}>
|
||||
{ contents }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form;
|
||||
38
client/src/components/ui/Form/FormRow.tsx
Normal file
38
client/src/components/ui/Form/FormRow.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
interface FormRowConfig {
|
||||
parent: any
|
||||
labelText: string
|
||||
idx?: number
|
||||
dataType?: string
|
||||
}
|
||||
|
||||
function FormRow({ parent, labelText, idx, dataType = "string" }) {
|
||||
const [row, setRow] = useState<JSX.Element>();
|
||||
|
||||
useEffect(() => {
|
||||
switch (dataType) {
|
||||
case "TINYMCE":
|
||||
break;
|
||||
case "string":
|
||||
default:
|
||||
setRow(
|
||||
<div className="form-row" id={`${parent}-row-${idx || v4()}`} key={v4()}>
|
||||
<label htmlFor={`${parent}-${each}`}>{labelText}</label>
|
||||
<input
|
||||
type={dataType}
|
||||
id={`${parent}-${each}`}
|
||||
onChange={(e) => update(e, idx)}
|
||||
value={state[i as keyof T] as string}>
|
||||
</input>
|
||||
</div>
|
||||
)
|
||||
break;
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
0
client/src/components/ui/Grid.tsx
Normal file
0
client/src/components/ui/Grid.tsx
Normal file
@@ -1,7 +1,6 @@
|
||||
import API from "../../../util/API";
|
||||
import { NavbarType } from "../../../util/types";
|
||||
import { Button, Dropdown } from '..'
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useAuthContext } from "../../../context/AuthContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -14,8 +13,7 @@ const LoggedIn = () => {
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
const success = await auth.logout();
|
||||
console.log(success);
|
||||
await auth.logout();
|
||||
|
||||
// nullify cookie and unset user/token data
|
||||
document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
@@ -40,10 +38,6 @@ const LoggedIn = () => {
|
||||
navigate(payload);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(user);
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="navbar">
|
||||
@@ -59,8 +53,9 @@ const LoggedIn = () => {
|
||||
</div>
|
||||
{
|
||||
dropdownActive && (
|
||||
<Dropdown extraStyles="top-menu-bar actions-bar">
|
||||
<Dropdown extraClasses="top-menu-bar actions-bar">
|
||||
<Button onClick={() => handleOptionSelect('/add-recipe')}>Add a Recipe</Button>
|
||||
<Button onClick={() => handleOptionSelect("/add-friends")}>Add Friends</Button>
|
||||
<Button onClick={() => handleOptionSelect('/collections')}>My Collections</Button>
|
||||
<Button onClick={() => handleOptionSelect('/subscriptions')}>Subscriptions</Button>
|
||||
<Button onClick={() => handleOptionSelect('/profile')}>Profile</Button>
|
||||
@@ -70,7 +65,7 @@ const LoggedIn = () => {
|
||||
}
|
||||
{
|
||||
searchActive && (
|
||||
<Dropdown extraStyles="top-menu-bar search-bar">
|
||||
<Dropdown extraClasses="top-menu-bar search-bar">
|
||||
<Button>Run Search</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
|
||||
@@ -5,10 +5,10 @@ import { PageComponent } from "../../util/types"
|
||||
import Navbar from "./Navbar";
|
||||
import "/src/sass/components/Page.scss";
|
||||
|
||||
const Page: PageComponent = ({ extraStyles, children }) => {
|
||||
const Page: PageComponent = ({ extraClasses, children }) => {
|
||||
return (
|
||||
<main id="view">
|
||||
<section className={`Page ${extraStyles || null}`}>
|
||||
<section className={`Page ${extraClasses || null}`}>
|
||||
{ children || null }
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { PanelComponent } from "../../util/types";
|
||||
import "/src/sass/components/Panel.scss";
|
||||
|
||||
const Panel: PanelComponent = ({ children, extraStyles }) => {
|
||||
return (
|
||||
<div className={`Panel ${extraStyles || ''}`}>
|
||||
{ children || null }
|
||||
</div>
|
||||
)
|
||||
const Panel: PanelComponent = ({ children, extraClasses, id }) => {
|
||||
if (id) {
|
||||
return (
|
||||
<div id={id} className={`Panel ${extraClasses || ''}`}>
|
||||
{ children || null }
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className={`Panel ${extraClasses || ''}`}>
|
||||
{ children || null }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Panel;
|
||||
@@ -25,7 +25,7 @@ const RichText: FC<RichTextProps> = ({ id, initialValue, getState }) => {
|
||||
onEditorChange={(txt, editor) => handleChange(txt, editor)}
|
||||
initialValue={initialValue || '<p></p>'}
|
||||
init={{
|
||||
height: 500,
|
||||
height: 300,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist autolink lists link image charmap print preview anchor',
|
||||
|
||||
23
client/src/components/ui/Selector.tsx
Normal file
23
client/src/components/ui/Selector.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { FormConfig } from "./Form/Form"
|
||||
|
||||
interface OptionType {
|
||||
value: number
|
||||
label: string
|
||||
}
|
||||
|
||||
interface SelectorProps<T> {
|
||||
config: FormConfig<T>
|
||||
idx: number
|
||||
update: (e: any, idx: number) => void
|
||||
optionList: Array<OptionType>
|
||||
loader?: Array<T>
|
||||
}
|
||||
|
||||
function Selector<T>({ config, idx, update, optionList }: SelectorProps<T>) {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Selector
|
||||
19
client/src/components/ui/Toast.tsx
Normal file
19
client/src/components/ui/Toast.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FC } from "react";
|
||||
import "/src/sass/components/Toast.scss";
|
||||
|
||||
type StyleVariant = "success" | "fail" | "warning"
|
||||
|
||||
interface ToastProps {
|
||||
children: JSX.Element | JSX.Element[]
|
||||
variant?: StyleVariant
|
||||
}
|
||||
|
||||
const Toast: FC<ToastProps> = ({ children, variant = "success" }) => {
|
||||
return (
|
||||
<div className={`ui-component-toast toast-variant-${variant}`}>
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toast;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FC } from "react";
|
||||
import { PortalBase } from "../../util/types";
|
||||
|
||||
const Tooltip: FC<PortalBase> = ({ children, extraStyles = null }) => {
|
||||
const Tooltip: FC<PortalBase> = ({ children, extraClasses = null }) => {
|
||||
return (
|
||||
<aside className={`ui-tooltip ${extraStyles}`}>
|
||||
<aside className={`ui-tooltip ${extraClasses}`}>
|
||||
{ children }
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -1,40 +1,57 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { addFriend, getPendingFriendRequests } from "../../util/apiUtils";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { UserCardType } from "../../util/types";
|
||||
import API from "../../util/API";
|
||||
import Button from "./Button";
|
||||
import Card from "./Card";
|
||||
|
||||
const UserCard: UserCardType = ({ extraStyles, user, canAdd = false, liftData }) => {
|
||||
const [shouldDisable, setShouldDisable] = useState<boolean>(canAdd);
|
||||
const UserCard: UserCardType = ({ extraClasses, targetUser }) => {
|
||||
const [buttonVariant, setButtonVariant] = useState(<></>);
|
||||
const { token } = useAuthContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
const requestsOpen = await getPendingFriendRequests();
|
||||
if (!requestsOpen) return;
|
||||
if (!token) return;
|
||||
|
||||
for (let req of requestsOpen) {
|
||||
if (req.targetid == user.id) {
|
||||
setShouldDisable(true);
|
||||
return;
|
||||
(async function() {
|
||||
try {
|
||||
const friends = new API.Friendship(token);
|
||||
const requestsOpen = await friends.getPendingFriendRequests();
|
||||
if (!requestsOpen) return;
|
||||
|
||||
for (let req of requestsOpen) {
|
||||
if (req.targetid == targetUser.id) {
|
||||
setButtonVariant(<Button disabled>Request Sent!</Button>)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setButtonVariant(<Button onClick={handleClick}>Send Request</Button>)
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
console.log(error.response?.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
setShouldDisable(false);
|
||||
})();
|
||||
}, [])
|
||||
|
||||
const handleClick = async () => {
|
||||
const { id } = user;
|
||||
const request = await addFriend(id!.toString());
|
||||
if (request) console.log("Friend request sent to " + user.firstname);
|
||||
if (!token) return;
|
||||
const friends = new API.Friendship(token);
|
||||
const request = await friends.addFriend(targetUser.id!.toString());
|
||||
if (request) {
|
||||
setButtonVariant(<Button disabled>Request Sent!</Button>)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card extraStyles={'user-card' + extraStyles}>
|
||||
<Card extraClasses={'user-card' + extraClasses}>
|
||||
<>
|
||||
<div className="avatar"></div>
|
||||
<h3>{user.firstname} {user.lastname.substring(0,1)}.</h3>
|
||||
<h4>@{user.handle}</h4>
|
||||
{ canAdd && <Button disabledText={"Request Sent"} disabled={shouldDisable} onClick={handleClick}>Add Me</Button> }
|
||||
<h3><a href={`/profile?id=${targetUser.id}`}>{targetUser.firstname} {targetUser.lastname.substring(0,1)}.</a></h3>
|
||||
<h4>@{targetUser.handle}</h4>
|
||||
{ buttonVariant }
|
||||
</>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
60
client/src/components/ui/Widgets/NewFriendWidget.tsx
Normal file
60
client/src/components/ui/Widgets/NewFriendWidget.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
|
||||
import { IUser } from "../../../schemas";
|
||||
import { TextField, UserCard } from "..";
|
||||
import { v4 } from "uuid";
|
||||
import API from "../../../util/API";
|
||||
import { useAuthContext } from "../../../context/AuthContext";
|
||||
|
||||
const FriendSearchWidget: FC<{}> = () => {
|
||||
const { token } = useAuthContext();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>();
|
||||
const [userPool, setUserPool] = useState<IUser[]>([]);
|
||||
const [pendingRequests, setPendingRequests] = useState();
|
||||
const [friendResults, setFriendResults] = useState<IUser[]>([]);
|
||||
|
||||
// load available user pool on mount
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
(async function() {
|
||||
const users = new API.User(token);
|
||||
const result = await users.getAll();
|
||||
if (result) setUserPool(result);
|
||||
})();
|
||||
|
||||
(async function() {
|
||||
const friends = new API.Friendship(token);
|
||||
const result = await friends.getAll();
|
||||
setFriendResults(result);
|
||||
})();
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(searchTerm);
|
||||
searchTerm && setUserPool((prev) => {
|
||||
const newPool = prev.filter(person => {
|
||||
if (person.firstname.toLowerCase().includes(searchTerm) || person.lastname.toLowerCase().includes(searchTerm) || person.handle.toLowerCase().includes(searchTerm)) return person;
|
||||
})
|
||||
|
||||
return newPool;
|
||||
})
|
||||
}, [searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(userPool);
|
||||
}, [userPool])
|
||||
|
||||
return (
|
||||
<div id="friend-search-widget">
|
||||
<TextField onChange={(e: ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value.toLowerCase())} placeholder={'Search'} />
|
||||
|
||||
{
|
||||
userPool.map((friend: IUser) => {
|
||||
return <UserCard key={v4()} user={friend} canAdd liftData={() => {}} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FriendSearchWidget;
|
||||
@@ -2,14 +2,16 @@ import Button from "./Button";
|
||||
import Card from "./Card";
|
||||
import Divider from "./Divider";
|
||||
import Dropdown from "./Dropdown";
|
||||
import Form from "./Form";
|
||||
import Form from "./Form/Form";
|
||||
import Navbar from "./Navbar";
|
||||
import Page from "./Page";
|
||||
import Panel from "./Panel";
|
||||
import RichText from "./RichText";
|
||||
import TextField from "./TextField";
|
||||
import Toast from "./Toast";
|
||||
import Tooltip from "./Tooltip";
|
||||
import UserCard from "./UserCard";
|
||||
|
||||
export {
|
||||
Button, Card, Dropdown, Divider, Form, Navbar, Page, Panel, TextField, Tooltip, UserCard
|
||||
Button, Card, Dropdown, Divider, Form, Navbar, Page, Panel, RichText, TextField, Toast, Tooltip, UserCard
|
||||
}
|
||||
38
client/src/context/SelectorContext.tsx
Normal file
38
client/src/context/SelectorContext.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createContext, Dispatch, SetStateAction, useContext } from "react"
|
||||
import { OptionType } from "../util/types"
|
||||
|
||||
interface SelectorContextProps<T> {
|
||||
data: Array<T>
|
||||
setData: Dispatch<SetStateAction<Array<T>>> | VoidFunction
|
||||
selected: Array<string>
|
||||
setSelected: Dispatch<SetStateAction<Array<string>>> | VoidFunction
|
||||
options: Array<OptionType>
|
||||
setOptions: Dispatch<SetStateAction<Array<OptionType>>> | VoidFunction
|
||||
selector: JSX.Element
|
||||
setSelector: Dispatch<SetStateAction<JSX.Element>> | VoidFunction
|
||||
onChange: (...params: any) => void
|
||||
onCreateOption: (label: string, generateObject: (label: string, id: number) => T) => void
|
||||
}
|
||||
|
||||
const defaultValue: SelectorContextProps<any> = {
|
||||
data: new Array<any>(),
|
||||
setData: () => {},
|
||||
selected: new Array<string>(),
|
||||
setSelected: () => {},
|
||||
options: new Array<OptionType>(),
|
||||
setOptions: () => {},
|
||||
selector: <></>,
|
||||
setSelector: () => {},
|
||||
onChange: () => {},
|
||||
onCreateOption: (label: string) => {},
|
||||
}
|
||||
|
||||
export function createOptionFromText(label: string, value?: number): OptionType {
|
||||
return {
|
||||
value: value || 0,
|
||||
label: label,
|
||||
}
|
||||
}
|
||||
|
||||
export const SelectorContext = createContext<SelectorContextProps<any>>(defaultValue);
|
||||
export const useSelectorContext = () => useContext(SelectorContext);
|
||||
40
client/src/context/SelectorProvider.tsx
Normal file
40
client/src/context/SelectorProvider.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState } from "react";
|
||||
import { OptionType } from "../util/types";
|
||||
import { SelectorContext } from "./SelectorContext";
|
||||
|
||||
function SelectorProvider<T>({ children }: { children: JSX.Element | JSX.Element[] }) {
|
||||
const [data, setData] = useState<Array<T>>([]);
|
||||
const [selector, setSelector] = useState<JSX.Element>(<></>)
|
||||
const [options, setOptions] = useState<Array<OptionType>>([]);
|
||||
const [selected, setSelected] = useState<Array<string>>([]);
|
||||
|
||||
/**
|
||||
* Event handler for a change in selection state
|
||||
*/
|
||||
const onChange = (data: Array<any>) => {
|
||||
setSelected((prev) => [...prev, ...data]);
|
||||
}
|
||||
|
||||
const onCreateOption = (label: string, generateObject: (label: string, id: number) => T) => {
|
||||
const newID = options.length + 1;
|
||||
const newOption: OptionType = { label: label, value: newID }
|
||||
|
||||
setOptions((prev) => [...prev, newOption]);
|
||||
setSelected((prev) => [...prev, newOption.label]);
|
||||
setData((prev) => [...prev, generateObject(label, newID)]);
|
||||
}
|
||||
|
||||
const providerValue = {
|
||||
data, setData, selector, setSelector,
|
||||
options, setOptions, selected, setSelected,
|
||||
onChange, onCreateOption
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectorContext.Provider value={ providerValue }>
|
||||
{ children }
|
||||
</SelectorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectorProvider;
|
||||
10
client/src/hooks/useDateFormat.tsx
Normal file
10
client/src/hooks/useDateFormat.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
function useDateFormat(input: string | undefined) {
|
||||
if (typeof input == 'undefined') return "unknown";
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: "long" });
|
||||
const output = dateFormatter.format(new Date(input));
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export default useDateFormat
|
||||
@@ -2,12 +2,15 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import AuthProvider from './context/AuthProvider'
|
||||
import SelectorProvider from './context/SelectorProvider'
|
||||
import './sass/index.scss'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<AuthProvider>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
<SelectorProvider>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</SelectorProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
|
||||
84
client/src/sass/components/Form.scss
Normal file
84
client/src/sass/components/Form.scss
Normal file
@@ -0,0 +1,84 @@
|
||||
.ui-form-component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
width: 75%;
|
||||
|
||||
.form-row {
|
||||
display: inline-flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
label {
|
||||
text-align: left;
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.MuiFormControl-root {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.MuiAutocomplete-root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#ingredients-label, #description-label {
|
||||
align-self: flex-start;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
// special properties for form containers on AddRecipe page
|
||||
.ingredient-container {
|
||||
width: 85%;
|
||||
|
||||
.ingredient-widget {
|
||||
width: 100%;
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
td {
|
||||
width: 30%;
|
||||
|
||||
.MuiFormControl-root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
width: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#add-ingredient-button {
|
||||
margin-top: 1rem;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-row-editor {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
|
||||
label {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
client/src/sass/components/Toast.scss
Normal file
18
client/src/sass/components/Toast.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.ui-component-toast {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
.success {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.fail {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: yellow;
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,8 @@
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.width-80 {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
8
client/src/sass/pages/AddRecipe.scss
Normal file
8
client/src/sass/pages/AddRecipe.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
#create-recipe-panel {
|
||||
.ingredient-card {
|
||||
.ingredient-container {
|
||||
background-color: blue;
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { AxiosHeaders, AxiosRequestHeaders } from "axios";
|
||||
import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList } from "../schemas";
|
||||
import { AxiosError, AxiosHeaders, AxiosRequestHeaders, AxiosResponse } from "axios";
|
||||
import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList, DropdownData, RecipeIngredient } from "../schemas";
|
||||
import { default as _instance } from "./axiosInstance";
|
||||
|
||||
module API {
|
||||
export enum CRUDMETHOD {
|
||||
GET,
|
||||
PUT,
|
||||
POST,
|
||||
DELETE
|
||||
}
|
||||
|
||||
export class Settings {
|
||||
private static APISTRING = import.meta.env.APISTRING || "http://localhost:8080";
|
||||
private static token?: string;
|
||||
@@ -23,7 +30,7 @@ module API {
|
||||
abstract class RestController<T> {
|
||||
protected instance = _instance;
|
||||
protected endpoint: string;
|
||||
protected headers?: any
|
||||
protected headers?: any;
|
||||
|
||||
constructor(endpoint: string, token: string) {
|
||||
this.endpoint = endpoint;
|
||||
@@ -35,30 +42,80 @@ module API {
|
||||
};
|
||||
}
|
||||
|
||||
protected async customRoute(method: CRUDMETHOD, path: string, data?: any, requireHeaders = true) {
|
||||
switch (method) {
|
||||
case CRUDMETHOD.GET:
|
||||
return this.instance.get(this.endpoint + path, (requireHeaders && this.headers));
|
||||
case CRUDMETHOD.PUT:
|
||||
return this.instance.put(this.endpoint + path, data, (requireHeaders && this.headers));
|
||||
case CRUDMETHOD.POST:
|
||||
return this.instance.post(this.endpoint + path, data, (requireHeaders && this.headers));
|
||||
case CRUDMETHOD.DELETE:
|
||||
return this.instance.delete(this.endpoint + path, (requireHeaders && this.headers));
|
||||
}
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const response = await this.instance.get(this.endpoint, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
const response = await this.instance.get(this.endpoint, this.headers)
|
||||
.catch((err: AxiosError) => {
|
||||
console.log(err.message);
|
||||
return null;
|
||||
});
|
||||
return Promise.resolve(response?.data);
|
||||
}
|
||||
|
||||
async getByID(id: string) {
|
||||
const response = await this.instance.get(this.endpoint + "/" + id, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
const response = await this.instance.get(this.endpoint + "/" + id, this.headers)
|
||||
.catch((err: AxiosError) => {
|
||||
console.log(err.message);
|
||||
return null;
|
||||
});
|
||||
return Promise.resolve(response?.data);
|
||||
}
|
||||
|
||||
async post(data: T) {
|
||||
console.log(data);
|
||||
const response = await this.instance.post(this.endpoint, data, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
try {
|
||||
const response = await this.instance.post(this.endpoint, JSON.stringify(data), this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
} catch(e: any) {
|
||||
if (e instanceof AxiosError) {
|
||||
const error = e as AxiosError;
|
||||
if (error.message) {
|
||||
console.log(error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async put(id: string, data: T | Partial<T>) {
|
||||
const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
try {
|
||||
const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
} catch(e: any) {
|
||||
if (e instanceof AxiosError) {
|
||||
const error = e as AxiosError;
|
||||
if (error.message) {
|
||||
console.log(error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const response = await this.instance.delete(this.endpoint + '/' + id, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
try {
|
||||
const response = await this.instance.delete(this.endpoint + '/' + id, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
} catch(e: any) {
|
||||
if (e instanceof AxiosError) {
|
||||
const error = e as AxiosError;
|
||||
if (error.message) {
|
||||
console.log(error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,25 +175,61 @@ module API {
|
||||
|
||||
export class Friendship extends RestController<IFriendship> {
|
||||
constructor(token: string) {
|
||||
super(Settings.getAPISTRING() + "/app/friends", token);
|
||||
super(Settings.getAPISTRING() + "/app/friend", token);
|
||||
}
|
||||
|
||||
async getTargetUserFriendships(id: string | number) {
|
||||
try {
|
||||
const response = await this.instance.get(this.endpoint + `?targetUser=${id}`, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
} catch (e) {
|
||||
const error = e as AxiosError;
|
||||
if (error.response?.status == 404) {
|
||||
console.log('no friends found');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPendingFriendRequests() {
|
||||
const response = await this.instance.get(this.endpoint + "?pending=true", this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
|
||||
async getActiveFriends() {
|
||||
const response = await this.instance.get(this.endpoint + "?accepted=true", this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
|
||||
async addFriend(id: string | number) {
|
||||
const response = await this.instance.post(this.endpoint + `/${id}`, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
export class Recipe extends RestController<IRecipe> {
|
||||
constructor(token: string) {
|
||||
super(Settings.getAPISTRING() + "/app/recipes", token);
|
||||
super(Settings.getAPISTRING() + "/app/recipe", token);
|
||||
}
|
||||
|
||||
async addIngredientToRecipe(ingredient: RecipeIngredient, recipeid: string | number) {
|
||||
const response = await this.instance.post(this.endpoint + `?addIngredients=true&recipeID=${recipeid}`, JSON.stringify(ingredient), this.headers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
return Promise.resolve(response?.data);
|
||||
}
|
||||
}
|
||||
|
||||
export class Ingredient extends RestController<IIngredient> {
|
||||
constructor(token: string) {
|
||||
if (!token) throw new Error("Missing required token");
|
||||
super(Settings.getAPISTRING() + "/app/ingredients", token);
|
||||
super(Settings.getAPISTRING() + "/app/ingredient", token);
|
||||
}
|
||||
|
||||
async getAllForRecipe(recipeID: string | number) {
|
||||
const response = await this.instance.get(this.endpoint + `?recipeID=${recipeID}`, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +237,23 @@ module API {
|
||||
constructor(token: string) {
|
||||
super(Settings.getAPISTRING() + "/app/collection", token);
|
||||
}
|
||||
|
||||
async getRecipesFromOne(id?: number | string) {
|
||||
const response = await this.instance.get(this.endpoint + `/${id}?getRecipes=true`, this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
|
||||
async getAllAuthored(id?: number | string) {
|
||||
let response: AxiosResponse;
|
||||
|
||||
if (id) {
|
||||
response = await this.customRoute(CRUDMETHOD.GET, `?authored=true&authorID=${id}`);
|
||||
} else {
|
||||
response = await this.customRoute(CRUDMETHOD.GET, "?authored=true");
|
||||
}
|
||||
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
export class GroceryList extends RestController<IGroceryList> {
|
||||
@@ -151,6 +261,22 @@ module API {
|
||||
super(Settings.getAPISTRING() + "/app/grocery-list", token)
|
||||
}
|
||||
}
|
||||
|
||||
export class Dropdowns extends RestController<DropdownData> {
|
||||
constructor(token: string) {
|
||||
super(Settings.getAPISTRING() + "/app/dropdown", token);
|
||||
}
|
||||
|
||||
async getAllMeasurements() {
|
||||
const response = await this.instance.get(this.endpoint + "?datatype=measurement", this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
|
||||
async getAllCourses() {
|
||||
const response = await this.instance.get(this.endpoint + "?datatype=course", this.headers);
|
||||
return Promise.resolve(response.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default API
|
||||
@@ -1,31 +1,45 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import AccessForbidden from "../components/pages/StatusPages/403";
|
||||
import { Button, Page } from "../components/ui";
|
||||
import Divider from "../components/ui/Divider";
|
||||
import { useAuthContext } from "../context/AuthContext";
|
||||
import API from "./API";
|
||||
import { ProtectPortal } from "./types";
|
||||
|
||||
const Protect: ProtectPortal = ({ children, redirect = '' }) => {
|
||||
const { user } = useAuthContext();
|
||||
const Protect: ProtectPortal = ({ children, redirect = '', accessRules = null }) => {
|
||||
const { user, token } = useAuthContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!user) {
|
||||
if (!user || !token) {
|
||||
return (
|
||||
<Page>
|
||||
<div className="content-unauthorized">
|
||||
<h1>Hi there! You don't look too familiar.</h1>
|
||||
<p>To view the content on this page, please log in below:</p>
|
||||
<Divider />
|
||||
<Button onClick={() => navigate(redirect ? `/login?redirect=${redirect}` : '/login')}>Log In</Button>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Page>
|
||||
{ children || <></> }
|
||||
</Page>
|
||||
<AccessForbidden>
|
||||
<>
|
||||
<h2>Hi there! You don't look too familiar.</h2>
|
||||
<p>To view the content on this page, please log in below:</p>
|
||||
<Button onClick={() => navigate(redirect ? `/login?redirect=${redirect}` : '/login')}>Log In</Button>
|
||||
</>
|
||||
</AccessForbidden>
|
||||
)
|
||||
}
|
||||
|
||||
if (accessRules !== null) {
|
||||
if (accessRules.mustBeRecipinAdmin && !(user?.isadmin)) {
|
||||
return (
|
||||
<AccessForbidden>
|
||||
<>
|
||||
<h2>This page requires administrator access.</h2>
|
||||
<p>If you believe you are receiving this message in error, please contact Recipin support.</p>
|
||||
</>
|
||||
</AccessForbidden>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{ children }
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default Protect;
|
||||
@@ -14,7 +14,9 @@ instance.interceptors.response.use((res: AxiosResponse<any,any>) => {
|
||||
|
||||
return res;
|
||||
}, (err) => {
|
||||
return Promise.reject(err);
|
||||
console.log(err);
|
||||
// return err;
|
||||
// return Promise.reject(err);
|
||||
})
|
||||
|
||||
export default instance;
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ChangeEvent, ChangeEventHandler, Dispatch, FC, ReactNode, SetStateAction } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Form } from "../components/ui";
|
||||
import { IUser } from "../schemas";
|
||||
import { DropdownData, IIngredient, IUser } from "../schemas";
|
||||
|
||||
export interface PortalBase {
|
||||
children?: ReactNode | ReactNode[]
|
||||
extraStyles?: string
|
||||
extraClasses?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
interface ButtonParams extends PortalBase {
|
||||
@@ -14,14 +15,19 @@ interface ButtonParams extends PortalBase {
|
||||
disabledText?: string
|
||||
}
|
||||
|
||||
export interface AccessRules {
|
||||
mustBeRecipinAdmin: boolean
|
||||
mustBeFriend: boolean
|
||||
mustBeSubscribed: boolean
|
||||
}
|
||||
|
||||
export interface ProtectParams extends PortalBase {
|
||||
redirect?: string
|
||||
accessRules?: Partial<AccessRules> | null
|
||||
}
|
||||
|
||||
interface UserCardProps extends PortalBase {
|
||||
user: IUser
|
||||
canAdd?: boolean
|
||||
liftData?: (data: any) => void
|
||||
targetUser: IUser
|
||||
}
|
||||
|
||||
interface NavbarProps {
|
||||
@@ -40,6 +46,22 @@ interface CheckboxProps {
|
||||
FormElement: typeof Form
|
||||
}
|
||||
|
||||
export interface IngredientFieldData {
|
||||
rowPosition: number
|
||||
quantity: number | undefined
|
||||
measurement: DropdownData['name'] | undefined
|
||||
ingredientSelection: IIngredient['name'] | IIngredient | undefined
|
||||
ingredients: Array<IIngredient>
|
||||
}
|
||||
|
||||
/**
|
||||
* Type declaration for react-select dropdown options
|
||||
*/
|
||||
export interface OptionType {
|
||||
value: number
|
||||
label: string
|
||||
}
|
||||
|
||||
export type PageComponent = FC<PortalBase>
|
||||
export type PanelComponent = FC<PortalBase>
|
||||
export type ButtonComponent = FC<ButtonParams>
|
||||
|
||||
2
dev.sh
Normal file
2
dev.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#! /bin/bash
|
||||
concurrently "cd server && npm run dev" "cd client && npm run dev"
|
||||
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "recipe-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "A concept developed by Mikayla Dobson",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bash dev.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/innocuous-symmetry/recipe-manager.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/innocuous-symmetry/recipe-manager/issues"
|
||||
},
|
||||
"homepage": "https://github.com/innocuous-symmetry/recipe-manager#readme",
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.6.0"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { IUser } from "../schemas";
|
||||
dotenv.config();
|
||||
|
||||
export function restrictAccess(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.session.user == undefined) {
|
||||
if (req.user == undefined) {
|
||||
res.send("content restricted");
|
||||
} else {
|
||||
next();
|
||||
|
||||
@@ -15,6 +15,14 @@ export default class CollectionCtl {
|
||||
return new ControllerResponse(code, data);
|
||||
}
|
||||
|
||||
async getRecipesFromOne(id: number | string) {
|
||||
const result = await CollectionInstance.getRecipesFromOne(id);
|
||||
const ok: boolean = result !== null;
|
||||
const code: StatusCode = ok ? StatusCode.OK : StatusCode.NotFound;
|
||||
const data: string | ICollection[] = result || "No collection found with this ID";
|
||||
return new ControllerResponse(code, data);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const result = await CollectionInstance.getAll();
|
||||
const ok = result !== null;
|
||||
@@ -23,6 +31,13 @@ export default class CollectionCtl {
|
||||
return new ControllerResponse(code, data);
|
||||
}
|
||||
|
||||
async getAllAuthored(id: number | string) {
|
||||
const result = await CollectionInstance.getAllAuthored(id);
|
||||
const code = (result !== null) ? StatusCode.OK : StatusCode.NotFound;
|
||||
const data = result || "No collections found";
|
||||
return new ControllerResponse(code, data);
|
||||
}
|
||||
|
||||
async getUserDefault(id: number | string) {
|
||||
const result = await CollectionInstance.getUserDefault(id);
|
||||
const code = (result !== null) ? StatusCode.OK : StatusCode.NotFound;
|
||||
|
||||
49
server/controllers/DropdownCtl.ts
Normal file
49
server/controllers/DropdownCtl.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import Dropdown from "../models/dropdownValues";
|
||||
import { DropdownDataType } from "../schemas";
|
||||
import ControllerResponse from "../util/ControllerResponse";
|
||||
import { StatusCode } from "../util/types";
|
||||
const DDInstance = new Dropdown();
|
||||
|
||||
export default class DropdownCtl {
|
||||
async getMeasurements() {
|
||||
try {
|
||||
const result = await DDInstance.getMeasurements();
|
||||
return new ControllerResponse(
|
||||
((result !== null) ? StatusCode.OK : StatusCode.NotFound),
|
||||
result || "Measurement unit data not found",
|
||||
(result !== null)
|
||||
);
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getCourses() {
|
||||
try {
|
||||
const result = await DDInstance.getCourses();
|
||||
return new ControllerResponse(
|
||||
((result !== null) ? StatusCode.OK : StatusCode.NotFound),
|
||||
result || "Course data not found",
|
||||
(result !== null)
|
||||
);
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getByType(type: DropdownDataType) {
|
||||
switch (type) {
|
||||
case "measurement":
|
||||
const result = await DDInstance.getMeasurements();
|
||||
return new ControllerResponse<any[] | string>(
|
||||
((result !== null) ? StatusCode.OK : StatusCode.NotFound),
|
||||
result || "Measurement unit data not found",
|
||||
(result !== null)
|
||||
);
|
||||
case "course":
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,17 @@ export default class IngredientCtl {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllForRecipe(recipeid: string) {
|
||||
try {
|
||||
const result = await IngredientInstance.getAllForRecipe(recipeid);
|
||||
const ok = result !== null;
|
||||
const code = ok ? StatusCode.OK : StatusCode.NotFound;
|
||||
return new ControllerResponse(code, (result || "No ingredient found with this recipe ID"));
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async getOne(id: string) {
|
||||
try {
|
||||
const result = await IngredientInstance.getOne(id);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IRecipe } from "../schemas";
|
||||
import { IRecipe, RecipeIngredient } from "../schemas";
|
||||
import { Recipe } from "../models/recipe";
|
||||
import ControllerResponse from "../util/ControllerResponse";
|
||||
import { StatusCode } from "../util/types";
|
||||
@@ -58,4 +58,15 @@ export default class RecipeCtl {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async addIngredientToRecipe(ingredient: RecipeIngredient, recipeid: string | number) {
|
||||
try {
|
||||
const result = await RecipeInstance.addIngredientToRecipe(ingredient, recipeid);
|
||||
const ok = result !== null;
|
||||
const code = ok ? StatusCode.NewContent : StatusCode.BadRequest;
|
||||
return new ControllerResponse(code, (result || "Something went wrong"));
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
4
server/controllers/UserCtl.d.ts
vendored
Normal file
4
server/controllers/UserCtl.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* @method getAll
|
||||
* @returns { ControllerResponse<IUser[] | string> }
|
||||
*/
|
||||
@@ -5,6 +5,18 @@ import { StatusCode } from '../util/types';
|
||||
const UserInstance = new User();
|
||||
|
||||
export default class UserCtl {
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* FIRST SECTION:
|
||||
* METHODS SPECIFIC TO USERS AND USER DATA
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* ### @method getAll
|
||||
* returns all available user entries
|
||||
*
|
||||
* @params (none)
|
||||
* @returns list of users, or an explanatory string if no response is received
|
||||
*/
|
||||
async getAll() {
|
||||
try {
|
||||
// attempt to get users from database
|
||||
@@ -22,6 +34,11 @@ export default class UserCtl {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ### @method post
|
||||
* @param body - serialized user data as { IUser }
|
||||
* @returns the newly inserted user entry, or an explanatory string
|
||||
*/
|
||||
async post(body: IUser) {
|
||||
try {
|
||||
const response = await UserInstance.post(body);
|
||||
@@ -34,6 +51,11 @@ export default class UserCtl {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ### @method getOne
|
||||
* @param id - user id to query
|
||||
* @returns the user entry, if found, or an explanatory string if none was found
|
||||
*/
|
||||
async getOne(id: number | string) {
|
||||
try {
|
||||
const user = await UserInstance.getOneByID(id);
|
||||
@@ -46,6 +68,12 @@ export default class UserCtl {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ### @method updateOne
|
||||
* @param id - user id to update
|
||||
* @param body - the new user body to update with
|
||||
* @returns the updated user body, or an explanatory string
|
||||
*/
|
||||
async updateOne(id: number | string, body: IUser) {
|
||||
try {
|
||||
const result = await UserInstance.updateOneByID(id, body);
|
||||
@@ -58,6 +86,16 @@ export default class UserCtl {
|
||||
}
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* SECOND SECTION:
|
||||
* METHODS SPECIFIC TO FRIENDSHIPS BETWEEN USERS
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* ### @method getFriends
|
||||
* @param id - get all friendship entries for a user, regardless of status
|
||||
* @returns a list of friendship entries, or an explanatory string if none are found
|
||||
*/
|
||||
async getFriends(id: number | string) {
|
||||
try {
|
||||
const result = await UserInstance.getFriends(id);
|
||||
@@ -70,6 +108,12 @@ export default class UserCtl {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ### @method getFriendshipByID
|
||||
* @param id - the ID of the friendship in question
|
||||
* @param userid - the user ID of the logged in user, to verify permissions
|
||||
* @returns a friendship entry, or an explanatory string
|
||||
*/
|
||||
async getFriendshipByID(id: number | string, userid: number | string) {
|
||||
try {
|
||||
const { ok, code, result } = await UserInstance.getFriendshipByID(id, userid);
|
||||
@@ -79,6 +123,11 @@ export default class UserCtl {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ### @method getPendingFriendRequests
|
||||
*
|
||||
* *IMPORTANT*: I don't think this one works the way I think it does
|
||||
*/
|
||||
async getPendingFriendRequests(recipient: string | number) {
|
||||
try {
|
||||
const { ok, code, result } = await UserInstance.getPendingFriendRequests(recipient);
|
||||
@@ -88,6 +137,15 @@ export default class UserCtl {
|
||||
}
|
||||
}
|
||||
|
||||
async getAcceptedFriends(userid: number | string) {
|
||||
try {
|
||||
const { code, result } = await UserInstance.getAcceptedFriends(userid);
|
||||
return new ControllerResponse(code, result);
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async addFriendship(userid: number | string, targetid: number | string) {
|
||||
try {
|
||||
const result = await UserInstance.addFriendship(userid, targetid);
|
||||
|
||||
@@ -101,10 +101,31 @@ export default async function populate() {
|
||||
;
|
||||
`
|
||||
|
||||
const populateMeasurements = `
|
||||
INSERT INTO recipin.dropdownVals
|
||||
(name, datatype, datecreated)
|
||||
VALUES
|
||||
('cup', 'MEASUREMENTS', $1),
|
||||
('tablespoon', 'MEASUREMENTS', $1),
|
||||
('teaspoon', 'MEASUREMENTS', $1),
|
||||
('gram', 'MEASUREMENTS', $1),
|
||||
('ounce', 'MEASUREMENTS', $1),
|
||||
('fluid ounce', 'MEASUREMENTS', $1),
|
||||
('pound', 'MEASUREMENTS', $1),
|
||||
('breakfast', 'COURSE', $1),
|
||||
('lunch', 'COURSE', $1),
|
||||
('dinner', 'COURSE', $1),
|
||||
('dessert', 'COURSE', $1),
|
||||
('appetizer', 'COURSE', $1),
|
||||
('side', 'COURSE', $1)
|
||||
;
|
||||
`
|
||||
|
||||
const allStatements: Array<string> = [
|
||||
populateUsers, populateCuisines, populateCourses,
|
||||
populateCollection, populateIngredients, populateRecipes,
|
||||
populateGroceryList, populateFriendships, populateComments
|
||||
populateGroceryList, populateFriendships, populateComments,
|
||||
populateMeasurements
|
||||
];
|
||||
|
||||
await pool.query(setup);
|
||||
|
||||
@@ -28,11 +28,12 @@ dotenv.config();
|
||||
const recipecollection = fs.readFileSync(appRoot + '/db/sql/create/createcmp_recipecollection.sql').toString();
|
||||
const usersubscriptions = fs.readFileSync(appRoot + '/db/sql/create/createcmp_usersubscriptions.sql').toString();
|
||||
const userfriendships = fs.readFileSync(appRoot + '/db/sql/create/createcmp_userfriendships.sql').toString();
|
||||
const dropdownValues = fs.readFileSync(appRoot + '/db/sql/create/createdropdown.sql').toString();
|
||||
|
||||
const allStatements = [
|
||||
setRole, appusers, ingredient, collection, cuisine, course,
|
||||
recipe, recipecomments, groceryList, recipeingredient,
|
||||
recipecollection, usersubscriptions, userfriendships
|
||||
recipecollection, usersubscriptions, userfriendships, dropdownValues
|
||||
]
|
||||
|
||||
try {
|
||||
|
||||
6
server/db/sql/create/createdropdown.sql
Normal file
6
server/db/sql/create/createdropdown.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS recipin.dropdownVals (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
datatype VARCHAR CHECK(datatype in ('MEASUREMENTS', 'COURSE', 'INGREDIENT')),
|
||||
datecreated VARCHAR NOT NULL
|
||||
);
|
||||
@@ -18,6 +18,39 @@ export class Collection {
|
||||
}
|
||||
}
|
||||
|
||||
async getRecipesFromOne(id: number | string) {
|
||||
try {
|
||||
const statement = `
|
||||
SELECT * FROM recipin.recipe
|
||||
INNER JOIN recipin.cmp_recipecollection
|
||||
ON recipe.id = cmp_recipecollection.recipeid
|
||||
WHERE cmp_recipecollection.collectionid = $1;
|
||||
`;
|
||||
const values = [id];
|
||||
const result = await pool.query(statement, values);
|
||||
if (result.rows.length) return result.rows;
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllAuthored(id: number | string) {
|
||||
console.log(id, typeof id);
|
||||
try {
|
||||
const statement = `
|
||||
SELECT * FROM recipin.collection
|
||||
WHERE ownerid = $1;
|
||||
`
|
||||
const result = await pool.query(statement, [id]);
|
||||
console.log(result.rows);
|
||||
if (result.rows.length) return result.rows;
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async getUserDefault(id: number | string) {
|
||||
try {
|
||||
const statement = `
|
||||
@@ -46,7 +79,6 @@ export class Collection {
|
||||
}
|
||||
|
||||
async post(data: ICollection) {
|
||||
console.log('new default collection');
|
||||
const { name, active, ismaincollection, ownerid } = data;
|
||||
try {
|
||||
const statement = `
|
||||
|
||||
25
server/models/dropdownValues.ts
Normal file
25
server/models/dropdownValues.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import pool from "../db";
|
||||
|
||||
export default class Dropdown {
|
||||
async getMeasurements() {
|
||||
try {
|
||||
const statement = `SELECT * FROM recipin.dropdownVals WHERE datatype = 'MEASUREMENTS'`;
|
||||
const result = await pool.query(statement);
|
||||
if (result.rows.length) return result.rows;
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getCourses() {
|
||||
try {
|
||||
const statement = `SELECT * FROM recipin.dropdownVals WHERE datatype = 'COURSE'`;
|
||||
const result = await pool.query(statement);
|
||||
if (result.rows.length) return result.rows;
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,17 @@ export class Ingredient {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllForRecipe(recipeid: string) {
|
||||
try {
|
||||
const statement = `SELECT * FROM recipin.cmp_recipeingredient WHERE recipeid = $1`;
|
||||
const result = await pool.query(statement, [recipeid]);
|
||||
if (result.rows.length) return result.rows[0];
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async post(data: IIngredient) {
|
||||
try {
|
||||
const statement = `
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IRecipe } from "../schemas";
|
||||
import { IIngredient, IRecipe, RecipeIngredient } from "../schemas";
|
||||
import fs from 'fs';
|
||||
import pool from "../db";
|
||||
import { CollectionCtl } from "../controllers";
|
||||
@@ -114,4 +114,24 @@ export class Recipe {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async addIngredientToRecipe(ingredient: RecipeIngredient, recipeid: string | number) {
|
||||
const { quantity, unit, ingredientid } = ingredient;
|
||||
|
||||
try {
|
||||
const statement = `
|
||||
INSERT INTO recipin.cmp_recipeingredient
|
||||
(quantity, unit, ingredientid, recipeid)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *
|
||||
`
|
||||
|
||||
const result = await pool.query(statement, [quantity, unit, ingredientid, recipeid]);
|
||||
|
||||
if (result.rows) return result.rows[0];
|
||||
return [];
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,18 @@ export class User {
|
||||
}
|
||||
}
|
||||
|
||||
async getAcceptedFriends(userid: number | string) {
|
||||
try {
|
||||
const statement = `SELECT * FROM recipin.cmp_userfriendships WHERE active = true AND (senderid = $1) OR (targetid = $1);`
|
||||
const result = await pool.query(statement, [userid]);
|
||||
|
||||
if (result.rows.length) return { ok: true, code: StatusCode.OK, result: result.rows }
|
||||
return { ok: true, code: StatusCode.NotFound, result: "No pending friend requests found" }
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async addFriendship(userid: number | string, targetid: number | string) {
|
||||
try {
|
||||
const statement = `
|
||||
|
||||
@@ -16,11 +16,6 @@ const router = Router();
|
||||
export const authRoute = (app: Express) => {
|
||||
app.use('/auth', router);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log(req.session);
|
||||
next();
|
||||
})
|
||||
|
||||
router.get('/', restrictAccess, (req, res, next) => {
|
||||
if (req.session.user) {
|
||||
const user = req.session.user;
|
||||
@@ -44,8 +39,6 @@ export const authRoute = (app: Express) => {
|
||||
router.post('/login', async (req, res, next) => {
|
||||
try {
|
||||
const data: IUserAuth = req.body;
|
||||
console.log(data);
|
||||
|
||||
const response: ControllerResponse<any> = await AuthInstance.login(data);
|
||||
|
||||
if (response.ok) {
|
||||
@@ -70,8 +63,6 @@ export const authRoute = (app: Express) => {
|
||||
return next(err);
|
||||
})
|
||||
|
||||
console.log(req.session);
|
||||
|
||||
res.cookie('token', token, { httpOnly: true });
|
||||
res.json({ token });
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Express, Router } from "express";
|
||||
import { checkIsAdmin, restrictAccess } from "../auth/middlewares";
|
||||
import CollectionCtl from "../controllers/CollectionCtl";
|
||||
import { IUser } from "../schemas";
|
||||
import { StatusCode } from "../util/types";
|
||||
const CollectionInstance = new CollectionCtl();
|
||||
|
||||
const router = Router();
|
||||
@@ -8,35 +10,50 @@ const router = Router();
|
||||
export const collectionRoute = (app: Express) => {
|
||||
app.use('/app/collection', router);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log('what gives');
|
||||
console.log(req.body);
|
||||
next();
|
||||
})
|
||||
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
router.get('/:id', restrictAccess, async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
const { getRecipes } = req.query;
|
||||
|
||||
try {
|
||||
const { code, data } = await CollectionInstance.getOne(id);
|
||||
res.status(code).send(data);
|
||||
if (getRecipes || getRecipes == "true") {
|
||||
const { code, data } = await CollectionInstance.getRecipesFromOne(id);
|
||||
res.status(code).send(data);
|
||||
} else {
|
||||
const { code, data } = await CollectionInstance.getOne(id);
|
||||
res.status(code).send(data);
|
||||
}
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
})
|
||||
|
||||
// implement is admin on this route
|
||||
router.get('/', checkIsAdmin, async (req, res, next) => {
|
||||
router.get('/', restrictAccess, async (req, res, next) => {
|
||||
const user = req.user as IUser;
|
||||
const { authored, authorID } = req.query;
|
||||
|
||||
try {
|
||||
const { code, data } = await CollectionInstance.getAll();
|
||||
res.status(code).send(data);
|
||||
if (authorID) {
|
||||
const { code, data } = await CollectionInstance.getAllAuthored(parseInt(authorID as string));
|
||||
res.status(code).send(data);
|
||||
} else if (authored || authored == "true") {
|
||||
const { code, data } = await CollectionInstance.getAllAuthored(user.id as number);
|
||||
res.status(code).send(data);
|
||||
} else {
|
||||
if (user.isadmin) {
|
||||
const { code, data } = await CollectionInstance.getAll();
|
||||
res.status(code).send(data);
|
||||
} else {
|
||||
res.status(403).send("Unauthorized");
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req, res, next) => {
|
||||
router.post('/', restrictAccess, async (req, res, next) => {
|
||||
const data = req.body;
|
||||
console.log(req.body ?? "sanity check");
|
||||
|
||||
try {
|
||||
const result = await CollectionInstance.post(data);
|
||||
|
||||
32
server/routes/dropdownValues.ts
Normal file
32
server/routes/dropdownValues.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Express, Router } from 'express';
|
||||
import DropdownCtl from '../controllers/DropdownCtl';
|
||||
import { DropdownDataType } from '../schemas';
|
||||
|
||||
const router = Router();
|
||||
const DDInstance = new DropdownCtl();
|
||||
|
||||
export const dropdownValueRouter = (app: Express) => {
|
||||
app.use('/app/dropdown', router);
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
const { datatype } = req.query;
|
||||
|
||||
try {
|
||||
switch (datatype) {
|
||||
case "measurement":
|
||||
const measurements = await DDInstance.getMeasurements();
|
||||
res.status(measurements.code).send(measurements.data);
|
||||
break;
|
||||
case "course":
|
||||
const courses = await DDInstance.getCourses();
|
||||
res.status(courses.code).send(courses.data);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
})
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Express, Router } from 'express';
|
||||
import { restrictAccess } from '../auth/middlewares';
|
||||
import { UserCtl } from '../controllers';
|
||||
import { IUser } from '../schemas';
|
||||
import { StatusCode } from '../util/types';
|
||||
|
||||
const UserInstance = new UserCtl();
|
||||
const router = Router();
|
||||
@@ -9,19 +10,8 @@ const router = Router();
|
||||
export const friendRouter = (app: Express) => {
|
||||
app.use('/app/friend', router);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
let test = req.session.user;
|
||||
|
||||
if (req.session.user == undefined) {
|
||||
throw new Error("No session found");
|
||||
} else {
|
||||
const narrowed = req.session.user;
|
||||
next();
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:targetid', restrictAccess, async (req, res, next) => {
|
||||
const user = req.session.user as IUser;
|
||||
const user = req.user as IUser;
|
||||
const { targetid } = req.params;
|
||||
|
||||
try {
|
||||
@@ -34,17 +24,28 @@ export const friendRouter = (app: Express) => {
|
||||
|
||||
// get all friendships for a user
|
||||
router.get('/', async (req, res, next) => {
|
||||
const user = req.session.user as IUser;
|
||||
const { pending } = req.query;
|
||||
const user = req.user as IUser;
|
||||
const { pending, accepted, targetUser } = req.query;
|
||||
|
||||
try {
|
||||
if (pending) {
|
||||
const { code, data } = await UserInstance.getPendingFriendRequests(user.id as number);
|
||||
res.status(code).send(data);
|
||||
} else {
|
||||
const { code, data } = await UserInstance.getFriends(user.id as number);
|
||||
} else if (accepted) {
|
||||
const { code, data } = await UserInstance.getAcceptedFriends(user.id as number);
|
||||
res.status(code).send(data);
|
||||
} else {
|
||||
if (targetUser) {
|
||||
const { code, data } = await UserInstance.getFriends(parseInt(targetUser as string));
|
||||
res.status(code).send(data);
|
||||
} else {
|
||||
const { code, data } = await UserInstance.getFriends(user.id as number);
|
||||
res.status(code).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
// send server error in case any of these conditions not landing
|
||||
res.status(StatusCode.ServerError).json({ message: "An unexpected error occurred." });
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export const friendRouter = (app: Express) => {
|
||||
// get one friendship by its id
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
const user = req.session.user as IUser;
|
||||
const user = req.user as IUser;
|
||||
|
||||
try {
|
||||
const { code, data } = await UserInstance.getFriendshipByID(id, user.id as number);
|
||||
@@ -76,7 +77,7 @@ export const friendRouter = (app: Express) => {
|
||||
router.put('/:id', async (req, res, next) => {
|
||||
const data = req.body;
|
||||
const { id } = req.params;
|
||||
const user = req.session.user as IUser;
|
||||
const user = req.user as IUser;
|
||||
|
||||
try {
|
||||
const response = await UserInstance.updateFriendship(id, user.id as number, data);
|
||||
@@ -85,4 +86,6 @@ export const friendRouter = (app: Express) => {
|
||||
next(e);
|
||||
}
|
||||
})
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -11,35 +11,52 @@ import { subscriptionRoute } from "./subscription";
|
||||
import { friendRouter } from "./friend";
|
||||
import { cuisineRouter } from "./cuisine";
|
||||
import { courseRouter } from "./course";
|
||||
import { dropdownValueRouter } from "./dropdownValues";
|
||||
import { IUser } from "../schemas";
|
||||
import { User } from "../models/user";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
let REQUESTCOUNT = 0;
|
||||
|
||||
export const routes = async (app: Express) => {
|
||||
// unprotected routes
|
||||
authRoute(app);
|
||||
// simple request counting middleware
|
||||
app.use('/', (req, res, next) => {
|
||||
REQUESTCOUNT++;
|
||||
console.log("count: ", REQUESTCOUNT);
|
||||
next();
|
||||
})
|
||||
|
||||
// middleware to check for auth on cookies on each request in protected routes
|
||||
app.use('/app', async (req, res, next) => {
|
||||
// pull jwt from request headers
|
||||
console.log(req.headers);
|
||||
const token = req.headers['authorization']?.split(" ")[1];
|
||||
console.log(token);
|
||||
|
||||
if (!token) {
|
||||
res.status(403).send("Unauthorized, did not receive token");
|
||||
} else {
|
||||
jwt.verify(token, process.env.SESSIONSECRET as string, (err, data) => {
|
||||
jwt.verify(token, process.env.SESSIONSECRET as string, async (err, data: any) => {
|
||||
if (err) {
|
||||
res.status(403).send(err);
|
||||
} else {
|
||||
console.log(data);
|
||||
req.user = data;
|
||||
const userInstance = new User();
|
||||
const foundUser = await userInstance.getOneByID(data.user.id);
|
||||
|
||||
if (foundUser) {
|
||||
req.user = data.user as IUser;
|
||||
} else {
|
||||
res.status(403).send("Unauthorized, user not registered");
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// unprotected routes
|
||||
authRoute(app);
|
||||
|
||||
// protected routes
|
||||
userRoute(app);
|
||||
friendRouter(app);
|
||||
@@ -49,6 +66,7 @@ export const routes = async (app: Express) => {
|
||||
subscriptionRoute(app);
|
||||
groceryListRoute(app);
|
||||
courseRouter(app);
|
||||
dropdownValueRouter(app);
|
||||
|
||||
// deprecate?
|
||||
cuisineRouter(app);
|
||||
|
||||
@@ -10,9 +10,16 @@ export const ingredientRoute = (app: Express) => {
|
||||
app.use('/app/ingredient', router);
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
const { recipeID } = req.query;
|
||||
|
||||
try {
|
||||
const result: CtlResponse<IIngredient[] | string> = await IngredientInstance.getAll();
|
||||
res.status(result.code).send(result.data);
|
||||
if (recipeID) {
|
||||
const result = await IngredientInstance.getAllForRecipe(recipeID as string);
|
||||
res.status(result.code).send(result.data);
|
||||
} else {
|
||||
const result: CtlResponse<IIngredient[] | string> = await IngredientInstance.getAll();
|
||||
res.status(result.code).send(result.data);
|
||||
}
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
@@ -51,4 +58,6 @@ export const ingredientRoute = (app: Express) => {
|
||||
next(e);
|
||||
}
|
||||
})
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -22,12 +22,12 @@ export const recipeRoute = (app: Express) => {
|
||||
})
|
||||
|
||||
router.get('/', restrictAccess, async (req, res, next) => {
|
||||
const user = req.session.user as IUser;
|
||||
const { filterby } = req.query;
|
||||
const user = req.user as IUser;
|
||||
const { filter } = req.query;
|
||||
|
||||
try {
|
||||
let result: CtlResponse<IRecipe[] | string>;
|
||||
switch (filterby) {
|
||||
switch (filter) {
|
||||
case "myrecipes":
|
||||
result = await recipectl.getAllAuthored(user.id as number);
|
||||
break;
|
||||
@@ -55,12 +55,20 @@ export const recipeRoute = (app: Express) => {
|
||||
})
|
||||
|
||||
router.post('/', restrictAccess, async (req, res, next) => {
|
||||
const user = req.session.user as IUser;
|
||||
const user = req.user as IUser;
|
||||
const data = req.body;
|
||||
const { addIngredients, recipeID } = req.query;
|
||||
|
||||
console.log(data);
|
||||
|
||||
try {
|
||||
const result = await recipectl.post(user.id as number, data);
|
||||
res.status(result.code).send(result.data);
|
||||
if (addIngredients) {
|
||||
const result = await recipectl.addIngredientToRecipe(data, recipeID as string);
|
||||
res.status(result.code).send(result.data);
|
||||
} else {
|
||||
const result = await recipectl.post(user.id as number, data);
|
||||
res.status(result.code).send(result.data);
|
||||
}
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Express, Router } from "express"
|
||||
import { restrictAccess } from "../auth/middlewares";
|
||||
import { CollectionCtl } from "../controllers";
|
||||
import { IUser } from "../schemas";
|
||||
const CollectionInstance = new CollectionCtl();
|
||||
const router = Router();
|
||||
|
||||
@@ -8,12 +9,11 @@ export const subscriptionRoute = (app: Express) => {
|
||||
app.use('/app/subscription', router);
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
// @ts-ignore
|
||||
const { user } = req.session.user;
|
||||
const user = req.user as IUser;
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const result = await CollectionInstance.getSubscriptions(user.id as string);
|
||||
const result = await CollectionInstance.getSubscriptions(user.id!);
|
||||
res.status(200).send(result);
|
||||
} catch(e) {
|
||||
next(e);
|
||||
@@ -21,12 +21,11 @@ export const subscriptionRoute = (app: Express) => {
|
||||
})
|
||||
|
||||
router.post('/', restrictAccess, async (req, res, next) => {
|
||||
// @ts-ignore
|
||||
const { user } = req.session.user;
|
||||
const user = req.user as IUser;
|
||||
const { collection } = req.query;
|
||||
|
||||
try {
|
||||
const result = await CollectionInstance.postSubscription(collection as string, user.id as string);
|
||||
const result = await CollectionInstance.postSubscription(collection as string, user.id!);
|
||||
res.status(201).send(result);
|
||||
} catch(e) {
|
||||
next(e);
|
||||
|
||||
@@ -44,6 +44,13 @@ export interface IIngredient extends HasHistory {
|
||||
createdbyid: string | number
|
||||
}
|
||||
|
||||
export interface RecipeIngredient extends Partial<IIngredient> {
|
||||
unit: string
|
||||
quantity: string | number
|
||||
ingredientid: string | number
|
||||
recipeid: string | number
|
||||
}
|
||||
|
||||
export interface ICollection extends HasHistory, CanDeactivate {
|
||||
name: string
|
||||
ismaincollection: boolean
|
||||
@@ -73,3 +80,10 @@ export interface FlavorProfile extends HasHistory, CanDeactivate {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface DropdownData extends HasHistory {
|
||||
name: string
|
||||
datatype: DropdownDataType
|
||||
}
|
||||
|
||||
export type DropdownDataType = "measurement" | "course"
|
||||
Reference in New Issue
Block a user