Flatlist have multiple textinput but state not updating - textinput

I need to ask how to change the state of every text Input if they are in a Flat List with each item separately. According to the item i want to store that text Input value. Here is my code. This is my code i need to save data and want to update the qty according to the item id.
constructor(props) {
super(props)
this.saveData = this.saveData.bind(this);
this.UpdateValue = this.UpdateValue.bind(this);
this.state = {
showToast: false,
qty: 0,
title: 'save',
product: [],
dataSource: [
{
id: 1, qty: 0, title: 'Black Hat', categoryId: 5, categoryTitle: 'MEN', price: 22, image: 'http://res.cloudinary.com/atf19/image/upload/c_crop,h_250,w_358,x_150/v1500465309/pexels-photo-206470_nwtgor.jpg', description: "Hello there, i'm a cool product with a heart of gold."
},
{ id: 2, qty: 0, title: 'V Neck T-Shirt', categoryId: 2, categoryTitle: 'WOMEN', price: 12, image: 'http://res.cloudinary.com/atf19/image/upload/c_crop,h_250,x_226,y_54/v1500465309/pexels-photo-521197_hg8kak.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 10, qty: 0, title: 'Black Leather Hat', categoryId: 1, categoryTitle: 'KIDS', price: 2, image: 'http://res.cloudinary.com/atf19/image/upload/c_crop,g_face,h_250,x_248/v1500465308/fashion-men-s-individuality-black-and-white-157675_wnctss.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 15, qty: 0, title: 'Long Sleeves T-Shirt', categoryId: 5, categoryTitle: 'MEN', price: 120, image: 'http://res.cloudinary.com/atf19/image/upload/c_crop,h_250,x_100,y_50/v1500465308/pexels-photo-500034_uvxwcq.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 11, qty: 0, title: 'Pink Diamond Watch', categoryId: 5, categoryTitle: 'MEN', price: 22, image: 'http://res.cloudinary.com/atf19/image/upload/c_crop,h_250/v1500465308/pexels-photo-179909_ddlsmt.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 22, qty: 0, title: 'Golden Tie', categoryId: 2, categoryTitle: 'WOMEN', price: 12, image: 'http://res.cloudinary.com/atf19/image/upload/c_scale,w_300/v1500284127/pexels-photo-497848_yenhuf.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 100, qty: 0, title: 'Black Pearl Earrings', categoryId: 1, categoryTitle: 'KIDS', price: 2, image: 'http://res.cloudinary.com/atf19/image/upload/c_crop,g_center,h_250/v1500465307/pexels-photo-262226_kbjbl3.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 215, qty: 0, title: 'Grey Blazer', categoryId: 5, categoryTitle: 'MEN', price: 120, image: 'http://res.cloudinary.com/atf19/image/upload/c_scale,w_300/v1500284127/pexels-photo-497848_yenhuf.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 12, qty: 0, title: 'Mirror Sunglasses', categoryId: 5, categoryTitle: 'MEN', price: 22, image: 'http://res.cloudinary.com/atf19/image/upload/c_crop,g_face,h_250/v1500465307/pexels-photo-488541_s0si3h.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 29, qty: 0, title: 'White Shirt', categoryId: 2, categoryTitle: 'WOMEN', price: 12, image: 'http://res.cloudinary.com/atf19/image/upload/c_scale,w_300/v1500284127/pexels-photo-497848_yenhuf.jpg', description: "Hello there, i'm a cool product with a heart of gold." },
{ id: 16, qty: 0, title: 'Tie', categoryId: 1, categoryTitle: 'KIDS', price: 2, image: 'http://res.cloudinary.com/atf19/image/upload/c_scale,w_300/v1500284127/pexels-photo-497848_yenhuf.jpg', description: "Hello there, i'm a cool product with a heart of gold." }
]
}
}
async saveData(item) {
console.log("We are in Save function")
var product = [];
var items = {
id: item.id,
title: item.title,
image: item.image,
price: item.price,
description: item.description,
qty: this.state.qty,
categoryTitle: item.categoryTitle,
}
const value1 = await AsyncStorage.getItem("Cart");
if (value1 === "" || value1 === null) {
product.push(items);
console.log(JSON.stringify(product));
AsyncStorage.setItem("Cart", JSON.stringify(product));
}
else if (items.qty <= 0 || items.qty == "" || items.qty == "undefined") {
//ToastAndroid.show('please enter quantity!', ToastAndroid.SHORT);
alert("pls enter quantity")
}
else {
var prodsArr = JSON.parse(value1);
for (var i = 0; i < prodsArr.length; i++) {
var item = prodsArr[i];
//console.log(JSON.stringify(item))
if (item.id === items.id) {
return (
this.setState({
}), alert(qty)
)
prodsArr.push(items)
AsyncStorage.setItem("Cart", JSON.stringify(prodsArr))
}
}
prodsArr.push(items);
console.log(JSON.stringify(prodsArr));
AsyncStorage.setItem("Cart", JSON.stringify(prodsArr))
ToastAndroid.show('item added!', ToastAndroid.SHORT);
}
}
UpdateValue = (update) => {
this.setState({ qty: update })
this.update = update;
}
renderItem = ({ item, index }) => {
return (
<ListItem>
<View>
<Image style={{ width: 280, height: 250 }} source={{ uri: item.image }} />
<KeyboardAvoidingView behavior="padding" style={{ flex: 1, alignItems: 'center', margin: 5 }}>
<TextInput
keyboardType="numeric"
multiline={false}
value={this.state.qty}
onChangeText={this.UpdateValue.bind(this)}
underlineColorAndroid="transparent" placeholder="0"
style={{ height: 30, width: 50, borderWidth: 0.5, borderColor: 'cyan', justifyContent: 'center' }}
/>
</KeyboardAvoidingView>
<Button title={this.state.title} onPress={this.saveData.bind(this, item)}> </Button>
<Button title="Cart" onPress={() => this.props.navigation.navigate('Second')} />
</View>
</ListItem>
);
}
render() {
return (
<View>
<FlatList
data={this.state.dataSource}
renderItem={this.renderItem.bind(this)}
keyExtractor={(item, index) => item.id}
keyboardShouldPersistTaps="always"
/>
</View>
)
}
};

