I have a Rails 7 app, where users can upload photos to a post. I've used Javascript (client side) to create the drag and drop upload file upload field, but it doesn't work when the page is loaded the first time. If you refresh the page, it works as expected.
I think this is something to do with Turbo, but I don't know how to fix it. I have tried adding <meta name="turbo-visit-control" content="reload"> but with no success.
Any help would be appreciated.
my file upload form and javascript:
<p class="text-2xl font-bold mb-4 text-blue-600 xl:pl-9">Add Some Photos</p>
<p class="text-l font-semibold mb-7 xl:pl-9">You can upload up to ten photos of your item.</p>
<article aria-label="File Upload Modal" class="xl:h-2/5 relative flex flex-col bg-white shadow-xl rounded-md xl:m-7" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);" ondragleave="dragLeaveHandler(event);" ondragenter="dragEnterHandler(event);">
<div id="overlay" class="w-full h-full absolute top-0 left-0 pointer-events-none z-50 flex flex-col items-center justify-center rounded-md">
<i>
<svg class="fill-current w-12 h-12 mb-3 text-blue-700" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19.479 10.092c-.212-3.951-3.473-7.092-7.479-7.092-4.005 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408zm-7.479-1.092l4 4h-3v4h-2v-4h-3l4-4z" />
</svg>
</i>
<p class="text-lg text-blue-700">Drop files to upload</p>
</div>
<section class="h-full overflow-auto p-8 w-full h-full flex flex-col">
<header class="border-dashed border-2 border-gray-400 py-12 flex flex-col justify-center items-center">
<p class="mb-3 font-semibold flex flex-wrap justify-center">
<span>Drag and drop your</span> <span>files anywhere or</span>
</p>
<%= f.file_field :photos, multiple: true, class: "hidden", id: "hidden-input", name: "part[photos][]" %>
<button id="button" type="button" class="mt-2 rounded-lg px-3 py-1 bg-blue-600 hover:bg-blue-700 font-bold text-white focus:shadow-outline focus:outline-none">
Upload a file
</button>
</header>
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-blue-600">
To Upload
</h1>
<ul id="gallery" class="flex flex-1 flex-wrap -m-1">
<li id="empty" class="h-full w-full text-center flex flex-col items-center justify-center items-center">
<img class="mx-auto w-32" src="https://user-images.githubusercontent.com/507615/54591670-ac0a0180-4a65-11e9-846c-e55ffce0fe7b.png" alt="no data" />
<span class="text-small">No files selected.</span>
</li>
</ul>
</section>
</article>
<template id="file-template">
<li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24">
<article tabindex="0" class="group w-full h-full rounded-md focus:outline-none focus:shadow-outline elative bg-gray-100 cursor-pointer relative shadow-sm">
<img alt="upload preview" class="img-preview hidden w-full h-full sticky object-cover rounded-md bg-fixed" />
<section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1 group-hover:text-blue-800"></h1>
<div class="flex">
<span class="p-1 text-blue-800">
<i>
<svg class="fill-current w-4 h-4 ml-auto pt-1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M15 2v5h5v15h-16v-20h11zm1-2h-14v24h20v-18l-6-6z" />
</svg>
</i>
</span>
<p class="p-1 size text-xs text-gray-700"></p>
<button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800">
<svg class="pointer-events-none fill-current w-4 h-4 ml-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="pointer-events-none" d="M3 6l3 18h12l3-18h-18zm19-4v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.316c0 .901.73 2 1.631 2h5.711z" />
</svg>
</button>
</div>
</section>
</article>
</li>
</template>
<template id="image-template">
<li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24">
<article tabindex="0" class="group hasImage w-full h-full rounded-md focus:outline-none focus:shadow-outline bg-gray-100 cursor-pointer relative text-transparent hover:text-white shadow-sm">
<img alt="upload preview" class="img-preview w-full h-full sticky object-cover rounded-md bg-fixed" />
<section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1"></h1>
<div class="flex">
<span class="p-1">
<i>
<svg class="fill-current w-4 h-4 ml-auto pt-" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M5 8.5c0-.828.672-1.5 1.5-1.5s1.5.672 1.5 1.5c0 .829-.672 1.5-1.5 1.5s-1.5-.671-1.5-1.5zm9 .5l-2.519 4-2.481-1.96-4 5.96h14l-5-8zm8-4v14h-20v-14h20zm2-2h-24v18h24v-18z" />
</svg>
</i>
</span>
<p class="p-1 size text-xs"></p>
<button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md">
<svg class="pointer-events-none fill-current w-4 h-4 ml-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="pointer-events-none" d="M3 6l3 18h12l3-18h-18zm19-4v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.316c0 .901.73 2 1.631 2h5.711z" />
</svg>
</button>
</div>
</section>
</article>
</li>
</template>
<script>
const photos = [];
// Get a reference to the file input element
const fileInput = document.getElementById('hidden-input');
// Listen for changes to the file input element
fileInput.addEventListener('change', function() {
// Get the list of selected files
const files = fileInput.files;
// Loop through the selected files
for (let i = 0; i < files.length; i++) {
// Add the file to the photos array
photos.push(files[i]);
}
});
const fileTempl = document.getElementById("file-template"),
imageTempl = document.getElementById("image-template"),
empty = document.getElementById("empty");
// use to store pre selected files
let FILES = {};
// check if file is of type image and prepend the initialied
// template to the target element
function addFile(target, file) {
const isImage = file.type.match("image.*"),
objectURL = URL.createObjectURL(file);
const clone = isImage
? imageTempl.content.cloneNode(true)
: fileTempl.content.cloneNode(true);
clone.querySelector("h1").textContent = file.name;
clone.querySelector("li").id = objectURL;
clone.querySelector(".delete").dataset.target = objectURL;
clone.querySelector(".size").textContent =
file.size > 1024
? file.size > 1048576
? Math.round(file.size / 1048576) + "mb"
: Math.round(file.size / 1024) + "kb"
: file.size + "b";
isImage &&
Object.assign(clone.querySelector("img"), {
src: objectURL,
alt: file.name
});
empty.classList.add("hidden");
target.prepend(clone);
FILES[objectURL] = file;
}
const gallery = document.getElementById("gallery"),
overlay = document.getElementById("overlay");
// click the hidden input of type file if the visible button is clicked
// and capture the selected files
const hidden = document.getElementById("hidden-input");
document.getElementById("button").onclick = () => hidden.click();
hidden.onchange = (e) => {
for (const file of e.target.files) {
addFile(gallery, file);
}
};
// use to check if a file is being dragged
const hasFiles = ({ dataTransfer: { types = [] } }) =>
types.indexOf("Files") > -1;
// use to drag dragenter and dragleave events.
// this is to know if the outermost parent is dragged over
// without issues due to drag events on its children
let counter = 0;
// reset counter and append file to gallery when file is dropped
function dropHandler(ev) {
ev.preventDefault();
for (const file of ev.dataTransfer.files) {
addFile(gallery, file);
overlay.classList.remove("draggedover");
counter = 0;
}
}
// only react to actual files being dragged
function dragEnterHandler(e) {
e.preventDefault();
if (!hasFiles(e)) {
return;
}
++counter && overlay.classList.add("draggedover");
}
function dragLeaveHandler(e) {
1 > --counter && overlay.classList.remove("draggedover");
}
function dragOverHandler(e) {
if (hasFiles(e)) {
e.preventDefault();
}
}
// event delegation to caputre delete events
// fron the waste buckets in the file preview cards
gallery.onclick = ({ target }) => {
if (target.classList.contains("delete")) {
const ou = target.dataset.target;
document.getElementById(ou).remove(ou);
gallery.children.length === 1 && empty.classList.remove("hidden");
delete FILES[ou];
}
};
function addFilesToPhotosArray() {
// Get the gallery element
const gallery = document.getElementById("gallery");
// Get the selected files from the gallery
const selectedFiles = gallery.querySelectorAll(".selected");
// Get the photos array from the input field
const photosArray = document.getElementById("hidden-input").value;
// Add the selected files to the photos array
for (const selectedFile of selectedFiles) {
photosArray.push(selectedFile);
}
// Update the input field with the new photos array
document.getElementById("hidden-input").value = photosArray;
}
// clear entire selection
document.getElementById("cancel").onclick = () => {
while (gallery.children.length > 0) {
gallery.lastChild.remove();
}
FILES = {};
empty.classList.remove("hidden");
gallery.append(empty);
};
</script>
</div>
Related
I am building a search input that fetchs data from my API and lists it in a dropdown list.
Here is the behavior I want my component to have:
If I start typing and my API founds data, it opens the dropdown menu and lists it.
If I click on one of the elements from the list, it is set as 'activeItem' and the dropdown list closes
Else, I can click out of the component (input and dropdown list) and the dropdown list closes
Else, no dropdown list appears and my input works like a regular text input
My issue has to do with Event Bubbling.
My list items (from API) have a #click input that set the clicked element as the 'activeItem'.
My input has both #focusin and #focusout events, that allow me to display or hide the dropdown list.
I can't click the elements in the dropdown list as the #focusout event from the input is being triggered first and closes the list.
import ...
export default {
components: {
...
},
props: {
...
},
data() {
return {
results: [],
activeItem: null,
isFocus: false,
}
},
watch: {
modelValue: _.debounce(function (newSearchText) {
... API Call
}, 350)
},
computed: {
computedLabel() {
return this.required ? this.label + '<span class="text-primary-600 font-bold ml-1">*</span>' : this.label;
},
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
},
methods: {
setActiveItem(item) {
this.activeItem = item;
this.$emit('selectItem', this.activeItem);
},
resetActiveItem() {
this.activeItem = null;
this.isFocus = false;
this.results = [];
this.$emit('selectItem', null);
},
},
emits: [
'selectItem',
'update:modelValue',
],
}
</script>
<template>
<div class="relative">
<label
v-if="label.length"
class="block text-tiny font-bold tracking-wide font-medium text-black/75 mb-1 uppercase"
v-html="computedLabel"
></label>
<div :class="widthCssClass">
<div class="relative" v-if="!activeItem">
<div class="flex items-center text-secondary-800">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5 ml-4 absolute"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<!-- The input that triggers the API call -->
<input
class="text-black py-2.5 pr-3.5 pl-10 text-black focus:ring-primary-800 focus:border-primary-800 block w-full rounded sm:text-sm border-gray-300"
placeholder="Search for anything..."
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="isFocus = false"
>
</div>
<!-- The Dropdown list -->
<Card
class="rounded-t-none shadow-2xl absolute w-full z-10 mt-1 overflow-y-auto max-h-48 px-0 py-0"
v-if="isFocus && results.length"
>
<div class="flow-root">
<ul role="list" class="divide-y divide-gray-200">
<!-- API results are displayed here -->
<li
v-for="(result, index) in results"
:key="index"
#click="setActiveItem(result)" <!-- The event I can't trigger -->
>
<div class="flex items-center space-x-4 cursor-pointer px-4 py-3">
<div class="flex-shrink-0">
<img
class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
:src="result.image ?? this.$page.props.page.defaultImage.url"
:alt="result.title"
/>
</div>
<div class="min-w-0 flex-1">
<p
class="truncate text-sm font-medium text-black"
:class="{
'text-primary-900 font-bold': result.id === activeItem?.id
}"
>
{{ result.title }}
</p>
<p class="truncate text-sm text-black/75">
{{ result.description }}
</p>
</div>
<div v-if="result.action">
<Link
:href="result.action?.url"
class="inline-flex items-center rounded-full border border-gray-300 bg-white px-2.5 py-0.5 text-sm font-medium leading-5 text-black/75 shadow-sm hover:bg-primary-50"
target="_blank"
>
{{ result.action?.text }}
</Link>
</div>
</div>
</li>
</ul>
</div>
</Card>
</div>
<!-- Display the active element, can be ignored for this example -->
<div v-else>
<article class="bg-primary-50 border-2 border-primary-800 rounded-md">
<div class="flex items-center space-x-4 px-4 py-3">
<div class="flex-shrink-0">
<img
class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
:src="activeItem.image ?? this.$page.props.page.defaultImage.url"
:alt="activeItem.title"
/>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-black font-bold">
{{ activeItem.title }}
</p>
<p class="truncate text-sm text-black/75 whitespace-pre-wrap">
{{ activeItem.description }}
</p>
</div>
<div class="flex">
<AppButton #click.stop="resetActiveItem();" #focusout.stop>
<svg
class="w-5 h-5 text-primary-800"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</AppButton>
</div>
</div>
</article>
</div>
</div>
</div>
</template>
Here is a look at the input:
With API results (can't click the elements):
When no data is found:
I tried:
handleFocusOut(e) {
console.log(e.relatedTarget, e.target, e.currentTarget)
// No matter where I click:
// e.relatedTarget = null
// e.target = <input id="search" class="...
// e.currentTarget = <input id="search" class="...
}
...
<input
id="search"
class="..."
placeholder="Search for anything..."
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="handleFocusOut($event)"
>
The solution:
relatedTarget will be null if the element you click on is not
focusable. by adding the tabindex attribute it should make the element
focusable and allow it to be set as relatedTarget. if you actually
happen to be clicking on some container or overlay element make sure
the element being clicked on has that tabindex="0" added to it so you
can maintain isFocus = true
Thanks to #yoduh for the solution
The root issue looks to be how the dropdown list is being removed from the DOM as soon as the input loses focus because of the v-if on it.
<Card
v-if="isFocus && results.length"
>
This is ok to have, but you'll need to work around it by coming up with a solution that keeps isFocus true whether the focus is on the input or the dropdown. I would suggest your input's #focusout to execute a method that only sets isFocus = false if the focus event's relatedTarget is not any of the dropdown items (can be determined via classname or other attribute). One roadblock to implementing this is that some elements aren't natively focusable, like <li> items, so they won't be set as the relatedTarget, but you can make them focusable by adding the tabindex attribute. Putting it all together should look something like this:
<input
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="loseFocus($event)"
/>
...
<li
v-for="(result, index) in results"
:key="index"
class="listResult"
tabindex="0"
#click="setActiveItem(result)"
>
loseFocus(event) {
if (event.relatedTarget?.className !== 'listResult') {
this.isFocus = false;
}
}
setActiveItem(item) {
this.activeItem = item;
this.isFocus = false;
this.$emit('selectItem', this.activeItem);
}
I am attempting to set up slot machine-style animation using Vue 3 TailwindCSS and HeadlessUI. So far, I have a basic green square set up to slide in from top and slide out the bottom based on cycles in a for-loop called when clicking the "click to transition" button. The resetIsShowing() function also handles the randomization of a sequential array (1-10). My goal is to display one of the 10 random numbers on the green square on each cycle. To do this, I refactored the main div displaying the square to pass a random number on each cycle:
<div v-for="number in numbers" :key="number.id" class="h-full w-full rounded-md bg-green-500 shadow-lg">
{{ number }}
</div>
But when I add list property to the above div, then I get the following error:
The current component <TransitionChild /> is rendering a "template".
However we need to passthrough the following props:
- ref
If I change "template" to "ref" in the TransitionRoot, then I lose the slide down effect that I am trying to achieve. How can I set this up so that numbers appear on the square on each cycle?
Here is the full code:
<template>
<div class="flex flex-col items-center py-16">
<div class="h-72 w-72">
<div class="flex justify-center items-center w-full h-full p-4 text-9xl rounded-md shadow-lg border-solid border-2 border-sky-500">
<TransitionRoot
appear
:show="isShowing"
as="template"
enter="transition ease-in-out duration-300 transform"
enter-from="-translate-y-full opacity-0"
enter-to="translate-y-0 opacity-100"
leave="transition ease-in-out duration-300 transform"
leave-from="translate-y-0 opacity-100"
leave-to="translate-y-full opacity-0"
>
<div v-for="number in numbers" :key="number.id" class="h-full w-full rounded-md bg-green-500 shadow-lg">
{{ number }}
</div>
</TransitionRoot>
</div>
</div>
<button
#click="resetIsShowing"
class="mt-8 flex transform items-center rounded-full bg-black bg-opacity-20 px-3 py-2 text-sm font-medium text-white transition hover:scale-105 hover:bg-opacity-30 focus:outline-none active:bg-opacity-40"
>
<svg viewBox="0 0 20 20" fill="none" class="h-5 w-5 opacity-70">
<path
d="M14.9497 14.9498C12.2161 17.6835 7.78392 17.6835 5.05025 14.9498C2.31658 12.2162 2.31658 7.784 5.05025 5.05033C7.78392 2.31666 12.2161 2.31666 14.9497 5.05033C15.5333 5.63385 15.9922 6.29475 16.3266 7M16.9497 2L17 7H16.3266M12 7L16.3266 7"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span class="ml-3">Click to transition</span>
</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { TransitionRoot } from '#headlessui/vue'
const isShowing = ref(true)
const numbers = ref([])
const numberArray = ref(Array.from({length: 10}, (e, i)=> i + 1))
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
async function resetIsShowing() {
for (let i = 0; i < numberArray.value.length; i++) {
const index = Math.floor(Math.random() * numberArray.value.length)
await sleep(800)
isShowing.value = false
setTimeout(() => {
isShowing.value = true
}, 500)
numbers.value = numberArray.value.filter(r => r === (index + 1))
}
}
</script>
RESOLVED
<template>
<div class="flex flex-col items-center py-16">
<div class="h-72 w-72">
<div class="flex justify-center items-center w-full h-full p-4 rounded-md shadow-lg border-solid border-2 border-sky-500">
<TransitionRoot
appear
:show="isShowing"
as="template"
enter="transition ease-in-out duration-[400ms]"
enter-from="-translate-y-full opacity-0"
enter-to="translate-y-0 opacity-100"
leave="transition ease-in-out duration-[400ms]"
leave-from="translate-y-0 opacity-100"
leave-to="translate-y-full opacity-0"
>
<div class="h-full w-full rounded-md pt-16">
<span v-if="numbers > 0" class="text-9xl">
{{ numbers }}
</span>
</div>
</TransitionRoot>
</div>
</div>
<button
#click="resetIsShowing"
class="mt-8 flex transform items-center rounded-full bg-black bg-opacity-20 px-3 py-2 text-sm font-medium text-white transition hover:scale-105 hover:bg-opacity-30 focus:outline-none active:bg-opacity-40"
>
<svg viewBox="0 0 20 20" fill="none" class="h-5 w-5 opacity-70">
<path
d="M14.9497 14.9498C12.2161 17.6835 7.78392 17.6835 5.05025 14.9498C2.31658 12.2162 2.31658 7.784 5.05025 5.05033C7.78392 2.31666 12.2161 2.31666 14.9497 5.05033C15.5333 5.63385 15.9922 6.29475 16.3266 7M16.9497 2L17 7H16.3266M12 7L16.3266 7"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span class="ml-3">Click to transition</span>
</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { TransitionRoot } from '#headlessui/vue'
const isShowing = ref(true)
const numbers = ref([])
const numberArray = ref(Array.from({length: 10}, (e, i)=> i + 1))
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
async function resetIsShowing() {
for (let i = 0; i < numberArray.value.length; i++) {
await sleep(1200)
const index = Math.floor(Math.random() * numberArray.value.length)
// const index = numberArray.value[i]
numbers.value = numberArray.value.filter(r => r === (index + 1))[0]
// numbers.value = numberArray.value.filter(r => r === (index))[0]
console.log(numbers.value)
isShowing.value = false
setTimeout(() => {
isShowing.value = true
}, 500)
}
}
</script>
I'm a React newbie, came across an interesting challenge to create "fake ecommerce page" with this api.
My Products.js file is this. I get api response and save it to a state(console logging it in one line to make sure it's working). You click on a SVG icon of + sign and that item gets "added" to the cart(don't want to add whole item for now, but just make it seems like it's adding it with setCartItems(cartItems + 1) so only number increases). Only thing is that the cart icon is in Header.js component, and I need to pass it there somehow. I've tried with props but couldn't get it to work.
Any help is very much appreciated since I want to learn from your code.
import React from 'react'
import {useState, useEffect} from 'react'
import axios from 'axios'
import plus from '../images/plus.svg'
export default function Products(props) {
const [products, setProducts] = useState([])
const [cartItems, setCartItems] = useState(0)
const updateCart = () => {
setCartItems(cartItems + 1)
console.log(cartItems)
}
useEffect(() => {
axios({
method: 'GET',
url: 'https://api.escuelajs.co/api/v1/products'
}).then((response) => {
setProducts(response.data)
console.log(response.data)
})
}, [])
return (
<section className="bg-white py-8">
<div className="container mx-auto flex items-center flex-wrap pt-4 pb-12">
<nav id="store" className="w-full z-30 top-0 px-6 py-1">
<div
className="w-full container mx-auto flex flex-wrap items-center justify-between mt-0 px-2 py-3">
<a className="uppercase tracking-wide no-underline hover:no-underline font-bold text-gray-800 text-xl "
href="#">
Store
</a>
<div className="flex items-center" id="store-nav-content">
<a className="pl-3 inline-block no-underline hover:text-black" href="#">
<svg className="fill-current hover:text-black" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24">
<path d="M7 11H17V13H7zM4 7H20V9H4zM10 15H14V17H10z"></path>
</svg>
</a>
<a className="pl-3 inline-block no-underline hover:text-black" href="#">
<svg className="fill-current hover:text-black" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24">
<path
d="M10,18c1.846,0,3.543-0.635,4.897-1.688l4.396,4.396l1.414-1.414l-4.396-4.396C17.365,13.543,18,11.846,18,10 c0-4.411-3.589-8-8-8s-8,3.589-8,8S5.589,18,10,18z M10,4c3.309,0,6,2.691,6,6s-2.691,6-6,6s-6-2.691-6-6S6.691,4,10,4z"></path>
</svg>
</a>
</div>
</div>
</nav>
{
products.slice(0, 20).map((product) => {
return (
<div className="w-full md:w-1/3 xl:w-1/4 p-6 flex flex-col" key={product.id}>
<div>
<img className="hover:grow hover:shadow-lg" src={product.category.image}
alt={product.description}/>
<div className="pt-3 flex items-center justify-between">
<p className="uppercase font-bold text-lg">{product.title}</p>
<img src={plus} width='20px' onClick={updateCart} alt={product.description}/>
</div>
<p className="pt-1 text-left text-gray-900">{product.price} €</p>
</div>
</div>
)
}
)
}
</div>
</section>
)
}
This is my Header.js file.
import React from 'react'
export default function Header() {
return (
<nav id="header" className="w-full z-30 top-0 py-1">
<div className="w-full container mx-auto flex flex-wrap items-center justify-between mt-0 px-6 py-3">
<label className="cursor-pointer md:hidden block">
<svg className="fill-current text-gray-900" xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 20 20">
<title>menu</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path>
</svg>
</label>
<input className="hidden" type="checkbox" id="menu-toggle" />
<div className="hidden md:flex md:items-center md:w-auto w-full order-3 md:order-1" id="menu">
<nav>
<ul className="md:flex items-center justify-between text-base text-gray-700 pt-4 md:pt-0">
<li><a className="inline-block no-underline hover:text-black hover:underline py-2 px-4" href="#">Shop</a></li>
<li><a className="inline-block no-underline hover:text-black hover:underline py-2 px-4" href="#">About</a></li>
</ul>
</nav>
</div>
<div className="order-1 md:order-2">
<a className="flex items-center tracking-wide no-underline hover:no-underline font-bold text-gray-800 text-xl " href="#">
<svg className="fill-current text-gray-800 mr-2" xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24">
<path d="M5,22h14c1.103,0,2-0.897,2-2V9c0-0.553-0.447-1-1-1h-3V7c0-2.757-2.243-5-5-5S7,4.243,7,7v1H4C3.447,8,3,8.447,3,9v11 C3,21.103,3.897,22,5,22z M9,7c0-1.654,1.346-3,3-3s3,1.346,3,3v1H9V7z M5,10h2v2h2v-2h6v2h2v-2h2l0.002,10H5V10z"></path>
</svg>
NORDICS
</a>
</div>
<div className="order-2 md:order-3 flex items-center" id="nav-content">
<a className="inline-block no-underline hover:text-black" href="#">
<svg className="fill-current hover:text-black" xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24">
<circle fill="none" cx="12" cy="7" r="3"></circle>
<path d="M12 2C9.243 2 7 4.243 7 7s2.243 5 5 5 5-2.243 5-5S14.757 2 12 2zM12 10c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3S13.654 10 12 10zM21 21v-1c0-3.859-3.141-7-7-7h-4c-3.86 0-7 3.141-7 7v1h2v-1c0-2.757 2.243-5 5-5h4c2.757 0 5 2.243 5 5v1H21z"></path>
</svg>
</a>
<a className="pl-3 inline-block no-underline hover:text-black" href="#">
<svg className="fill-current hover:text-black" xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24">
<path d="M21,7H7.462L5.91,3.586C5.748,3.229,5.392,3,5,3H2v2h2.356L9.09,15.414C9.252,15.771,9.608,16,10,16h8 c0.4,0,0.762-0.238,0.919-0.606l3-7c0.133-0.309,0.101-0.663-0.084-0.944C21.649,7.169,21.336,7,21,7z M17.341,14h-6.697L8.371,9 h11.112L17.341,14z"></path>
<circle cx="10.5" cy="18.5" r="1.5"></circle>
<circle cx="17.5" cy="18.5" r="1.5"></circle>
</svg>
<div className="inline-flex absolute top-4 justify-center items-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">NUMBER OF ITEMS IN CART GOES HERE</div>
</a>
</div>
</div>
</nav>
)
}
And finally, if needed, this is my App.js file.
import './App.css';
import About from './components/About';
import Header from './components/Header';
import Hero from './components/Hero';
import Products from './components/Products';
function App() {
return (
<div className="App">
<Header />
<Hero />
<Products />
<About />
</div>
);
}
export default App;
I provide two methods for your refrence:
1.By using props:
just put your state and set function at their father element, in your situation you can put in App.js, then props to any components you want:
App.js
import './App.css';
import About from './components/About';
import Header from './components/Header';
import Hero from './components/Hero';
import Products from './components/Products';
import axios from 'axios'
function App() {
const [products, setProducts] = useState([]) // put here
const [cartItems, setCartItems] = useState(0) // put here
// get your api here
useEffect(() => {
axios({
method: 'GET',
url: 'https://api.escuelajs.co/api/v1/products'
}).then((response) => {
setProducts(response.data)
})
}, [])
return (
<div className="App">
<Header setCartItems={setCartItems} cartItems={cartItems}/>
<Hero />
<Products cartItems={cartItems} setCartItems={setCartItems} products={products} setProducts={setProducts}/>
<About />
</div>
);
}
export default App;
after then in your Products.js and Header.js can get props data, and can get props set feunction to update state.
But I think put global state is better,
I recommend useContext or zustand to do it!
2.Put in zustand:
create file named store.js, then npm install zustand
store.js
import create from 'zustand';
// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useStore = create((set, get) => ({
cartItems: 0,
setCartItems: (value) =>
set((state) => ({
cartItems: value,
})),
}));
export default useStore;
Product.js and Header.js add below code:
import useStore from './store'; // import the store you writed before
export default function Products(props) {
// get your store state and function
const { cartItems, setCartItems } = useStore();
// update your store state
const updateCart = () => {
setCartItems(cartItems + 1)
}
const minusCart = () => {
setCartItems(cartItems - 1)
}
//...your other code
}
You either need to use global state management like redux or pass a callback function from the header to the products component to update the state. React is unidirectional data flow downward, so you can't pass props back up like you can from parent to child.
I have 3 button. I want to add red background when one of them is clicked. By default the first button is active. When I click on the second or the third one first button should lose it's background styles red to gray. Also I want to add slide in animation. ex, If I'm currently on first button and click on the 2nd button, background should animate left to right.
My current code:
import React from "react";
const TopOptions = () => {
return (
<>
<div className="flex flex-col border-b shadow-lg">
<div className="flex flex-col px-4">
<p className="text-xs pl-6 pt-4">PUNE</p>
<div className="flex flex-row items-center">
<select className="pl-2">
<option value="Kothrud Outlet">Kothrud Outlet</option>
</select>
</div>
</div>
<div className=" bg-gray-100 my-5 text-center flex flex-row items-center justify-evenly text-sm rounded-md mx-4">
<button className="w-full py-3 font-bold ">DELIVERY</button>
<button className="w-full py-3 font-bold text-gray-600">
TAKEAWAY
</button>
<button className="w-full py-3 font-bold text-gray-600">
DINE IN
</button>
</div>
</div>
</>
);
};
export default TopOptions;
You can use state for each individual button's class. After clicking each button you have to change that button's state as per following:
import React, {useState} from "react";
const TopOptions = () => {
const [btnClass, setBtnClass] = useState({"delivery": "btn-danger", "takeaway": "", "dinein": ""});
const handleClick = (e) => {
// after handling the click event add following code
let updatedBtnClass = {"delivery": "", "takeaway": "", "dinein": ""};
updatedBtnClass[e.target.name] = "btn-danger"; // for red background class
setBtnClass(updatedBtnClass);
}
return (
<>
<div className="flex flex-col border-b shadow-lg">
<div className="flex flex-col px-4">
<p className="text-xs pl-6 pt-4">PUNE</p>
<div className="flex flex-row items-center">
<select className="pl-2">
<option value="Kothrud Outlet">Kothrud Outlet</option>
</select>
</div>
</div>
<div
className=" bg-gray-100 my-5 text-center flex flex-row items-center justify-evenly text-sm rounded-md mx-4">
<button name={"delivery"} className={"w-full py-3 font-bold " + btnClass.delivery} onClick={handleClick}>
DELIVERY
</button>
<button name={"takeaway"} className={"w-full py-3 font-bold " + btnClass.takeaway} onClick={handleClick}>
TAKEAWAY
</button>
<button name={"dinein"} className={"w-full py-3 font-bold " + btnClass.dinein} onClick={handleClick}>
DINE IN
</button>
</div>
</div>
</>
);
};
export default TopOptions;
I would approach this by giving each button a number, initialise a state variable with the number of the first button and change this state on each btn press:
import React, {useState} from "react";
export const TopOptions = () => {
const [clickedButton, setClickedButton] = useState(0);
const [yPos, setYPos] = useState(0);
const buttons = ["DELIVERY", "TAKEAWAY", "DINE IN"];
const speed = 10;
let direction = 0;
const updateState = () => {
setYPos(yPos + (speed * direction));
}
const clickHandler = (index) => {
direction = index > clickedButton ? 1 : -1;
let duration = Math.abs(clickedButton - index) * 1000; // 1sec
setClickedButton(index);
const animRef = requestAnimationFrame(updateState);
setTimeout(() => {
cancelAnimationFrame(animRef);
}, duration);
}
return (
<>
<div className="flex flex-col border-b shadow-lg">
<div className="flex flex-col px-4">
<p className="text-xs pl-6 pt-4">PUNE</p>
<div className="flex flex-row items-center">
<select className="pl-2">
<option value="Kothrud Outlet">Kothrud Outlet</option>
</select>
</div>
</div>
<div
className=" bg-gray-100 my-5 text-center flex flex-row items-center justify-evenly text-sm rounded-md mx-4">
{buttons.map((btn, index) => (
<button className={"w-full py-3 font-bold " + (clickedButton === index ? "" : "text-gray-600")}
onClick={() => {
clickHandler(index);
}} key={"btn" + index}
>{btn}</button>
))}
</div>
<image src={"yourbackgroundimage.jpg"}
style={"position:fixed; width: 100vw; height: 100vh; top: 0; left: " + yPos}/>
{
/*
This is not a god example, just to show how to use yPos now.
assuming left btn is curently active:
if clicked on middle button it lets the image slide from left to right for one second
if clicked on right button it lets the image slide from left to right for two second
*/
}
</div>
</>
);
};
I'm trying to maintain a track of use inputs with react's use state.
for some reason this is quite difficult.
the function i'm using right now is,
function handleClick(e) {
setAnswers([...answers, answer ]);
// setTheArray([, newElement]);
setAnswer()
setTime(true);
setStage(Stage + 1);
e.preventDefault();
console.log("The link was clicked.");
console.log(answers);
}
the input section looks like this,
<div className=" items-center ">
<label for="answer" className="sr-only font-thin px-2 pb-4 text-lg">
Answer
</label>
<input
onChange={(e) => setAnswer(e.target.value)}
type="text"
value={answer}
name="answer"
id="answer"
className=" items-center shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Enter Here"
/>
</div>
and the button
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Submit </span>
</button>
Essentially I want to keep track of the user inputs over time - I could just create a class based component, but i'd rather just use functions if possible.
This is the entire component,
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { requireAuth } from "util/auth.js";
import Sidebar_Users from "components/dashboard/Sidebar/Sidebar_Users";
import MindVaultSection from "components/dashboard/MindVault/MindVaultSection";
import BottomNavigation from "../../components/dashboard/BottomNavigation/BottomNavigation";
/*
This page takes a measurement of someone's working memory,
being how many digits they
are able to remember in a set time period.
To do this there needs to be:
- Digits made randomly,
- Timer,
- Input for user answers based on the progress through the task,
- Record of medication, or lack thereof
Ideally doing 6 tests total - sending data at each point if possible, perhaps just as a total query at end though.
User Flow
- Initial Component
- Component for remembering Text
- Component for entering
- Rest Component
*/
function makeid(length) {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function ButtonBaseline(props) {
const { time, Colour, handleClick, Stage, onSubmit } = props;
if (time == false)
return (
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Get Started</span>
</button>
);
else if (time == "input" && Stage < 6)
return (
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Next Step </span>
</button>
);
else if (time == "input" && Stage == 6)
return (
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Submit </span>
</button>
);
else
return (
<div className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none">
Waiting!
</div>
);
}
function Baseline(props) {
const [Stage, setStage] = useState(0); // walkthrough Status
const [time, setTime] = useState(false); //timer off, on, or in input stage.
const [answer, setAnswer] = useState();
const [answers, setAnswers] = useState([]);
// RandomString generates a random string of length n
var numbers = makeid(0 + 2 * Stage);
//qSLeD9D0PlSE
useEffect(() => {
//this sets the time to wait, being 3 seconds
if (time) {
const timeout = setTimeout(() => {
// do anything, this block runs after the timeout has "expired"
// could even set state
setTime("input");
console.log("The timeout was called.");
}, 3000); // timeout expires in 6000 ms
// make sure to clear the timeout on component unmount to avoid memory loss issues
return () => clearTimeout(timeout);
// this callback function runs only on component unmount, not re-renders
}
}, [time]);
function handleOnChange(e) {
setAnswer(e.target.value);
}
function handleClick(e) {
setAnswers([...answers, answer ]);
// setTheArray([, newElement]);
setAnswer()
setTime(true);
setStage(Stage + 1);
e.preventDefault();
console.log("The link was clicked.");
console.log(answers);
}
let testInput;
if (time == true) {
testInput = (
<>
<div>Stage {Stage}</div>
<div className="underline font-light items-center justify-center">
Hold the numbers in your head!
</div>
<div className="uppercase bg-white tracking-widest text-center font-bold text-5xl shadow p-4 items-center">
{numbers}
</div>
</>
);
} else if (time == "input") {
testInput = (
<>
<div>Stage {Stage}</div>
<div className="font-thin px-2 pb-4 text-lg">Enter Numbers</div>
<div className="bg-white rounded-lg shadow p-4 items-center justify-center">
<div className=" items-center ">
<label for="answer" className="sr-only font-thin px-2 pb-4 text-lg">
Answer
</label>
<input
onChange={handleOnChange}
type="text"
value={answer}
name="answer"
id="answer"
className=" items-center shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Enter Here"
/>
</div>
</div>
</>
);
} else if (time == false) {
testInput = (
<>
<p className="text-lg leading-7 text-gray-500 mb-5">
You are about to do a small short term memory test. A few letters will
flash on your computer monitor for 3 seconds. Your job is to write
down as many letters as you can remember
</p>
</>
);
}
return (
<>
<Sidebar_Users dashboard={"Progress Map"}>
<div className="py-8 min-h-full container mx-auto ">
<div className="mt-2 mb-8 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl sm:leading-10">
Working Memory Test
</div>
{testInput}
<ButtonBaseline
time={time}
Stage={Stage}
handleClick={handleClick}
// onSubmit={onSubmit}
/>
</div>
</Sidebar_Users>
</>
);
}
export default Baseline;
Cheers!
Here is how you would keep track of all your answers. Since you don't have all your code dealing with stages I can't make this answer more specific
but, if this is not enough letme know where to elaborate.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [answers, setAnswers] = useState([]);
const [answer, setAnswer] = useState("");
const [isSubmit, setIsSubmit] = useState(false);
useEffect(() => {
if (answers.length === 6) {
//here you would fire your submit to server
setIsSubmit(true);
}
}, [answers]);
function handleClick(e) {
if (!isSubmit) setAnswers([...answers, answer]);
}
function handleOnChange(e) {
setAnswer(e.target.value);
}
return (
<div className="App">
<input value={answer} onChange={handleOnChange} />
<button onClick={(e) => handleClick(e)}>Submit</button>
<ul>
{answers.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
{isSubmit ? "Has Submitted" : "Has not Submitted"}
</div>
);
}
Update: Added some stage logic