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"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.10.5",
|
||||||
|
"@emotion/styled": "^11.10.5",
|
||||||
|
"@mui/material": "^5.11.9",
|
||||||
"@tinymce/tinymce-react": "^4.2.0",
|
"@tinymce/tinymce-react": "^4.2.0",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.4.3",
|
"react-router-dom": "^6.4.3",
|
||||||
|
"react-select": "^5.7.0",
|
||||||
"sass": "^1.56.1",
|
"sass": "^1.56.1",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import { useAuthContext } from './context/AuthContext';
|
import { useAuthContext } from './context/AuthContext';
|
||||||
import jwtDecode from 'jwt-decode';
|
|
||||||
import API from './util/API';
|
|
||||||
|
|
||||||
// pages, ui, components, styles
|
// pages, ui, components, styles
|
||||||
import Subscriptions from './components/pages/Subscriptions/Subscriptions';
|
import Subscriptions from './components/pages/Subscriptions/Subscriptions';
|
||||||
@@ -19,9 +17,10 @@ import CollectionBrowser from './components/pages/CollectionBrowser';
|
|||||||
import { Navbar } from './components/ui';
|
import { Navbar } from './components/ui';
|
||||||
import GroceryList from './components/pages/GroceryList';
|
import GroceryList from './components/pages/GroceryList';
|
||||||
import GroceryListCollection from './components/pages/GroceryListCollection';
|
import GroceryListCollection from './components/pages/GroceryListCollection';
|
||||||
import { TokenType } from './util/types';
|
|
||||||
import './sass/App.scss';
|
|
||||||
import handleToken from './util/handleToken';
|
import handleToken from './util/handleToken';
|
||||||
|
import AddFriends from './components/pages/AddFriends';
|
||||||
|
import Sandbox from './components/Sandbox';
|
||||||
|
import './sass/App.scss';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { setUser, token, setToken } = useAuthContext();
|
const { setUser, token, setToken } = useAuthContext();
|
||||||
@@ -48,20 +47,26 @@ function App() {
|
|||||||
<div className="App">
|
<div className="App">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Routes>
|
<Routes>
|
||||||
|
{/* Base access privileges */}
|
||||||
<Route path="/" element={<Welcome />} />
|
<Route path="/" element={<Welcome />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|
||||||
|
{/* Protected routes */}
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/collections" element={<CollectionBrowser />} />
|
<Route path="/collections" element={<CollectionBrowser />} />
|
||||||
<Route path="/collections/:id" element={<Collection />} />
|
<Route path="/collections/:id" element={<Collection />} />
|
||||||
|
<Route path="/add-friends" element={<AddFriends />} />
|
||||||
<Route path="/explore" element={<Browser header="" searchFunction={() => {}} />} />
|
<Route path="/explore" element={<Browser header="" searchFunction={() => {}} />} />
|
||||||
<Route path="/recipe/:id" element={<Recipe />} />
|
<Route path="/recipe/:id" element={<Recipe />} />
|
||||||
<Route path="/subscriptions" element={<Subscriptions />} />
|
<Route path="/subscriptions" element={<Subscriptions />} />
|
||||||
<Route path="/subscriptions/:id" element={<Collection />} />
|
<Route path="/subscriptions/:id" element={<Collection />} />
|
||||||
|
|
||||||
<Route path="/add-recipe" element={<AddRecipe />} />
|
<Route path="/add-recipe" element={<AddRecipe />} />
|
||||||
<Route path="/grocery-list" element={<GroceryListCollection />} />
|
<Route path="/grocery-list" element={<GroceryListCollection />} />
|
||||||
<Route path="/grocery-list/:id" element={<GroceryList />} />
|
<Route path="/grocery-list/:id" element={<GroceryList />} />
|
||||||
|
|
||||||
|
{/* For dev use */}
|
||||||
|
<Route path="/sandbox" element={<Sandbox />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</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 { 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 UserCard from "../ui/UserCard";
|
||||||
import { IUser, IFriendship } from "../../schemas";
|
import { IUser, IFriendship } from "../../schemas";
|
||||||
import { Card, Divider, Panel } from "../ui";
|
import { Card, Divider, Panel } from "../ui";
|
||||||
import FriendSearchWidget from "../ui/Widgets/FriendSearchWidget";
|
import FriendSearchWidget from "../ui/Widgets/FriendSearchWidget";
|
||||||
|
|
||||||
export default function Friends() {
|
const Friends: FC<{ targetUser?: IUser }> = ({ targetUser }) => {
|
||||||
const [friends, setFriends] = useState<IFriendship[]>();
|
const [friends, setFriends] = useState<IFriendship[]>();
|
||||||
const [userList, setUserList] = useState(new Array<IUser>());
|
const [userList, setUserList] = useState(new Array<IUser>());
|
||||||
const { user } = useAuthContext();
|
const { user, token } = useAuthContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
(async function() {
|
(async function() {
|
||||||
try {
|
try {
|
||||||
const rawResult = await getFriendships();
|
const Friends = new API.Friendship(token);
|
||||||
|
const result: IFriendship[] | null = await Friends.getAll();
|
||||||
|
|
||||||
if (rawResult.length) {
|
if (result?.length) {
|
||||||
const result = rawResult.filter((item: IFriendship) => (item.senderid == user.id) && !(item.pending));
|
|
||||||
setFriends(result);
|
setFriends(result);
|
||||||
}
|
}
|
||||||
} catch(e) {
|
|
||||||
console.error(e);
|
console.log(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
})()
|
})();
|
||||||
}, [user])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
friends && friends.map(async (friend: IFriendship) => {
|
if (!token || !friends) return;
|
||||||
const userData = await getUserByID(friend.targetid);
|
|
||||||
|
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[]) => {
|
if (userData) setUserList((prev: IUser[]) => {
|
||||||
|
if (prev.includes(userData)) {
|
||||||
|
return prev;
|
||||||
|
} else {
|
||||||
return [...prev, userData]
|
return [...prev, userData]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [friends]);
|
}, [friends]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(userList);
|
|
||||||
}, [setUserList])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ userList.length ?
|
{ userList.length ?
|
||||||
(
|
(
|
||||||
<Panel extraStyles="flex-row">
|
<Card extraClasses="flex-row">
|
||||||
<h2>Your friendships:</h2>
|
<h2>Friends ({ userList?.length ?? "0" }):</h2>
|
||||||
|
|
||||||
|
<div className="friends-list">
|
||||||
{
|
{
|
||||||
userList.map((user: IUser) => {
|
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>
|
<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 { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Autocomplete, TextField } from "@mui/material";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
|
// util/api
|
||||||
import { useAuthContext } from "../../context/AuthContext";
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
import { IRecipe } from "../../schemas";
|
import { DropdownData, IIngredient, IRecipe, RecipeIngredient } from "../../schemas";
|
||||||
import { Button, Divider, Form, Page, Panel } from "../ui"
|
import { IngredientFieldData } from "../../util/types";
|
||||||
|
import Protect from "../../util/Protect";
|
||||||
|
import API from "../../util/API";
|
||||||
|
|
||||||
const AddRecipe = () => {
|
// ui/components
|
||||||
const authContext = useAuthContext();
|
import { Button, Card, Divider, Panel, RichText, Toast } from "../ui"
|
||||||
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '', ingredients: [] })
|
import IngredientSelector from "../derived/IngredientSelector";
|
||||||
const [form, setForm] = useState<JSX.Element>();
|
|
||||||
|
|
||||||
const getFormState = useCallback((data: IRecipe) => {
|
export default function AddRecipe() {
|
||||||
setInput(data);
|
/**********************************
|
||||||
}, [input])
|
* STATE AND CONTEXT
|
||||||
|
*********************************/
|
||||||
|
const { user, token } = useAuthContext();
|
||||||
|
|
||||||
const handleCreate = () => {
|
// received recipe data
|
||||||
for (let field of Object.keys(input)) {
|
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '' })
|
||||||
if (!input[field as keyof IRecipe]) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('good to go!')
|
// 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);
|
||||||
|
|
||||||
useEffect(() => {
|
// status reporting
|
||||||
authContext.user && setInput((prev: IRecipe) => {
|
const [toast, setToast] = useState(<></>)
|
||||||
return {
|
|
||||||
...prev,
|
/**********************************
|
||||||
authoruserid: authContext.user!.id!
|
* 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)];
|
||||||
})
|
})
|
||||||
}, [authContext])
|
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
console.log(input);
|
token && (async() => {
|
||||||
}, [input])
|
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)) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<Page>
|
<Protect redirect="/add-recipe">
|
||||||
<h1>Add a New Recipe</h1>
|
<h1>Add a New Recipe</h1>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Panel>
|
<Panel id="create-recipe-panel" extraClasses="ui-form-component width-80">
|
||||||
<Form parent={input} _config={{
|
<div className="form-row">
|
||||||
parent: "AddRecipe",
|
<label>Recipe Name:</label>
|
||||||
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
|
<TextField variant="outlined" label="Recipe Name" onChange={(e) => setInput({ ...input, name: e.target.value })}/>
|
||||||
labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"],
|
</div>
|
||||||
dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'custom picker', 'TINYMCE'],
|
|
||||||
initialState: input,
|
|
||||||
getState: getFormState,
|
|
||||||
richTextInitialValue: "<p>Enter recipe details here!</p>"
|
|
||||||
}} />
|
|
||||||
|
|
||||||
{ 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>
|
<Button onClick={handleCreate}>Create Recipe!</Button>
|
||||||
|
|
||||||
|
<div id="toast">{ toast }</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Page>
|
</Protect>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddRecipe;
|
|
||||||
@@ -1,38 +1,69 @@
|
|||||||
import { useAuthContext } from "../../context/AuthContext";
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
import Protect from "../../util/Protect";
|
import Protect from "../../util/Protect";
|
||||||
import { useParams } from "react-router-dom";
|
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 Collection = () => {
|
||||||
const [isDefault, setIsDefault] = useState(true);
|
const [data, setData] = useState<ICollection>();
|
||||||
const { user } = useAuthContext();
|
const [owner, setOwner] = useState<IUser>();
|
||||||
|
const [recipes, setRecipes] = useState<IRecipe[]>();
|
||||||
|
const [content, setContent] = useState(<></>);
|
||||||
|
const { user, token } = useAuthContext();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
if (id) {
|
useEffect(() => {
|
||||||
setIsDefault(false);
|
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 (
|
return (
|
||||||
<Protect>
|
<Protect redirect={`/collections/${id}`}>
|
||||||
{ isDefault ?
|
{ content }
|
||||||
|
|
||||||
<>
|
|
||||||
<h1>Mikayla's collection</h1>
|
|
||||||
<p>37 recipes</p>
|
|
||||||
<p>71 ingredients</p>
|
|
||||||
<p>11 types of cuisine</p>
|
|
||||||
</>
|
|
||||||
|
|
||||||
:
|
|
||||||
|
|
||||||
<>
|
|
||||||
|
|
||||||
</>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{/* recipes */}
|
|
||||||
</Protect>
|
</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 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 (
|
return (
|
||||||
<Page>
|
<Panel key={v4()}>
|
||||||
<h1>Browsing your {2} collections:</h1>
|
<h2>{each.name}</h2>
|
||||||
</Page>
|
<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 (
|
||||||
|
<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 { useNavigate, useParams } from "react-router-dom";
|
||||||
import { IUser, IUserAuth } from "../../schemas";
|
import { IUser, IUserAuth } from "../../schemas";
|
||||||
import { Button, Form, Page, Panel } from "../ui";
|
import { Button, Form, Page, Panel } from "../ui";
|
||||||
import { FormConfig } from "../ui/Form";
|
import { FormConfig } from "../ui/Form/Form";
|
||||||
import API from "../../util/API";
|
import API from "../../util/API";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
@@ -29,7 +29,7 @@ export default function Login() {
|
|||||||
setToken(result.token);
|
setToken(result.token);
|
||||||
|
|
||||||
// if there is a redirect, go there, else go home
|
// if there is a redirect, go there, else go home
|
||||||
navigate(`/${redirect ?? ''}`);
|
navigate(redirect ?? '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for logged in user and mount form
|
// check for logged in user and mount form
|
||||||
@@ -41,9 +41,9 @@ export default function Login() {
|
|||||||
<Page>
|
<Page>
|
||||||
<h1>Hello! Nice to see you again.</h1>
|
<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',
|
parent: 'login',
|
||||||
keys: Object.keys(input),
|
keys: Object.keys(input),
|
||||||
labels: ["Email", "Password"],
|
labels: ["Email", "Password"],
|
||||||
|
|||||||
@@ -1,22 +1,196 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import { IUser } from "../../schemas";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AuthContext, useAuthContext } from "../../context/AuthContext";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Page } from "../ui";
|
import { AxiosError } from "axios";
|
||||||
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
|
import API from "../../util/API";
|
||||||
import Protect from "../../util/Protect";
|
import Protect from "../../util/Protect";
|
||||||
|
import { ICollection, IUser } from "../../schemas";
|
||||||
import Friends from "../derived/Friends";
|
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() {
|
export default function Profile() {
|
||||||
const [message, setMessage] = useState<JSX.Element>();
|
// globals and router utils
|
||||||
const { user } = useAuthContext();
|
const { user, token } = useAuthContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
// UI state info
|
||||||
<Protect redirect="profile">
|
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">
|
<div className="profile-authenticated">
|
||||||
<h1>{user && user.firstname}'s Profile</h1>
|
<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 />
|
<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>
|
</div>
|
||||||
</Protect>
|
</Protect>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}, [metadata])
|
||||||
|
|
||||||
|
// STEP 3: mount the UI
|
||||||
|
return contents
|
||||||
|
}
|
||||||
@@ -1,36 +1,109 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Page, Panel } from "../ui";
|
import { Divider, Page, Panel } from "../ui";
|
||||||
import { IRecipe } from "../../util/types";
|
import { IRecipe, IUser, IIngredient, RecipeIngredient } from "../../schemas";
|
||||||
import { getRecipeByID } from "../../util/apiUtils";
|
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() {
|
export default function Recipe() {
|
||||||
const [recipe, setRecipe] = useState<IRecipe>();
|
const { user, token } = useAuthContext();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
if (!id) {
|
const [recipe, setRecipe] = useState<IRecipe | "no recipe">();
|
||||||
return (
|
const [userData, setUserData] = useState<IUser>();
|
||||||
<Page>
|
const [ingredientData, setIngredientData] = useState<RecipeIngredient[]>([]);
|
||||||
<h1>404 | Not Found</h1>
|
const [view, setView] = useState<JSX.Element>(<h1>Loading...</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getRecipeByID(id);
|
if (token && id) {
|
||||||
}, [])
|
(async() => {
|
||||||
|
const recipeAPI = new API.Recipe(token);
|
||||||
return (
|
const result = await recipeAPI.getByID(id);
|
||||||
<Page>
|
if (result) {
|
||||||
{ recipe && (
|
setRecipe(result);
|
||||||
<Panel>
|
} else {
|
||||||
<h1>{recipe.name}</h1>
|
setRecipe("no recipe");
|
||||||
<p>{recipe.description}</p>
|
}
|
||||||
<p>{recipe.preptime}</p>
|
})()
|
||||||
</Panel>
|
}
|
||||||
)}
|
}, [token])
|
||||||
</Page>
|
|
||||||
)
|
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>
|
||||||
|
<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 { useNavigate } from "react-router-dom";
|
||||||
import { v4 } from "uuid";
|
|
||||||
import { RegisterVariantType, VariantLabel } from ".";
|
import { RegisterVariantType, VariantLabel } from ".";
|
||||||
import { useAuthContext } from "../../../context/AuthContext";
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
import { IUser, IUserAuth } from "../../../schemas";
|
import { IUser } from "../../../schemas";
|
||||||
import { attemptLogin, attemptRegister } from "../../../util/apiUtils";
|
|
||||||
import API from "../../../util/API";
|
import API from "../../../util/API";
|
||||||
import { Button, Page, Panel } from "../../ui";
|
import { Button, Page, Panel } from "../../ui";
|
||||||
import Divider from "../../ui/Divider";
|
import Divider from "../../ui/Divider";
|
||||||
import Form from "../../ui/Form";
|
import Form from "../../ui/Form/Form";
|
||||||
|
|
||||||
const blankUser: IUser = {
|
const blankUser: IUser = {
|
||||||
firstname: '',
|
firstname: '',
|
||||||
@@ -54,9 +52,9 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
|
|||||||
|
|
||||||
<h2>Tell us a bit about yourself:</h2>
|
<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",
|
parent: "register",
|
||||||
keys: ['firstname', 'lastname', 'handle', 'email', 'password'],
|
keys: ['firstname', 'lastname', 'handle', 'email', 'password'],
|
||||||
initialState: input,
|
initialState: input,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RegisterVariantType, VariantLabel } from ".";
|
import { RegisterVariantType, VariantLabel } from ".";
|
||||||
import { useNow } from "../../../hooks/useNow";
|
import { useNow } from "../../../hooks/useNow";
|
||||||
import { ICollection, IUser, IUserAuth } from "../../../schemas";
|
import { ICollection } from "../../../schemas";
|
||||||
import { attemptLogin, createNewCollection } from "../../../util/apiUtils";
|
|
||||||
import API from "../../../util/API";
|
import API from "../../../util/API";
|
||||||
import { Button, Divider, Page, Panel } from "../../ui";
|
import { Button, Divider, Page, Panel } from "../../ui";
|
||||||
import TextField from "../../ui/TextField";
|
|
||||||
import { useAuthContext } from "../../../context/AuthContext";
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
|
|
||||||
const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) => {
|
const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) => {
|
||||||
@@ -28,10 +26,7 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) =>
|
|||||||
datemodified: now
|
datemodified: now
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(collection);
|
|
||||||
|
|
||||||
const result = await collectionAPI.post(collection);
|
const result = await collectionAPI.post(collection);
|
||||||
console.log(result);
|
|
||||||
if (result) transitionDisplay(VariantLabel.AddFriends);
|
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>
|
<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'} /> */}
|
{/* <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>
|
</Panel>
|
||||||
|
|
||||||
<Button onClick={handleClick}>Next</Button>
|
<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 { useAuthContext } from "../../../context/AuthContext";
|
||||||
import { IUser } from "../../../schemas";
|
import { IUser } from "../../../schemas";
|
||||||
import AboutYou from "./aboutyou";
|
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 { useNavigate } from "react-router-dom";
|
||||||
import { useAuthContext } from "../../context/AuthContext";
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
import { attemptLogout, checkCredientials } from "../../util/apiUtils";
|
|
||||||
import { Button, Page, Panel } from "../ui"
|
import { Button, Page, Panel } from "../ui"
|
||||||
import Divider from "../ui/Divider";
|
import Divider from "../ui/Divider";
|
||||||
|
|
||||||
const Welcome = () => {
|
const Welcome = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, setUser } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
|
||||||
const authUserActions = (
|
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('/explore')}>Browse Recipes</Button>
|
||||||
<Button onClick={() => navigate('/subscriptions')}>Subscriptions</Button>
|
<Button onClick={() => navigate('/subscriptions')}>Subscriptions</Button>
|
||||||
<Button onClick={() => navigate('/grocery-list')}>Grocery Lists</Button>
|
<Button onClick={() => navigate('/grocery-list')}>Grocery Lists</Button>
|
||||||
@@ -18,30 +16,28 @@ const Welcome = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const callToRegister = (
|
const callToRegister = (
|
||||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||||
<h2>Ready to get started?</h2>
|
<h2>Ready to get started?</h2>
|
||||||
<Button onClick={() => navigate('/register')}>Register</Button>
|
<Button onClick={() => navigate('/register')}>Register</Button>
|
||||||
<Button onClick={attemptLogout}>Log Out</Button>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page extraStyles="narrow-dividers">
|
<Page extraClasses="narrow-dividers">
|
||||||
<Panel extraStyles='inherit-background c-papyrus uppercase'>
|
<Panel extraClasses='inherit-background c-papyrus uppercase'>
|
||||||
<h1>Welcome to Recipin</h1>
|
<h1>Welcome to Recipin</h1>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Divider />
|
<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>
|
<h2>Simple Recipe Management and Sharing for the Home</h2>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||||
<h2>Build Shopping Lists Directly from Your Recipes</h2>
|
<h2>Build Shopping Lists Directly from Your Recipes</h2>
|
||||||
<button onClick={checkCredientials}></button>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
import Protect from "../../util/Protect";
|
import Protect from "../../util/Protect";
|
||||||
import Form from "./Form";
|
import Form from "./Form/Form";
|
||||||
|
|
||||||
interface BrowserProps {
|
interface BrowserProps {
|
||||||
children?: JSX.Element[]
|
children?: JSX.Element[]
|
||||||
@@ -9,6 +10,8 @@ interface BrowserProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => {
|
const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => {
|
||||||
|
const { user, token } = useAuthContext();
|
||||||
|
|
||||||
const [form, setForm] = useState<any>();
|
const [form, setForm] = useState<any>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -16,7 +19,7 @@ const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Protect>
|
<Protect redirect="/explore">
|
||||||
<h1>{header}</h1>
|
<h1>{header}</h1>
|
||||||
</Protect>
|
</Protect>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { ButtonComponent } from "../../util/types"
|
import { ButtonComponent } from "../../util/types"
|
||||||
import "/src/sass/components/Button.scss";
|
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 (
|
return (
|
||||||
<button onClick={onClick} disabled={disabled} className={`ui-button ${extraStyles || ''}`}>
|
<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 ${extraClasses || ''}`}>
|
||||||
{ disabled ? (disabledText || children || "Button") : (children || "Button") }
|
{ disabled ? (disabledText || children || "Button") : (children || "Button") }
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { MultiChildPortal } from "../../util/types"
|
|
||||||
|
|
||||||
const Card: FC<MultiChildPortal> = ({ children = <></>, extraStyles = ""}) => {
|
|
||||||
|
const Card: FC<{ children?: JSX.Element | JSX.Element[], extraClasses?: string }> = ({ children = <></>, extraClasses = ""}) => {
|
||||||
return (
|
return (
|
||||||
<div className={`ui-card ${extraStyles}`}>
|
<div className={`ui-card ${extraClasses}`}>
|
||||||
{ children }
|
{ Array.isArray(children) ? <>{children}</> : children }
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { PortalBase } from "../../util/types";
|
|||||||
import "/src/sass/components/Dropdown.scss";
|
import "/src/sass/components/Dropdown.scss";
|
||||||
|
|
||||||
// expects to receive buttons as children
|
// expects to receive buttons as children
|
||||||
const Dropdown: FC<PortalBase> = ({ children, extraStyles = null }) => {
|
const Dropdown: FC<PortalBase> = ({ children, extraClasses = null }) => {
|
||||||
return (
|
return (
|
||||||
<Panel extraStyles={`ui-dropdown ${extraStyles}`}>
|
<Panel extraClasses={`ui-dropdown ${extraClasses}`}>
|
||||||
{ children }
|
{ children }
|
||||||
</Panel>
|
</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 API from "../../../util/API";
|
||||||
import { NavbarType } from "../../../util/types";
|
|
||||||
import { Button, Dropdown } from '..'
|
import { Button, Dropdown } from '..'
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAuthContext } from "../../../context/AuthContext";
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@@ -14,8 +13,7 @@ const LoggedIn = () => {
|
|||||||
const [searchActive, setSearchActive] = useState(false);
|
const [searchActive, setSearchActive] = useState(false);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
const success = await auth.logout();
|
await auth.logout();
|
||||||
console.log(success);
|
|
||||||
|
|
||||||
// nullify cookie and unset user/token data
|
// nullify cookie and unset user/token data
|
||||||
document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
@@ -40,10 +38,6 @@ const LoggedIn = () => {
|
|||||||
navigate(payload);
|
navigate(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(user);
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div id="navbar">
|
<div id="navbar">
|
||||||
@@ -59,8 +53,9 @@ const LoggedIn = () => {
|
|||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
dropdownActive && (
|
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-recipe')}>Add a Recipe</Button>
|
||||||
|
<Button onClick={() => handleOptionSelect("/add-friends")}>Add Friends</Button>
|
||||||
<Button onClick={() => handleOptionSelect('/collections')}>My Collections</Button>
|
<Button onClick={() => handleOptionSelect('/collections')}>My Collections</Button>
|
||||||
<Button onClick={() => handleOptionSelect('/subscriptions')}>Subscriptions</Button>
|
<Button onClick={() => handleOptionSelect('/subscriptions')}>Subscriptions</Button>
|
||||||
<Button onClick={() => handleOptionSelect('/profile')}>Profile</Button>
|
<Button onClick={() => handleOptionSelect('/profile')}>Profile</Button>
|
||||||
@@ -70,7 +65,7 @@ const LoggedIn = () => {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
searchActive && (
|
searchActive && (
|
||||||
<Dropdown extraStyles="top-menu-bar search-bar">
|
<Dropdown extraClasses="top-menu-bar search-bar">
|
||||||
<Button>Run Search</Button>
|
<Button>Run Search</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { PageComponent } from "../../util/types"
|
|||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import "/src/sass/components/Page.scss";
|
import "/src/sass/components/Page.scss";
|
||||||
|
|
||||||
const Page: PageComponent = ({ extraStyles, children }) => {
|
const Page: PageComponent = ({ extraClasses, children }) => {
|
||||||
return (
|
return (
|
||||||
<main id="view">
|
<main id="view">
|
||||||
<section className={`Page ${extraStyles || null}`}>
|
<section className={`Page ${extraClasses || null}`}>
|
||||||
{ children || null }
|
{ children || null }
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { PanelComponent } from "../../util/types";
|
import { PanelComponent } from "../../util/types";
|
||||||
import "/src/sass/components/Panel.scss";
|
import "/src/sass/components/Panel.scss";
|
||||||
|
|
||||||
const Panel: PanelComponent = ({ children, extraStyles }) => {
|
const Panel: PanelComponent = ({ children, extraClasses, id }) => {
|
||||||
|
if (id) {
|
||||||
return (
|
return (
|
||||||
<div className={`Panel ${extraStyles || ''}`}>
|
<div id={id} className={`Panel ${extraClasses || ''}`}>
|
||||||
|
{ children || null }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={`Panel ${extraClasses || ''}`}>
|
||||||
{ children || null }
|
{ children || null }
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Panel;
|
export default Panel;
|
||||||
@@ -25,7 +25,7 @@ const RichText: FC<RichTextProps> = ({ id, initialValue, getState }) => {
|
|||||||
onEditorChange={(txt, editor) => handleChange(txt, editor)}
|
onEditorChange={(txt, editor) => handleChange(txt, editor)}
|
||||||
initialValue={initialValue || '<p></p>'}
|
initialValue={initialValue || '<p></p>'}
|
||||||
init={{
|
init={{
|
||||||
height: 500,
|
height: 300,
|
||||||
menubar: false,
|
menubar: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
'advlist autolink lists link image charmap print preview anchor',
|
'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 { FC } from "react";
|
||||||
import { PortalBase } from "../../util/types";
|
import { PortalBase } from "../../util/types";
|
||||||
|
|
||||||
const Tooltip: FC<PortalBase> = ({ children, extraStyles = null }) => {
|
const Tooltip: FC<PortalBase> = ({ children, extraClasses = null }) => {
|
||||||
return (
|
return (
|
||||||
<aside className={`ui-tooltip ${extraStyles}`}>
|
<aside className={`ui-tooltip ${extraClasses}`}>
|
||||||
{ children }
|
{ children }
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,40 +1,57 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { addFriend, getPendingFriendRequests } from "../../util/apiUtils";
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
import { UserCardType } from "../../util/types";
|
import { UserCardType } from "../../util/types";
|
||||||
|
import API from "../../util/API";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
|
|
||||||
const UserCard: UserCardType = ({ extraStyles, user, canAdd = false, liftData }) => {
|
const UserCard: UserCardType = ({ extraClasses, targetUser }) => {
|
||||||
const [shouldDisable, setShouldDisable] = useState<boolean>(canAdd);
|
const [buttonVariant, setButtonVariant] = useState(<></>);
|
||||||
|
const { token } = useAuthContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
(async function() {
|
(async function() {
|
||||||
const requestsOpen = await getPendingFriendRequests();
|
try {
|
||||||
|
const friends = new API.Friendship(token);
|
||||||
|
const requestsOpen = await friends.getPendingFriendRequests();
|
||||||
if (!requestsOpen) return;
|
if (!requestsOpen) return;
|
||||||
|
|
||||||
for (let req of requestsOpen) {
|
for (let req of requestsOpen) {
|
||||||
if (req.targetid == user.id) {
|
if (req.targetid == targetUser.id) {
|
||||||
setShouldDisable(true);
|
setButtonVariant(<Button disabled>Request Sent!</Button>)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setShouldDisable(false);
|
setButtonVariant(<Button onClick={handleClick}>Send Request</Button>)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
console.log(error.response?.statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
const { id } = user;
|
if (!token) return;
|
||||||
const request = await addFriend(id!.toString());
|
const friends = new API.Friendship(token);
|
||||||
if (request) console.log("Friend request sent to " + user.firstname);
|
const request = await friends.addFriend(targetUser.id!.toString());
|
||||||
|
if (request) {
|
||||||
|
setButtonVariant(<Button disabled>Request Sent!</Button>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card extraStyles={'user-card' + extraStyles}>
|
<Card extraClasses={'user-card' + extraClasses}>
|
||||||
|
<>
|
||||||
<div className="avatar"></div>
|
<div className="avatar"></div>
|
||||||
<h3>{user.firstname} {user.lastname.substring(0,1)}.</h3>
|
<h3><a href={`/profile?id=${targetUser.id}`}>{targetUser.firstname} {targetUser.lastname.substring(0,1)}.</a></h3>
|
||||||
<h4>@{user.handle}</h4>
|
<h4>@{targetUser.handle}</h4>
|
||||||
{ canAdd && <Button disabledText={"Request Sent"} disabled={shouldDisable} onClick={handleClick}>Add Me</Button> }
|
{ buttonVariant }
|
||||||
|
</>
|
||||||
</Card>
|
</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 Card from "./Card";
|
||||||
import Divider from "./Divider";
|
import Divider from "./Divider";
|
||||||
import Dropdown from "./Dropdown";
|
import Dropdown from "./Dropdown";
|
||||||
import Form from "./Form";
|
import Form from "./Form/Form";
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
import Panel from "./Panel";
|
import Panel from "./Panel";
|
||||||
|
import RichText from "./RichText";
|
||||||
import TextField from "./TextField";
|
import TextField from "./TextField";
|
||||||
|
import Toast from "./Toast";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
import UserCard from "./UserCard";
|
import UserCard from "./UserCard";
|
||||||
|
|
||||||
export {
|
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 ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import AuthProvider from './context/AuthProvider'
|
import AuthProvider from './context/AuthProvider'
|
||||||
|
import SelectorProvider from './context/SelectorProvider'
|
||||||
import './sass/index.scss'
|
import './sass/index.scss'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<SelectorProvider>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
</SelectorProvider>
|
||||||
</AuthProvider>
|
</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;
|
flex-flow: row wrap;
|
||||||
justify-content: center;
|
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 { AxiosError, AxiosHeaders, AxiosRequestHeaders, AxiosResponse } from "axios";
|
||||||
import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList } from "../schemas";
|
import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList, DropdownData, RecipeIngredient } from "../schemas";
|
||||||
import { default as _instance } from "./axiosInstance";
|
import { default as _instance } from "./axiosInstance";
|
||||||
|
|
||||||
module API {
|
module API {
|
||||||
|
export enum CRUDMETHOD {
|
||||||
|
GET,
|
||||||
|
PUT,
|
||||||
|
POST,
|
||||||
|
DELETE
|
||||||
|
}
|
||||||
|
|
||||||
export class Settings {
|
export class Settings {
|
||||||
private static APISTRING = import.meta.env.APISTRING || "http://localhost:8080";
|
private static APISTRING = import.meta.env.APISTRING || "http://localhost:8080";
|
||||||
private static token?: string;
|
private static token?: string;
|
||||||
@@ -23,7 +30,7 @@ module API {
|
|||||||
abstract class RestController<T> {
|
abstract class RestController<T> {
|
||||||
protected instance = _instance;
|
protected instance = _instance;
|
||||||
protected endpoint: string;
|
protected endpoint: string;
|
||||||
protected headers?: any
|
protected headers?: any;
|
||||||
|
|
||||||
constructor(endpoint: string, token: string) {
|
constructor(endpoint: string, token: string) {
|
||||||
this.endpoint = endpoint;
|
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() {
|
async getAll() {
|
||||||
const response = await this.instance.get(this.endpoint, this.headers);
|
const response = await this.instance.get(this.endpoint, this.headers)
|
||||||
return Promise.resolve(response.data);
|
.catch((err: AxiosError) => {
|
||||||
|
console.log(err.message);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return Promise.resolve(response?.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByID(id: string) {
|
async getByID(id: string) {
|
||||||
const response = await this.instance.get(this.endpoint + "/" + id, this.headers);
|
const response = await this.instance.get(this.endpoint + "/" + id, this.headers)
|
||||||
return Promise.resolve(response.data);
|
.catch((err: AxiosError) => {
|
||||||
|
console.log(err.message);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return Promise.resolve(response?.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async post(data: T) {
|
async post(data: T) {
|
||||||
console.log(data);
|
try {
|
||||||
const response = await this.instance.post(this.endpoint, data, this.headers);
|
const response = await this.instance.post(this.endpoint, JSON.stringify(data), this.headers);
|
||||||
return Promise.resolve(response.data);
|
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>) {
|
async put(id: string, data: T | Partial<T>) {
|
||||||
|
try {
|
||||||
const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers);
|
const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers);
|
||||||
return Promise.resolve(response.data);
|
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) {
|
async delete(id: string) {
|
||||||
|
try {
|
||||||
const response = await this.instance.delete(this.endpoint + '/' + id, this.headers);
|
const response = await this.instance.delete(this.endpoint + '/' + id, this.headers);
|
||||||
return Promise.resolve(response.data);
|
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> {
|
export class Friendship extends RestController<IFriendship> {
|
||||||
constructor(token: string) {
|
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() {
|
async getPendingFriendRequests() {
|
||||||
const response = await this.instance.get(this.endpoint + "?pending=true", this.headers);
|
const response = await this.instance.get(this.endpoint + "?pending=true", this.headers);
|
||||||
return Promise.resolve(response.data);
|
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> {
|
export class Recipe extends RestController<IRecipe> {
|
||||||
constructor(token: string) {
|
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> {
|
export class Ingredient extends RestController<IIngredient> {
|
||||||
constructor(token: string) {
|
constructor(token: string) {
|
||||||
if (!token) throw new Error("Missing required token");
|
super(Settings.getAPISTRING() + "/app/ingredient", token);
|
||||||
super(Settings.getAPISTRING() + "/app/ingredients", 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) {
|
constructor(token: string) {
|
||||||
super(Settings.getAPISTRING() + "/app/collection", token);
|
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> {
|
export class GroceryList extends RestController<IGroceryList> {
|
||||||
@@ -151,6 +261,22 @@ module API {
|
|||||||
super(Settings.getAPISTRING() + "/app/grocery-list", token)
|
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
|
export default API
|
||||||
@@ -1,31 +1,45 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import AccessForbidden from "../components/pages/StatusPages/403";
|
||||||
import { Button, Page } from "../components/ui";
|
import { Button, Page } from "../components/ui";
|
||||||
import Divider from "../components/ui/Divider";
|
|
||||||
import { useAuthContext } from "../context/AuthContext";
|
import { useAuthContext } from "../context/AuthContext";
|
||||||
|
import API from "./API";
|
||||||
import { ProtectPortal } from "./types";
|
import { ProtectPortal } from "./types";
|
||||||
|
|
||||||
const Protect: ProtectPortal = ({ children, redirect = '' }) => {
|
const Protect: ProtectPortal = ({ children, redirect = '', accessRules = null }) => {
|
||||||
const { user } = useAuthContext();
|
const { user, token } = useAuthContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (!user) {
|
if (!user || !token) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<AccessForbidden>
|
||||||
<div className="content-unauthorized">
|
<>
|
||||||
<h1>Hi there! You don't look too familiar.</h1>
|
<h2>Hi there! You don't look too familiar.</h2>
|
||||||
<p>To view the content on this page, please log in below:</p>
|
<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>
|
<Button onClick={() => navigate(redirect ? `/login?redirect=${redirect}` : '/login')}>Log In</Button>
|
||||||
</div>
|
</>
|
||||||
</Page>
|
</AccessForbidden>
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
{ children || <></> }
|
|
||||||
</Page>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
export default Protect;
|
||||||
@@ -14,7 +14,9 @@ instance.interceptors.response.use((res: AxiosResponse<any,any>) => {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
return Promise.reject(err);
|
console.log(err);
|
||||||
|
// return err;
|
||||||
|
// return Promise.reject(err);
|
||||||
})
|
})
|
||||||
|
|
||||||
export default instance;
|
export default instance;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { ChangeEvent, ChangeEventHandler, Dispatch, FC, ReactNode, SetStateAction } from "react";
|
import { ChangeEvent, ChangeEventHandler, Dispatch, FC, ReactNode, SetStateAction } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Form } from "../components/ui";
|
import { Form } from "../components/ui";
|
||||||
import { IUser } from "../schemas";
|
import { DropdownData, IIngredient, IUser } from "../schemas";
|
||||||
|
|
||||||
export interface PortalBase {
|
export interface PortalBase {
|
||||||
children?: ReactNode | ReactNode[]
|
children?: ReactNode | ReactNode[]
|
||||||
extraStyles?: string
|
extraClasses?: string
|
||||||
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonParams extends PortalBase {
|
interface ButtonParams extends PortalBase {
|
||||||
@@ -14,14 +15,19 @@ interface ButtonParams extends PortalBase {
|
|||||||
disabledText?: string
|
disabledText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccessRules {
|
||||||
|
mustBeRecipinAdmin: boolean
|
||||||
|
mustBeFriend: boolean
|
||||||
|
mustBeSubscribed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProtectParams extends PortalBase {
|
export interface ProtectParams extends PortalBase {
|
||||||
redirect?: string
|
redirect?: string
|
||||||
|
accessRules?: Partial<AccessRules> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserCardProps extends PortalBase {
|
interface UserCardProps extends PortalBase {
|
||||||
user: IUser
|
targetUser: IUser
|
||||||
canAdd?: boolean
|
|
||||||
liftData?: (data: any) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
@@ -40,6 +46,22 @@ interface CheckboxProps {
|
|||||||
FormElement: typeof Form
|
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 PageComponent = FC<PortalBase>
|
||||||
export type PanelComponent = FC<PortalBase>
|
export type PanelComponent = FC<PortalBase>
|
||||||
export type ButtonComponent = FC<ButtonParams>
|
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();
|
dotenv.config();
|
||||||
|
|
||||||
export function restrictAccess(req: Request, res: Response, next: NextFunction) {
|
export function restrictAccess(req: Request, res: Response, next: NextFunction) {
|
||||||
if (req.session.user == undefined) {
|
if (req.user == undefined) {
|
||||||
res.send("content restricted");
|
res.send("content restricted");
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ export default class CollectionCtl {
|
|||||||
return new ControllerResponse(code, data);
|
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() {
|
async getAll() {
|
||||||
const result = await CollectionInstance.getAll();
|
const result = await CollectionInstance.getAll();
|
||||||
const ok = result !== null;
|
const ok = result !== null;
|
||||||
@@ -23,6 +31,13 @@ export default class CollectionCtl {
|
|||||||
return new ControllerResponse(code, data);
|
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) {
|
async getUserDefault(id: number | string) {
|
||||||
const result = await CollectionInstance.getUserDefault(id);
|
const result = await CollectionInstance.getUserDefault(id);
|
||||||
const code = (result !== null) ? StatusCode.OK : StatusCode.NotFound;
|
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) {
|
async getOne(id: string) {
|
||||||
try {
|
try {
|
||||||
const result = await IngredientInstance.getOne(id);
|
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 { Recipe } from "../models/recipe";
|
||||||
import ControllerResponse from "../util/ControllerResponse";
|
import ControllerResponse from "../util/ControllerResponse";
|
||||||
import { StatusCode } from "../util/types";
|
import { StatusCode } from "../util/types";
|
||||||
@@ -58,4 +58,15 @@ export default class RecipeCtl {
|
|||||||
throw new Error(error);
|
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();
|
const UserInstance = new User();
|
||||||
|
|
||||||
export default class UserCtl {
|
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() {
|
async getAll() {
|
||||||
try {
|
try {
|
||||||
// attempt to get users from database
|
// 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) {
|
async post(body: IUser) {
|
||||||
try {
|
try {
|
||||||
const response = await UserInstance.post(body);
|
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) {
|
async getOne(id: number | string) {
|
||||||
try {
|
try {
|
||||||
const user = await UserInstance.getOneByID(id);
|
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) {
|
async updateOne(id: number | string, body: IUser) {
|
||||||
try {
|
try {
|
||||||
const result = await UserInstance.updateOneByID(id, body);
|
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) {
|
async getFriends(id: number | string) {
|
||||||
try {
|
try {
|
||||||
const result = await UserInstance.getFriends(id);
|
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) {
|
async getFriendshipByID(id: number | string, userid: number | string) {
|
||||||
try {
|
try {
|
||||||
const { ok, code, result } = await UserInstance.getFriendshipByID(id, userid);
|
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) {
|
async getPendingFriendRequests(recipient: string | number) {
|
||||||
try {
|
try {
|
||||||
const { ok, code, result } = await UserInstance.getPendingFriendRequests(recipient);
|
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) {
|
async addFriendship(userid: number | string, targetid: number | string) {
|
||||||
try {
|
try {
|
||||||
const result = await UserInstance.addFriendship(userid, targetid);
|
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> = [
|
const allStatements: Array<string> = [
|
||||||
populateUsers, populateCuisines, populateCourses,
|
populateUsers, populateCuisines, populateCourses,
|
||||||
populateCollection, populateIngredients, populateRecipes,
|
populateCollection, populateIngredients, populateRecipes,
|
||||||
populateGroceryList, populateFriendships, populateComments
|
populateGroceryList, populateFriendships, populateComments,
|
||||||
|
populateMeasurements
|
||||||
];
|
];
|
||||||
|
|
||||||
await pool.query(setup);
|
await pool.query(setup);
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ dotenv.config();
|
|||||||
const recipecollection = fs.readFileSync(appRoot + '/db/sql/create/createcmp_recipecollection.sql').toString();
|
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 usersubscriptions = fs.readFileSync(appRoot + '/db/sql/create/createcmp_usersubscriptions.sql').toString();
|
||||||
const userfriendships = fs.readFileSync(appRoot + '/db/sql/create/createcmp_userfriendships.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 = [
|
const allStatements = [
|
||||||
setRole, appusers, ingredient, collection, cuisine, course,
|
setRole, appusers, ingredient, collection, cuisine, course,
|
||||||
recipe, recipecomments, groceryList, recipeingredient,
|
recipe, recipecomments, groceryList, recipeingredient,
|
||||||
recipecollection, usersubscriptions, userfriendships
|
recipecollection, usersubscriptions, userfriendships, dropdownValues
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
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) {
|
async getUserDefault(id: number | string) {
|
||||||
try {
|
try {
|
||||||
const statement = `
|
const statement = `
|
||||||
@@ -46,7 +79,6 @@ export class Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async post(data: ICollection) {
|
async post(data: ICollection) {
|
||||||
console.log('new default collection');
|
|
||||||
const { name, active, ismaincollection, ownerid } = data;
|
const { name, active, ismaincollection, ownerid } = data;
|
||||||
try {
|
try {
|
||||||
const statement = `
|
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) {
|
async post(data: IIngredient) {
|
||||||
try {
|
try {
|
||||||
const statement = `
|
const statement = `
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IRecipe } from "../schemas";
|
import { IIngredient, IRecipe, RecipeIngredient } from "../schemas";
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import pool from "../db";
|
import pool from "../db";
|
||||||
import { CollectionCtl } from "../controllers";
|
import { CollectionCtl } from "../controllers";
|
||||||
@@ -114,4 +114,24 @@ export class Recipe {
|
|||||||
throw new Error(error);
|
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) {
|
async addFriendship(userid: number | string, targetid: number | string) {
|
||||||
try {
|
try {
|
||||||
const statement = `
|
const statement = `
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ const router = Router();
|
|||||||
export const authRoute = (app: Express) => {
|
export const authRoute = (app: Express) => {
|
||||||
app.use('/auth', router);
|
app.use('/auth', router);
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
|
||||||
console.log(req.session);
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/', restrictAccess, (req, res, next) => {
|
router.get('/', restrictAccess, (req, res, next) => {
|
||||||
if (req.session.user) {
|
if (req.session.user) {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -44,8 +39,6 @@ export const authRoute = (app: Express) => {
|
|||||||
router.post('/login', async (req, res, next) => {
|
router.post('/login', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const data: IUserAuth = req.body;
|
const data: IUserAuth = req.body;
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
const response: ControllerResponse<any> = await AuthInstance.login(data);
|
const response: ControllerResponse<any> = await AuthInstance.login(data);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -70,8 +63,6 @@ export const authRoute = (app: Express) => {
|
|||||||
return next(err);
|
return next(err);
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(req.session);
|
|
||||||
|
|
||||||
res.cookie('token', token, { httpOnly: true });
|
res.cookie('token', token, { httpOnly: true });
|
||||||
res.json({ token });
|
res.json({ token });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Express, Router } from "express";
|
import { Express, Router } from "express";
|
||||||
import { checkIsAdmin, restrictAccess } from "../auth/middlewares";
|
import { checkIsAdmin, restrictAccess } from "../auth/middlewares";
|
||||||
import CollectionCtl from "../controllers/CollectionCtl";
|
import CollectionCtl from "../controllers/CollectionCtl";
|
||||||
|
import { IUser } from "../schemas";
|
||||||
|
import { StatusCode } from "../util/types";
|
||||||
const CollectionInstance = new CollectionCtl();
|
const CollectionInstance = new CollectionCtl();
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -8,35 +10,50 @@ const router = Router();
|
|||||||
export const collectionRoute = (app: Express) => {
|
export const collectionRoute = (app: Express) => {
|
||||||
app.use('/app/collection', router);
|
app.use('/app/collection', router);
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.get('/:id', restrictAccess, async (req, res, next) => {
|
||||||
console.log('what gives');
|
|
||||||
console.log(req.body);
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/:id', async (req, res, next) => {
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
const { getRecipes } = req.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (getRecipes || getRecipes == "true") {
|
||||||
|
const { code, data } = await CollectionInstance.getRecipesFromOne(id);
|
||||||
|
res.status(code).send(data);
|
||||||
|
} else {
|
||||||
const { code, data } = await CollectionInstance.getOne(id);
|
const { code, data } = await CollectionInstance.getOne(id);
|
||||||
res.status(code).send(data);
|
res.status(code).send(data);
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// implement is admin on this route
|
// 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 {
|
try {
|
||||||
|
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();
|
const { code, data } = await CollectionInstance.getAll();
|
||||||
res.status(code).send(data);
|
res.status(code).send(data);
|
||||||
|
} else {
|
||||||
|
res.status(403).send("Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/', async (req, res, next) => {
|
router.post('/', restrictAccess, async (req, res, next) => {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
console.log(req.body ?? "sanity check");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await CollectionInstance.post(data);
|
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 { restrictAccess } from '../auth/middlewares';
|
||||||
import { UserCtl } from '../controllers';
|
import { UserCtl } from '../controllers';
|
||||||
import { IUser } from '../schemas';
|
import { IUser } from '../schemas';
|
||||||
|
import { StatusCode } from '../util/types';
|
||||||
|
|
||||||
const UserInstance = new UserCtl();
|
const UserInstance = new UserCtl();
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -9,19 +10,8 @@ const router = Router();
|
|||||||
export const friendRouter = (app: Express) => {
|
export const friendRouter = (app: Express) => {
|
||||||
app.use('/app/friend', router);
|
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) => {
|
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;
|
const { targetid } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -34,17 +24,28 @@ export const friendRouter = (app: Express) => {
|
|||||||
|
|
||||||
// get all friendships for a user
|
// get all friendships for a user
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
const user = req.session.user as IUser;
|
const user = req.user as IUser;
|
||||||
const { pending } = req.query;
|
const { pending, accepted, targetUser } = req.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (pending) {
|
if (pending) {
|
||||||
const { code, data } = await UserInstance.getPendingFriendRequests(user.id as number);
|
const { code, data } = await UserInstance.getPendingFriendRequests(user.id as number);
|
||||||
res.status(code).send(data);
|
res.status(code).send(data);
|
||||||
|
} 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 {
|
} else {
|
||||||
const { code, data } = await UserInstance.getFriends(user.id as number);
|
const { code, data } = await UserInstance.getFriends(user.id as number);
|
||||||
res.status(code).send(data);
|
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) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ export const friendRouter = (app: Express) => {
|
|||||||
// get one friendship by its id
|
// get one friendship by its id
|
||||||
router.get('/:id', async (req, res, next) => {
|
router.get('/:id', async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const user = req.session.user as IUser;
|
const user = req.user as IUser;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { code, data } = await UserInstance.getFriendshipByID(id, user.id as number);
|
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) => {
|
router.put('/:id', async (req, res, next) => {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const user = req.session.user as IUser;
|
const user = req.user as IUser;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await UserInstance.updateFriendship(id, user.id as number, data);
|
const response = await UserInstance.updateFriendship(id, user.id as number, data);
|
||||||
@@ -85,4 +86,6 @@ export const friendRouter = (app: Express) => {
|
|||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return router;
|
||||||
}
|
}
|
||||||
@@ -11,35 +11,52 @@ import { subscriptionRoute } from "./subscription";
|
|||||||
import { friendRouter } from "./friend";
|
import { friendRouter } from "./friend";
|
||||||
import { cuisineRouter } from "./cuisine";
|
import { cuisineRouter } from "./cuisine";
|
||||||
import { courseRouter } from "./course";
|
import { courseRouter } from "./course";
|
||||||
|
import { dropdownValueRouter } from "./dropdownValues";
|
||||||
|
import { IUser } from "../schemas";
|
||||||
|
import { User } from "../models/user";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
let REQUESTCOUNT = 0;
|
||||||
|
|
||||||
export const routes = async (app: Express) => {
|
export const routes = async (app: Express) => {
|
||||||
// unprotected routes
|
// simple request counting middleware
|
||||||
authRoute(app);
|
app.use('/', (req, res, next) => {
|
||||||
|
REQUESTCOUNT++;
|
||||||
|
console.log("count: ", REQUESTCOUNT);
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
|
||||||
// middleware to check for auth on cookies on each request in protected routes
|
// middleware to check for auth on cookies on each request in protected routes
|
||||||
app.use('/app', async (req, res, next) => {
|
app.use('/app', async (req, res, next) => {
|
||||||
// pull jwt from request headers
|
// pull jwt from request headers
|
||||||
console.log(req.headers);
|
|
||||||
const token = req.headers['authorization']?.split(" ")[1];
|
const token = req.headers['authorization']?.split(" ")[1];
|
||||||
console.log(token);
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
res.status(403).send("Unauthorized, did not receive token");
|
res.status(403).send("Unauthorized, did not receive token");
|
||||||
} else {
|
} 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) {
|
if (err) {
|
||||||
res.status(403).send(err);
|
res.status(403).send(err);
|
||||||
} else {
|
} else {
|
||||||
console.log(data);
|
const userInstance = new User();
|
||||||
req.user = data;
|
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();
|
next();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// unprotected routes
|
||||||
|
authRoute(app);
|
||||||
|
|
||||||
// protected routes
|
// protected routes
|
||||||
userRoute(app);
|
userRoute(app);
|
||||||
friendRouter(app);
|
friendRouter(app);
|
||||||
@@ -49,6 +66,7 @@ export const routes = async (app: Express) => {
|
|||||||
subscriptionRoute(app);
|
subscriptionRoute(app);
|
||||||
groceryListRoute(app);
|
groceryListRoute(app);
|
||||||
courseRouter(app);
|
courseRouter(app);
|
||||||
|
dropdownValueRouter(app);
|
||||||
|
|
||||||
// deprecate?
|
// deprecate?
|
||||||
cuisineRouter(app);
|
cuisineRouter(app);
|
||||||
|
|||||||
@@ -10,9 +10,16 @@ export const ingredientRoute = (app: Express) => {
|
|||||||
app.use('/app/ingredient', router);
|
app.use('/app/ingredient', router);
|
||||||
|
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
|
const { recipeID } = req.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
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();
|
const result: CtlResponse<IIngredient[] | string> = await IngredientInstance.getAll();
|
||||||
res.status(result.code).send(result.data);
|
res.status(result.code).send(result.data);
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
@@ -51,4 +58,6 @@ export const ingredientRoute = (app: Express) => {
|
|||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return router;
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,12 @@ export const recipeRoute = (app: Express) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.get('/', restrictAccess, async (req, res, next) => {
|
router.get('/', restrictAccess, async (req, res, next) => {
|
||||||
const user = req.session.user as IUser;
|
const user = req.user as IUser;
|
||||||
const { filterby } = req.query;
|
const { filter } = req.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result: CtlResponse<IRecipe[] | string>;
|
let result: CtlResponse<IRecipe[] | string>;
|
||||||
switch (filterby) {
|
switch (filter) {
|
||||||
case "myrecipes":
|
case "myrecipes":
|
||||||
result = await recipectl.getAllAuthored(user.id as number);
|
result = await recipectl.getAllAuthored(user.id as number);
|
||||||
break;
|
break;
|
||||||
@@ -55,12 +55,20 @@ export const recipeRoute = (app: Express) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.post('/', restrictAccess, async (req, res, next) => {
|
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 data = req.body;
|
||||||
|
const { addIngredients, recipeID } = req.query;
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
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);
|
const result = await recipectl.post(user.id as number, data);
|
||||||
res.status(result.code).send(result.data);
|
res.status(result.code).send(result.data);
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Express, Router } from "express"
|
import { Express, Router } from "express"
|
||||||
import { restrictAccess } from "../auth/middlewares";
|
import { restrictAccess } from "../auth/middlewares";
|
||||||
import { CollectionCtl } from "../controllers";
|
import { CollectionCtl } from "../controllers";
|
||||||
|
import { IUser } from "../schemas";
|
||||||
const CollectionInstance = new CollectionCtl();
|
const CollectionInstance = new CollectionCtl();
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -8,12 +9,11 @@ export const subscriptionRoute = (app: Express) => {
|
|||||||
app.use('/app/subscription', router);
|
app.use('/app/subscription', router);
|
||||||
|
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
// @ts-ignore
|
const user = req.user as IUser;
|
||||||
const { user } = req.session.user;
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await CollectionInstance.getSubscriptions(user.id as string);
|
const result = await CollectionInstance.getSubscriptions(user.id!);
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
@@ -21,12 +21,11 @@ export const subscriptionRoute = (app: Express) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.post('/', restrictAccess, async (req, res, next) => {
|
router.post('/', restrictAccess, async (req, res, next) => {
|
||||||
// @ts-ignore
|
const user = req.user as IUser;
|
||||||
const { user } = req.session.user;
|
|
||||||
const { collection } = req.query;
|
const { collection } = req.query;
|
||||||
|
|
||||||
try {
|
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);
|
res.status(201).send(result);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ export interface IIngredient extends HasHistory {
|
|||||||
createdbyid: string | number
|
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 {
|
export interface ICollection extends HasHistory, CanDeactivate {
|
||||||
name: string
|
name: string
|
||||||
ismaincollection: boolean
|
ismaincollection: boolean
|
||||||
@@ -73,3 +80,10 @@ export interface FlavorProfile extends HasHistory, CanDeactivate {
|
|||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DropdownData extends HasHistory {
|
||||||
|
name: string
|
||||||
|
datatype: DropdownDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DropdownDataType = "measurement" | "course"
|
||||||
Reference in New Issue
Block a user