`How apply multiple event on single button?` - javascript

How apply multiple event on single button in Vue.js?
I have a single button component
When a button is clicked in parent, several events are
must be applied
tytle should change from "Buy" to "In a basket"
icon shold apply done
style should change to button_1
my button component
<template>
<div class="btn"
#click="click"
:class="className">
<i class="material-icons"> {{ icon }} </i>
<span> {{ title }} </span>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: 'Buy'
},
disabled: {
type: Boolean,
default: false
},
button_1: {
type: Boolean,
default: false
},
icon: {
type: String,
default: ''
},
},
data() {
return {}
},
computed: {
className() {
return {
'btn__disabled': this.disabled,
'btn__button_1': this.button_1,
}
}
},
methods: {
click() {
this.$emit('click')
}
},
name: "BaseButton"
}
</script>
<style lang="scss" scoped>
.material-icons {
font-size: 16px;
}
.btn {
position: relative;
font-family: 'Merriweather', sans-serif;
color: var(--color-primary-light);
left: 45%;
bottom: 18%;
height: 48px;
width: 122px;
cursor: pointer;
background-color: var(--color-grey-light-4);
display: flex;
justify-content: space-evenly;
align-items: center;
&:hover,
&:active {
background-color: var(--color-grey-light-2);
border: none;
}
&__button_1 {
color: var(--color-primary-light);
background-color: var(--color-grey-light-3);
border: none;
}
&__disabled {
background-color: var(--color-grey-light-1);
border: none;
pointer-events: none;
}
}
</style>
my parent component
<template>
<base-button #click="clickBtn"></base-button>
</template>
<script>
import BaseButton from '../components/ui/BaseButton'
export default {
name: "GalleryOverview",
components: {BaseButton},
methods: {
clickBtn() {
console.log('BTN clicked')
}
}
}
}
</script>
How can I apply multiple event on single button?

You are almost done, as you are sending emit to parent component, you can use that to change.
So, first you will need to pass the required props to the child component, as:
<template>
<base-button #click="clickBtn" :title="title" :icon="icon" :button_1="button_1"></base-button>
</template>
<script>
import BaseButton from '../components/ui/BaseButton'
export default {
name: "GalleryOverview",
data() {
return {
title: 'My Title',
icon: '',
button_1: false
}
}
methods: {
// This is where you can change.
clickBtn() {
this.icon = 'change icon';
this.title = 'change title';
this.button_1 = true;
}
}
}
</script>
Now when you click the button it will change the title, icon and button_1.

Related

TypeError: this.$refs is not a function