Related

Mongoose - FindByIdAndUpdate() - How do I increment a field in an object array if the object exist or push the object to the array if it doesn't

When I add more items to the line_items array I want to either:
Add the item to array in the case that it isn't already there
Increment the quantity of the item in the case that it is already there
// the schema
{
_id: 1,
line_items: [
{ item_id: 101, color: 'white', quantity: 1 },
{ item_id: 101, color: 'green', quantity: 1 },
{ item_id: 102, color: 'white', quantity: 1 },
]
}
const cart_id = 1;
const new_item = { item_id: 101, color: 'white', quantity: 1 };
update_cart(cart_id, new_item);
async function update_cart(cart_id, new_item) {
await Cart.findByIdAndUpdate(cart_id, {}) // What do I do here??
}
(edit) PS: note that an item is uniquely identified by its id AND color, not the id alone

I'm trying to use async-react-select or react-select with fetching and updating details

const loadOptions = (searchValue, callback) => {
setTimeout(() => {
const filteredOptions = size.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
);
console.log("loadOptions", searchValue, filteredOptions);
callback(filteredOptions);
}, 2000);
};
Could someone please help me out, all I'm trying to do here is to be able to fetch and edit my product details, but with react-select I can't get it to work. please any recommendation on what to do here:
<label htmlFor="size">Size</label>
<AsyncSelect
loadOptions={loadOptions}
className="select-tag react-select"
id="size"
value={size}
onChange={(e) => setSize(e.target.value)}
isMulti
closeMenuOnSelect={false}
/>
from the image above for size i can't edit, select new options nor delete
here is my api call from my backend for creating the product:
//CREATE PRODUCT
productRouter.post(
"/",
isAuth,
isAdmin,
expressAsyncHandler(async (req, res) => {
const newProduct = new Product({
name: "sample name" + Date.now(),
slug: "sample-name-" + Date.now(),
keygen: "Men BK3569",
gender: "Male",
category: ["Men", "Dresses"],
size: [
{
value: "XS",
label: "XS",
},
{
value: "S",
label: "S",
},
{
value: "M",
label: "M",
},
{
value: "L",
label: "L",
},
{
value: "XL",
label: "XL",
},
{
value: "XXL",
label: "XXL",
},
],
color: ["fa-solid fa-circle cl-1", "fa-solid fa-circle cl-2"],
brand: ["ASOS"],
image: "/imgs/shirt1.png",
desc: "Sample Description",
price: 0,
countInStock: 0,
rating: 0,
numReviews: 0,
});
const product = await newProduct.save();
res.send({ message: "Sample Product Created Successfully", product });
})
);

