Working form and selecting photos

master
franciszek 4 years ago
parent 724b047eaa
commit 2082dc1866

43
package-lock.json generated

@ -1061,6 +1061,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/caseless": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==",
"dev": true
},
"@types/connect": { "@types/connect": {
"version": "3.4.34", "version": "3.4.34",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
@ -1198,9 +1204,9 @@
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q=="
}, },
"@types/node": { "@types/node": {
"version": "14.14.19", "version": "14.14.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz",
"integrity": "sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ==" "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A=="
}, },
"@types/prop-types": { "@types/prop-types": {
"version": "15.7.3", "version": "15.7.3",
@ -1239,6 +1245,31 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/request": {
"version": "2.48.5",
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz",
"integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==",
"dev": true,
"requires": {
"@types/caseless": "*",
"@types/node": "*",
"@types/tough-cookie": "*",
"form-data": "^2.5.0"
},
"dependencies": {
"form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
}
}
},
"@types/serve-static": { "@types/serve-static": {
"version": "1.13.8", "version": "1.13.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz",
@ -1248,6 +1279,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==",
"dev": true
},
"abab": { "abab": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",

@ -35,6 +35,8 @@
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.5.19", "@types/leaflet": "^1.5.19",
"@types/node": "^14.14.20",
"@types/request": "^2.48.5",
"sass": "^1.32.0", "sass": "^1.32.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
} }

