summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHombreLaser <sebastian-440@live.com>2023-05-26 18:57:36 -0600
committerHombreLaser <sebastian-440@live.com>2023-05-26 18:57:36 -0600
commite6fa7db87b5f879de71bfc9e4f8afea9d722ab95 (patch)
tree599188426b9ab131e09e6d2a5d79b486562a6899
parenta0318f76f40deaeba367badf18af6141325d29e9 (diff)
Añade creación de reseñas
-rw-r--r--package.json1
-rw-r--r--src/clients/actions.ts64
-rw-r--r--src/components/forms/address_form.tsx2
-rw-r--r--src/components/forms/review_form.tsx48
-rw-r--r--src/components/review.tsx6
-rw-r--r--src/components/user_account_dropdown_menu.tsx2
-rw-r--r--src/lib/session.ts7
-rw-r--r--src/lib/token.ts9
-rw-r--r--src/main.tsx4
-rw-r--r--src/routes/products/product.tsx27
10 files changed, 137 insertions, 33 deletions
diff --git a/package.json b/package.json
index 0d301bd..451f8b1 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.10.0",
"react-select-country-list": "^2.2.3",
+ "react-star-picker": "^2.0.13",
"tw-elements": "^1.0.0-beta2"
},
"devDependencies": {
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>