Hover effect on only one item out of multiple displayed - VUEJS - firebase

I have put in a mouseenter and mouseleave on the li tag that i want when a person hovers over it, it will display the price on product.price. However, when i hover over it, it will display the price for all 6 rendered data instead of just the 1 its hovered on. I only want it to display pricing on the specific item its hovered on and not all. The data is being loaded from firebase. Please see below template code and image here for reference.
<div class="relative w-full pb-6 -mb-6 overflow-x-auto scrollbar-hide">
<ul role="list" class="mx-4 inline-flex space-x-0 gap-2 sm:mx-6 lg:mx-0 lg:space-x-0 lg:grid lg:grid-cols-6 lg:gap-x-4">
<li v-if="products.length" v-for="product in products" :key="product.id" #mouseenter="hover = true" #mouseleave="hover = false" class="w-44 inline-flex border hover:border-black rounded-lg p-4 lg:w-auto">
<div class="group relative">
<div class="w-[70%] lg:w-[55%] bg-gray-white overflow-hidden">
<img :src="product.imageSrc" :alt="product.imageAlt" class="w-full h-20 overflow-hidden object-center object-contain" />
</div>
<div class="mt-2">
<h3 class="mt-1 font-rubikreg h-11 overflow-hidden text-xs lg:text-base uppercase text-gray-900">
<a :href="product.href">
<span class="absolute inset-0" />
{{ product.name }}
</a>
</h3>
<p class="mt-3 lg:mt-6 font-rubiklight uppercase text-xs lg:text-sm text-gray-900">
Cheapest At
</p>
<p class="mt-1 font-rubikreg underline-offset-2 underline uppercase text-xs lg:text-sm text-gray-900">
{{ product.cheapestat }}
</p>
<p v-if="hover" class="mt-5 text-2xl uppercase font-rubik text-gray-900">
<span class="text-xs">From</span>
A${{ product.price }}
</p>
</div>
</div>
</li>
</ul>
</div>
script code on firebase data
setup() {
onMounted(() => {
onSnapshot(collection(db, "malesneakers") , (querySnapshot) => {
const maleProducts = [];
querySnapshot.forEach((doc) => {
const mlproducts = {
id: doc.id,
imageSrc: doc.data().imageSrc,
name: doc.data().name,
price: doc.data().price,
cheapestat: doc.data().cheapestat,
svgSrc: doc.data().svgSrc,
href: doc.data().href,
}
maleProducts.push(mlproducts)
});
products.value = maleProducts
});
});

Try with product.id instead of boolean for hover variable:
const { ref } = Vue
const app = Vue.createApp({
setup() {
const products = ref([{id: 1, name: 'aaa', href: '#', cheapestat: 5, price: 7}, {id: 2, name: 'bbb', href: '#', cheapestat: 5, price: 5}])
const hover = ref(null)
return {
products, hover
};
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" integrity="sha512-wnea99uKIC3TJF7v4eKk4Y+lMz2Mklv18+r4na2Gn1abDRPPOeef95xTzdwGD9e6zXJBteMIhZ1+68QC5byJZw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<div id="demo">
<div class="relative w-full pb-6 -mb-6 overflow-x-auto scrollbar-hide">
<ul v-if="products.length" role="list" class="mx-4 inline-flex space-x-0 gap-2 sm:mx-6 lg:mx-0 lg:space-x-0 lg:grid lg:grid-cols-6 lg:gap-x-4">
<!-- 👇 here you set hover to product.id -->
<li v-for="product in products" :key="product.id"
#mouseenter="hover = product.id" #mouseleave="hover = null" class="w-44 inline-flex border hover:border-black rounded-lg p-4 lg:w-auto">
<div class="group relative">
<div class="w-[70%] lg:w-[55%] bg-gray-white overflow-hidden">
<img :src="product.imageSrc" :alt="product.imageAlt" class="w-full h-20 overflow-hidden object-center object-contain" />
</div>
<div class="mt-2">
<h3 class="mt-1 font-rubikreg h-11 overflow-hidden text-xs lg:text-base uppercase text-gray-900">
<a :href="product.href">
<span class="absolute inset-0" />
{{ product.name }}
</a>
</h3>
<p class="mt-3 lg:mt-6 font-rubiklight uppercase text-xs lg:text-sm text-gray-900">
Cheapest At
</p>
<p class="mt-1 font-rubikreg underline-offset-2 underline uppercase text-xs lg:text-sm text-gray-900">
{{ product.cheapestat }}
</p>
<!-- 👇 here you check hover for product.id -->
<p v-if="hover === product.id" class="mt-5 text-2xl uppercase font-rubik text-gray-900">
<span class="text-xs">From</span>
A${{ product.price }}
</p>
</div>
</div>
</li>
</ul>
</div>
</div>

Related

Tailwind CSS: Background white '#F2F2F2' is not covering the whole screen when responsive

In my react.js frontend, I have 2 columns (Dislay event list, create event) This is how my website look like in full screen.
However, when I tried to view it with smaller screen to test the responsiveness, the background white '#F2F2F2' is not showing for the 2nd column.
How do I solve this issue?
import axios from "axios";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "../Components/customButton";
import KeyboardArrowLeftIcon from "#mui/icons-material/KeyboardArrowLeft";
import TopicList from "../Components/topicList";
import EventList from "../Components/eventList";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
const BASE_URL = process.env.REACT_APP_BASE_URL;
function CreateEvent(success, message) {
const navigate = useNavigate();
const [eventName, setEventName] = useState("");
const [eventDesc, setEventDesc] = useState("");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [multiplierType, setMultiplierType] = useState("");
const [multiplier, setMultiplier] = useState("");
const [topic, setTopic] = useState("");
const [eventNameCharLeft, setEventNameCharLeft] = useState(100);
const [eventDescCharLeft, setEventDescCharLeft] = useState(255);
const getDataFromTopicList = (val) => {
setTopic(val);
};
const handleEventNameChange = (event) => {
setEventName(event.target.value);
setEventNameCharLeft(100 - event.target.value.length);
};
const handleEventDescChange = (event) => {
setEventDesc(event.target.value);
setEventDescCharLeft(255 - event.target.value.length);
};
const handleSubmit = (event) => {
event.preventDefault();
axios({
method: "POST",
url: BASE_URL + "events/submitEvent",
data: {
eventName: eventName,
eventDesc: eventDesc,
eventStart: startDate,
eventEnd: endDate,
topicId: topic,
multiplierType: multiplierType,
multiplier: multiplier,
status: "Upcoming",
},
headers: { "Content-Type": "application/json" },
})
.then((response) => {
if (response.status === 200) {
toast.success("Successfully Created", {
position: toast.POSITION.TOP_CENTER,
});
} else {
toast.error(response.data.message, {
position: toast.POSITION.TOP_CENTER,
});
}
})
.catch((err) => {
if (err.response) {
toast.error(err.response.data.message, {
position: toast.POSITION.TOP_CENTER,
});
} else {
toast.error("Failed to Create", {
position: toast.POSITION.TOP_CENTER,
});
}
});
};
return (
<div className="min-h-screen">
<Button
variant="primary"
className="absolute top-4 left-6 px-0 py-2 font-bold btn btn-primary text-main-blue"
onClick={() => {navigate(`/Admin`);}}
isDisabled={false}
buttonText="Back"
icon={<KeyboardArrowLeftIcon color="main-green" />}
/>
<div className=" grid grid-cols-2 p-20 space-x-8 sm:grid-cols-1 md:grid-cols-1 h-screen lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2">
<div className="px-4 pt-4 pb-8 mb-4 bg-slate-50 drop-shadow-xl rounded-2xl">
{/* Ongoing Events */}
<h1 className="py-3 my-2 font-semibold border-b-2 text-main-blue border-main-blue">Ongoing Events</h1>
<div className="grid grid-cols-3 pt-2 gap-x-10">
<div className="">
<h1 className="pb-3 text-sm font-semibold text-main-blue">Event Name</h1>
</div>
<div className="">
<h1 className="pb-3 text-sm font-semibold text-main-blue">Start Date</h1>
</div>
<div className="">
<h1 className="pb-3 text-sm font-semibold text-main-blue">End Date</h1>
</div>
</div>
<EventList url="events/getOngoing" />
{/* Upcoming Events */}
<h1 className="py-3 my-2 font-semibold border-b-2 text-main-blue border-main-blue">Upcoming Events</h1>
<div className="grid grid-cols-3 pt-2 gap-x-10">
<div className="">
<h1 className="pb-3 text-sm font-semibold text-main-blue">Event Name</h1>
</div>
<div className="">
<h1 className="pb-3 text-sm font-semibold text-main-blue">Start Date</h1>
</div>
<div className="">
<h1 className="pb-3 text-sm font-semibold text-main-blue">End Date</h1>
</div>
</div>
<EventList url="events/getUpcoming" />
{/* Past Events */}
<h1 className="py-3 my-2 font-semibold text-gray-400 border-b-2 border-gray-400">Past Events</h1>
<div className="grid grid-cols-3 pt-2 gap-x-10">
<div className="">
<h1 className="pb-3 text-sm font-semibold text-gray-400">Event Name</h1>
</div>
<div className="">
<h1 className="pb-3 text-sm font-semibold text-gray-400">Start Date</h1>
</div>
<div className="">
<h1 className="pb-3 text-sm font-semibold text-gray-400">End Date</h1>
</div>
</div>
<EventList url="events/getPast" />
</div>
<form
className="px-8 pt-6 pb-8 mb-4 rounded sm:grid-cols-1 md:grid-cols-1 bg-white" onSubmit={handleSubmit}>
<h1 className="text-2xl font-bold pt-15 text-main-blue">Create an Event</h1>
<div>
<textarea
className="block w-full px-5 py-2 mt-2 overflow-y-auto text-sm break-words border border-gray-300 rounded-md bg-slate-50 text-main-blue drop-shadow-lg"
name="eventName" placeholder="Event Name" required onChange={handleEventNameChange} value={eventName}maxLength={100}>
</textarea>
<div className="mt-2 text-sm text-gray-500">
{eventNameCharLeft}/100 characters left
</div>
<textarea
className="block w-full px-5 py-2 mt-2 overflow-y-auto text-sm break-words border border-gray-300 rounded-md bg-slate-50 text-main-blue drop-shadow-lg"
name="eventDesc" placeholder="Event Description" required onChange={handleEventDescChange} value={eventDesc} maxLength={255}>
</textarea>
<div className="mt-2 text-sm text-gray-500">
{eventDescCharLeft}/255 characters left
</div>
<div className="grid grid-cols-2 gap-x-2 mt-4">
<div className="relative">
<label className="absolute text-sm text-gray-500">Start Date</label>
<input
className="bg-slate-50 text-main-blue border border-gray-300 drop-shadow-lg text-sm rounded-md my-5 block w-full p-2.5"
type="datetime-local" name="startDate" placeholder="Start Date" required onChange={(event) => setStartDate(event.target.value)} value={startDate}/>
</div>
<div className="relative">
<label className="absolute text-sm text-gray-500">End Date</label>
<input
className="bg-slate-50 text-main-blue border border-gray-300 drop-shadow-lg text-sm rounded-md my-5 block w-full p-2.5"
type="datetime-local" name="endDate" placeholder="End Date" required onChange={(event) => setEndDate(event.target.value)} value={endDate}/>
</div>
</div>
<div className="grid grid-cols-2 gap-x-2">
<TopicList getDataFromTopicList={getDataFromTopicList} />
<select
className="bg-slate-50 text-main-blue border border-gray-300 drop-shadow-lg text-sm rounded-md block w-auto p-2.5"
name="multiplierType" required onChange={(event) => setMultiplierType(event.target.value)} value={multiplierType}>
<option value="" disabled selected>Select Multiplier</option>
<option value="+">Add</option>
<option value="*">Multiply</option>
</select>
</div>
<input
className="bg-slate-50 text-main-blue border border-gray-300 drop-shadow-lg text-sm rounded-md my-5 block w-full p-2.5"
type="number" name="multiplier" placeholder="Multiplier Value (Eg 1-100)" min="1" max="100" required onChange={(event) => setMultiplier(event.target.value)} value={multiplier}/>
</div>
<div className="relative p-4">
<Button
variant="primary"
className="absolute bottom-0 right-0 px-4 py-2 -my-5 font-bold border rounded btn btn-primary bg-slate-50 text-main-blue border-main-blue hover:border-transparent hover:bg-main-blue hover:text-slate-50"
isDisabled={false}
buttonText="Submit"
type="submit"
/>
<ToastContainer autoClose={4000} />
</div>
</form>
</div>
</div>
);
}
export default CreateEvent;
This is the output for small screen in tailwind playground.
This seems to work just fine for me !
Code Link: tailwind_playground
Output in large screen:
Output in small screen:
Your problem is "h-screen" in the class name on the element you have selected in your screenshot. This prevents it from extending to the height of the elements inside it. You can change this to "min-h-screen" as well however, I would recommend just removing it entirely.

reload captcha after captcha validation error

I have captcha in ajax form.
captcha is reloaded after each successfull POST request this.mathcaptchaLabel = response.data.mathcaptchaLabel;
captcha can be reloaded by click on the button <x-modules.button icon="refresh" style="black-outline" class="px-4" #click.prevent="mathcaptchaReset()" />
But I cannot understand how to make captcha reload in case of captcha validation error.
I tried to play with isErrorCaptcha() inside .catch( (error), but it doesn't work.
For example when there is no error, then errorData object doesn't have mathcaptcha array. So I suppose that error.response.data.errors.mathcaptcha should be undefined.
And when there is captcha error I expect error.response.data.errors.mathcaptcha !== 'undefined' and this.errorStates.mathcaptcha = true.
But in console I see
Uncaught (in promise) ReferenceError: error is not defined
at Proxy.isErrorName
And in AlpineJS devtools errorStates.mathcaptcha is false, though in devtools I see that mathcaptcha array exists in errorData object.
blade view
<form method="POST" action="/modalform" method="POST" #submit.prevent="submitData()">
#csrf
<div class="bg-white">
<div class="modalheader flex place-items-center text-center border-b cursor-pointer text-lg leading-6 font-medium text-gray-900">
<h3 class="p-2 hover:bg-blue-500 hover:text-white"
#click="$dispatch('callback')"
:class="callback ? 'bg-blue-500 text-white' : ''"
>
Перезвоните мне
</h3>
<h3 class="p-2 hover:bg-blue-500 hover:text-white"
#click="$dispatch('zamer')"
:class="zamer ? 'bg-blue-500 text-white' : ''"
>
Записаться на замер
</h3>
<h3 class="p-2 hover:bg-blue-500 hover:text-white"
#click="$dispatch('eskiz')"
:class="eskiz ? 'bg-blue-500 text-white' : ''"
>
Отправить эскиз
</h3>
<div class="p-2 place-self-stretch hover:bg-blue-500 hover:text-white" #click="closeModal()" >
<span class="text-3xl">×</span>
</div>
</div>
<div class="modalbody flex items-center w-full h-full p-5"
x-show="sent"
x-text="message"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 "
x-transition:leave-end="opacity-0 "
>
</div>
<div class="modalbody flex items-start flex-wrap p-5"
x-show="!sent"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
>
<div class="text-left w-full">
<div class="mt-2 grid grid-cols-2 gap-x-4 gap-y-2 mb-2">
<!-- Name -->
<div class="name"
:class="errorData.name ? 'text-red-500' : ''"
>
<x-modules.label for="name" :value="__('auth.user.name')" />
<div class="relative text-gray-400 focus-within:text-gray-800">
<div class="absolute flex border border-transparent left-0 top-0 h-full w-10" >
<x-modules.svg type="user-solid" class="flex items-center justify-center rounded-l bg-gray-100 h-full w-full px-0.5"/>
</div>
<x-modules.input id="name" class="block w-full pl-12" type="text" name="name" :value="old('name')" x-model="formData.name" placeholder="Введите имя" autofocus />
</div>
<span x-text="errorData.name" class="text-red-500 text-xs"> </span>
</div>
<!-- Phone -->
<div class="phone"
:class="errorData.phone ? 'text-red-500' : ''"
>
<x-modules.label for="phone" :value="__('auth.user.phone')" />
<div class="relative text-gray-400 focus-within:text-gray-800">
<div class="absolute flex border border-transparent left-0 top-0 h-full w-10 ">
<x-modules.svg type="phone-ringing-outline" class="flex items-center justify-center rounded-l bg-gray-100 h-full w-full px-0.5"/>
</div>
<x-modules.input id="phone" class="block w-full pl-12" type="text" name="phone" :value="old('phone')" x-model="formData.phone" placeholder="Введите телефон" required autofocus />
</div>
<span x-text="errorData.phone" class="text-red-500 text-xs"> </span>
</div>
<!-- Email Address -->
<div class="email"
x-show="zamer || eskiz"
:class="errorData.email ? 'text-red-500' : ''"
>
<x-modules.label for="email" :value="__('email')" />
<div class="relative text-gray-400 focus-within:text-gray-800">
<div class="absolute flex border border-transparent left-0 top-0 h-full w-10 ">
<x-modules.svg type="envelope-outline" class="flex items-center justify-center rounded-l bg-gray-100 h-full w-full px-0.5"/>
</div>
<x-modules.input id="email" class="block w-full pl-12" type="email" name="email" :value="old('email')" x-model="formData.email" autofocus />
</div>
<span x-text="errorData.email" class="text-red-500 text-xs"> </span>
</div>
<!-- Address -->
<div class="address"
x-show="zamer || eskiz"
:class="errorData.address ? 'text-red-500' : ''"
>
<x-modules.label for="address" :value="__('auth.user.address')" />
<div class="relative text-gray-400 focus-within:text-gray-800">
<div class="absolute flex border border-transparent left-0 top-0 h-full w-10 ">
<x-modules.svg type="facade" class="flex items-center justify-center rounded-l bg-gray-100 h-full w-full px-0.5"/>
</div>
<x-modules.input id="address" class="block w-full pl-12" type="text" name="address" :value="old('address')" x-model="formData.address" autofocus />
</div>
<span x-text="errorData.address" class="text-red-500 text-xs"> </span>
</div>
<!-- Upload field -->
<div class="upload" x-show="eskiz">
<label class="flex items-center justify-evenly p-2 bg-white text-gray-700 rounded-lg shadow-lg border border-gray-300 cursor-pointer hover:bg-blue-500 hover:text-white">
<x-modules.svg type="upload" class="w-8 h-8"/>
<span>Выберите файл</span>
<input type="file" class="hidden" multiple />
</label>
</div>
</div>
<!-- Message -->
<div class="message">
<x-modules.label for="message" :value="__('auth.user.message')" />
<x-modules.textarea rows="2" id="message" class="block w-full" name="message" x-model="formData.message" placeholder="Кратко опишите ваш вопрос"/></textarea>
<span x-text="errorData.message" class="text-red-500 text-xs"> </span>
</div>
{{--
<!-- captcha -->
<div class="mt-4 captcha flex items-center space-x-2">
<div class="w-32 img">{!! captcha_img() !!}</div>
<x-modules.svg type="refresh" class="w-10 h-10 reload cursor-pointer" id="reload"/>
<x-modules.input id="captcha" class="block w-full" type="text" name="captcha" placeholder="Введите результат с картинки" x-model="formData.captcha" required />
</div>
--}}
<!-- mathcaptcha -->
<div class="mathcaptcha" :class="errorData.mathcaptcha ? 'text-red-500' : ''">
<label class="" for="mathcaptcha">Введите результат функции: <span x-text="mathcaptchaLabel" ></span></label>
<div class="flex space-x-4 text-gray-400 focus-within:text-gray-800">
<x-modules.button icon="refresh" style="black-outline" class="px-4" #click.prevent="mathcaptchaReset()" />
{!! app('mathcaptcha')->input(['class' => 'appearance-none rounded-md shadow-sm border-gray-300 placeholder-gray-400 focus:border-sky-500 focus:ring-1 focus:ring-sky-500 focus:outline-none valid:border-green-500 invalid:border-red-500 block w-full', 'id' => 'mathcaptcha', 'type' => 'text', 'name' => 'mathcaptcha', 'x-model' => 'formData.mathcaptcha']) !!}
</div>
<span x-text="errorData.mathcaptcha" class="text-red-500 text-xs"> </span>
</div>
</div>
</div>
<div class="modalfooter bg-gray-50 px-4 py-3 sm:px-6 flex justify-between ">
<x-modules.button text="Отмена" style="black-outline" class="px-4" #click.prevent="closeModal()" />
<x-modules.button x-text="buttonLabel" style="blue-solid" class="px-4" #click.prevent="submitData()" />
</div>
</div>
</form>
</div>
</div>
</div>
<!-- /Modal -->
</div>
<script>
function topbar() {
return {
mailTooltip: false,
instagramTooltip: false,
openModal: false,
callback: true,
zamer: false,
eskiz: false,
mathcaptchaLabel: '{{ app('mathcaptcha')->label() }}',
formData: {
name: '',
phone: '',
email: '',
address: '',
message: '',
mathcaptcha: '',
_token: '{{ csrf_token() }}'
},
message: '',
responseData: [],
errorStates: {
name: false,
phone: false,
email: false,
address: false,
message: false,
mathcaptcha: false
},
errorData: [],
loading: false,
sent: false,
buttonLabel: 'Отправить',
resetFields() {
this.formData.name = '',
this.formData.phone = '',
this.formData.email = '',
this.formData.address = '',
this.formData.message = '',
this.formData.mathcaptcha = ''
},
closeModal() {
this.openModal = false;
this.callback = true;
this.zamer = false;
this.eskiz = false;
},
mathcaptchaReset() {
axios.get('/reload-captcha')
.then( (response) => {
console.log(response);
this.mathcaptchaLabel = response.data.mathcaptchaLabel;
});
},
submitData() {
axios.post('/modalform', this.formData)
.then( (response) => {
this.buttonLabel = 'Отправляем...';
this.loading = true;
console.log(response);
this.resetFields();
this.sent = true;
this.message = 'Сообщение успешно отправлено!';
this.responseData = response.data;
this.mathcaptchaLabel = response.data.mathcaptchaLabel;
})
.then( () => {
this.loading = false;
this.sent = false;
this.closeModal();
this.buttonLabel = 'Отправить';
this.message = '';
})
.catch( (error) => {
console.log(error);
this.message = 'Ooops! Что-то пошло не так!'
this.errorData = error.response.data.errors;
this.isErrorCaptcha();
});
},
isErrorCaptcha() {
if (typeof (error.response.data.errors.mathcaptcha) !== 'undefined') {
this.errorStates.mathcaptcha = true;
this.mathcaptchaReset();
}
},
}
}
</script>
routes/web.php
Route::post('/modalform', 'MainController#modalform')->name('modalform');
Route::get('/reload-captcha','MainController#reloadCaptcha')->name('reloadCaptcha');
app/Http/Controllers/MainController.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\Modalform;
use App\Http\Requests\ModalformRequest;
class MainController extends Controller
{
public function modalform(ModalformRequest $request) {
Mail::to( config('mail.to.address') )->send(new Modalform());
app('mathcaptcha')->reset();
return response()->json([
'status' => 'success',
'messageHeader' => 'Ваш вопрос отправлен!',
'messageContent' => 'В ближайшее время мы свяжемся с вами.',
'mathcaptchaLabel' => app('mathcaptcha')->label(),
]);
}
public function reloadCaptcha()
{
app('mathcaptcha')->reset();
return response()->json([
'mathcaptchaLabel' => app('mathcaptcha')->label(),
]);
}
}
app/Mail/Modalform.php
use Illuminate\Http\Request;
use App\Http\Requests\ModalformRequest;
use Illuminate\Mail\Mailable;
class Modalform extends Mailable
{
public function build(ModalformRequest $request)
{
$this->from( config('mail.from.address') )
->view('emails.modalform')
->withRequest($request);
}
}
App/Http/Requests/ModalformRequest.php
<?php
use Illuminate\Foundation\Http\FormRequest;
class ModalformRequest extends FormRequest
{
public function rules()
{
return [
'name' => 'bail|required|string|between:2,20',
'phone' => 'bail|required',
'email' => 'bail|email:rfc|nullable',
'address' => 'bail|string|max:100|nullable',
'message' => 'bail|string|max:500|nullable',
'mathcaptcha' => 'required|mathcaptcha',
];
}
}

Livewire The server returned a "405 Method Not Allowed". in production and not problem in develop stage

I've working with app made with TALL. On local work fine, and on production one page with 3 forms, get error "405 Method Not Allowed"
Error trace show a problem on POST method instead GET method.
The form is a mix of three forms.
1- The one with the profile
2. The one with the password
3. The one with the token
All three forms are components of livewire.
Each one has their buton, to save.
Thus, the error occurs in any of the three save buttons, in production.
wep.php
Route::group(['prefix' => 'dashboard', 'as' => 'admin.', 'middleware' => ['auth']], function () {
...
Route::get('/profile', Profile::class)->name('profile');
...
}
Profile.php
<?php
namespace App\Http\Livewire\Auth;
use App\Models\User;
use Livewire\Component;
class Profile extends Component
{
public User $user;
public function mount() { $this->user = auth()->user(); }
public function render()
{
return view('livewire.auth.profile');
}
}
UpdatePassword.php
<?php
namespace App\Http\Livewire\Auth\Profile;
use Livewire\Component;
use App\Models\User;
class UpdatePassword extends Component
{
//public User $user;
public $password;
public $password_confirmation;
public function render()
{
return view('livewire.auth.profile.update-password');
}
protected $rules = [
'password' => [
'required',
'confirmed',
'min:10',
'regex:/^.*(?=.{3,})(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[\d\x])(?=.*[!$#%]).*$/',
],
];
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
public function save()
{
$this->validate();
auth()->user()->update([
'password' => bcrypt($this->password)
]);
$this->emitSelf('notify-saved');
$this->resetForm();
}
protected function resetForm()
{
$this->password = '';
$this->password_confirmation = '';
}
}
UpdateProfile.php
<?php
namespace App\Http\Livewire\Auth\Profile;
use App\Models\User;
use Livewire\Component;
use Livewire\WithFileUploads;
class UpdateProfile extends Component
{
use WithFileUploads;
public User $user;
public $upload;
public function render()
{
return view('livewire.auth.profile.update-profile');
}
protected function rules(): array
{
return [
'user.name' => [
'string',
'required',
'min:5',
],
'user.email' => [
'email:rfc',
'required',
'regex:/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*#([a-z0-9\-]+\.)+[a-z]{2,6}$/ix',
'unique:users,email,' . $this->user->id,
],
'upload' => [
'nullable',
'image',
'mimes:jpg,bmp,png',
'max:200'
],
];
}
public function mount() { $this->user = auth()->user(); }
public function save()
{
$this->validate();
$this->user->save();
$this->upload && $this->user->update([
'avatar' => $this->upload->store('/', 'avatars'),
]);
$this->emitSelf('notify-saved');
}
}
UpdateToken.php
<?php
namespace App\Http\Livewire\Auth\Profile;
use App\Helpers\ApiHelpers;
use Livewire\Component;
class UpdateToken extends Component
{
public string $token;
public function mount()
{
$this->resetState();
}
public function updateToken()
{
$user = auth()->user();
ApiHelpers::deleteTokenUserType($user->id, 'auth_token');
$this->token = auth()->user()->createToken('auth_token')->plainTextToken;
}
public function render()
{
return view('livewire.auth.profile.update-token');
}
protected function resetState()
{
$this->token = '';
}
}
profile.blade.php
<div>
#livewire('auth.profile.update-profile')
<!-- Contraseña -->
<div class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200"></div>
</div>
</div>
#livewire('auth.profile.update-password')
<!-- Token -->
<div class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200"></div>
</div>
</div>
#livewire('auth.profile.update-token')
</div>
update-profile.blade.php
<div>
<div class="md:grid md:grid-cols-3 md:gap-6 border-gray-300">
<div class="md:col-span-1">
<h3 class="text-lg font-medium leading-6 text-gray-900">Perfil</h3>
<p class="mt-1 text-sm text-gray-500">
Esta información es privada y sólo tiene efectos administrativos.
</p>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form wire:submit.prevent="save">
<div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
<!-- Nombre -->
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-2">
<label for="name" class="block text-sm font-medium text-gray-700">
Nombre
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<input wire:model.defer="user.name" type="text" name="username" id="username" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Nombre y apellidos">
</div>
<div class="mt-1 relative rounded-md shadow-sm">
#error('user.name')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
#enderror
</div>
</div>
</div>
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-2">
<label for="email" class="block text-sm font-medium text-gray-700">
Email
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<input wire:model.defer="user.email" type="text" name="email" id="email" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Correo electrónico">
</div>
<div class="mt-1 relative rounded-md shadow-sm">
#error('user.email')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
#enderror
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">
Foto
</label>
<div class="mt-1 flex items-center space-x-5">
<span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">
#if ($upload)
<img src="{{ $upload->temporaryUrl() }}" alt="Profile Photo">
#else
<img src="{{ auth()->user()->avatarUrl() }}" alt="Profile Photo">
#endif
</span>
<input type="file" wire:model="upload" id="photo">
<div class="mt-1 relative rounded-md shadow-sm">
#error('user.avatar')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
#enderror
</div>
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
<span x-data="{ open: false }"
x-init="#this.on('notify-saved',
() => {
if (open === false) setTimeout(() => { open = false }, 3500);
open = true;
})"
x-show.transition.out.duration.1000ms="open"
style="display: none;"
class="text-gray-500">¡Guardado!</span>
</div>
</div>
</form>
</div>
</div>
</div>
update-password.blade.php
<div>
<div class="md:grid md:grid-cols-3 md:gap-6 border-gray-300">
<div class="md:col-span-1">
<h3 class="text-lg font-medium leading-6 text-gray-900">Contraseña</h3>
<p class="mt-1 text-sm text-gray-500">
Puede cambiar su contraseña en este formulario
</p>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form wire:submit.prevent="save">
<div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-2">
<label for="password" class="block text-sm font-medium text-gray-700">
Contraseña
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<input wire:model.defer="password" type="password" name="password" id="password" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Nueva contraseña">
</div>
<div class="mt-1 relative rounded-md shadow-sm">
#error('password')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
#enderror
</div>
</div>
</div>
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-2">
<label for="password_confirmation" class="block text-sm font-medium text-gray-700">
Confirma la contraseña
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<input wire:model.defer="password_confirmation" type="password" name="password_confirmation" id="password_confirmation" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300">
</div>
<div class="mt-1 relative rounded-md shadow-sm">
#error('password_confirmation')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
#enderror
</div>
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cambiar
</button>
<span x-data="{ open: false }"
x-init="#this.on('notify-saved',
() => {
if (open === false) setTimeout(() => { open = false }, 3500);
open = true;
})"
x-show.transition.out.duration.1000ms="open"
style="display: none;"
class="text-gray-500">¡Contraseña cambiada!
</span>
</div>
</div>
</form>
</div>
</div>
</div>
update-token.php
<div>
<div class="md:grid md:grid-cols-3 md:gap-6 border-gray-300">
<div class="md:col-span-1">
<h3 class="text-lg font-medium leading-6 text-gray-900">Token API</h3>
<p class="mt-1 text-sm text-gray-500">
El token solo se muestra una vez generado, por seguridad.
<br />Copielo y guardelo en un lugar seguro.
<br />El anterior se elimina del sistema, dejan de ser operativo.
</p>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form wire:submit.prevent="updateToken">
<div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-2">
<label for="token" class="block text-sm font-medium text-gray-700">
Token
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<input wire:model="token" type="text" name="token" id="token" disabled class="disabled:opacity-50 focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Haz click en el botón para regenerar el token">
</div>
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Regenerar
</button>
</div>
</div>
</form>
</div>
</div>
</div>
An unforgivable mistake.
As a system administrator, I have forgotten, that you have to review the logs. And among others the mod_security log.
--9ac02665-F--
HTTP/1.1 405 Method Not Allowed
allow: POST
Cache-Control: no-cache, private
date: Fri, 12 Nov 2021 15:18:31 GMT
Connection: close
Content-Type: text/html; charset=UTF-8
Server: Apache
--9ac02665-H--
Message: Warning. Matched phrase ".profile" at REQUEST_FILENAME. [file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "124"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .profile found within REQUEST_FILENAME: /livewire/message/auth.profile.update-profile"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.2"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/255/153/126"] [tag "PCI/6.5.4"]

Tailwind + Headless UI: Close Mobile Menu after click or clickaway

I am using Tailwind + Headless UI to create a hamburger Menu bar on mobile to show the menu on click. But when I click on the menu it does not close automatically and creates a bad UX.
<Disclosure
as="nav"
className="fixed top-0 left-0 right-0 z-10 w-full bg-white shadow"
>
{({ open }) => (
<>
<div className="px-2 mx-auto max-w-7xl sm:px-4 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex px-2 lg:px-0">
<div className="flex items-center flex-shrink-0">
<Link href="/">
<a className="relative block w-12 h-12">
<Image
src="/img/logo.png"
alt="NFT Volt Logo"
layout="fill"
className="w-auto h-6 lg:block"
/>
</a>
</Link>
</div>
<div className="hidden lg:ml-6 lg:flex lg:space-x-1">
{navLinks.map((link) => (
<NavLink key={link.id} href={link.href}>
{link.name}
</NavLink>
))}
</div>
</div>
<div className="flex items-center justify-center flex-1 px-2 lg:ml-6 lg:justify-end">
<div className="w-full max-w-lg lg:max-w-xs">
<label htmlFor="search" className="sr-only">
Search
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<SearchIcon
className="w-5 h-5 text-gray-400"
aria-hidden="true"
/>
</div>
<input
id="search"
name="search"
className="block w-full py-2 pl-10 pr-3 leading-5 placeholder-gray-500 bg-white border border-gray-300 rounded-md focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="Search NFT projects..."
type="search"
/>
</div>
</div>
<Link href="/list-project" passHref>
<a
href="#"
className="items-center hidden px-4 py-2 ml-6 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm lg:inline-flex hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 whitespace-nowrap"
>
List your Project
</a>
</Link>
</div>
<div className="flex items-center lg:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center p-2 text-gray-400 rounded-md hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500">
<span className="sr-only">
Open main menu
</span>
{open ? (
<XIcon
className="block w-6 h-6"
aria-hidden="true"
/>
) : (
<MenuIcon
className="block w-6 h-6"
aria-hidden="true"
/>
)}
</Disclosure.Button>
</div>
</div>
</div>
<Disclosure.Panel className="lg:hidden">
<div className="pt-2 pb-3 space-y-1">
{/* {navLinks.map((link) => (
<NavLinkMobile
key={link.id}
href={link.href}
onClick={() => {
console.log('click');
close();
}}
>
{link.name}
</NavLinkMobile>
))} */}
{navLinks.map((link) => (
<Disclosure.Button
as={NavLinkMobile}
key={link.id}
href={link.href}
>
{link.name}
</Disclosure.Button>
))}
<NavLinkMobile href="/list-project">
List your Project
</NavLinkMobile>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
Tried to add manually close on click but doesn't seem to work.
The trick is to wrap the links with <Disclosure.Button> </Disclosure.Button> and it will close automatically the panel.
See: https://headlessui.dev/react/disclosure#closing-disclosures-manually
Use can use the close prop from Disclose itself.
import it this way ({ open, close }) and use it
onClick={() => {
close();
}}
Try using Next.js Router's push function to navigate the user when the Disclosure.Button is clicked.
At the top of your component, call the useRouter hook:
const router = useRouter();
With that, you can modify your JSX by adding an onClick property where you then call router.push({path}) , like this:
{
navLinks.map((link) => (
<Disclosure.Button
as="a"
key={link.id}
onClick={() => {
router.push(`${link.href}`);
}}
>
{link.name}
</Disclosure.Button>
));
}
Using the Next.js router will navigate the user to the desired href while still allowing the Disclosure render prop to toggle from open to closed.
For more infomation, check out - https://nextjs.org/docs/api-reference/next/router#routerpush

When using TailwindCSS, how do I avoid the template from adding a divider to divide-y when rendering a list?

Divide-y serves to add dividers 'in between' stacked elements. When rendering a list with AlpineJS (x-for) and TailwindCSS, however, we find that the template tag is causing the CSS to add a divider there as well, which is an undesired effect.
Is there a Tailwind-way to prevent this?
e.g.
<ul role="list" class="divide-y divide-gray-200" x-data="taskModel()">
<template x-for="task in taskList">
<li class="py-4 flex">
<img class="h-10 w-10 rounded-full" src="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" alt="">
<div class="ml-3">
<p class="text-sm font-medium text-gray-900" x-text="task.name"></p>
<p class="text-sm text-gray-500" x-text="task.email"></p>
<p class="text-sm text-gray-500" x-text="task.due"></p>
</div>
</li>
</template>
</ul>
<script>
function taskModel() {
return {
taskList: [
{
name: 'Calvin Hawkins',
email: 'calvin.hawkins#example.com',
due: '2021-08-28'
}
]
};
}
</script>
Thank you!
This is a pretty well documented and accounted for issue. As described in the docs https://tailwindcss.com/docs/upgrading-to-v2#add-hidden-to-any-template-tags-within-space-or-divide-elements All you need to do is add the hidden attribute to your template tag.
In your case the code should be:
<ul role="list" class="divide-y divide-gray-200" x-data="taskModel()">
<!-- Add hidden attribute -->
<template x-for="task in taskList" hidden>
<li class="py-4 flex">
<img class="h-10 w-10 rounded-full" src="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" alt="">
<div class="ml-3">
<p class="text-sm font-medium text-gray-900" x-text="task.name"></p>
<p class="text-sm text-gray-500" x-text="task.email"></p>
<p class="text-sm text-gray-500" x-text="task.due"></p>
</div>
</li>
</template>
</ul>
<script>
function taskModel() {
return {
taskList: [
{
name: 'Calvin Hawkins',
email: 'calvin.hawkins#example.com',
due: '2021-08-28'
}
]
};
}
</script>

Resources