I can see data from axios call in the console, but it won't render in my view (Vuejs) I've included what I'm seeing in the console (no errors)

This is an example of a div I'm trying to populate. This was working just fine yesterday and now all of a sudden I'm not seeing anything. I've included below what I'm seeing in the console.
<div class="card_title">Win a {{ prizes.name }}</div>
import axios from 'axios'
export default {
name: 'Prizesbyid',
props: {
prizeId: String
},
data () {
return {
prizes: [],
prize: {},
}
},
mounted () {
this.getPrizeById()
},
methods: {
getPrizeById () {
axios.get('http://localhost:3000/prizes/' + this.prizeId).then(response => {
this.prize = response.data.prize
this.name = response.data.name
console.log(response.data)
})
}
}
}
0: {id: 1, name: "Cordoba C5", description: "The Cordoba C5 Classical Guitar is perfect for any…tension, and low action make it a breeze to play.", image_url: "../assets/c5Cor.png", quantity: 5}
1: {id: 2, name: "Merano MC400 Cello", description: "Established in 2000, Merano have made it their mis… fine examples of professional equipment as well.", image_url: "../assets/cello.png", quantity: 3}
2: {id: 3, name: "Guarnerius del Gesu", description: "A repreduction of the most expensive violin in the…e Akiko Meyers, on loan for the rest of her life.", image_url: "../assets/gesu.png", quantity: 7}
3: {id: 4, name: "Japanese Shamisen", description: "The shamisen or samisen, also sangen, is a three-s…d as a suffix, according to regular sound change.", image_url: "../assets/shamisen.png", quantity: 2}
4: {id: 5, name: "Descant Lacewood Lute", description: "One of the most popular lutes is the Descant Lute.… overall length of the Descant Lute is 25 inches.", image_url: "../assets/lute.png", quantity: 2}
5: {id: 6, name: "Russian Balalaika", description: "The balalaika is a Russian stringed musical instru… and the third string is a perfect fourth higher.", image_url: "../assets/balalaika.png", quantity: 9}
6: {id: 7, name: "Pyrophone", description: "A pyrophone is a musical instrument in which notes…ylindrical glass tubes, creating light and sound.", image_url: "../assets/pyrophone.png", quantity: 3}
length: 7

Reconfigured Grid does not work on ExtJS 4.2

