Basic sealious setup
parent
2082dc1866
commit
525660221b
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"phabricator.uri": "https://hub.sealcode.org/",
|
||||||
|
"load": [
|
||||||
|
"arcanist-linters"
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"linters": {
|
||||||
|
"prettier": {
|
||||||
|
"type": "prettier",
|
||||||
|
"bin": "./node_modules/.bin/prettier",
|
||||||
|
"include": ["(\\.js$)", "(\\.ts$)", "(\\.css$)"]
|
||||||
|
},
|
||||||
|
"eslint": {
|
||||||
|
"type": "eslint",
|
||||||
|
"include": ["(\\.ts$)", "(\\.js$)"]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { node: true },
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint", "prettier"],
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
modules: true,
|
||||||
|
},
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/require-await": 0,
|
||||||
|
"jsdoc/require-description": 2,
|
||||||
|
|
||||||
|
"no-await-in-loop": 2,
|
||||||
|
},
|
||||||
|
settings: { jsdoc: { mode: "typescript" } },
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.subtest.ts", "*.test.ts"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": 0,
|
||||||
|
"prefer-const": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-call": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-return": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": 0,
|
||||||
|
"no-await-in-loop": 1, // sometimes it's easier to debug when requests run sequentially
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -1,6 +1,11 @@
|
|||||||
/.cache/
|
node_modules
|
||||||
/dist/
|
.node_repl_history
|
||||||
/node_modules/
|
.config
|
||||||
/@types/
|
.npm
|
||||||
/lib/
|
.idea
|
||||||
/public/
|
.DS_Store
|
||||||
|
lib
|
||||||
|
dist
|
||||||
|
@types
|
||||||
|
.cache
|
||||||
|
public
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.yml",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mongo:4.4-bionic
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:20723:27017"
|
||||||
|
test:
|
||||||
|
image: sealious-test:latest
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: test.Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./:/opt/app/
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
user: ${UID:-1000}:${GID:-1000}
|
File diff suppressed because it is too large
Load Diff
@ -1,43 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "zglaszansko-web",
|
"name": "zglaszansko-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Web aplikacja ułatwiająca wysyłanie zgłoszeń do Poznańskiej Straży Miejskiej",
|
"description": "Web aplikacja ułatwiająca wysyłanie zgłoszeń do Poznańskiej Straży Miejskiej",
|
||||||
"main": "index.js",
|
"main": "./dist/back/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"back:build": "tsc",
|
"back:build": "tsc",
|
||||||
"back:watch": "npm run back:build -- --watch",
|
"back:watch": "npm run back:build -- --watch",
|
||||||
"front:build": "parcel build --out-dir public src/index.html",
|
"front:build": "parcel build --out-dir public src/front/index.html",
|
||||||
"front:watch": "parcel watch --out-dir public src/index.html",
|
"front:watch": "parcel watch --out-dir public src/front/index.html",
|
||||||
"start": "node lib/index.js"
|
"start": "nodemon lib/back/index.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "gitea@git.kuba-orlik.name:kuba/zglaszansko-web.git"
|
"url": "gitea@git.kuba-orlik.name:kuba/zglaszansko-web.git"
|
||||||
},
|
},
|
||||||
"author": "foki",
|
"author": "foki",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/router": "^10.0.0",
|
"@koa/router": "^10.0.0",
|
||||||
"@types/koa": "^2.11.6",
|
"@types/koa": "^2.11.6",
|
||||||
"@types/koa-mount": "^4.0.0",
|
"@types/koa-mount": "^4.0.0",
|
||||||
"@types/koa-static": "^4.0.1",
|
"@types/koa-static": "^4.0.1",
|
||||||
"@types/koa__router": "^8.0.3",
|
"@types/koa__router": "^8.0.3",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"koa": "^2.13.0",
|
"koa": "^2.13.0",
|
||||||
"koa-mount": "^4.0.0",
|
"koa-mount": "^4.0.0",
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
"parcel-bundler": "^1.12.4",
|
"parcel-bundler": "^1.12.4",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-leaflet": "^3.0.5"
|
"react-leaflet": "^3.0.5",
|
||||||
},
|
"sealious": "^0.13.8",
|
||||||
"devDependencies": {
|
"typescript": "^4.1.3"
|
||||||
"@types/leaflet": "^1.5.19",
|
},
|
||||||
"@types/node": "^14.14.20",
|
"devDependencies": {
|
||||||
"@types/request": "^2.48.5",
|
"@types/leaflet": "^1.5.19",
|
||||||
"sass": "^1.32.0",
|
"@types/node": "^14.14.20",
|
||||||
"typescript": "^4.1.3"
|
"@types/request": "^2.48.5",
|
||||||
}
|
"sass": "^1.32.0",
|
||||||
|
"typescript": "^4.1.3",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
|
"eslint-plugin-jsdoc": "^30.0.3",
|
||||||
|
"eslint": "^7.5.0",
|
||||||
|
"eslint-config-prettier": "^6.11.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,270 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useState,
|
|
||||||
useReducer,
|
|
||||||
MouseEvent,
|
|
||||||
FC,
|
|
||||||
ChangeEvent,
|
|
||||||
FormEvent,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import { offenses } from "./offenses";
|
|
||||||
//Importing fileReducer to use in useReducer hook, and File interface to use in looping over array of files
|
|
||||||
import { fileReducer, filesInitialState, File } from "./fileReducer";
|
|
||||||
import { formReducer, fromInitialState } from "./formReducer";
|
|
||||||
import "regenerator-runtime/runtime";
|
|
||||||
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
|
|
||||||
import "./styles/reset.css";
|
|
||||||
import "./styles/app.scss";
|
|
||||||
import { getAddress } from "./location";
|
|
||||||
|
|
||||||
const App: FC = () => {
|
|
||||||
//This reducer handles adding and selecting files.
|
|
||||||
//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})
|
|
||||||
//State can be accsesed by using fileState object
|
|
||||||
//Example: fileState.files
|
|
||||||
//Reducer it self is imported from fileReducer.ts in src/ dir
|
|
||||||
const [fileState, fileDispatch] = useReducer(fileReducer, filesInitialState);
|
|
||||||
|
|
||||||
const [formState, formDispatch] = useReducer(formReducer, fromInitialState);
|
|
||||||
|
|
||||||
//This hook handles pining location on the map
|
|
||||||
const [mapPin, setMapPin] = useState({ lat: 52.39663, lon: 16.89866 });
|
|
||||||
|
|
||||||
const handleSubmit = async (
|
|
||||||
event: FormEvent<HTMLFormElement> | MouseEvent<HTMLButtonElement>
|
|
||||||
): Promise<void> => {
|
|
||||||
event.preventDefault();
|
|
||||||
console.log(fileState, formState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = e.target.files;
|
|
||||||
const readAndPreview = (file: any) => {
|
|
||||||
// Make sure `file.name` matches our extensions criteria
|
|
||||||
if (/\.(jpe?g|png|svg)$/i.test(file.name)) {
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.addEventListener(
|
|
||||||
"load",
|
|
||||||
function () {
|
|
||||||
fileDispatch({
|
|
||||||
type: "ADD_FILE",
|
|
||||||
payload: {
|
|
||||||
img: this.result,
|
|
||||||
id: Date.now().toString(),
|
|
||||||
selected: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (files) {
|
|
||||||
[].forEach.call(files, readAndPreview);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
getAddress(mapPin).then((blob) =>
|
|
||||||
formDispatch({
|
|
||||||
type: "CHANGE_FIELD",
|
|
||||||
payload: {
|
|
||||||
field: "address",
|
|
||||||
value: blob,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [mapPin]);
|
|
||||||
const MapComponent = () => {
|
|
||||||
useMapEvents({
|
|
||||||
click: async (e) => {
|
|
||||||
setMapPin({ lat: e.latlng.lat, lon: e.latlng.lng });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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 (
|
|
||||||
<div className="container">
|
|
||||||
<section className="container__section">
|
|
||||||
<h2 className="container__section__title">Zdjęcia</h2>
|
|
||||||
<div className="container__section__photos">
|
|
||||||
{fileState.files.map((file: File, index: number) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
onClick={(e: MouseEvent) =>
|
|
||||||
fileDispatch({
|
|
||||||
type: "SELECT_FILE",
|
|
||||||
payload: { id: (e.target as any).id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="section__photos__item"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={file.img}
|
|
||||||
id={file.id}
|
|
||||||
className={file.selected ? "img--active" : "img"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
name="image-upload"
|
|
||||||
id="input"
|
|
||||||
accept="image/*"
|
|
||||||
className="input"
|
|
||||||
multiple
|
|
||||||
onChange={(e) => handleUpload(e)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section className="container__section">
|
|
||||||
<h2 className="container__section__title">Mapa</h2>
|
|
||||||
<div>
|
|
||||||
<MapContainer
|
|
||||||
center={[52.39663, 16.89866]}
|
|
||||||
className="container__section__map"
|
|
||||||
zoom={16}
|
|
||||||
scrollWheelZoom={true}
|
|
||||||
>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
|
|
||||||
/>
|
|
||||||
<Marker position={[mapPin.lat, mapPin.lon]}></Marker>
|
|
||||||
<MapComponent />
|
|
||||||
</MapContainer>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="container__section">
|
|
||||||
<h2 className="container__section__title">Formularz zgłoszeniowy</h2>
|
|
||||||
<form
|
|
||||||
className="container__section__form"
|
|
||||||
onSubmit={(e) => handleSubmit(e)}
|
|
||||||
>
|
|
||||||
<label htmlFor="name">Twoje imię i nazwisko</label>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="input"
|
|
||||||
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) => (
|
|
||||||
<div key={index}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={offence.id}
|
|
||||||
value={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">
|
|
||||||
{offence.name}
|
|
||||||
</label>
|
|
||||||
</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)}>
|
|
||||||
Wysłanie zgłoszenia
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById("root"));
|
|
Binary file not shown.
After Width: | Height: | Size: 328 B |
@ -0,0 +1,50 @@
|
|||||||
|
import Koa from "koa";
|
||||||
|
import _locreq from "locreq";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import Static from "koa-static";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import mount from "koa-mount";
|
||||||
|
const locreq = _locreq(__dirname);
|
||||||
|
import Sealious, { App, Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
|
||||||
|
declare module "koa" {
|
||||||
|
interface BaseContext {
|
||||||
|
$context: Sealious.Context;
|
||||||
|
$app: Sealious.App;
|
||||||
|
$body: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new (class extends App {
|
||||||
|
config = {
|
||||||
|
upload_path: locreq.resolve("uploaded_files"),
|
||||||
|
datastore_mongo: {
|
||||||
|
host: "localhost",
|
||||||
|
port: 20723,
|
||||||
|
db_name: "sealious-playground",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
from_address: "zglszanskoweb@example.com",
|
||||||
|
from_name: "Zgłaszańsko web",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
manifest = {
|
||||||
|
name: "Zgłaszańsko web",
|
||||||
|
logo: resolve(__dirname, "../assets/logo.png"),
|
||||||
|
version: "0.0.1",
|
||||||
|
default_language: "pl",
|
||||||
|
base_url: "localhost:8080",
|
||||||
|
admin_email: "admin@example.com",
|
||||||
|
colors: {
|
||||||
|
primary: "#5294a1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
collections = {
|
||||||
|
...App.BaseCollections,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
||||||
|
const port = 8080;
|
||||||
|
|
||||||
|
console.log(`Listening on 127.0.0.1:${port}`);
|
||||||
|
app.start();
|
@ -1,50 +0,0 @@
|
|||||||
export interface File {
|
|
||||||
img: string;
|
|
||||||
id: string;
|
|
||||||
selected: boolean;
|
|
||||||
}
|
|
||||||
interface FilesState {
|
|
||||||
files: Array<File>;
|
|
||||||
}
|
|
||||||
interface FilesAction {
|
|
||||||
type: string;
|
|
||||||
payload: {
|
|
||||||
img?: any;
|
|
||||||
id: string;
|
|
||||||
selected?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export const filesInitialState = {
|
|
||||||
files: [],
|
|
||||||
};
|
|
||||||
export function fileReducer(state: FilesState, action: FilesAction) {
|
|
||||||
switch (action.type) {
|
|
||||||
case "ADD_FILE":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
files: [
|
|
||||||
...state.files,
|
|
||||||
{
|
|
||||||
img: action.payload.img,
|
|
||||||
id: action.payload.id,
|
|
||||||
selected: action.payload.selected,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
case "SELECT_FILE":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
files: state.files.map((file: File) =>
|
|
||||||
file.id == action.payload.id
|
|
||||||
? { ...file, selected: true }
|
|
||||||
: { ...file, selected: false }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
case "CLEAR_FILES":
|
|
||||||
return {
|
|
||||||
files: [],
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
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,22 @@
|
|||||||
|
import React, { ReactElement } from "react";
|
||||||
|
import "./styles/app.scss";
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
const logged = [{ name: "Login" }];
|
||||||
|
function Nav({}: Props): ReactElement {
|
||||||
|
return (
|
||||||
|
<nav className="navbar">
|
||||||
|
<h1 className="navbar__title">Zgłaszańsko web</h1>
|
||||||
|
<div className="navbar__links">
|
||||||
|
<a href="" className="button-link">
|
||||||
|
test
|
||||||
|
</a>{" "}
|
||||||
|
<a href="" className="button-link">
|
||||||
|
test
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Nav;
|
@ -0,0 +1,331 @@
|
|||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
useReducer,
|
||||||
|
MouseEvent,
|
||||||
|
FC,
|
||||||
|
ChangeEvent,
|
||||||
|
FormEvent,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import { offenses } from "./offenses";
|
||||||
|
import Nav from "./Nav";
|
||||||
|
//Importing fileReducer to use in useReducer hook, and File interface to use in looping over array of files
|
||||||
|
import { fileReducer, filesInitialState, File } from "./fileReducer";
|
||||||
|
import { formReducer, fromInitialState } from "./formReducer";
|
||||||
|
import "regenerator-runtime/runtime";
|
||||||
|
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
|
||||||
|
import "./styles/reset.css";
|
||||||
|
import "./styles/app.scss";
|
||||||
|
import { getAddress } from "./location";
|
||||||
|
|
||||||
|
const App: FC = () => {
|
||||||
|
//This reducer handles adding and selecting files.
|
||||||
|
//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})
|
||||||
|
//State can be accsesed by using fileState object
|
||||||
|
//Example: fileState.files
|
||||||
|
//Reducer it self is imported from fileReducer.ts in src/ dir
|
||||||
|
const [fileState, fileDispatch] = useReducer(
|
||||||
|
fileReducer,
|
||||||
|
filesInitialState
|
||||||
|
);
|
||||||
|
|
||||||
|
const [formState, formDispatch] = useReducer(formReducer, fromInitialState);
|
||||||
|
|
||||||
|
//This hook handles pining location on the map
|
||||||
|
const [mapPin, setMapPin] = useState({ lat: 52.39663, lon: 16.89866 });
|
||||||
|
|
||||||
|
const handleSubmit = async (
|
||||||
|
event: FormEvent<HTMLFormElement> | MouseEvent<HTMLButtonElement>
|
||||||
|
): Promise<void> => {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log(fileState, formState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
const readAndPreview = (file: any) => {
|
||||||
|
// Make sure `file.name` matches our extensions criteria
|
||||||
|
if (/\.(jpe?g|png|svg)$/i.test(file.name)) {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.addEventListener(
|
||||||
|
"load",
|
||||||
|
function () {
|
||||||
|
fileDispatch({
|
||||||
|
type: "ADD_FILE",
|
||||||
|
payload: {
|
||||||
|
img: this.result,
|
||||||
|
id: Date.now().toString(),
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (files) {
|
||||||
|
[].forEach.call(files, readAndPreview);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
getAddress(mapPin).then((blob) =>
|
||||||
|
formDispatch({
|
||||||
|
type: "CHANGE_FIELD",
|
||||||
|
payload: {
|
||||||
|
field: "address",
|
||||||
|
value: blob,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [mapPin]);
|
||||||
|
const MapComponent = () => {
|
||||||
|
useMapEvents({
|
||||||
|
click: async (e) => {
|
||||||
|
setMapPin({ lat: e.latlng.lat, lon: e.latlng.lng });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Nav />
|
||||||
|
<div className="container">
|
||||||
|
<section className="container__section">
|
||||||
|
<h2 className="container__section__title">Zdjęcia</h2>
|
||||||
|
<div className="container__section__photos">
|
||||||
|
{fileState.files.map((file: File, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={(e: MouseEvent) =>
|
||||||
|
fileDispatch({
|
||||||
|
type: "SELECT_FILE",
|
||||||
|
payload: { id: (e.target as any).id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="section__photos__item"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={file.img}
|
||||||
|
id={file.id}
|
||||||
|
className={
|
||||||
|
file.selected ? "img--active" : "img"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="image-upload"
|
||||||
|
id="input"
|
||||||
|
accept="image/*"
|
||||||
|
className="input"
|
||||||
|
multiple
|
||||||
|
onChange={(e) => handleUpload(e)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section className="container__section">
|
||||||
|
<h2 className="container__section__title">Mapa</h2>
|
||||||
|
<div>
|
||||||
|
<MapContainer
|
||||||
|
center={[52.39663, 16.89866]}
|
||||||
|
className="container__section__map"
|
||||||
|
zoom={16}
|
||||||
|
scrollWheelZoom={true}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
<Marker
|
||||||
|
position={[mapPin.lat, mapPin.lon]}
|
||||||
|
></Marker>
|
||||||
|
<MapComponent />
|
||||||
|
</MapContainer>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Wybrany adres:{" "}
|
||||||
|
{formState.address.road === undefined
|
||||||
|
? ""
|
||||||
|
: formState.address.road}{" "}
|
||||||
|
{formState.address.house_number === undefined
|
||||||
|
? ""
|
||||||
|
: formState.address.house_number}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="container__section">
|
||||||
|
<h2 className="container__section__title">
|
||||||
|
Formularz zgłoszeniowy
|
||||||
|
</h2>
|
||||||
|
<form
|
||||||
|
className="container__section__form"
|
||||||
|
onSubmit={(e) => handleSubmit(e)}
|
||||||
|
>
|
||||||
|
<label htmlFor="name">Twoje imię i nazwisko</label>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
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) => (
|
||||||
|
<div key={index}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={offence.id}
|
||||||
|
value={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"
|
||||||
|
>
|
||||||
|
{offence.name}
|
||||||
|
</label>
|
||||||
|
</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)}
|
||||||
|
>
|
||||||
|
Wysłanie zgłoszenia
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById("root"));
|
@ -0,0 +1,50 @@
|
|||||||
|
export interface File {
|
||||||
|
img: string;
|
||||||
|
id: string;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
interface FilesState {
|
||||||
|
files: Array<File>;
|
||||||
|
}
|
||||||
|
interface FilesAction {
|
||||||
|
type: string;
|
||||||
|
payload: {
|
||||||
|
img?: any;
|
||||||
|
id: string;
|
||||||
|
selected?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const filesInitialState = {
|
||||||
|
files: [],
|
||||||
|
};
|
||||||
|
export function fileReducer(state: FilesState, action: FilesAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_FILE":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
files: [
|
||||||
|
...state.files,
|
||||||
|
{
|
||||||
|
img: action.payload.img,
|
||||||
|
id: action.payload.id,
|
||||||
|
selected: action.payload.selected,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
case "SELECT_FILE":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
files: state.files.map((file: File) =>
|
||||||
|
file.id == action.payload.id
|
||||||
|
? { ...file, selected: true }
|
||||||
|
: { ...file, selected: false }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case "CLEAR_FILES":
|
||||||
|
return {
|
||||||
|
files: [],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
@ -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,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;
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
.navbar {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: #000000 2px solid;
|
||||||
|
}
|
||||||
|
.navbar__title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
.navbar__links {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.button-link {
|
||||||
|
background-color: #999;
|
||||||
|
height: 2rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
border: rgba(0, 0, 0, 0.116) 1px solid;
|
||||||
|
background-color: none;
|
||||||
|
margin: 15px 0;
|
||||||
|
width: 150px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
.container__section {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.container__section__title {
|
||||||
|
margin: 15px 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.container__section__photos {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
row-gap: 35px;
|
||||||
|
column-gap: 15px;
|
||||||
|
justify-items: stretch;
|
||||||
|
}
|
||||||
|
.section__photos__item {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
.section__photos__item--active {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
.container__section__map {
|
||||||
|
cursor: default;
|
||||||
|
height: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.container__section__form {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 350px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: none;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
.textarea--small {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.container__section__form__offenses {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
//to change name
|
||||||
|
.img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.img--active {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
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*/
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
body {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.container__section__title {
|
||||||
|
margin: 25px 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.container__section__photos {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
.section__photos__item {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
width: 250px;
|
||||||
|
margin: 30px 0;
|
||||||
|
height: 75px;
|
||||||
|
}
|
||||||
|
.container__section__form {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
width: 90%;
|
||||||
|
height: 75px;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
.input--textarea {
|
||||||
|
height: 450px;
|
||||||
|
width: 90%;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
import Koa from "koa";
|
|
||||||
import Static from "koa-static";
|
|
||||||
import Router from "@koa/router";
|
|
||||||
import { resolve } from "path";
|
|
||||||
import mount from "koa-mount";
|
|
||||||
|
|
||||||
const app = new Koa();
|
|
||||||
|
|
||||||
const router = new Router();
|
|
||||||
|
|
||||||
router.get("/api", (ctx) => {
|
|
||||||
ctx.body = "THIS IS API RESPONSE";
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(router.routes());
|
|
||||||
|
|
||||||
app.use(mount("/", Static(resolve(__dirname, "../public"))));
|
|
||||||
|
|
||||||
const port = 3000;
|
|
||||||
|
|
||||||
app.listen(port);
|
|
||||||
|
|
||||||
console.log(`Listening on 127.0.0.1:${port}`);
|
|
@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
border: rgba(0, 0, 0, 0.116) 1px solid;
|
|
||||||
background-color: none;
|
|
||||||
margin: 15px 0;
|
|
||||||
width: 150px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
.container__section {
|
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.container__section__title {
|
|
||||||
margin: 15px 0;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
.container__section__photos {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
||||||
row-gap: 35px;
|
|
||||||
column-gap: 15px;
|
|
||||||
justify-items: stretch;
|
|
||||||
}
|
|
||||||
.section__photos__item {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
.section__photos__item--active {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
.container__section__map {
|
|
||||||
cursor: default;
|
|
||||||
height: 600px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.container__section__form {
|
|
||||||
width: 100%;
|
|
||||||
height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
height: 30px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
margin-top: 5px;
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.textarea {
|
|
||||||
width: 100%;
|
|
||||||
height: 350px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
margin-top: 5px;
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
resize: none;
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: 3px 8px;
|
|
||||||
}
|
|
||||||
.textarea--small {
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
.label {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.container__section__form__offenses {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
//to change name
|
|
||||||
.img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.img--active {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
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*/
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
body {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.container__section__title {
|
|
||||||
margin: 25px 0;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
.container__section__photos {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
}
|
|
||||||
.section__photos__item {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
width: 250px;
|
|
||||||
margin: 30px 0;
|
|
||||||
height: 75px;
|
|
||||||
}
|
|
||||||
.container__section__form {
|
|
||||||
width: 100%;
|
|
||||||
height: 600px;
|
|
||||||
}
|
|
||||||
.input {
|
|
||||||
width: 90%;
|
|
||||||
height: 75px;
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
.input--textarea {
|
|
||||||
height: 450px;
|
|
||||||
width: 90%;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,24 @@
|
|||||||
|
FROM node:15-alpine
|
||||||
|
LABEL maintainer="Jakub Pieńkowski <jakski@sealcode.org>"
|
||||||
|
|
||||||
|
ENV UID=node \
|
||||||
|
GID=node \
|
||||||
|
HOME=/opt/sealious
|
||||||
|
|
||||||
|
RUN sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/mirrors.dotsrc.org/g' /etc/apk/repositories
|
||||||
|
# Tini will ensure that any orphaned processes get reaped properly.
|
||||||
|
RUN apk add --no-cache tini
|
||||||
|
RUN apk --update add git
|
||||||
|
RUN apk --update add python
|
||||||
|
RUN apk --update add make
|
||||||
|
RUN apk --update add g++
|
||||||
|
|
||||||
|
VOLUME $HOME
|
||||||
|
WORKDIR $HOME
|
||||||
|
|
||||||
|
USER $UID:$GID
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
CMD ["/usr/local/bin/node", "."]
|
@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["dom"],
|
"lib": ["dom"],
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"declarationDir": "@types",
|
"declarationDir": "@types",
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue