Getting null when I use context.query with getServerSideProps() in Nextjs? - javascript
Intro
Basically, I am building an e-commerce website. But I got stuck in this problem while saving the order details after I click payment button (although I had not used any payment gateway).
Problem
After selecting the items in cart , I fill the details in Checkout page and go to pay option to confirm order. In this checkout page I call addorder api which first do some checks(like whether item got out of stock or not) on the product in cart and then create new order.
After creating a new order in order DB (MOngo DB I am using), I goto order page like this with my orderId
res.redirect('/order/' + orderToAdd.orderId, 200);
After clicking the pay button shown in the image above, it redirects to http://localhost:3000/order/814407650978 giving 404 (Not Found).
Thus when I fetch the order details from the MyOrder.js using context.query.orderId, from getServerSideProps() , I am getting null.
export async function getServerSideProps(context) {
if (!mongoose.connections[0].readyState) {
await mongoose.connect(process.env.MONGO_URI);
}
let order = await Order.findById({orderId:context.query.orderId})
console.log("got = ", order);
return {
props: { order: JSON.parse(JSON.stringify(order)) }, // will be passed to the page component as props
};
}
You can see full code below
Code
This is a complete order DB entry, where I am only sending the products object to MyOrder.js
complete order = {
email: 'mohit6#test.com',
orderId: '869972292107',
paymentInfo: 'No payment info',
products: {
'wear-the-chess-king-M-Red': {
qty: 2,
price: 599,
name: 'King Chess Stylish Men’s t-Shirt',
size: 'M',
variant: 'Red'
}
},
address: 'A-22\nVinayak campus',
subtotal: 1198,
status: 'Paid',
_id: new ObjectId("626bd1bd01597b226fc76531"),
createdAt: 2022-04-29T11:53:33.046Z,
updatedAt: 2022-04-29T11:53:33.046Z,
__v: 0
}
Checkout.js
import React, { useState } from "react";
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import { MdOutlinePayment } from "react-icons/md";
import { useRouter } from "next/router";
import "react-toastify/dist/ReactToastify.css";
import { toast, ToastContainer } from "react-toastify";
// Added payment AiOutlineGateway, but due to no merchant key, it is creating invalid checksum => hence push to different branch in both local & remote
const Checkout = ({ cart, clearCart, subtotal, addToCart, removeFromCart }) => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
const [pincode, setPincode] = useState("");
const [city, setCity] = useState("");
const [statemap, setStatemap] = useState("");
const [disable, setDisable] = useState(true);
const router = useRouter();
const handleChange = async (e) => {
if (e.target.name === "name") {
setName(e.target.value);
} else if (e.target.name === "email") {
setEmail(e.target.value);
} else if (e.target.name === "phone") {
setPhone(e.target.value);
} else if (e.target.name === "address") {
setAddress(e.target.value);
} else if (e.target.name === "pincode") {
setPincode(e.target.value);
if (e.target.value.length == 6) {
let pins = await fetch(`${process.env.NEXT_PUBLIC_HOST}/api/pincode`);
let pinjson = await pins.json();
//console.log(pinjson)
if (Object.keys(pinjson).includes(e.target.value)) {
setCity(pinjson[e.target.value][0]);
setStatemap(pinjson[e.target.value][1]);
} else {
setCity("");
setStatemap("");
}
} else {
setCity("");
setStatemap("");
}
}
if (name && email && phone && address && pincode) setDisable(false);
else setDisable(true);
};
const initiateOrder = async () => {
let oid = Math.floor(Math.random() * Date.now());
const data = {
cart: cart,
subtotal: subtotal,
oid: oid,
email: email,
name: name,
address: address,
pincode: pincode,
phone: phone,
};
//console.log(JSON.stringify(data));
let res = await fetch(`${process.env.NEXT_PUBLIC_HOST}/api/addorder`, {
method: "POST", // or 'PUT'
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
let response = await res.json();
//console.log("response from order- ", response);
if (response.success === true) {
localStorage.setItem("token", response.authToken);
toast.success("Order Added Successfully!", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
setTimeout(() => {
router.push("/order");
}, 2500);
} else {
if(response.error === "err1"){
toast.error("Total price of your cart have changed accidently", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
else if(response.error === "err2"){
toast.error("Some items in your cart went out of stock !", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
else if(response.error === "err5"){
toast.error("Prices of some of the items in your cart have changed !", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
else {
toast.error("Error in Adding Order !", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
}
};
return (
<>
<div className="container px-2 sm:m-auto">
<ToastContainer
position="bottom-center"
autoClose={2000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
<h1 className="text-xl md:text-3xl text-center my-8 font-semibold">
Checkout
</h1>
{/* part 1 */}
<h2 className="text-xl my-4 font-semibold">1. Delivery Details</h2>
<div className="mx-auto flex">
<div className="px-2 w-1/2">
<div className="mb-4">
<label htmlFor="name" className="leading-7 text-sm text-gray-600">
Name
</label>
<input
type="name"
id="name"
name="name"
value={name}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<div className="px-2 w-1/2">
<div className=" mb-4">
<label
htmlFor="email"
className="leading-7 text-sm text-gray-600"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
</div>
<div className="px-2 w-full">
<div className=" mb-4">
<label
htmlFor="address"
className="leading-7 text-sm text-gray-600"
>
Address
</label>
<textarea
id="address"
name="address"
value={address}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<div className="mx-auto flex">
<div className="px-2 w-1/2">
<div className="mb-4">
<label
htmlFor="phone"
className="leading-7 text-sm text-gray-600"
>
Phone
</label>
<input
type="text"
id="phone"
name="phone"
value={phone}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
{/* city */}
<div className="px-2 w-1/2">
<div className=" mb-4">
<label
htmlFor="pincode"
className="leading-7 text-sm text-gray-600"
>
Pincode
</label>
<input
type="text"
id="pincode"
name="pincode"
value={pincode}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
</div>
<div className="mx-auto flex">
<div className="px-2 w-1/2">
<div className="mb-4">
<label
htmlFor="state"
className="leading-7 text-sm text-gray-600"
>
State
</label>
<input
type="text"
id="state"
name="state"
value={statemap}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
onChange={handleChange}
/>
</div>
</div>
<div className="px-2 w-1/2">
<div className=" mb-4">
<label htmlFor="city" className="leading-7 text-sm text-gray-600">
City
</label>
<input
type="text"
id="city"
name="city"
value={city}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
onChange={handleChange}
/>
</div>
</div>
</div>
{/* part 2 */}
<h2 className="text-xl my-4 font-semibold">2. Review Cart Items</h2>
<div className="z-10 sideCart p-6 mx-2 my-4 bg-blue-100 border-[1px] border-blue-800 rounded">
<ol className="list-decimal font-semibold">
{Object.keys(cart).length === 0 && (
<div className="mt-5 text-center text-xl font-extralight">
Your Cart is Empty :(
</div>
)}
{
//k is cart {k: {name, price,qty,size,variant} }
Object.keys(cart).map((k) => {
return (
<li key={k}>
<div className="flex item my-5 items-center justify-between">
<div className=" font-semibold">
{cart[k].name} [{cart[k].variant} - {cart[k].size}]
</div>
<div className="w-1/3 font-semibold flex items-center justify-center">
<AiFillPlusCircle
onClick={() =>
addToCart(
k,
1,
cart[k].price,
cart[k].name,
cart[k].size,
cart[k].variant
)
}
className="text-blue-700 text-xl cursor-pointer"
/>
<span className="mx-2 text-sm">{cart[k].qty}</span>
<AiFillMinusCircle
onClick={() =>
removeFromCart(
k,
1,
cart[k].price,
cart[k].name,
cart[k].size,
cart[k].variant
)
}
className="text-blue-700 text-xl cursor-pointer"
/>
</div>
</div>
</li>
);
})
}
</ol>
<span className="subtotal text-xl font-extrabold">
Subtotal : ₹ {subtotal} /-
</span>
</div>
<button
disabled={disable}
onClick={initiateOrder}
className="disabled:bg-blue-300 flex text-white bg-blue-500 border-0 py-2 px-3 focus:outline-none hover:bg-blue-600 rounded text-base mx-2 my-4"
>
<MdOutlinePayment className="m-1" />
Pay ₹ {subtotal}
</button>
</div>
</>
);
};
export default Checkout;
/api/addorder.js
import connectDB from "../../middleware/mongoose";
import Order from "../../models/Order";
import Product from "../../models/Product";
const handler = async (req, res) => {
if (req.method === "POST") {
let product,
sumtotal = 0;
for (let item in req.body.cart) {
//got key(slug) as item
console.log(item);
sumtotal += req.body.cart[item].qty * req.body.cart[item].price;
product = await Product.findOne({ slug: item });
//TODO1: check if cart is tampered or not
if (product.price !== req.body.cart[item].price) {
res.status(500).json({ success: false, error: "err1" });
//console.log("ResT - ",res.json)
return;
}
//TODO2: check if cart items are out of stock
if (product.availableQty < req.body.cart[item].qty) {
res.status(500).json({ success: false, error: "err2" });
return;
}
}
//console.log("sumtotal = ",sumtotal," and subtotal = ",req.body.subtotal)
if (sumtotal != req.body.subtotal) {
res.status(500).json({ success: false, error: "err5" });
//console.log("ResP - ",res)
return;
}
//TODO3: check if details are valid or not
let orderToAdd = new Order({
email: req.body.email,
orderId: req.body.oid,
address: req.body.address,
subtotal: req.body.subtotal,
products: req.body.cart,
status: "Paid",
});
await orderToAdd.save();
let ordered_products = orderToAdd.products;
console.log("Orederd pro = ", ordered_products);
console.log("complete order = ", orderToAdd)
for (let slug in ordered_products) {
await Product.findOneAndUpdate(
{ slug: slug },
{ $inc: { availableQty: -ordered_products[slug].qty } }
);
}
res.redirect('/order/' + orderToAdd.orderId, 200)
} else {
// Handle any other HTTP method
res.status(400).json({ success: false });
}
};
export default connectDB(handler);
MyOrder.js
import React from "react";
import mongoose from "mongoose";
import Order from "../models/Order";
//TODO: To change the orderdetail fecthing resource from cart,subtotal to orders DB (user specific orders)
const MyOrder = ({ order }) => {
console.log("order = ",order)
const products = order.products;
console.log(order,products);
return (
<div>
<section className="text-gray-600 body-font overflow-hidden">
<div className="container px-5 py-24 mx-auto">
<div className="lg:w-4/5 mx-auto flex flex-wrap">
<div className="lg:w-1/2 w-full lg:pr-10 lg:py-6 mb-6 lg:mb-0">
<h2 className="text-sm title-font text-gray-500 tracking-widest">
CHESSWEAR.COM
</h2>
<h1 className="text-gray-900 text-3xl title-font font-medium mb-4">
Order Id: 2242434535
</h1>
<p className="leading-relaxed mb-4 text-green-700 rounded-3xl bg-green-100 hover:bg-green-100/70 p-3 border-[1px] border-green-700 inline-flex items-center justify-center ">
Your Order has been successfully placed !
</p>
<div className="flex mb-4 justify-evenly">
<a className="flex-grow w-12 py-2 text-lg px-1">Description</a>
<a className="flex-grow py-2 text-lg px-1">Quantity</a>
<a className="flex-grow py-2 text-lg px-1">Price</a>
</div>
{Object.keys(cart).map((key) => {
return (
<div key={key} className="flex border-t border-gray-200 py-2">
<span className="text-gray-500 w-28 ">
{cart[key].name} ({cart[key].size} - {cart[key].variant})
</span>
<span className="m-auto text-gray-900">
{cart[key].qty}
</span>
<span className="mr-auto mt-auto mb-auto text-gray-900">
₹ {cart[key].price}
</span>
</div>
);
})}
<div className="flex py-2 md:py-4">
<span className="title-font font-medium text-2xl text-gray-900">
Subtotal : <span className="ml-4 text-red-900 font-bold text-3xl leading-tight">₹ {subtotal} /-</span>
</span>
<button className="flex ml-auto text-white bg-blue-500 border-0 py-2 px-6 focus:outline-none hover:bg-blue-600 rounded">
Track Order
</button>
</div>
</div>
<img
alt="ecommerce"
className="lg:w-1/2 w-full lg:h-auto h-64 object-cover object-center rounded"
src="https://dummyimage.com/400x400"
/>
</div>
</div>
</section>
</div>
);
};
export async function getServerSideProps(context) {
if (!mongoose.connections[0].readyState) {
await mongoose.connect(process.env.MONGO_URI);
}
let order = await Order.findById({orderId:context.query.orderId})
console.log("got = ", order);
return {
props: { order: JSON.parse(JSON.stringify(order)) }, // will be passed to the page component as props
};
}
export default MyOrder;
File Structure
Please help me???
Related
React Hook Form not validating nested input field until I call onChange
I have a form which has 2 fields. Heading Dynamic List which contains Name & Image I'm able to validate changes whenever Heading or Name inside dynamic list is changed. But when I change Image, the validation is failing. It returns true instead of showing error message. I tried adding a text input to test the image. Whenever I call onChange it is working fine. Here is my code: import { useForm } from "react-hook-form"; import { zodResolver } from "#hookform/resolvers/zod"; import * as z from "zod"; const schema = z.object({ heading: z .string() .min(3, { message: "Min 3 characters" }) .max(40, { message: "Max 40 characters" }), testimonials: z.array( z.object({ name: z.string().min(3, { message: "Min 3 characters" }), image: z.string().url({ message: "Invalid URL" }), }) ), }); const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(schema), }); const [dataObj, setDataObj] = useState({ "heading": "", "testimonials": [] }); const fetchData = () => { // fetch list from server // bind data setDataObj(dataFromServer); }; fetchData(); const listChanged = (type, value, index) => { let list = dataObj.data.testimonials; if (type == "name") { list[index].name = value; } else if (type == "image") { list[index].image = value; } setDataObj({ ...dataObj, testimonials: list, }); }; const headingChanged = (value) => { setDataObj({ ...dataObj, heading: value, }); }; <form className="mt-10 space-y-6" onSubmit={handleSubmit(onSubmit)}> <div className={`relative appearance-none block w-full px-3 py-2 border rounded-md shadow-sm placeholder-gray-400 sm:text-sm ${ errors.heading ? "border-red-400 focus:outline-none focus:ring-red-500 focus:border-red-500" : "border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" }`} > <label htmlFor="name" className="absolute -top-2 left-2 -mt-px inline-block bg-white px-1 text-xs font-medium text-gray-400" > Section Heading </label> <div className="mt-1"> <input type="text" autoComplete="name" {...register("heading", { required: true, })} className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm" value={dataObj.data.title} onChange={(e) => headingChanged( e.target.value)} /> </div> {errors.heading && ( <small className="mt-2 text-xs text-red-500"> {errors.heading.message} </small> )} </div> <div className="mt-2 w-full py-2"> <div> <div className="mt-2 flex justify-center mx-auto text-left"> <ul role="list" className="w-full"> {dataObj.data.testimonials.map((item, index) => ( <li key={index} className="py-3"> <div className="flex items-center space-x-4 justify-start w-full"> <div className="isolate -space-y-px rounded-md shadow-sm w-full"> <div className={`appearance-none relative w-full px-3 py-2 border rounded-md rounded-b-none shadow-sm sm:text-sm ${ errors.testimonials?.[index]?.name ? "border-red-400 focus:outline-none focus:ring-red-500 focus:border-red-500" : "border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" }`} > <label htmlFor="name" className="absolute -top-2 left-2 -mt-px inline-block bg-white px-1 text-xs font-medium text-gray-400" > Name </label> <input {...register(`testimonials.${index}.name`)} type="text" onChange={(e) => { listChanged(index, e.target.value, "name"); }} value={item.name} placeholder="New Label" className="block w-full border-0 p-1.5 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm" /> <span className="invalid-feedback text-red-500 text-xs"> {errors.testimonials?.[index]?.name?.message} </span> </div> <div className="relative rounded-md rounded-t-none border border-gray-300 px-3 py-2 focus-within:ring-0 focus-within:ring-indigo-600"> <label htmlFor="name" className="absolute -top-2 left-2 -mt-px inline-block bg-white px-1 text-xs font-medium text-gray-400" > Image </label> <input {...register(`testimonials.${index}.image`)} type="text" value={item.image} /> <UploadImage size={`small-round`} index={index} imageUrl={item.image} imageChanged={listChanged} /> <span className="invalid-feedback text-red-500 text-xs"> {errors.testimonials?.[index]?.image?.message} </span> </div> </div> </div> </li> ))} </ul> </div> </div> </div> </form>; How do I solve this?
I have created following minimum example on how to use image with useFieldArray to create dynamic form. This has been created in codesandbox and tested for validation only. Let me know if you stuck anywhere or want more information. SandBoxLink : import React from "react"; import { useForm, useFieldArray } from "react-hook-form"; import { zodResolver } from "#hookform/resolvers/zod"; import * as z from "zod"; const MAX_FILE_SIZE = 200000; const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png"]; const schema = z.object({ heading: z .string() .min(3, { message: "Min 3 characters" }) .max(40, { message: "Max 40 characters" }), testimonials: z.array( z.object({ name: z.string().min(3, { message: "Min 3 characters" }), testimonial: z.string().min(3, { message: "Min 3 characters" }), image: z .any() .refine( (files) => files?.[0]?.size <= MAX_FILE_SIZE, `Max image size is 2MB.` ) .refine( (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), "Only .jpg, .png formats are supported." ) }) ) }); const App = () => { const appendValues = { Name: "", testimonial: "", image: [] }; const defaultValues = { heading: "", testimonials: [appendValues] }; const { register, handleSubmit, control, formState: { errors } } = useForm({ defaultValues, resolver: zodResolver(schema) }); const { fields, append, remove } = useFieldArray({ control, name: "testimonials" }); const onSubmit = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> <label htmlFor="heading">Heading : </label> <input {...register("heading")} id="heading" /> <p>{errors?.heading?.message}</p> {fields.map((field, index) => ( <div key={field.id} style={{ border: "1px solid black", padding: "10px" }} > <label htmlFor={`testimonials.${index}.name`}>Name : </label> <input id={`test.${index}.name`} {...register(`testimonials.${index}.name`)} /> <p>{errors.testimonials?.[index]?.name?.message}</p> <br /> <label htmlFor={`testimonials.${index}.testimonial`}> Testimonial : </label> <input id={`test.${index}.testimonial`} {...register(`testimonials.${index}.testimonial`)} /> <p>{errors.testimonials?.[index]?.testimonial?.message}</p> <br /> <label htmlFor={`testimonials.${index}.image`}>Image :</label> <input id={`test.${index}.image`} type="file" {...register(`testimonials.${index}.image`)} /> <p>{errors.testimonials?.[index]?.image?.message}</p> <br /> <button type="button" onClick={() => remove(index)}> Remove </button> </div> ))} <button type="button" onClick={() => append(appendValues)}> append </button> <input type="submit" /> </form> ); }; export default App;
Passing Function between components Vue
I'm currently trying to pass the getUser function from a file called Login.vue which gets the login details to an Authenticated.vue page to display the user and their email. How can I go about doing this. I've tried following the official vue documents but can't get it to work. Not sure whats going wrong. Login.Vue <template> <form #submit.prevent="submitLogin"> <div> <!-- Email --> <div> <label for="email" class="block font-medium text-sm text-gray-700"> Email </label> <input v-model="loginForm.email" id="email" type="email" class="block mt-1 w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" required autofocus autocomplete="username"> <!-- Validation Errors --> <div class="text-red-600 mt-1"> <div v-for="message in validationErrors?.email"> {{ message }} </div> </div> </div> <!-- Password --> <div class="mt-4"> <label for="password" class="block font-medium text-sm text-gray-700"> Password </label> <input v-model="loginForm.password" id="password" type="password" class="block mt-1 w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" required autocomplete="current-password"> <!-- Validation Errors --> <div class="text-red-600 mt-1"> <div v-for="message in validationErrors?.password"> {{ message }} </div> </div> </div> <!-- Remember me --> <div class="block mt-4"> <label class="flex items-center"> <input type="checkbox" name="remember" v-model="loginForm.remember" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" /> <span class="ml-2 text-sm text-gray-600">Remember me</span> </label> </div> <!-- Buttons --> <div class="flex items-center justify-end mt-4"> <button class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:shadow-outline-gray transition ease-in-out duration-150 ml-4" :class="{ 'opacity-25': processing }" :disabled="processing"> Log in </button> </div> </div> </form> </template> <script> import { useRouter} from 'vue-router'; import { reactive } from '#vue/runtime-core' export default { data() { return { categories: {}, router: useRouter(), loginForm: reactive({ email: '', password: '', remember: false }), user: reactive({ name: '', email: '', }), processing: false, validationErrors: {}, } }, mounted() { }, methods: { submitLogin() { if (this.processing) return this.processing = true; axios.post('/login', this.loginForm) .then(response => this.loginUser(response)) .catch(error => { if (error.response?.data) { this.validationErrors = error.response.data.errors } }) .finally(this.processing = false) }, loginUser(response){ this.user.name = response.data.name this.user.email = response.data.email localStorage.setItem('loggedIn', JSON.stringify(true)) this.router.push({name: 'posts.index'}) }, getUser(){ axios.get('/api/user') .then(response => { loginUser(response) }) this.user.name = response.data.name this.user.email = response.data.email console.log(this.user.name); console.log(this.user.email); }, }, } </script> Authenticated.vue <template> <div class="min-h-screen bg-gray-100"> <nav class="bg-white border-b border-gray-100"> <!-- Primary Navigation Menu --> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex justify-between h-16"> <div class="flex"> <!-- Logo --> <div class="shrink-0 flex items-center"> <a href="/"> <svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" class="block h-10 w-auto fill-current text-gray-600"> <path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/> </svg> </a> </div> <!-- Navigation Links --> <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> <router-link :to="{name:'posts.index'}" active-class="border-b-2 border-indigo-400" class="inline-flex items-center px-1 pt-1 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out"> Posts </router-link> <router-link :to="{name:'posts.create'}" active-class="border-b-2 border-indigo-400" class="inline-flex items-center px-1 pt-1 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out"> Create Post </router-link> </div> </div> <div class="flex items-center"> <div> <div>Hi, {{user.name}}</div> <div class="text-sm text-gray-500"> {{user.email}} </div> </div> </div> <button #click="logout" type="button" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:shadow-outline-gray transition ease-in-out duration-150 ml-4" :class="{ 'opacity-25': processing }" :disabled="processing"> Log out </button> </div> </div> </nav> <!-- Page Heading --> <header class="bg-white shadow"> <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> {{currentPageTitle}} </h2> </div> </header> <!-- Page Content --> <main> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 bg-white border-b border-gray-200"> <router-view></router-view> </div> </div> </div> </div> </main> </div> </template> <script> import { reactive } from '#vue/runtime-core' export default { props: { getUser: {type: Function} }, computed: { currentPageTitle(){ return this.$route.meta.title; } }, mounted() { this.getUser() }, methods: { } } </script> App.js require('./bootstrap'); import { createApp, onMounted } from 'vue' //import App from './layouts/App' import LaravelVuePagination from 'laravel-vue-pagination'; import VueSweetalert2 from 'vue-sweetalert2'; import 'sweetalert2/dist/sweetalert2.min.css'; import router from './routes/index' const app = createApp({ }) app.use(router) app.use(VueSweetalert2) app.component('Pagination', LaravelVuePagination) app.mount('#app')
you should vuex for this perpose. getUser(){ axios.get('/api/user') .then(response => { this.$store.commit('storeUser',response) this.router.push({name: 'posts.index'}) }) }, in store/index.js (create folder of store in root ) import { createApp } from 'vue' import { createStore } from 'vuex' // Create a new store instance. const store = createStore({ state () { return { userData:{} } }, getters: { userData(state){ return state.userData } }, mutations: { storeUser (state,payload) { state.userData= payload } } }) export default store in main.js import store from 'store/index.js' const app = createApp({ /* your root component */ }) // Install the store instance as a plugin app.use(store) in Authenticated.vue (or whereevet you want to use any value from store) computed:{ userData(){ return this.$store.getters.userData // here you can get your response object from api which you call during login } }
Toggle #headlessui/vue Popover component
I'm not entirely sure how to toggle the popover based on a boolean, in my case: if there are search results. I thought I could just use the open attribute. Any help is appreciated! To sum up this component: Type in a search term in the input An API call is being made and results are returned from te API The PopoverPanel should then be opened <template> <form :action="action" method="GET" class="flex-1"> <Popover :open="searchResults.length > 0" class="relative w-full"> <PopoverOverlay :class="`bg-black ${open ? 'fixed inset-0 opacity-10' : 'opacity-0'}`" #click="() => emptySearchResults()" /> <div class="relative"> <label for="search" class="sr-only">Search...</label> <input id="search" v-model="searchTerm" type="search" placeholder="Search..." class="h-10 w-full px-4 py-2 text-sm rounded-lg placeholder-gray-400 focus:outline-none" > </div> <PopoverPanel class="absolute top-full right-0 w-full mt-3 py-2 px-2 bg-gray-700 rounded-lg shadow-lg"> <a v-for="result in searchResults" :key="result.id" :href="result.url" class="flex items-center font-semibold p-2 text-sm text-white leading-none rounded-lg hover:bg-gray-600" > <span class="mr-3 py-1 px-2 text-xs text-gray-900 leading-none bg-gray-300 rounded-full"> {{ result.module }} </span> {{ result.title }} </a> </PopoverPanel> </Popover> </form> </template> <script setup> import { ref, watch } from 'vue'; import { Popover, PopoverPanel } from '#headlessui/vue'; import debounce from '#/Helpers/debounce'; import xhr from '#/Helpers/xhr'; const searchTerm = ref(''); const searchResults = ref([]); const getSearchResults = debounce(async (value) => { if (value === '') return; const { data } = await xhr.post('search', { q: value }); const { results } = await data; searchResults.value = results; }, 250); const emptySearchResults = () => { searchTerm.value = ''; searchResults.value = []; }; watch(searchTerm, (value) => getSearchResults(value)); </script>
Use `ListBox` from `#headlessui/react` with Mobx?
Before using ListBox: store/index.ts import { action, makeObservable, observable } from 'mobx' import type { IFrameItStore, TrafficSignal } from '#/types/index' export class FrameItStore implements IFrameItStore { trafficSignal: TrafficSignal = { shape: 'circle', } constructor() { makeObservable(this, { trafficSignal: observable, updateTrafficSignal: action.bound, }) } updateTrafficSignal({ shape }: TrafficSignal) { if (shape) this.trafficSignal.shape = shape } } Shape.tsx import { observer } from 'mobx-react' import * as React from 'react' import { useFrameItStore } from '#/store/index' import type { TrafficSignalShape } from '#/types/index' export const Shape = observer(() => { const frameItStore = useFrameItStore() return ( <> <label htmlFor="shape" className="mb-1 text-sm font-medium text-blue-gray-500"> Shape </label> <select id="shape" className="block w-full px-3 py-2 mb-2 bg-white border border-gray-300 rounded-md shadow-sm text-blue-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" value={frameItStore.trafficSignal.shape} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { const shape = e.target.value as TrafficSignalShape frameItStore.updateTrafficSignal({ shape }) }} > <option value="circle">Circle</option> <option value="square">Square</option> </select> </> ) }) App.tsx <Shape /> After using ListBox: Select.tsx import * as React from 'react' import { Listbox, Transition } from '#headlessui/react' import clsx from 'clsx' import { Selector, Check } from '#/components/icons/index' type Option = { id: string name: string img: string } interface IProps { label?: string options: Array<Option> } export const Select = ({ label, options }: IProps) => { const [selectedOption, setSelectedOption] = React.useState<Option>(options[0]) return ( <Listbox value={selectedOption} onChange={setSelectedOption}> {({ open }) => ( <> <Listbox.Label className="mb-1 text-sm font-medium text-blue-gray-500"> {label} </Listbox.Label> <div className="relative mt-1"> <Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white border border-gray-300 rounded-md shadow-sm cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> <span className="flex items-center"> <img src={selectedOption.img} alt={selectedOption.name} className="flex-shrink-0 w-6 h-6 rounded-full" /> <span className="block ml-3 truncate">{selectedOption.name}</span> </span> <span className="absolute inset-y-0 right-0 flex items-center pr-2 ml-3 pointer-events-none"> <Selector /> </span> </Listbox.Button> <div className="absolute w-full mt-1 bg-white rounded-md shadow-lg"> <Transition show={open} leave="transition duration-100 ease-in" leaveFrom="opacity-100" leaveTo="opacity-0" > <Listbox.Options static className="py-1 overflow-auto text-base rounded-md max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" > {options.map((option) => ( <Listbox.Option as={React.Fragment} key={option.id} value={option}> {({ active, selected }) => ( <li className={clsx('relative py-2 pl-3 cursor-default select-none pr-9', { 'text-white bg-indigo-600': active, 'text-gray-900': !active, })} > <div className="flex items-center"> <img src={option.img} alt={option.name} className="flex-shrink-0 w-6 h-6 rounded-full" /> <span className={clsx('ml-3 block truncate', { 'font-semibold': selected, 'font-normal': !selected, })} > {option.name} </span> </div> {selected && ( <span className={clsx('absolute inset-y-0 right-0 flex items-center pr-4', { 'text-white': active, 'text-indigo-600': !active, })} > <Check /> </span> )} </li> )} </Listbox.Option> ))} </Listbox.Options> </Transition> </div> </div> </> )} </Listbox> ) } App.tsx const shapes = [ { id: '1', name: 'Circle', img: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', }, { id: '2', name: 'Square', img: 'https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', }, ] <Select label="Shape" options={shapes} /> How do I convert the After part to use MobX like the Before part? I tried passing value & onChange as it is in the Before part to Select like: App.tsx <Select label="Shape" options={shapes} value={frameItStore.trafficSignal.shape} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { const shape = e.target.value as TrafficSignalShape frameItStore.updateTrafficSignal({ shape }) }} /> Select.tsx interface IProps { label?: string value: any onChange: (value: any) => void options: Array<Option> } export const Select = ({ label, options, value, onChange }: IProps) => { const [selectedOption, setSelectedOption] = React.useState<Option>(options[0]) return ( <Listbox value={value} onChange={onChange}> . . . </Listbox> ) } But it doesn't select anything & I don't know what to do of selectedOption?
Alright I solved it. Removed the local hook state & just used the MobX state. Also, had 1 minor issue. I was setting the value as uppercase in the store when the store originally had lowercase values. The uppercase values were only for display in the UI. Here's the modified code that works: App.tsx <Select label="Shape" options={shapes} value={shapes.filter({ name }) => name.toLowerCase() === frameItStore.trafficSignal.shape)[0]} onChange={(value) => { const shape = value.toLowerCase() as TrafficSignalShape frameItStore.updateTrafficSignal({ shape }) }} /> Select.tsx import * as React from 'react' import { Listbox, Transition } from '#headlessui/react' import clsx from 'clsx' import { Selector, Check } from '#/components/icons/index' type Option = { id: string name: string img: string } interface IProps { label?: string value: Option onChange: (name: string) => void options: Array<Option> } export const Select = ({ label, options, value, onChange }: IProps) => { return ( <Listbox value={value} onChange={(value: Option) => { onChange(value.name) }} > {({ open }) => ( <> <Listbox.Label className="mb-1 text-sm font-medium text-blue-gray-500"> {label} </Listbox.Label> <div className="relative mt-1"> <Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white border border-gray-300 rounded-md shadow-sm cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> <span className="flex items-center"> <img src={value.img} alt={value.name} className="flex-shrink-0 w-6 h-6 rounded-full" /> <span className="block ml-3 truncate">{value.name}</span> </span> <span className="absolute inset-y-0 right-0 flex items-center pr-2 ml-3 pointer-events-none"> <Selector /> </span> </Listbox.Button> <div className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg"> <Transition show={open} leave="transition duration-100 ease-in" leaveFrom="transform opacity-100" leaveTo="transform opacity-0" > <Listbox.Options static className="py-1 overflow-auto text-base rounded-md max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" > {options.map((option) => { return ( <Listbox.Option as={React.Fragment} key={option.id} value={option}> {({ active, selected }) => { return ( <li className={clsx( 'relative py-2 pl-3 cursor-default select-none pr-9', { 'text-white bg-indigo-600': active, 'text-gray-900': !active, } )} > <div className="flex items-center"> <img src={option.img} alt={option.name} className="flex-shrink-0 w-6 h-6 rounded-full" /> <span className={clsx('ml-3 block truncate', { 'font-semibold': selected, 'font-normal': !selected, })} > {option.name} </span> </div> {selected && ( <span className={clsx( 'absolute inset-y-0 right-0 flex items-center pr-4', { 'text-white': active, 'text-indigo-600': !active, } )} > <Check /> </span> )} </li> ) }} </Listbox.Option> ) })} </Listbox.Options> </Transition> </div> </div> </> )} </Listbox> ) }
Why my redux state is go back to intial value on Page Refresh?
After SignIn, my authState becomes true, which is expected. and then It redirected to the home page. But after that If I refresh the page, my state goes back to initial value. what could be the reason of this? Login Component: import React, { useState, useEffect } from 'react'; import { toast } from 'react-toastify'; import axios from 'axios'; import { Redirect } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { setCurrentUser } from 'src/store/reducer/authReducer'; import jwtDecode from 'jwt-decode'; import { authenticate, isAuth } from '../helpers/auth'; import Protection from '../assets/Protection.svg'; import useInputState from '../hooks/useInputState'; const Login = (props) => { const auth = useSelector((state) => state.auth); const [submitted, setSubmitted] = useState(false); const [btnText, setBtnText] = useState('Sign In'); const dispatch = useDispatch(); const { location } = props; const initialValue = { name: '', email: '', password: '' }; const [state, onChangeHandler, reset] = useInputState(initialValue); const { email, password } = state; // submit data to backend const handleSubmit = async (e) => { e.preventDefault(); try { const res = await axios.post(`${process.env.API_URL}/api/signin`, { email, password, }); reset(); setSubmitted(true); setBtnText('Submitted'); const { token: jwtToken } = res.data; localStorage.setItem('jwtToken', jwtToken); const decoded = jwtDecode(jwtToken); dispatch(setCurrentUser(decoded)); } catch (err) { const { errors } = err.response.data; for (const error of errors) { const msg = Object.values(error).toString(); toast.error(msg); } } }; useEffect(() => { if (location.message) { toast.success(location.message); } }, [location.message]); if (submitted && auth.isAuthenticated) { return <Redirect to={{ pathname: '/', message: 'You are signed in' }} />; } return ( <div className="min-h-screen bg-gray-100 text-gray-900 flex justify-center"> <div className="max-w-screen-xl m-0 sm:m-20 bg-white shadow sm:rounded-lg flex justify-center flex-1"> <div className="lg:w-1/2 xl:w-5/12 p-6 sm:p-12"> <div className="mt-12 flex flex-col items-center"> <h1 className="text-2xl xl:text-3xl font-extrabold"> Sign In for MernAuth </h1> <form className="w-full flex-1 mt-8 text-indigo-500" onSubmit={handleSubmit} > <div className="mx-auto max-w-xs relative"> <input className="w-full px-8 py-4 rounded-lg font-medium bg-gray-100 border-gray-200 placeholder-gray-500 text-sm focus:outline-none focus:border-gray-400 focus:bg-white mt-5" type="email" name="email" placeholder="Email" onChange={onChangeHandler} value={email} /> <input className="w-full px-8 py-4 rounded-lg font-medium bg-gray-100 border-gray-200 placeholder-gray-500 text-sm focus:outline-none focus:border-gray-400 focus:bg-white mt-5" type="password" name="password" placeholder="Password" onChange={onChangeHandler} value={password} /> <button className="mt-5 tracking-wide font-semibold bg-indigo-500 text-gray-100 w-full py-4 rounded-lg hover:bg-indigo-700 transition-all duration-300 ease-in-out flex items-center justify-center focus:shadow-outline focus:outline-none border-none outline-none" type="submit" > <i className="fas fa-user-plus fa 1x w-6 -ml-2" /> <span className="ml-3">{btnText}</span> </button> </div> <div className="my-12 border-b text-center"> <div className="leading-node px-2 inline-block text-sm text-gray-600 tracking-wide font-medium bg-white transform tranlate-y-1/2"> Or sign in with email or social login </div> </div> <div className="flex flex-col items-center"> <a className="w-full max-w-xs font-bold shadow-sm rounded-lg py-3 bg-indigo-100 text-gray-800 flex items-center justify-center transition-all duration-300 ease-in-out focus:outline-none hover:shadow focus:shadow-sm focus:shadow-outline mt-5" href="/login" target="_self" > <i className="fas fa-sign-in-alt fa 1x w-6 -ml-2 text-indigo-500" /> <span className="ml-4">Sign Up</span> </a> </div> </form> </div> </div> <div className="flex-1 bg-indigo-100 text-center hidden lg:flex"> <div className="m-12 xl:m-16 w-full bg-contain bg-center bg-no-repeat" style={{ backgroundImage: `url(${Protection})` }} /> </div> </div> </div> ); }; export default Login; my home component import React, { useEffect } from 'react'; import { toast } from 'react-toastify'; const App = (props) => { const { location } = props; useEffect(() => { if (location.message) { toast.success(location.message); } }, [location.message]); return <div>App</div>; }; export default App; I don't want the state to go back to its initial value on page refresh.
Your data is not persisted. Save the user token in the localStorage and when the app is initializing verify the localStorage and if there have value simply populate the store with this data.
Since you want to store user token, I would suggest that you use a cookie to store it, here is a link where you can find how to create and use a cookie.