I'm trying to use a reconfigure grid on extjs 4.2, you can find its example with its original code here (go to Reconfigure Grid under Grids) but it doesn't find the file Office.js and Empolyee.js.
Code:
function init() {
Ext.application({
name: 'MyApp',
launch: function() {
const grid = Ext.define('KitchenSink.view.grid.Reconfigure', {
extend: 'Ext.container.Container',
requires: [
'Ext.grid.*',
'Ext.layout.container.HBox',
'Ext.layout.container.VBox',
'KitchenSink.model.grid.Office',
'KitchenSink.model.grid.Employee'
],
xtype: 'reconfigure-grid',
layout: {
type: 'vbox',
align: 'stretch'
},
width: 500,
height: 330,
lastNames: ['Jones', 'Smith', 'Lee', 'Wilson', 'Black', 'Williams', 'Lewis', 'Johnson', 'Foot', 'Little', 'Vee', 'Train', 'Hot', 'Mutt'],
firstNames: ['Fred', 'Julie', 'Bill', 'Ted', 'Jack', 'John', 'Mark', 'Mike', 'Chris', 'Bob', 'Travis', 'Kelly', 'Sara'],
cities: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Philadelphia', 'Phoenix', 'San Antonio', 'San Diego', 'Dallas', 'San Jose'],
departments: ['Development', 'QA', 'Marketing', 'Accounting', 'Sales'],
initComponent: function(){
Ext.apply(this, {
items: [{
xtype: 'container',
layout: 'hbox',
defaultType: 'button',
items: [{
itemId: 'showOffices',
text: 'Show Offices',
scope: this,
handler: this.onShowOfficesClick
}, {
itemId: 'showEmployees',
margin: '0 0 0 10',
text: 'Show Employees',
scope: this,
handler: this.onShowEmployeesClick
}]
}, {
margin: '10 0 0 0',
xtype: 'grid',
flex: 1,
columns: [],
viewConfig: {
emptyText: 'Click a button to show a dataset',
deferEmptyText: false
}
}]
});
this.callParent();
},
onShowOfficesClick: function(){
var grid = this.down('grid');
Ext.suspendLayouts();
grid.setTitle('Employees');
grid.reconfigure(this.createOfficeStore(), [{
flex: 1,
text: 'City',
dataIndex: 'city'
}, {
text: 'Total Employees',
dataIndex: 'totalEmployees',
width: 140
}, {
text: 'Manager',
dataIndex: 'manager',
width: 120
}]);
this.down('#showEmployees').enable();
this.down('#showOffices').disable();
Ext.resumeLayouts(true);
},
onShowEmployeesClick: function(){
var grid = this.down('grid');
Ext.suspendLayouts();
grid.setTitle('Employees');
grid.reconfigure(this.createEmployeeStore(), [{
text: 'First Name',
dataIndex: 'forename'
}, {
text: 'Last Name',
dataIndex: 'surname'
}, {
width: 130,
text: 'Employee No.',
dataIndex: 'employeeNo'
}, {
flex: 1,
text: 'Department',
dataIndex: 'department'
}]);
this.down('#showOffices').enable();
this.down('#showEmployees').disable();
Ext.resumeLayouts(true);
},
createEmployeeStore: function(){
var data = [],
i = 0,
usedNames = {},
name;
for (; i < 20; ++i) {
name = this.getUniqueName(usedNames);
data.push({
forename: name[0],
surname: name[1],
employeeNo: this.getEmployeeNo(),
department: this.getDepartment()
});
}
return new Ext.data.Store({
model: KitchenSink.model.grid.Employee,
data: data
});
},
createOfficeStore: function(){
var data = [],
i = 0,
usedNames = {},
usedCities = {};
for (; i < 7; ++i) {
data.push({
city: this.getUniqueCity(usedCities),
manager: this.getUniqueName(usedNames).join(' '),
totalEmployees: Ext.Number.randomInt(10, 25)
});
}
return new Ext.data.Store({
model: KitchenSink.model.grid.Office,
data: data
});
},
// Fake data generation functions
generateName: function(){
var lasts = this.lastNames,
firsts = this.firstNames,
lastLen = lasts.length,
firstLen = firsts.length,
getRandomInt = Ext.Number.randomInt,
first = firsts[getRandomInt(0, firstLen - 1)],
last = lasts[getRandomInt(0, lastLen - 1)];
return [first, last];
},
getUniqueName: function(used) {
var name = this.generateName(),
key = name[0] + name[1];
if (used[key]) {
return this.getUniqueName(used);
}
used[key] = true;
return name;
},
getCity: function(){
var cities = this.cities,
len = cities.length;
return cities[Ext.Number.randomInt(0, len - 1)];
},
getUniqueCity: function(used){
var city = this.getCity();
if (used[city]) {
return this.getUniqueCity(used);
}
used[city] = true;
return city;
},
getEmployeeNo: function() {
var out = '',
i = 0;
for (; i < 6; ++i) {
out += Ext.Number.randomInt(0, 7);
}
return out;
},
getDepartment: function() {
var departments = this.departments,
len = departments.length;
return departments[Ext.Number.randomInt(0, len - 1)];
}
});
Ext.create('Ext.container.Viewport', {
layout: 'border',
title: "",
region: 'center',
collapsible: false,
layout: 'fit',
items: {
items: grid
},
});
}
});
}
When I try to open it on Google Chrome, I get these errors:
I have already tried to download both files from here and I put them somewhere of my project folder but the issue still persists.
Also I've tried to remove KitchenSink.model.grid.Office and KitchenSink.model.grid.Office from requires and then it works but the buttons don't work. I get this when I click on them:
Note: I'm using Node.js with Express for displaying my website since it's going to have server connections. So that's my folders tree:
-assets
|-css
|-main.css
|-util.css
|-img
|-js
|-adminPage.js (My ExtJS file)
|-jquery-3.2.1.min.js
-views
|-adminPage.ejs (I call adminPage.js from this file)
|-login.ejs
-dbConnection.js
-server.js
What am I doing wrong?
To use view you need to do Ext.create instead of Ext.define.
Instantiate a class by either full name, alias or alternate name. If Ext.Loader is enabled and the class has not been defined yet, it will attempt to load the class via synchronous loading.
Ext.define
Defines a class or override. Doesn't return anything
Btw. you should require the view definitions in Ext.app.Controller, and use alias/xtype parameter for auto-creating views by shortcut.
function init() {
Ext.application({
name: 'MyApp',
launch: function() {
Ext.define('KitchenSink.view.grid.Reconfigure', {
extend: 'Ext.container.Container',
requires: [
'Ext.grid.*',
'Ext.layout.container.HBox',
'Ext.layout.container.VBox',
'KitchenSink.model.grid.Office',
'KitchenSink.model.grid.Employee'
],
xtype: 'reconfigure-grid',
layout: {
type: 'vbox',
align: 'stretch'
},
width: 500,
height: 330,
lastNames: ['Jones', 'Smith', 'Lee', 'Wilson', 'Black', 'Williams', 'Lewis', 'Johnson', 'Foot', 'Little', 'Vee', 'Train', 'Hot', 'Mutt'],
firstNames: ['Fred', 'Julie', 'Bill', 'Ted', 'Jack', 'John', 'Mark', 'Mike', 'Chris', 'Bob', 'Travis', 'Kelly', 'Sara'],
cities: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Philadelphia', 'Phoenix', 'San Antonio', 'San Diego', 'Dallas', 'San Jose'],
departments: ['Development', 'QA', 'Marketing', 'Accounting', 'Sales'],
initComponent: function(){
Ext.apply(this, {
items: [{
xtype: 'container',
layout: 'hbox',
defaultType: 'button',
items: [{
itemId: 'showOffices',
text: 'Show Offices',
scope: this,
handler: this.onShowOfficesClick
}, {
itemId: 'showEmployees',
margin: '0 0 0 10',
text: 'Show Employees',
scope: this,
handler: this.onShowEmployeesClick
}]
}, {
margin: '10 0 0 0',
xtype: 'grid',
flex: 1,
columns: [],
viewConfig: {
emptyText: 'Click a button to show a dataset',
deferEmptyText: false
}
}]
});
this.callParent();
},
onShowOfficesClick: function(){
var grid = this.down('grid');
Ext.suspendLayouts();
grid.setTitle('Employees');
grid.reconfigure(this.createOfficeStore(), [{
flex: 1,
text: 'City',
dataIndex: 'city'
}, {
text: 'Total Employees',
dataIndex: 'totalEmployees',
width: 140
}, {
text: 'Manager',
dataIndex: 'manager',
width: 120
}]);
this.down('#showEmployees').enable();
this.down('#showOffices').disable();
Ext.resumeLayouts(true);
},
onShowEmployeesClick: function(){
var grid = this.down('grid');
Ext.suspendLayouts();
grid.setTitle('Employees');
grid.reconfigure(this.createEmployeeStore(), [{
text: 'First Name',
dataIndex: 'forename'
}, {
text: 'Last Name',
dataIndex: 'surname'
}, {
width: 130,
text: 'Employee No.',
dataIndex: 'employeeNo'
}, {
flex: 1,
text: 'Department',
dataIndex: 'department'
}]);
this.down('#showOffices').enable();
this.down('#showEmployees').disable();
Ext.resumeLayouts(true);
},
createEmployeeStore: function(){
var data = [],
i = 0,
usedNames = {},
name;
for (; i < 20; ++i) {
name = this.getUniqueName(usedNames);
data.push({
forename: name[0],
surname: name[1],
employeeNo: this.getEmployeeNo(),
department: this.getDepartment()
});
}
return new Ext.data.Store({
model: KitchenSink.model.grid.Employee,
data: data
});
},
createOfficeStore: function(){
var data = [],
i = 0,
usedNames = {},
usedCities = {};
for (; i < 7; ++i) {
data.push({
city: this.getUniqueCity(usedCities),
manager: this.getUniqueName(usedNames).join(' '),
totalEmployees: Ext.Number.randomInt(10, 25)
});
}
return new Ext.data.Store({
model: KitchenSink.model.grid.Office,
data: data
});
},
// Fake data generation functions
generateName: function(){
var lasts = this.lastNames,
firsts = this.firstNames,
lastLen = lasts.length,
firstLen = firsts.length,
getRandomInt = Ext.Number.randomInt,
first = firsts[getRandomInt(0, firstLen - 1)],
last = lasts[getRandomInt(0, lastLen - 1)];
return [first, last];
},
getUniqueName: function(used) {
var name = this.generateName(),
key = name[0] + name[1];
if (used[key]) {
return this.getUniqueName(used);
}
used[key] = true;
return name;
},
getCity: function(){
var cities = this.cities,
len = cities.length;
return cities[Ext.Number.randomInt(0, len - 1)];
},
getUniqueCity: function(used){
var city = this.getCity();
if (used[city]) {
return this.getUniqueCity(used);
}
used[city] = true;
return city;
},
getEmployeeNo: function() {
var out = '',
i = 0;
for (; i < 6; ++i) {
out += Ext.Number.randomInt(0, 7);
}
return out;
},
getDepartment: function() {
var departments = this.departments,
len = departments.length;
return departments[Ext.Number.randomInt(0, len - 1)];
}
});
Ext.create('Ext.container.Viewport', {
layout: 'border',
title: "",
region: 'center',
collapsible: false,
layout: 'fit',
items: [{
xtype: 'reconfigure-grid'
}]
});
}
});
}

