diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/clients/actions.ts | 64 | ||||
-rw-r--r-- | src/components/forms/address_form.tsx | 2 | ||||
-rw-r--r-- | src/components/forms/review_form.tsx | 48 | ||||
-rw-r--r-- | src/components/review.tsx | 6 | ||||
-rw-r--r-- | src/components/user_account_dropdown_menu.tsx | 2 | ||||
-rw-r--r-- | src/lib/session.ts | 7 | ||||
-rw-r--r-- | src/lib/token.ts | 9 | ||||
-rw-r--r-- | src/main.tsx | 4 | ||||
-rw-r--r-- | src/routes/products/product.tsx | 27 |
9 files changed, 136 insertions, 33 deletions
diff --git a/src/clients/actions.ts b/src/clients/actions.ts index 447d054..852e473 100644 --- a/src/clients/actions.ts +++ b/src/clients/actions.ts @@ -79,27 +79,6 @@ export async function create({ request }) { } } -export async function addToCart({ params, request }) { - const client = new ApiClient(); - const product = await client.get(`/products/${ params.productId }`); - const post_request_path = "/account/cart"; - const id = product.data.data.id; - const form = await request.formData(); - - form.append('product_id', id); - - if(form.get('quantity') == '') - form.set('quantity', 1); - - try { - await client.post(post_request_path, form, client.authorizationHeaders()); - } catch(error) { - return error.response.status; - } - - return 200; -} - export async function deleteFromCart({ request }) { const client = new ApiClient(); const form = await request.formData(); @@ -124,4 +103,47 @@ export async function placeOrder({ request }) { } catch(error) { return redirect('/account/cart') } +} + +export async function productAction({ params, request }) { + const client = new ApiClient(); + const product = await client.get(`/products/${ params.productId }`); + + if(!product) + redirect(`/products`); + + const product_id = product.data.data.id; + const form = await request.formData(); + + if(form.get('intent') == 'create_review') { + return await createReview(params.productId, form, client); + } + + return await addToCart(product_id, form, client); +} + +async function addToCart(product_id: string, form: FormData, client: ApiClient) { + const post_request_path = "/account/cart"; + form.append('product_id', product_id); + + if(form.get('quantity') == '') + form.set('quantity', 1); + + try { + await client.post(post_request_path, form, client.authorizationHeaders()); + } catch(error) { + return error.response.status; + } + + return 200; +} + +async function createReview(product_id: string, form: FormData, client: ApiClient) { + try { + await client.post(`/products/${product_id}/reviews`, form, client.authorizationHeaders()); + + return null; + } catch(error) { + return error.response; + } }
\ No newline at end of file diff --git a/src/components/forms/address_form.tsx b/src/components/forms/address_form.tsx index 897b09b..2d3b8f4 100644 --- a/src/components/forms/address_form.tsx +++ b/src/components/forms/address_form.tsx @@ -54,7 +54,7 @@ export default function AddressForm({ address = null, errors = null }) { id: "country-field", type: "select", name: "country", - label: "Ciudad" + label: "País" } if(errors) diff --git a/src/components/forms/review_form.tsx b/src/components/forms/review_form.tsx new file mode 100644 index 0000000..8ed3848 --- /dev/null +++ b/src/components/forms/review_form.tsx @@ -0,0 +1,48 @@ +import { Form } from "react-router-dom"; +import StarPicker from 'react-star-picker'; +import "../stylesheets/shared.css"; +import { useState } from "react"; + +function getTextArea(errors) { + if(errors && errors.review) { + const error_messages = errors.review.map(message => + <p className="mt-2 text-sm text-red-600 dark:text-red-500">{message}</p> + ); + + return ( + <div className="my-2"> + <textarea id="review" name="review" rows={4} className="block p-2.5 w-full text-sm bg-red-50 border border-red-500 text-red-900 placeholder-red-700 rounded-lg border focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Deje una reseña..."></textarea> + {error_messages} + </div> + ); + } + + return ( + <div className="my-2"> + <textarea id="review" name="review" rows={4} className="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Deje una reseña..."></textarea> + </div> + ); +} + +export default function ReviewForm({ product, errors }) { + const [rating, setRating] = useState(0); + const review_field = getTextArea(errors); + + const onChange = (value, name) => { + setRating(value); + }; + + return ( + <Form method="post" action={`/products/${product.public_id}`} className="flex flex-col justify-end"> + <input type="hidden" id="rating-field" name="rating" value={rating}/> + <StarPicker onChange={onChange} value={rating} name="rating" /> + {review_field} + <div className="my-2"> + <button type="submit" className="text-white button hover:bg-blue-800 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center" + name="intent" value="create_review"> + Enviar + </button> + </div> + </Form> + ); +}
\ No newline at end of file diff --git a/src/components/review.tsx b/src/components/review.tsx index 153c202..d97e410 100644 --- a/src/components/review.tsx +++ b/src/components/review.tsx @@ -7,7 +7,7 @@ export default function Review({ review }) { return( <> - <div className="grid grid-cols-10 w-3/5 my-4"> + <div className="grid grid-cols-10 w-4/6 my-4"> <div className="flex flex-col col-span- justify-center mx-2"> <div className="text-black"> {review.attributes.author_name} @@ -20,9 +20,9 @@ export default function Review({ review }) { </div> </div> <div className="flex col-span-8 justify-start"> - <div className="border-solid border-2 border-gray-200"> + <textarea id="message" rows={4} className="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" disabled> {review.attributes.review} - </div> + </textarea> </div> </div> </> diff --git a/src/components/user_account_dropdown_menu.tsx b/src/components/user_account_dropdown_menu.tsx index 2bd0d28..e4ab023 100644 --- a/src/components/user_account_dropdown_menu.tsx +++ b/src/components/user_account_dropdown_menu.tsx @@ -1,5 +1,5 @@ import { PersonFill, DoorOpenFill, CardList } from "react-bootstrap-icons"; -import { BrowserRouter, Form } from "react-router-dom"; +import { BrowserRouter } from "react-router-dom"; import RedirectTo from '../lib/redirect_to'; import Token from "../lib/token"; import redirectTo from "../lib/redirect_to"; diff --git a/src/lib/session.ts b/src/lib/session.ts new file mode 100644 index 0000000..5d4967f --- /dev/null +++ b/src/lib/session.ts @@ -0,0 +1,7 @@ +import Token from "./token"; + +export function isUserAuthenticated() { + const session = new Token(); + + return session.get() != null && !session.expired(); +}
\ No newline at end of file diff --git a/src/lib/token.ts b/src/lib/token.ts index 93ec0cf..f27df28 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -59,6 +59,15 @@ export default class Token { return this.decode()?.exp; } + expired() { + const exp = this.decode()?.exp; + + if(!exp) + return true; + + return exp < Math.floor(Date.now() / 1000); + } + private decode(): { data: string; aud: string; jti: string; exp: number; } | null { const token = this.get(); diff --git a/src/main.tsx b/src/main.tsx index 8f9c586..ab98f13 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,7 +9,7 @@ import Cart from "./routes/account/cart"; import Orders from "./routes/account/orders"; import { EditAccount } from './routes/account/edit'; import { Edit as EditAddress } from "./routes/account/addresses/edit"; -import { create, editAccount, authenticatedEdit, addToCart, deleteFromCart, placeOrder } from './clients/actions'; +import { create, editAccount, authenticatedEdit, deleteFromCart, placeOrder, productAction } from './clients/actions'; import Layout from "./components/layout"; import { accountLoader, loader, productLoader, addressLoader, cardLoader, cartLoader, ordersLoader } from "./clients/loaders"; import './index.css'; @@ -26,7 +26,7 @@ const routes = [ { path: "products/:productId", loader: productLoader, - action: addToCart, + action: productAction, element: <Product/>, }, { diff --git a/src/routes/products/product.tsx b/src/routes/products/product.tsx index cca4073..fb883a8 100644 --- a/src/routes/products/product.tsx +++ b/src/routes/products/product.tsx @@ -2,17 +2,20 @@ import { useLoaderData, Form, useActionData } from "react-router-dom"; import { CartPlusFill } from "react-bootstrap-icons" import { InfoModal } from "../../components/info_modal"; import { Modal } from "flowbite"; +import { isUserAuthenticated } from "../../lib/session"; import ProductListing from "../../components/product_listing"; import MainContentLayout from "../../components/main_content_layout"; import Review from "../../components/review"; +import ReviewForm from "../../components/forms/review_form"; import "../../components/stylesheets/shared.css" import { useEffect } from "react"; export default function Product() { let quantity_field; + let review_form_error_messages; const response = useLoaderData(); - const response_status = useActionData(); + const action_response = useActionData(); const product = response[0].data; const reviews = response[1].data.data.map(review => <li key={review.id}> @@ -33,12 +36,22 @@ export default function Product() { modal.hide(); }); - if(response_status == 200) + if(action_response == 200) modal.show(); - }, [response_status]); + }, [action_response]); + let review_form; - if(response_status == 422) { + if(isUserAuthenticated()) { + if(action_response?.status == 422) + review_form_error_messages = action_response.data.errors; + review_form = <ReviewForm product={product.data.attributes} errors={review_form_error_messages}/>; + } + else { + review_form = null; + } + + if(action_response == 422) { quantity_field = ( <div className="mx-2 w-17"> <input type="number" id="quantity-field" name="quantity" className="bg-red-50 border border-red-500 text-red-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" @@ -62,13 +75,17 @@ export default function Product() { <div className="my-2 flex flex-flow-reverse w-4/6"> <Form method="post" id="cart-button" className="flex h-10"> {quantity_field} - <button className="flex inline-block rounded button w-50 px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white dark:hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)]"> + <button className="flex inline-block rounded button w-50 px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white dark:hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)]" + name="intent" value="add_to_cart"> <CartPlusFill size={16}/> <span className="mx-2">Añadir al carrito</span> </button> </Form> </div> <div className="my-4"> + <div className="my-2 w-3/5"> + {review_form} + </div> <ul> {reviews} </ul> |