add recipe workflow, viewing for collections
This commit is contained in:
@@ -1,33 +1,53 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useRef, useEffect, useState, createRef } from "react";
|
||||||
import { useAuthContext } from "../../context/AuthContext";
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
|
import { Button, Card, Divider, Form, Page, Panel } from "../ui"
|
||||||
import { IRecipe } from "../../schemas";
|
import { IRecipe } from "../../schemas";
|
||||||
import { Button, Divider, Form, Page, Panel } from "../ui"
|
import API from "../../util/API";
|
||||||
|
|
||||||
const AddRecipe = () => {
|
const AddRecipe = () => {
|
||||||
const authContext = useAuthContext();
|
const { user, token } = useAuthContext();
|
||||||
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '', ingredients: [] })
|
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '', ingredients: [] })
|
||||||
const [form, setForm] = useState<JSX.Element>();
|
const [toast, setToast] = useState(<></>)
|
||||||
|
|
||||||
const getFormState = useCallback((data: IRecipe) => {
|
const getFormState = useCallback((data: IRecipe) => {
|
||||||
setInput(data);
|
setInput(data);
|
||||||
}, [input])
|
}, [input])
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = async () => {
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
for (let field of Object.keys(input)) {
|
for (let field of Object.keys(input)) {
|
||||||
if (!input[field as keyof IRecipe]) return;
|
if (!input[field as keyof IRecipe]) {
|
||||||
|
console.log(field);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('good to go!')
|
console.log('good to go!')
|
||||||
|
|
||||||
|
const recipe = new API.Recipe(token);
|
||||||
|
const result = await recipe.post(input);
|
||||||
|
|
||||||
|
const recipeID = result.recipe.id;
|
||||||
|
const recipeName = result.recipe.name;
|
||||||
|
|
||||||
|
setToast(
|
||||||
|
<Card>
|
||||||
|
<p>Created recipe {recipeName} successfully!</p>
|
||||||
|
<p>View your new recipe <a href={`/recipe/${recipeID}`}>here!</a></p>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authContext.user && setInput((prev: IRecipe) => {
|
if (!user) return;
|
||||||
|
user && setInput((prev: IRecipe) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
authoruserid: authContext.user!.id!
|
authoruserid: user.id!
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [authContext])
|
}, [user])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(input);
|
console.log(input);
|
||||||
@@ -38,7 +58,7 @@ const AddRecipe = () => {
|
|||||||
<h1>Add a New Recipe</h1>
|
<h1>Add a New Recipe</h1>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Panel>
|
<Panel extraStyles="width-80">
|
||||||
<Form parent={input} _config={{
|
<Form parent={input} _config={{
|
||||||
parent: "AddRecipe",
|
parent: "AddRecipe",
|
||||||
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
|
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
|
||||||
@@ -49,9 +69,9 @@ const AddRecipe = () => {
|
|||||||
richTextInitialValue: "<p>Enter recipe details here!</p>"
|
richTextInitialValue: "<p>Enter recipe details here!</p>"
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
{ form || <h2>Loading...</h2> }
|
|
||||||
|
|
||||||
<Button onClick={handleCreate}>Create Recipe!</Button>
|
<Button onClick={handleCreate}>Create Recipe!</Button>
|
||||||
|
|
||||||
|
<div id="toast">{ toast }</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
{ 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,9 +1,53 @@
|
|||||||
import { Page } from "../ui";
|
import { useEffect, useState } from "react";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
|
import { ICollection, IRecipe } from "../../schemas";
|
||||||
|
import API from "../../util/API";
|
||||||
|
import { Card, Page } from "../ui";
|
||||||
|
|
||||||
|
interface CollectionDetails {
|
||||||
|
idx: number
|
||||||
|
collection: ICollection
|
||||||
|
recipes: IRecipe[]
|
||||||
|
}
|
||||||
|
|
||||||
const CollectionBrowser = () => {
|
const CollectionBrowser = () => {
|
||||||
|
const [list, setList] = useState<ICollection[]>();
|
||||||
|
const { token } = useAuthContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) return;
|
||||||
|
(async() => {
|
||||||
|
const collections = new API.Collection(token);
|
||||||
|
const recipes = new API.Recipe(token);
|
||||||
|
|
||||||
|
const allRecipes = await collections.getAllAuthored();
|
||||||
|
if (allRecipes) {
|
||||||
|
const result = new Array<CollectionDetails[]>();
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (let each of allRecipes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
setList(allRecipes);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [token])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<h1>Browsing your {2} collections:</h1>
|
<h1>Browsing your {2} collections:</h1>
|
||||||
|
|
||||||
|
{
|
||||||
|
list && list.map(each => {
|
||||||
|
return (
|
||||||
|
<Card key={v4()}>
|
||||||
|
<h2>{each.name}</h2>
|
||||||
|
<a href={`/collections/${each.id}`}>Link to details</a>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ChangeEvent, FC, useEffect, useState } from "react"
|
import { ChangeEvent, FC, useEffect, useState } from "react"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import RichText from "./RichText"
|
import RichText from "./RichText"
|
||||||
|
import "/src/sass/components/Form.scss";
|
||||||
|
|
||||||
export interface FormConfig<T> {
|
export interface FormConfig<T> {
|
||||||
parent: string
|
parent: string
|
||||||
@@ -72,14 +73,14 @@ const Form: FC<FormProps> = ({ parent, _config }) => {
|
|||||||
|
|
||||||
if (config.dataTypes![i] == 'TINYMCE') {
|
if (config.dataTypes![i] == 'TINYMCE') {
|
||||||
return (
|
return (
|
||||||
<div id={`${config.parent}-row-${i}`} key={v4()}>
|
<div className="form-row-editor" id={`${config.parent}-row-${i}`} key={v4()}>
|
||||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||||
<RichText id={`${config.parent}-${each}`} initialValue={config.richTextInitialValue} getState={(txt) => updateRichText(txt, i)} />
|
<RichText id={`${config.parent}-${each}`} initialValue={config.richTextInitialValue} getState={(txt) => updateRichText(txt, i)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div id={`${config.parent}-row-${i}`} key={v4()}>
|
<div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}>
|
||||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||||
<input
|
<input
|
||||||
type={config.dataTypes![i]}
|
type={config.dataTypes![i]}
|
||||||
@@ -98,7 +99,7 @@ const Form: FC<FormProps> = ({ parent, _config }) => {
|
|||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`ui-form-component ${_config.extraStyles}`}>
|
<div className={`ui-form-component ${_config.extraStyles ?? ""}`}>
|
||||||
{ contents }
|
{ contents }
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
0
client/src/components/ui/Grid.tsx
Normal file
0
client/src/components/ui/Grid.tsx
Normal file
32
client/src/sass/components/Form.scss
Normal file
32
client/src/sass/components/Form.scss
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
.ui-form-component {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: center;
|
||||||
|
width: 75%;
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
text-align: left;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row-editor {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,4 +24,8 @@
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.width-80 {
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ module API {
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +183,11 @@ module API {
|
|||||||
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) {
|
async getAllAuthored(id?: number | string) {
|
||||||
let response: AxiosResponse;
|
let response: AxiosResponse;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -18,6 +18,23 @@ 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) {
|
async getAllAuthored(id: number | string) {
|
||||||
console.log(id, typeof id);
|
console.log(id, typeof id);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ const router = Router();
|
|||||||
export const collectionRoute = (app: Express) => {
|
export const collectionRoute = (app: Express) => {
|
||||||
app.use('/app/collection', router);
|
app.use('/app/collection', router);
|
||||||
|
|
||||||
router.get('/:id', async (req, res, next) => {
|
router.get('/:id', restrictAccess, 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);
|
||||||
}
|
}
|
||||||
@@ -45,7 +52,7 @@ export const collectionRoute = (app: Express) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/', async (req, res, next) => {
|
router.post('/', restrictAccess, async (req, res, next) => {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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,7 +55,7 @@ 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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user