Change colour of money formatted cell to red if negative

I have a column which is formatted using the built in money formatter. I would like to change the text of the cell to red if the numeric value of the cell is negative. I can't create a custom formatter because the column formatter option is already set to money.
You have to use a custom formatter, Mimic the money formatter in this code. I can't think of anything else
let tabledata = [{
id: 1,
name: "Oli ",
money: 1,
col: "red",
dob: ""
},
{
id: 2,
name: "Mary ",
money: -1,
col: "blue",
dob: "14/05/1982"
},
{
id: 3,
name: "Christine ",
money: 0,
col: "green",
dob: "22/05/1982"
},
{
id: 4,
name: "Brendon ",
money: 10,
col: "orange",
dob: "01/08/1980"
},
{
id: 5,
name: "Margret ",
money: -10,
col: "yellow",
dob: "31/01/1999"
},
];
for (let i = 0; i < tabledata.length; i++) {
if (tabledata[i].money < 0) {
tabledata[i].money = "<span class='red'>$" +
tabledata[i].money +
"</span>"
} else {
tabledata[i].money = '$' + tabledata[i].money;
}
}
const table = new Tabulator("#example-table", {
data: tabledata,
layout: "fitColumns",
columns: [{
title: "id",
field: "id"
},
{
title: "name",
field: "name"
},
{
title: "money",
field: "money",
formatter: "html"
},
{
title: "col",
field: "col"
},
{
title: "dob",
field: "dob"
},
]
});
.red {
color: red;
}
<!DOCTYPE html>
<html lang="en">
<script src="https://unpkg.com/tabulator-tables#4.2.4/dist/js/tabulator.min.js"></script>
<link href="https://unpkg.com/tabulator-tables#4.2.4/dist/css/tabulator.min.css" rel="stylesheet" />
<body>
<div id="example-table"></div>
</body>
</html>
I think I found a solution for this problem:
formatter:function(cell,params,callback) {
let money = cell.getTable().modules.format.getFormatter("money");
cell.getElement().style...
return money(cell,params,callback);
}
https://jsfiddle.net/baumrock/vxpbhLsg/14/

Resources