So I have a problem with VueJs. I created a "Confirmation Dialogue" and added it to a On-Click-Event on buttons. It worked fine.
Now I tried to copy the implementation to add it to another button on a different parent. It says "TypeError: this.$refs.confirmDialogue.show is not a function" in the Console whenever I try to click the button. The other button still works completly normal.
Am I missing something? I already tried to remove the working button, so only one component uses the Confirm Dialogue but that also didn't work.
I'm new to VueJs. Hope someone can help me with this problem.
Child PopupModal:
<template>
<transition name="fade">
<div class="popup-modal" v-if="isVisible">
<div class="window">
<slot></slot>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'PopupModal',
data: () => ({
isVisible: false,
}),
methods: {
open() {
this.isVisible = true
},
close() {
this.isVisible = false
},
},
}
</script>
<style scoped>
/* css class for the transition */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.popup-modal {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem;
display: flex;
align-items: center;
z-index: 1;
}
.window {
background: #fff;
border-radius: 5px;
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
max-width: 480px;
margin-left: auto;
margin-right: auto;
padding: 1rem;
}
</style>
Parent ConfirmDialogue:
<template>
<popup-modal ref="popup">
<h2 style="margin-top: 0">{{ title }}</h2>
<p>{{ message }}</p>
<div class="btns">
<button class="cancel-btn" #click="_cancel">{{ cancelButton }}</button>
<span class="ok-btn" #click="_confirm">{{ okButton }}</span>
</div>
</popup-modal>
</template>
<script>
import PopupModal from "../confirmDialogue/PopupModal.vue"
export default {
name: 'ConfirmDialogue',
components: { PopupModal },
data: () => ({
// Parameters that change depending on the type of dialogue
title: undefined,
message: undefined, // Main text content
okButton: undefined, // Text for confirm button; leave it empty because we don't know what we're using it for
cancelButton: 'Abbrechen', // text for cancel button
// Private variables
resolvePromise: undefined,
rejectPromise: undefined,
}),
methods: {
show(opts = {}) {
this.title = opts.title
this.message = opts.message
this.okButton = opts.okButton
if (opts.cancelButton) {
this.cancelButton = opts.cancelButton
}
// Once we set our config, we tell the popup modal to open
this.$refs.popup.open()
// Return promise so the caller can get results
return new Promise((resolve, reject) => {
this.resolvePromise = resolve
this.rejectPromise = reject
})
},
_confirm() {
this.$refs.popup.close()
this.resolvePromise(true)
},
_cancel() {
this.$refs.popup.close()
this.resolvePromise(false)
// Or you can throw an error
// this.rejectPromise(new Error('User cancelled the dialogue'))
},
},
}
</script>
<style scoped>
.btns {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.ok-btn {
padding: 0.5em 1em;
background-color: #1F51FF;
color: #fff;
border: 2px solid #0ec5a4;
border-radius: 5px;
font-size: 16px;
text-transform: uppercase;
cursor: pointer;
}
.cancel-btn {
padding: 0.5em 1em;
background-color: #d5eae7;
color: #000;
border: 2px solid #0ec5a4;
border-radius: 5px;
font-size: 16px;
text-transform: uppercase;
cursor: pointer;
}
</style>
Working button:
<td class="last-td last-row">
<div class="button-wrapper">
<div class="wrapper-edit">
<button class="button button-edit">Bearbeiten</button>
</div>
<div class="wrapper-cancel">
<button class="button button-cancel" #click="doDelete">Löschen</button> <!-- Here is the working button -->
<confirm-dialogue ref="confirmDialogue"></confirm-dialogue>
</div>
</div>
</td>
</tr>
</thead>
</template>
<script>
import ConfirmDialogue from '../confirmDialogue/ConfirmDialogue.vue'
export default {
name: "bookingElements",
components: { ConfirmDialogue },
methods: {
async doDelete() {
const ok = await this.$refs.confirmDialogue.show({
title: 'Buchung löschen',
message: 'Sind Sie sicher, dass Sie die Buchung löschen wollen?',
okButton: 'Buchung löschen',
})
if (ok) {
alert('Die Buchung wurde Erfolgreich gelöscht')
}
},
Button that isn't working:
<td id="buttonCell">
<button class="button" #click="doDelete">Buchen</button>
<confirm-dialogue ref="confirmDialogue"></confirm-dialogue>
</td>
</tr>
</tbody>
</template>
<script>
import ConfirmDialogue from "../confirmDialogue/ConfirmDialogue.vue"
export default {
name: "bookElements",
components: { ConfirmDialogue },
methods: {
async doDelete() {
const ok = await this.$refs.confirmDialogue.show({
title: 'Buchung löschen',
message: 'Sind Sie sicher, dass Sie die Buchung löschen wollen?',
okButton: 'Buchung löschen',
})
if (ok) {
alert('Die Buchung wurde Erfolgreich gelöscht')
}
},
I got found the mistake.
I created a for-each loop to create the Table.
At the working button there was no for-each loop though.
So VueJs tried to generate the Confirmation Dialogue 9 times and this resulted to an error.
So I just need to put the
<confirm-dialogue ref="confirmDialogue"></confirm-dialogue>
to the top of Template like this
<template>
<confirm-dialogue ref="confirmDialogue"></confirm-dialogue>
...
</template>
because it's vanished and only shows when the button is clicked.

how to open definite element of tree

can you help me with this, i dont know how to open element of tree by vue js.i mean open definite element. if future every element of tree will be wraped by rouer-link, but right now i dont know how to trigger mechanism to open element.
tree example here
enter link description here
or here
[enter link description here][2]
let tree = {
label: 'root',
nodes: [
{
label: 'item1',
nodes: [
{
label: 'item1.1'
},
{
label: 'item1.2',
nodes: [
{
label: 'item1.2.1'
}
]
}
]
},
{
label: 'item2'
}
]
}
Vue.component('tree-menu', {
template: '#tree-menu',
props: [ 'nodes', 'label', 'depth' ],
data() {
return {
showChildren: false
}
},
computed: {
iconClasses() {
return {
'fa-plus-square-o': !this.showChildren,
'fa-minus-square-o': this.showChildren
}
},
labelClasses() {
return { 'has-children': this.nodes }
},
indent() {
return { transform: `translate(${this.depth * 50}px)` }
}
},
methods: {
toggleChildren() {
this.showChildren = !this.showChildren;
}
}
});
new Vue({
el: '#app',
data: {
tree
}
})
body {
font-family: "Open Sans", sans-serif;
font-size: 18px;
font-weight: 300;
line-height: 1em;
}
.container {
width: 300px;
margin: 0 auto;
}
.tree-menu {
.label-wrapper {
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid #ccc;
.has-children {
cursor: pointer;
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.3/vue.js"></script>
<div class="container">
<h4>Vue.js Expandable Tree Menu<br/><small>(Recursive Components)</small></h4>
<div id="app">
<tree-menu
:nodes="tree.nodes"
:depth="0"
:label="tree.label"
></tree-menu>
</div>
</div>
<script type="text/x-template" id="tree-menu">
<div class="tree-menu">
<div class="label-wrapper" #click="toggleChildren">
<div :style="indent" :class="labelClasses">
<i v-if="nodes" class="fa" :class="iconClasses"></i>
{{ label }}
</div>
</div>
<tree-menu
v-if="showChildren"
v-for="node in nodes"
:nodes="node.nodes"
:label="node.label"
:depth="depth + 1"
>
</tree-menu>
</div>
</script>

Flickity Events + Vue3 + TypeScript: Error - this$refs.flickity.on is not a function

I've created a custom Flickity.vue object for Vue 3 and TypeScript support, following the accepted answer here.
When I try to listen for events on my flickity carousel however, i'm plagued by runtime type errors in my console: this$refs.flickity.on is not a function
What's causing my current approach to break?
DisplayPage.vue
<template>
<div class="row">
<div class="col d-block m-auto">
<flickity ref="flickity" #init="onInit" :options="flickityOptions">
</flickity>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import Flickity from "./widgets/Flickity.vue";
export default defineComponent({
components: {
Flickity
},
data() {
return {
flickityOptions: {
initialIndex: 1,
prevNextButtons: true,
pageDots: true,
wrapAround: false,
}
};
},
methods: {
configureBankAccountCarousel() {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
(this.$refs.flickity as any).append(this.makeFlickityCell())
},
makeFlickityCell() {
const cell = document.createElement('div')
cell.className = 'carousel-cell'
cell.textContent = "Bank Acct"
}
},
mounted() {
this.configureBankAccountCarousel();
this.configureBankAccountCarousel();
this.$nextTick(() => {
// EVENTS
(this.$refs.flickity as any).on('ready', function () { //Type ERROR here
console.log('Flickity is ready!')
})
})
},
});
</script>
Flickity.vue
<template>
<div ref="root" class="flickity">
<slot />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue'
import Flickity from 'flickity'
export default defineComponent({
props: {
options: Object,
},
setup(props) {
let flickity: typeof Flickity | null = null
const root = ref<HTMLElement | null>(null)
onMounted(() => flickity = new Flickity(root.value as HTMLElement, props.options))
onUnmounted(() => flickity?.destroy())
return {
root,
append(element: HTMLElement) {
flickity?.append(element);
flickity?.select(-1)
}
}
},
})
</script>
<style scoped>
#import '~flickity/dist/flickity.css';
.flickity .carousel {
background: #EEE;
margin-bottom: 40px;
}
.flickity::v-deep .carousel-cell {
height: 200px;
width: 25%;
margin: 0 10px;
background: #6C6;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
}
.carousel-cell {
background-color: #248742;
width: 300px; /* full width */
height: 160px; /* height of carousel */
margin-right: 10px;
}
/* position dots in carousel */
.flickity-page-dots {
bottom: 0px;
}
/* white circles */
.flickity-page-dots .dot {
width: 12px;
height: 12px;
opacity: 1;
background: white;
border: 2px solid white;
}
/* fill-in selected dot */
.sr-flickity-page-dots .dot.is-selected {
background: white;
}
/* no circle */
.flickity-button {
background: transparent;
}
/* big previous & next buttons */
.flickity-prev-next-button {
width: 100px;
height: 100px;
}
/* icon color */
.flickity-button-icon {
fill: white;
}
/* hide disabled button */
.flickity-button:disabled {
display: none;
}
</style>
flickity.d.ts
interface IFlickity {
new (el: string | HTMLElement, options?: Record<string, unknown>): this;
append(element: HTMLElement);
destroy();
select(id: string | number);
}
declare module 'flickity' {
const Flickity: IFlickity;
export = Flickity;
}
this.$refs.flickity is the Flickity.vue component, and not the underlying flickity instance. Your Flickity.vue component has no on() method, which causes the error you observed.
You could expose the flickity instance's on() method via a component method in Flickity.vue:
// Flickity.vue
export default defineComponent({
setup() {
return {
on(eventName: string, listener: () => void) {
flickity?.on(eventName, listener)
}
}
}
})
And update the type declaration to add on():
// flickity.d.ts
interface IFlickity {
//...
on(eventName: string, listener: () => void)
}
declare module 'flickity' {
const Flickity: IFlickity;
export = Flickity;
}
demo

how to get id from notes[] array present inside Getnote.vue and based on that ID how to update a particular card?

I have some cards in my dashboard and in the dashboard I am getting some cards which contains title and description of the card, which I am getting from backend(axios.js).Upto display section i am getting, but in my case i want to update a particular card, when i click on any card the update popup card is coming. when i click on any card it opens popup card this card purpose is to update the content based on the card id(card details stored inside notes[] array in Getnote.vue),Here my question is How to get that particular card id when user clicks on card and how to connect this thing to the back end Api for update the particular note. please help me to fix this issue
Updatenote.vue
<template>
<div class="updatecard-notes">
<form class="updatecard-container" #submit.prevent="handleSubmit" id="update-id">
<input name="title" id="name-in" v-model="title" placeholder="Title" autocomplete="off" />
<textarea name="content" id="text-in" v-model="description" placeholder="Take a note..." autocomplete="off"></textarea>
<Icon />
<button #click="updateNotes()" type="submit">Close</button>
</form>
</div>
</template>
<script>
import service from '../../service/User'
import Icon from '../../components/pages/Icon.vue'
export default {
name: 'Updatenote',
components: {
Icon
},
data() {
return {
id:'',
title:'',
description:''
}
},
methods: {
updateNotes: function() {
const updateData ={
id:this.id,
title:this.title,
description:this.description
};
service.userUpdateNote(updateData).then(response => {
console.log("update",response);
localStorage.setItem('token',response.data.token);
this.notes.push(...response.data);
})
},
}
}
</script>
<style scoped>
.updatecard-container {
/* width: 100px; */
padding: 4px 10px;
font-size: 1rem;
/* margin-left: 200px; */
margin-bottom: -200px;
margin-top: -160px;
position: fixed;
width: 620px;
height: 150px;
box-shadow: 5px 5px 10px #e0dede;
/* margin-left: 731px; */
font-family: 'Times New Roman', Times, serif;
}
form textarea {
width: 100%;
border: none;
padding: 0px 10px;
outline: none;
font-size: 1rem;
resize: none;
}
form input {
width: 90%;
border: none;
padding: 4px 10px;
margin-bottom: 10px;
margin-left: 0px;
margin-top: 2px;
font-size: 1.1rem;
}
form button {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
right: 25px;
bottom: 10px;
background: white;
color: rgb(0, 0, 0);
border: none;
width: 50px;
height: 36px;
cursor: pointer;
font-size: 1.1rem;
font-family: 'Times New Roman', Times, serif;
}
</style>
User.js
import AxiosService from '../service/axios';
const axios = new AxiosService()
export default{
userRegister(data){
return axios.postData('/register', data);
},
userLogin(data){
return axios.postData("/login",data);
},
userForgotPassword(data){
return axios.postData("/auth/sendPasswordResetLink",data);
},
userResetPassword(data){
return axios.postData("/auth/resetPassword",data);
},
userCreateNote(data){
return axios.postData("/notes",data);
},
userGetNote(){
return axios.getData("/notes");
},
userUpdateNote(id, data){
console.log("ID: ",id);
return axios.putData(`/notes/${id}`, data);
}
}
axios.js
import axios from 'axios'
axios.defaults.baseURL=process.env.VUE_APP_ROOT_URL
axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('token');
export default class AxiosService{
postData(url, data){
return axios.post(url, data).then(response =>{
return response;
}).catch(error=>{
return error;
})
}
getData(url){
return axios.get(url).then(response =>{
return response;
}).catch(error=>{
return error;
})
}
putData(url, data){
return axios.put(url, data).then(response=>{
return response;
}).catch(error=>{
return error;
})
}
}
Getnotes.vue
<template>
<div class="carddisplay-notes">
<div class="carddisplay-container" v-for="note in notes" :key="note.data">
<div class="carddisplay" #click="togglePopup()">
<h3>{{note.title}}</h3>
<p>{{note.description}}</p>
</div>
<div class="carddisplay-icons">
<Icon />
<button class="card-button" type="button" v-if="flag" #click="handleSubmit();ToggleButton();">Close</button>
</div>
</div>
<div class="cardupdate-popup" id="popup">
<Updatenote />
</div>
</div>
</template>
<script>
import service from '../../service/User'
import Icon from '../../components/pages/Icon.vue'
import Updatenote from '../../components/pages/Updatenote.vue'
export default {
name: 'Getnote',
components: {
Icon, Updatenote
},
data() {
return {
flag: true,
notes:[{
id:1,
title: 'notes',
description: 'display notes'
},],
}
},
methods: {
ToggleButton(){
this.flag = !this.flag;
},
async handleSubmit() {
service.userGetNote().then(response => {
this.notes.push(...response.data);
})
},
togglePopup(){
var popup=document.getElementById('popup');
popup.classList.toggle('active');
}
}
}
</script>
<style lang="scss" scoped>
#import "#/SCSS/Getnote.scss";
</style>
In GetNotes.vue create a property in the data option, which will track the currently clicked card using the id and then pass that id to the UpdateNote component.
GetNotes.vue
<template>
<div class="carddisplay-notes">
<div class="carddisplay-container" v-for="note in notes" :key="note.id">
<div class="carddisplay" #click="togglePopup(note.id)">
<h3>{{note.title}}</h3>
<p>{{note.description}}</p>
</div>
<div class="carddisplay-icons">
<Icon />
<button class="card-button" type="button" v-if="flag" #click="handleSubmit();ToggleButton();">Close</button>
</div>
</div>
<div class="cardupdate-popup" id="popup">
<Updatenote :cardId="clickedCard"/>
</div>
</div>
</template>
<script>
import service from '../../service/User'
import Icon from '../../components/pages/Icon.vue'
import Updatenote from '../../components/pages/Updatenote.vue'
export default {
name: 'Getnote',
components: {
Icon, Updatenote
},
data() {
return {
flag: true,
notes:[{
id:1,
title: 'notes',
description: 'display notes'
},],
clickedCard: '',
}
},
methods: {
ToggleButton(){
this.flag = !this.flag;
},
async handleSubmit() {
service.userGetNote().then(response => {
this.notes.push(...response.data);
})
},
togglePopup(id){
var popup=document.getElementById('popup');
popup.classList.toggle('active');
this.clickedCard = id;
}
}
}
</script>
<style lang="scss" scoped>
#import "#/SCSS/Getnote.scss";
</style>
In Updatenote component use that id to make the api call. Here the cardId will contain the id of currently clicked card.
Updatenote.vue
<template>
<div class="updatecard-notes">
<form class="updatecard-container" #submit.prevent="handleSubmit" id="update-id">
<input name="title" id="name-in" v-model="title" placeholder="Title" autocomplete="off" />
<textarea name="content" id="text-in" v-model="description" placeholder="Take a note..." autocomplete="off"></textarea>
<Icon />
<button #click="updateNotes()" type="submit">Close</button>
</form>
</div>
</template>
<script>
import service from '../../service/User'
import Icon from '../../components/pages/Icon.vue'
export default {
name: 'Updatenote',
components: {
Icon
},
props: ['cardId'],
data() {
return {
title:'',
description:''
}
},
methods: {
updateNotes: function() {
const updateData ={
id:this.cardId,
title:this.title,
description:this.description
};
service.userUpdateNote(updateData).then(response => {
console.log("update",response);
localStorage.setItem('token',response.data.token);
this.notes.push(...response.data);
})
},
}
}
</script>
<style scoped>
...........
</style>

Property or method "message" is not defined on the instance but referenced during render

** Here's The Screenshot:-**
Here's The MessageComposer.vue:-
<template>
<div class="composer">
<textarea v-model="message" #keydown.enter="send" placeholder="Message..."></textarea>
</div>
</template>
<script>
export default {
data() {
return {
messsage: ''
};
},
methods: {
send(e) {
e.preventDefault();
if (this.message == '') {
return;
}
this.$emit('send', this.message);
this.message = '';
}
}
}
</script>
<style lang="scss" scoped>
.composer textarea {
width: 96%;
margin: 10px;
resize: none;
border-radius: 3px;
border: 1px solid lightgray;
padding: 6px;
}
</style>
Here's The Conversation.vue:-
<template>
<div class="conversation">
<h1>{{contact ? contact.name : 'Select a Contact'}}</h1>
<MessageFeed :contact="contact" :messages="messages"/>
<MessageComposer #send="sendMessage"/>
</div>
</template>
<script>
import MessageFeed from './MessageFeed.vue';
import MessageComposer from './MessageComposer.vue';
export default {
props: {
contact: {
type: Object,
default: null
},
messages: {
type: Array,
default: []
}
},
methods: {
sendMessage(text){
if (!this.contact) {
return;
}
axios.post('/conversation/send', {
contact_id: this.contact.id,
text: text
}).then((response) => {
this.$emit('new', response.data);
})
}
},
components: {MessageFeed, MessageComposer}
}
</script>
<style lang="scss" scoped>
.conversation {
flex: 5;
display: flex;
flex-direction: column;
justify-content: space-between;
h1 {
font-size: 20px;
padding: 10px;
margin: 0;
border-bottom: 1px dashed lightgray;
}
}
</style>
Here's The ChatApp.vue:-
<template>
<div class="chat-app">
<Conversation :contact="selectedContact" :messages="messages" #new="saveNewMessage"/>
<ContactsList :contacts="contacts" #selected="startConversationWith"/>
</div>
</template>
<script>
import Conversation from './Conversation.vue';
import ContactsList from './ContactsList.vue';
export default {
props: {
user:{
type: Object,
required: true
}
},
data() {
return{
selectedContact: null,
messages: [],
contacts: []
};
},
mounted() {
axios.get('/contacts')
.then((response) => {
this.contacts = response.data;
});
},
methods: {
startConversationWith(contact){
axios.get(`/conversation/${contact.id}`)
.then((response) => {
this.message = response.data;
this.selectedContact = contact;
})
},
saveNewMessage(text){
this.messages.push(text);
}
},
components: {Conversation, ContactsList}
}
</script>
<style lang="scss" scoped>
.chat-app {
display: flex;
}
</style>
I can't seem to find any error or mistake in the codes,
The mistake must probably be really silly (sorry for that).
I've been stuck on this since yesterday,
I hope this info is enough to resolve this error
I was just following a Tutorial, Please Help
-ThankYou
In your MessageComposer.vue, the return data
return {
messsage: '' // spelling error. Should be message
},

Categories

Resources