@ -5,32 +5,39 @@ import React, {
FC, FC,
ChangeEvent, ChangeEvent,
FormEvent, FormEvent,
useEffect,
} from "react"; } from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import { offenses } from "./offenses"; import { offenses } from "./offenses";
//Importing fileReducer to use in useReducer hook, and File interface to use in looping over array of files //Importing fileReducer to use in useReducer hook, and File interface to use in looping over array of files
import { fileReducer, File } from "./fileReducer"; import { fileReducer, filesInitialState, File } from "./fileReducer";
import { formReducer, fromInitialState } from "./formReducer";
import "regenerator-runtime/runtime"; import "regenerator-runtime/runtime";
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet"; import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
import "./styles/reset.css"; import "./styles/reset.css";
import "./styles/app.scss"; import "./styles/app.scss";
import { getAddress } from "./location";
const App: FC = () => { const App: FC = () => {
//This reducer handles adding and selecting files. //This reducer handles adding and selecting files.
//I'm passing in initial state "{files: []}" which can be changed by using dispatch function with object contaning right type and payload. //I'm passing in initial state "{files: []}" from fileReducer file.
//State can be changed by using dispatch function with object contaning right type and payload.
//Example: dispatch({type: "ADD_FILE", payload: {img: thisIsImgURL, selected: false}) //Example: dispatch({type: "ADD_FILE", payload: {img: thisIsImgURL, selected: false})
//State can be accsesed by using fileState object //State can be accsesed by using fileState object
//Example: fileState.files //Example: fileState.files
//Reducer it self is imported from fileReducer.ts in src/ dir //Reducer it self is imported from fileReducer.ts in src/ dir
const [fileState, dispatch] = useReducer(fileReducer, { files: [] }); const [fileState, fileDispatch] = useReducer(fileReducer, filesInitialState);
const [formState, formDispatch] = useReducer(formReducer, fromInitialState);
//This hook handles pining location on the map //This hook handles pining location on the map
const [mapPin, setMapPin] = useState([52, 16]); const [mapPin, setMapPin] = useState({ lat: 52.39663, lon: 16.89866 });
const handleSubmit = async ( const handleSubmit = async (
event: FormEvent<HTMLFormElement> | MouseEvent<HTMLButtonElement> event: FormEvent<HTMLFormElement> | MouseEvent<HTMLButtonElement>
): Promise<void> => { ): Promise<void> => {
event.preventDefault(); event.preventDefault();
console.log(fileState, formState);
}; };
const handleUpload = (e: ChangeEvent<HTMLInputElement>) => { const handleUpload = (e: ChangeEvent<HTMLInputElement>) => {
@ -42,11 +49,10 @@ const App: FC = () => {
reader.addEventListener( reader.addEventListener(
"load", "load",
function () { function () {
dispatch({ fileDispatch({
type: "ADD_FILE", type: "ADD_FILE",
payload: { payload: {
img: this.result, img: this.result,
//
id: Date.now().toString(), id: Date.now().toString(),
selected: false, selected: false,
}, },
@ -61,15 +67,38 @@ const App: FC = () => {
[].forEach.call(files, readAndPreview); [].forEach.call(files, readAndPreview);
} }
}; };
useEffect(() => {
getAddress(mapPin).then((blob) =>
formDispatch({
type: "CHANGE_FIELD",
payload: {
field: "address",
value: blob,
},
})
);
}, [mapPin]);
const MapComponent = () => { const MapComponent = () => {
useMapEvents({ useMapEvents({
click: (e) => { click: async (e) => {
setMapPin([e.latlng.lat, e.latlng.lng]); setMapPin({ lat: e.latlng.lat, lon: e.latlng.lng });
}, },
}); });
return null; return null;
}; };
// console.log(getAddress(mapPin));
const formMessage = `Rejestracja: ${formState.plate}\nMiejsce: ${
formState.address.road === undefined ? "" : formState.address.road
} ${
formState.address.house_number === undefined
? ""
: formState.address.house_number
}\nData: ${new Date().toLocaleString()}\nMoje dane: ${formState.name + ","} ${
formState.email
}\n${formState.my_address}\nPowód: ${formState.offenses.join(
", \n"
)}\nKomentarz: ${formState.comment}
`;
return ( return (
<div className="container"> <div className="container">
@ -80,7 +109,7 @@ const App: FC = () => {
<div <div
key={index} key={index}
onClick={(e: MouseEvent) => onClick={(e: MouseEvent) =>
dispatch({ fileDispatch({
type: "SELECT_FILE", type: "SELECT_FILE",
payload: { id: (e.target as any).id }, payload: { id: (e.target as any).id },
}) })
@ -111,14 +140,14 @@ const App: FC = () => {
<MapContainer <MapContainer
center={[52.39663, 16.89866]} center={[52.39663, 16.89866]}
className="container__section__map" className="container__section__map"
zoom={18} zoom={16}
scrollWheelZoom={true} scrollWheelZoom={true}
> >
<TileLayer <TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
/> />
<Marker position={mapPin as any}></Marker> <Marker position={[mapPin.lat, mapPin.lon]}></Marker>
<MapComponent /> <MapComponent />
</MapContainer> </MapContainer>
</div> </div>
@ -130,14 +159,58 @@ const App: FC = () => {
onSubmit={(e) => handleSubmit(e)} onSubmit={(e) => handleSubmit(e)}
> >
<label htmlFor="name">Twoje imię i nazwisko</label> <label htmlFor="name">Twoje imię i nazwisko</label>
<input type="text" className="input" id="name" /> <input
type="text"
className="input"
id="name"
value={formState.name}
onChange={(e) =>
formDispatch({
type: "CHANGE_FIELD",
payload: { field: e.target.id, value: e.target.value },
})
}
/>
<label htmlFor="email">Twój adres email</label> <label htmlFor="email">Twój adres email</label>
<input type="text" className="input" id="email" /> <input
type="text"
className="input"
id="email"
value={formState.email}
onChange={(e) =>
formDispatch({
type: "CHANGE_FIELD",
payload: { field: e.target.id, value: e.target.value },
})
}
/>
<label htmlFor="my_address">Twój adres zamieszkania</label>
<input
type="text"
className="input"
id="my_address"
value={formState.my_address}
onChange={(e) =>
formDispatch({
type: "CHANGE_FIELD",
payload: { field: e.target.id, value: e.target.value },
})
}
/>
<label htmlFor="plate">Numer tablicy rejestracyjnej</label> <label htmlFor="plate">Numer tablicy rejestracyjnej</label>
<input type="text" className="input" id="plate" /> <input
<label htmlFor="message">Wiadomość dla straży miejskiej</label> type="text"
<textarea className="input input--textarea" id="message" /> className="input"
<div> id="plate"
value={formState.plate}
onChange={(e) =>
formDispatch({
type: "CHANGE_FIELD",
payload: { field: e.target.id, value: e.target.value },
})
}
/>
<div className="container__section__form__offenses">
{offenses.map((offence, index) => ( {offenses.map((offence, index) => (
<div key={index}> <div key={index}>
<input <input
@ -145,14 +218,45 @@ const App: FC = () => {
id={offence.id} id={offence.id}
value={offence.car_is} value={offence.car_is}
name={offence.car_is} name={offence.car_is}
className="chceckbox"
onChange={(e) =>
e.target.checked === true
? formDispatch({
type: "ADD_OFFENCE",
payload: { value: e.target.value, field: "checkbox" },
})
: formDispatch({
type: "DELETE_OFFENCE",
payload: { value: e.target.value, field: "checkbox" },
})
}
/> />
<label htmlFor={offence.id} className="label"> <label htmlFor={offence.id} className="label">
{offence.name} {offence.name}
</label> </label>
;
</div> </div>
))} ))}
</div> </div>
<label htmlFor="comment">Twój komentarz do zgłoszenia</label>
<textarea
className="textarea textarea--small"
id="comment"
name="comment"
value={formState.comment}
onChange={(e) =>
formDispatch({
type: "CHANGE_FIELD",
payload: { field: e.target.id, value: e.target.value },
})
}
/>
<label htmlFor="message">Wiadomość dla straży miejskiej</label>
<textarea
className="textarea"
id="message"
disabled={true}
value={formMessage}
/>
<button className="button" onClick={(e) => handleSubmit(e)}> <button className="button" onClick={(e) => handleSubmit(e)}>
Wysłanie zgłoszenia Wysłanie zgłoszenia

@ -14,6 +14,9 @@ interface FilesAction {
selected?: boolean; selected?: boolean;
}; };
} }
export const filesInitialState = {
files: [],
};
export function fileReducer(state: FilesState, action: FilesAction) { export function fileReducer(state: FilesState, action: FilesAction) {
switch (action.type) { switch (action.type) {
case "ADD_FILE": case "ADD_FILE":

@ -0,0 +1,60 @@
import { Address } from "./location";
interface FormState {
name: string;
email: string;
plate: string;
offenses: string[];
comment: string;
my_address: string;
address: Address;
}
interface FormAction {
type: string;
payload: {
field: string;
value: string | Address;
};
}
export const fromInitialState = {
name: "",
email: "",
plate: "",
my_address: "",
offenses: [],
comment: "",
address: {
house_number: "",
road: "",
suburb: "",
neighbourhood: "",
hamlet: "",
},
};
export function formReducer(state: FormState, action: FormAction) {
switch (action.type) {
case "CHANGE_FIELD":
return {
...state,
//action.payload.field is equal to name of the imput, example: e.target.name = plate
[action.payload.field]: action.payload.value,
};
case "ADD_OFFENCE":
return {
...state,
offenses: [...state.offenses, action.payload.value],
};
case "DELETE_OFFENCE":
return {
...state,
offenses: state.offenses.filter(
(offence: string) => offence !== action.payload.value
),
};
case "CHANGE_ADDRESS":
return {
...state,
};
default:
throw new Error();
}
}

@ -0,0 +1,18 @@
import request from "request";
export async function getJSON(uri: string): Promise<Object> {
return new Promise(function (resolve, reject) {
let data = "";
const reply = request({
uri,
headers: {
"User-Agent": "Poznanski Parkingowy Patrol - telegram bot",
},
});
reply.on("data", (chunk) => {
data += chunk;
});
reply.on("error", reject);
reply.on("end", () => resolve(JSON.parse(data)));
});
}

@ -0,0 +1,22 @@
import { getJSON } from "./http";
export type Location = {
lat: number;
lon: number;
};
export type Address = {
house_number: string;
road: string;
suburb: string;
neighbourhood: string;
hamlet: string;
};
export async function getAddress(location: Location): Promise<Address> {
const nominatim_url = `https://nominatim.openstreetmap.org/reverse?lat=${location.lat}&lon=${location.lon}&format=jsonv2`;
const nominatim_data = (await getJSON(nominatim_url)) as {
address: Address;
};
return nominatim_data.address;
}

@ -43,23 +43,35 @@
width: 100%; width: 100%;
height: 600px; height: 600px;
} }
.input { .input {
width: 80%; width: 100%;
height: 30px; height: 30px;
margin-bottom: 15px; margin-bottom: 15px;
margin-top: 5px; margin-top: 5px;
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
} }
.input--textarea { .textarea {
width: 100%;
height: 350px; height: 350px;
margin-bottom: 15px;
margin-top: 5px;
display: block;
box-sizing: border-box;
resize: none; resize: none;
font-size: 1.2em; font-size: 1.2em;
padding: 3px 8px; padding: 3px 8px;
} }
.textarea--small {
height: 150px;
}
.label { .label {
width: 100%; width: 100%;
} }
.container__section__form__offenses {
margin-bottom: 15px;
}
//to change name //to change name
.img { .img {
width: 100%; width: 100%;
@ -70,6 +82,20 @@
height: 100%; height: 100%;
border: 3px solid seagreen; border: 3px solid seagreen;
} }
.chceckbox {
appearance: none;
vertical-align: middle;
font-size: inherit;
cursor: pointer;
width: 1.5em;
height: 1.5em;
background: white;
border: 0.1em solid #000000;
position: relative;
}
input[type="checkbox"]:checked {
background: #000000;
}
/*breakpoint for mobile*/ /*breakpoint for mobile*/
@media (max-width: 1200px) { @media (max-width: 1200px) {
body { body {

Loading…
Cancel
Save