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>
Related
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>
I'm building out filters for a job board where by I have three types of filters: location, category, and employment type. Switching between the categories filter sorts the job board between the selected category quite fine.
Location and Employment filters is where the issue occurs. They are both dropdown menus whereby the state renders nothing the first time you click them. Only when you click one of the dropdown options again does it actually render. I've attached photos to visually show what the job board looks like and the dropdown options, as well as the devtools open to show you what states render.
Here is the code sandbox for a more interactive look: codesandbox.io/s/mutable-pond-h08wst?file=/src/App.js
App.js
import React, {useState, useEffect} from 'react'
import jobData from "./assets/data.json"
import JobBoardComponent from './components/JobBoardComponent';
import NavBar from './components/NavBar'
import EmailBar from './components/EmailBar'
import BannerPage from "./components/BannerPage"
import FuncFilter from "./components/FuncFilter"
import LocFilter from "./components/LocFilter"
import ConFilter from './components/ConFilter';
import Filters from "./components/Filters"
function App() {
const [jobs, setJobs] = useState([])
const [filtered, setFiltered] = useState([])
const [activeCategory, setActiveCategory] = useState("all")
const [activeLocation, setActiveLocation] = useState("all")
const [activeContract, setActiveContract] = useState("all")
const [selectedId, setSelectedId] = useState("")
const sortedJobs = jobData.sort((a, b) => Number(b.id) - parseFloat(a.id))
useEffect(() => {
setJobs(sortedJobs)
}, [])
function handleClick(id) {
if (selectedId !== id) {
setSelectedId(id)
} else
setSelectedId("")
}
console.log(filtered)
return (
<div className="App w-auto">
<NavBar />
<BannerPage />
<EmailBar />
<Filters
jobs={jobs}
setFiltered={setFiltered}
activeLocation={activeLocation}
setActiveLocation={setActiveLocation}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
activeContract={activeContract}
setActiveContract={setActiveContract}
/>
<div>
{activeCategory === "all" ? jobs.map((job) => <JobBoardComponent handleClick={handleClick} selected={selectedId === job.id} job={job} key={job.id} />)
: filtered.map((job) => <JobBoardComponent handleClick={handleClick} selected={selectedId === job.id} job={job} key={job.id} />)}
</div>
</div>
);
}
export default App;
FuncFilter
import LocFilter from "./LocFilter"
import ConFilter from "./ConFilter"
function FuncFilter({setActiveCategory, activeCategory, activeLocation, setActiveLocation, activeContract, setActiveContract, setFiltered, jobs}) {
useEffect(() => {
if (activeCategory === "all") {
setFiltered(jobs)
return
}
const filtered = jobs.filter((job) => job.category.includes(activeCategory))
setFiltered(filtered)
}, [activeCategory])
const obj = jobs.map(function(job) {
return job.category
})
function markCategory() {
const marketingCount = []
for (const jobCat of obj) {
if (jobCat === "Marketing") {
marketingCount.push(jobCat)
}
}
return marketingCount.length
}
function cusCategory() {
const cusCount = []
for (const jobCat of obj) {
if (jobCat === "Customer Service") {
cusCount.push(jobCat)
}
}
return cusCount.length
}
function creativeCategory() {
const creativeCount = []
for (const jobCat of obj) {
if (jobCat === "Creative") {
creativeCount.push(jobCat)
}
}
return creativeCount.length
}
function webCategory() {
const webCount = []
for (const jobCat of obj) {
if (jobCat === "Web Development") {
webCount.push(jobCat)
}
}
return webCount.length
}
function salesCategory() {
const salesCount = []
for (const jobCat of obj) {
if (jobCat === "Sales") {
salesCount.push(jobCat)
}
}
return salesCount.length
}
function peopleCategory() {
const peopleCount = []
for (const jobCat of obj) {
if (jobCat === "People & HR") {
peopleCount.push(jobCat)
}
}
return peopleCount.length
}
function opsCategory() {
const opsCount = []
for (const jobCat of obj) {
if (jobCat === "Biz Ops") {
opsCount.push(jobCat)
}
}
return opsCount.length
}
function finCategory() {
const finCount = []
for (const jobCat of obj) {
if (jobCat === "Finance") {
finCount.push(jobCat)
}
}
return finCount.length
}
function proCategory() {
const proCount = []
for (const jobCat of obj) {
if (jobCat === "Product") {
proCount.push(jobCat)
}
}
return proCount.length
}
const resetLocation = event => {
setActiveLocation(1);
};
const resetContract = event => {
setActiveContract(1);
};
const resetCategory = event => {
setActiveCategory(1)
};
//1. Marketing DONE
// 2. Biz Ops DONE
// 3. Sales DONE
// 4. Finance DONE
// 5. Product
// 6. Web Development DONE
// 7. Creative DONE
// 8. People & HR DONE
// 9. Customer Service DONE
return (
<div className="mb-20 -mt-20">
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => setActiveCategory("all")}>All</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Marketing");}}>Marketing ({markCategory()}) </button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Customer Service");}}>Customer Service ({cusCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Creative");}}>Creative ({creativeCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Web Development");}}>Web Development ({webCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("People & HR");}}>People & HR ({peopleCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Sales");}}>Sales ({salesCategory()}) </button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Biz Ops");}}> Biz Ops ({opsCategory()}) </button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Finance");}}>Finance ({finCategory()}) </button>
<button className="hidden text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Product");}}> Product ({proCategory()}) </button>
<LocFilter
jobs={jobs}
setFiltered={setFiltered}
activeLocation={activeLocation}
setActiveLocation={setActiveLocation}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
activeContract={activeContract}
setActiveContract={setActiveContract}
/>
<ConFilter
jobs={jobs}
setFiltered={setFiltered}
activeLocation={activeLocation}
setActiveLocation={setActiveLocation}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
activeContract={activeContract}
setActiveContract={setActiveContract}
/>
</div>
)
}
export default FuncFilter
LocFilter.js
import { Fragment } from 'react'
import { Menu, Transition } from '#headlessui/react'
import { ChevronDownIcon } from '#heroicons/react/solid'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
function LocFilter({setActiveLocation, activeLocation, activeCategory, setActiveCategory, activeContract, setActiveContract, setFiltered, jobs}) {
useEffect(() => {
const filtered = jobs.filter((job) => job.location.includes(activeLocation))
setFiltered(filtered)
}, [activeLocation])
const resetContract = event => {
setActiveContract(1);
};
const resetCategory = event => {
setActiveCategory(1);
};
return (
<Menu as="div" className="mb-20 relative inline-block text-left">
<div>
<Menu.Button
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Locations
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
// active category cannot be 0
// if active category is a category, it works to select a location filter
// you cannot click that exact category right after though, as AC is labeled as that category
onClick={() => {resetCategory(); resetContract(); setActiveLocation("USA"); }}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
USA
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetContract(); setActiveLocation("Asia");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Asia
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetContract(); setActiveLocation("Europe");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Europe
</a>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)
}
export default LocFilter
ConFilter,js
import { Fragment } from 'react'
import { Menu, Transition } from '#headlessui/react'
import { ChevronDownIcon } from '#heroicons/react/solid'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
function ConFilter({ setActiveCategory, activeLocation, activeCategory, setActiveContract, setActiveLocation, activeContract, setFiltered, jobs}) {
useEffect(() => {
const filtered = jobs.filter((job) => job.contract.includes(activeContract))
setFiltered(filtered)
}, [activeContract])
const resetLocation = event => {
setActiveLocation(1);
};
const resetCategory = event => {
setActiveCategory(1);
};
return (
<Menu as="div" className="mb-20 relative inline-block text-left">
<div>
<Menu.Button
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Employment
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
// active category cannot be 0
// if active category is a category, it works to select a location filter
// you cannot click that exact category right after though, as AC is labeled as that category
onClick={() => {resetCategory(); resetLocation(); setActiveContract("Remote");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Remote
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetLocation(); setActiveContract("Part Time");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Part Time
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetLocation(); setActiveContract("Full Time");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Full Time
</a>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)
}
export default ConFilter```
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>
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