Deleting single value from Pinia reactive data store - javascript

I am creating a multistep register form in which I provide an avatar upload. Because it is a multistep form, I want to store the data in a Pinia store until the form finally gets submitted. Everything works fine so far. But I want to be able to delete the value that contains the Blob URL for the avatar, so the user can choose a different image. What I am trying to do is this userRegisterStore.cardOwner.avatar = '' cause the initial state of that value is just an empty string. But I get this error message:
runtime-core.esm-bundler.js:218 Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'avatar'
I also use cropperjs and vue-cropperjs. But I think that's irrelevant in this case.
I Googled all day and found nothing. So, I hope someone here can help.
[EDIT]
I created a codesandbox.io I hope it works. The first file you should see is RegisterFormFive.vue. To view it, you need to go to this link or use the integrated preview in codesandbox: https://n9dfv3-5173.preview.csb.app/register. Then upload an image, crop it (orange button beneath the image), and then try to delete it (red button)
Here's my code:
// RegisterDataStore.js
export const useRegisterDataStore = defineStore('RegisterDataStore', {
state: () => ({
imgReady: false,
cardOwner: reactive({
firstName: '',
lastName: '',
email: '',
password: '',
agbAccepted: false,
dsgvoAccepted: false,
title: '',
companyName: '',
companyPublic: false,
position: '',
positionPublic: false,
avatar: '',
addresses: [],
contacts: [],
links: [],
}),
}),
// Cropper part
<Cropper
v-if="registerDataStore.cardOwner.avatar && !registerDataStore.imgReady"
class="mx-auto max-h-[350px] max-w-[350px] overflow-hidden rounded-lg border-2 border-skin-primary bg-skin-primary"
ref="cropper"
alt="User avatar"
drag-mode="move"
:src="registerDataStore.cardOwner.avatar"
:aspect-ratio="1 / 1"
:crop-box-movable="false"
:crop-box-resizable="false"
:auto-crop-area="0.6"
:guides="false"
:movable="true"
:scalable="true"
:zoomable="true"
:zoo-on-touch="true"
:max-canvas-width="350"
:max-canvas-height="350"
:zoom-on-wheel="true"
:rotate-on-drag="false"
:rotatable="false"
:background="false"
:modal="true"
:initial-aspect-ration="1 / 1"
:view-mode="1"
></Cropper>
// Conponent script
<script setup>
import HeaderNav from '#/components/HeaderNav.vue'
import HeaderTitle from '#/components/HeaderTitle.vue'
import { useRegisterDataStore } from '#/stores/RegisterDataStore'
import Cropper from 'vue-cropperjs'
import 'cropperjs/dist/cropper.css'
import { ref } from 'vue'
import { useObjectUrl } from '#vueuse/core'
name: 'RegisterFormFive'
const registerDataStore = useRegisterDataStore()
const avatarInput = ref(null)
const cropper = ref(null)
const fileChanged = (event) => {
const file = event.target.files[0] || e.dataTrtansfer.files[0]
const reader = new FileReader()
reader.onload = (e) => {
registerDataStore.cardOwner.avatar = e.target.result
}
reader.readAsDataURL(file)
}
const deleteAvatar = (event) => {
registerDataStore.cardOwner.avatar = null
registerDataStore.imgReady = false
}
</script>
// The button that tiggers the storage
<div class="mt-4 flex justify-center">
<button
v-if="!registerDataStore.imgReady"
#click.prevent="
cropper.getCroppedCanvas().toBlob((blob) => {
registerDataStore.cardOwner.avatar = useObjectUrl(blob)
registerDataStore.imgReady = true
})
"
type="button"
class="hover:bg-skin-primary-dark inline-flex items-center rounded-md border border-transparent bg-skin-primary px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-skin-primary focus:ring-offset-2"
>
// The file input field
<input
type="file"
ref="avatarInput"
accept=".jpg,.png"
#change="fileChanged"
:style="{ display: 'none' }"
/>
// The button that should "delete" the value
<button
v-if="registerDataStore.imgReady"
#click.prevent="deleteAvatar"
type="button"
class="hover:bg-skin-primary-dark inline-flex items-center rounded-md border border-transparent bg-red-700 px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-skin-primary focus:ring-offset-2"
>
<IconWarning
class="mr-2 h-5 w-5 fill-current text-skin-primary"
aria-hidden="true"
/>
Bild löschen
</button>

Nested reactive isn't needed in state, Pinia state is already reactive. #click.prevent handler doesn't need to be created in a template, it doesn't affect how it works but makes debugging harder.
VueUse useObjectUrl composable is the problem. Due to how Vue reactive API works, refs are unwrapped inside reactive object. Since useObjectUrl returns readonly ref, it makes cardOwner.avatar property readonly and prevents from reassigning a value. Changing it would require the whole object to be reassigned:
registerDataStore.cardOwner = { ...registerDataStore.cardOwner, avatar: ... }
The actual problem is that useObjectUrl is misused. Since blob value doesn't change in the scope of then function, it can't benefit from being reactive. The composable should be replaced with the actual thing that it does:
registerDataStore.cardOwner.avatar = URL.createObjectURL(newObject)

Related

How to Dynamically add nodes in the graph using react-sigma

I'm new to Sigma and I'm trying to dynamically add a node to the graph but I'm receiving an error
react-dom.development.js:22839 Uncaught Error: Sigma: invalid graph instance.
The above error occurred in the <ForwardRef> component:
I want to create a simple react Sigma application where users can dynamically add nodes to the graph. In Chrome, I'm able to successfully add a single node, but the moment I update my input tag it breaks and prints the error messages shown above.
here below is my code
import React, { useState } from "react";
import Graph from "graphology";
import { SigmaContainer } from "#react-sigma/core";
const GraphPage = () => {
const [graph, setGraph] = useState(new Graph());
var i = 0;
const [node, setNode] = useState("");
const addNode = (e) => {
e.preventDefault();
console.log(
"just want to see what the e.target.node_name.value is : " +
e.target.node_name.value
);
const nodeName = e.target.node_name.value;
setGraph(
graph.addNode(nodeName, {
x: i,
y: i,
label: nodeName,
size: 15,
color: "#FA4F40",
})
);
setGraph(
graph.addNode(nodeName + i, {
x: i + 1,
y: i + 1,
label: nodeName + i,
size: 15,
color: "#FA4F40",
})
);
setGraph(
graph.addEdge(nodeName, nodeName + i, {
type: "arrow",
label: "works with",
size: 5,
})
);
i++;
};
return (
<div>
<h1>Graph</h1>
<h2>Add Node</h2>
<form onSubmit={addNode}>
<label
htmlFor="first_name"
className="block mb-2 text-sm font-medium text-gray-900 "
>
Node name
</label>
<input
type="text"
id="node_name"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
// value={name}
// onChange={handleChange}
required
/>
<button className="btn-start px-8">Submit</button>
</form>
<SigmaContainer
style={{ height: "1000px" }}
graph={graph}
></SigmaContainer>
</div>
);
};
export default GraphPage;
My guess is somewhere sigma doesn't like to rerender the graph. Not sure how to go about it, and I don't see any examples of anyone dynamically adding nodes. Any help would be appreciated. Thank you

Dynamically create object literals based on prop title and color

I am working on my Next.js website. Using Tailwind CSS I have managed to change the color dynamically before returning a component with static names & colors.
Now I am fetching data from the API and the title + color needs to be dynamically created with the values from the API.
the title prop equals the hardcoded project (1,2,3) name. The color should be color prop.
Is there an intelligent way to create an object literal with the dynamic data?
Hardcoded values work just perfectly fine.
import { motion } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';
import { projectsArrayProps } from '#typings/propTypes';
const Projects = ({ allProjects }) => {
console.log(allProjects);
return (
<div className="card--grid grid grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[100px] sm:auto-rows-[120px] md:auto-rows-[200px]">
{allProjects.map(
({ id, logo, title, color }: projectsArrayProps, index) => {
// const projectColor: { [key: string]: string } = {
// project1: 'bg-[#fbc340]/[0.07]',
// project2: 'bg-[#70d1db]/[0.07]',
// project3: 'bg-[#ea5b52]/[0.07]',
// project4: 'bg-[#ff1e00]/[0.07]',
// project5: 'bg-[#ff99f3]/[0.07]',
// };
const projectColor: { [key: string]: string } = {};
return (
<motion.div
key={id}
className={`rounded-md bg-[${projectColor['title']}]`}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{
duration: 0.1,
stiffness: 200,
delay: index * 0.085,
type: 'spring'
}}
variants={{
hidden: { opacity: 0, scale: 0.6 },
visible: { opacity: 1, scale: 1 }
}}
>
<Link href="">
<a
target="_blank"
className="flex flex-col items-center justify-center w-full h-full"
>
<div className="flex flex-col items-center justify-center relative w-full h-full max-w-[64px] md:max-w-[100px] lg:max-w-[120px]">
<img
className="object-contain"
src={logo.url}
alt={logo.alt}
/>
</div>
</a>
</Link>
</motion.div>
);
}
)}
</div>
);
};
export default Projects;
Tailwind extracts classes at build time, so you cannot create dynamic classes at runtime. You could safelist some classes to make sure they are created even when they are not present in the code at build time (https://tailwindcss.com/docs/content-configuration#safelisting-classes). Unfortunately this only helps you if you know all possible colors that can be returned from the API.

Invalid Quill container vue js

stuck while implementing [this quill to pdf][1] package to my laravel vue3 project. It is working fine to use, but when i try to save the rich text as pdf it i am getting stucked.
<form #submit.prevent="store">
<div class="flex flex-wrap -mb-8 -mr-6 p-8">
<text-input v-model="form.title" :error="form.errors.title" class="pb-8 pr-6 w-full lg:w-1/2" label="title" />
</div>
<div class="-mb-8 -mr-6 p-8 h-100">
<QuillEditor theme="snow" :toolbar="modules.toolbar" v-model:content="form.content" />
</div>
<div class="flex items-center justify-end px-8 py-4 bg-gray-50 border-t border-gray-100">
<loading-button :loading="form.processing" class="btn-indigo" type="submit">Create Organization</loading-button>
</div>
</form>
<button #click="saveAsFile()" class="btn-indigo">Get Pdf</button>
and vue js goes:
data() {
return {
loading: true,
form: this.$inertia.form({
title: null,
content: null,
}),
}
},
setup: () => {
//setTimeout(true, 4000)
var toolbarOptions = [['bold', 'italic'], ['link', 'image']];
const modules = {
name: 'blotFormatter',
toolbar: toolbarOptions,
module: BlotFormatter,
options: {/* options */}
}
return { modules }
},
methods: {
store() {
this.form.post('/templates')
},
async saveAsFile(){
const quillInstance = new Quill()
const delta = quillInstance.getContents(this.form.content); // gets the Quill delta
const pdfAsBlob = await pdfExporter.generatePdf(delta); // converts to PDF
saveAs(pdfAsBlob, 'pdf-export.pdf'); // downloads from the browser
}
},
i keep getting the error saying "Invalid Quill container" help me out please if you can!

Vue 3 wait for object to be fetched

I am new to Vuejs and would need some help, please. I am calling a component in order to create a 'member'. While calling this component and passing the member data, the component raises an error related to one of the functions inside. The issue seems that the member data is not being initially fulfilled considering that the member data is being fetched via an API. I have tried converting the function which causes the error to Asynchronous but that does not seem to work either. Below please find some code snippets for my issue.
Home.vue
<Member :member="memb" />
data() {
return {
memb: {},
};
},
//Method fetching the member from api
async fetchMember(memberId) {
const res = await axios.get(`http://localhost:3000/members/${memberId}`, { apiHeaders });
return res.data;
},
//Method which fetched the member
async created() {
this.memb = await this.fetchMember(1);
},
Member.vue
<template>
<div class="m-auto">
<p>Name: {{ member.name }}</p>
<p>Surname: {{ member.surname }}</p>
<p>Date of Birth: {{ formatDob(member.dob) }}</p>
<p>Age: {{ calculateAge(member.dob) }}</p>
<p>Relationship: {{ member.relationship }}</p>
<button class="bg-blue-500 py-1 px-4 rounded-md text-gray-100 text-sm mx-1 mt-3 font-bold" #click="$emit('get-member-id', member.id)">Update</button>
<button class="bg-red-500 py-1 px-4 rounded-md text-gray-100 text-sm mx-1 mt-3 font-bold" #click="$emit('delete-member', member.id)">Delete</button>
</div>
</template>
<script>
export default {
name: 'Member',
props: {
member: Object,
},
methods: {
// Converting the date of birth format
formatDob(dob) {
return dob.split('/').reverse().join('/');
},
// Calculating the date of birth by comparing today's date with the member's date of birth
calculateAge(dob) {
const birthdate = new Date(dob);
const todayDate = new Date();
const difference = todayDate - birthdate; // This is the difference in milliseconds
return Math.floor(difference / 31557600000);
},
},
};
</script>
The issue that I am getting is related to calling the formatDOB from the Member component. In fact the issue says: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'split')
Thanks in advance.
Will be cool if we can implement a progress indicator meant for informing the users about loading activity of data
Once the member data loads, you can start showing the member component and hide the progress indicator.
Pseudocode would look like something below.
<Member v-if="isLoaded" :member="memb" />
<ProgressBar v-if="!isLoaded" />
<script>
export default {
data: (() => {
return {
isLoaded: false,
memb: null
}
})
async mounted() {
this.isLoaded = false;
this.memb = await this.fetchMember(1);
this.isLoaded = true;
}
}
</script>

having problems with stripe/ redirecttocheckout

i have been trying to understand stripe for quite a while now. the major problem i am having is that i am a front end developer (with about a year of experience) and while i have some node.js/backend experience it is simply not enough to handle server processing of payments. i am going for the JAMstack serverless function approach using netlify. and thus far everything seems to be working out EXCEPT right at redirect to checkout i am getting the error "stripe.redirectToCheckout is not a function"
here is some of my code :
const inventory = require('./data/products.json');
exports.handler = async (event) => {
const { sku, quantity } = JSON.parse(event.body);
const product = inventory.find((p) => p.sku === sku);
const validatedQuantity = quantity > 0 && quantity < 2 ? quantity : 1;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
billing_address_collection: 'auto',
shipping_address_collection: {
allowed_countries: ['US'],
},
success_url: `${process.env.URL}/success`,
cancel_url: process.env.URL,
line_items: [
{
name: 'bitch',
currency:'USD',
amount: 299,
quantity: validatedQuantity,
},
],
});
return {
statusCode: 200,
body: JSON.stringify({
sessionId: session.id,
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
}),
};
};
^^^^this is where i create the checkout through a serverless function
although it took some time i have been able to create a lambda function through netlify, hide my public and private keys, create a stripe element, but i am just so confused as to why i am getting this error...
blow is where the error seems to be
//client sides
import Stripe from 'stripe'
export async function handleFormSubmission(event) {
event.preventDefault();
const form = new FormData(event.target);
const data = {
sku: form.get('sku'),
quantity: Number(form.get('quantity')),
};
const response = await fetch('/.netlify/functions/create-checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((res) => res.json());
const stripe=Stripe(response.publishableKey);
const {err}=await stripe.redirectToCheckout({
sessionId:response.sessionId
})
if(err){
console.log(err)
}
}
also if it is any help here is where i am calling the function (handleformsubmit or whatever)
import React from 'react'
import {loadStripe} from "#stripe/stripe-js"
import './Checkout.css'
import {useState} from 'react'
import {Elements,
CardElement,
useStripe,
useElements} from '#stripe/react-stripe-js'
import axios from 'axios'
import {loadProducts} from './load-products'
import {handleFormSubmission} from './stripe-purchase'
const stripePromise=loadStripe(`${process.env.STRIPE_}`)
const CheckoutForm=()=>{
const stripe=useStripe()
const elements=useElements()
const handleSubmit=async(e)=>{
e.preventDefault()
if (!stripe || !elements) {
// Stripe.js has not loaded yet. Make sure to disable
// form submission until Stripe.js has loaded.
return;
}
// Get a reference to a mounted CardElement. Elements knows how
// to find your CardElement because there can only ever be one of
// each type of element.
const cardElement = elements.getElement(CardElement);
const {error, paymentMethod}=await stripe.createPaymentMethod({
type:'card',
card:cardElement
})
loadProducts();
}
return (
<form onSubmit={handleFormSubmission} method='POST'>
<img className='checkoutImage' src='./logo2.png' />
<label class="ml-2 font-bold text-blue-700 text-md">
Enter card details below:
</label>
<fieldset className="my-2 FormGroup">
<div className="FormRow">
<CardElement options={{
style: {
base: {
fontSmoothing: 'antialiased',
fontWeight: 900,
iconColor: '#60A5FA',
fontSize: '30px',
color: '#374151',
'::placeholder': {
color: '#3182ce',
},
},
invalid: {
iconColor: '#EF4444',
color: '#DC2626',
},
complete:{
iconColor:'green',
color: 'green',
}
},
}}/>
</div>
</fieldset>
<div className='checkoutbuttonContainer'>
<button type="submit" className="scoreButtons scoreButtonGrey flex justify-center rounded-md border border-gray-300 bg-pink-600 shadow-sm px-4 py-2 bg-white text-base font-medium text-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 hover:bg-pink-500 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Submit Payment <span className='priceLine'></span> <span className='price'> </span>
</button>
</div>
</form>
)
}
const StripeSetup = () => {
return (
<div><Elements stripe={stripePromise}><CheckoutForm /></Elements></div>
)
}
export default StripeSetup
this is the first time in a while i have felt really overwhelmed by new material. im not even sure if i'm learning at a proper pace anymore nor even learning as much as just copying in code i find online lol... but this did take a good amount of work on my end nonetheless. would really love if someone could help. BTW i am now realizing that i created stripe elements etc which i don't even know if are necessary when using redirect to checkout as this is supposed to lead the client to a stripe checkout? can someone clarify all this for me. and please help if they can! thanks so much in advance
ONE last thing i wanted to say. i do not need a cart, products listing or anything. this is a one time payment for 2.99 and it will lead to the next page is the user submits the payment. not sure if that changes anything but figured the more details the better
Problem was in AWFUL documentation as well as netlify's instructions.
i needed to call loadstripe again first:
const stripe=await loadStripe(response.publishableKey);
const {err}=await stripe.redirectToCheckout({
sessionId:response.sessionId
})

Categories