Compare commits

...

13 Commits

@ -0,0 +1,38 @@
module.exports = {
env: { node: true },
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "prettier", "jsdoc"],
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",
tsconfigRootDir: __dirname,
},
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
},
},
],
};

14
.gitignore vendored

@ -1,3 +1,11 @@
/.cache/ node_modules
/dist/ .node_repl_history
/node_modules/ .config
.npm
.idea
.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,11 @@
version: "3.7"
services:
db:
image: mongo:4.4-bionic
ports:
- "127.0.0.1:20724:27017"
mailcatcher:
image: schickling/mailcatcher:latest
ports:
- "127.0.0.1:1080:1080"
- "127.0.0.1:1025:1025"

3745
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -2,9 +2,13 @@
"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": {
"start": "parcel src/index.html" "back:build": "tsc",
"back:watch": "npm run back:build -- --watch",
"front:build": "parcel build --out-dir public src/front/*.html",
"front:watch": "parcel watch --out-dir public src/front/*.html",
"start": "nodemon lib/back/index.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,6 +17,34 @@
"author": "foki", "author": "foki",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"parcel-bundler": "^1.12.4" "@koa/router": "^10.0.0",
"@types/koa": "^2.11.6",
"@types/koa-mount": "^4.0.0",
"@types/koa-static": "^4.0.1",
"@types/koa__router": "^8.0.3",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"axios": "^0.21.1",
"eslint-plugin-prettier": "^3.3.1",
"koa": "^2.13.0",
"koa-mount": "^4.0.0",
"koa-static": "^5.0.0",
"leaflet": "^1.7.1",
"parcel-bundler": "^1.12.4",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-leaflet": "^3.0.5",
"sealious": "^0.13.8"
},
"devDependencies": {
"@types/leaflet": "^1.5.19",
"@types/node": "^14.14.20",
"@types/request": "^2.48.5",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-jsdoc": "^30.0.3",
"prettier": "^2.0.5",
"sass": "^1.32.0",
"typescript": "^4.1.3"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

@ -0,0 +1,51 @@
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 } 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: 20724,
db_name: "zglaszansko-web",
},
email: {
from_address: "sealious-playground@example.com",
from_name: "Sealious playground app",
},
};
manifest = {
name: "Sealious Playground",
logo: resolve(__dirname, "../assets/logo.png"),
version: "0.0.1",
default_language: "en",
base_url: "http://localhost:8080",
admin_email: "admin@example.com",
colors: {
primary: "#5294a1",
},
};
collections = {
...App.BaseCollections,
};
})();
const port = 8080;
console.log(`Listening on 127.0.0.1:${port}`);
app.start();
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));

@ -0,0 +1,52 @@
import axios from "axios";
import React, { ReactElement } from "react";
import "./styles/nav.scss";
interface Props {
isLogged: boolean;
logoutState: () => void;
}
const NOT_LOGGED = [
{ name: "aplikacja", href: "/" },
{ name: "logowanie", href: "login.html" },
{ name: "rejestracja", href: "register.html" },
];
const LOGGED = [{ name: "aplikacja", href: "/" }];
function Nav({ isLogged, logoutState }: Props): ReactElement {
const logout = async () => {
await axios.delete(
"http://localhost:8080/api/v1/collections/sessions/current"
);
logoutState();
};
return (
<nav className="navbar">
<h2 className="navbar__title">Zgłaszańsko web</h2>
<div className="navbar__links">
{isLogged
? LOGGED.map((ele) => (
<>
<a href={ele.href} className="button-link">
{ele.name}
</a>
<button
onClick={logout}
className="button-link"
>
Wyloguj
</button>
</>
))
: NOT_LOGGED.map((ele) => (
<a href={ele.href} className="button-link">
{ele.name}
</a>
))}
</div>
</nav>
);
}
export default Nav;

@ -0,0 +1,347 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, {
useState,
useReducer,
MouseEvent,
FC,
ChangeEvent,
FormEvent,
useEffect,
} from "react";
import { render } from "react-dom";
import { offenses } from "./offenses";
import axios from "axios";
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";
import { FileType } from "./file";
const App: FC = () => {
const [isLogged, setIsLogged] = useState(false);
const [fileState, fileDispatch] = useReducer(
fileReducer,
filesInitialState
);
const [formState, formDispatch] = useReducer(formReducer, fromInitialState);
const [mapPin, setMapPin] = useState({ lat: 0, lon: 0 });
const checkUser = async (): Promise<void> => {
try {
await axios.get(
"http://localhost:8080/api/v1/collections/users/me"
);
setIsLogged(true);
} catch (error) {
setIsLogged(false);
}
};
useEffect(() => {
void checkUser();
}, []);
useEffect(() => {
void getAddress(mapPin).then((blob) =>
formDispatch({
type: "CHANGE_FIELD",
payload: {
field: "address",
value: blob,
},
})
);
}, [mapPin]);
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: FileType) => {
console.log(file);
// Make sure `file.name` matches our extensions criteria
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (/\.(jpe?g|png|svg)$/i.test(file.name)) {
const 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 as unknown) as Blob);
}
};
if (files) {
[].forEach.call(files, readAndPreview);
}
};
const MapComponent = () => {
useMapEvents({
click: async (e) => {
setMapPin({ lat: e.latlng.lat, lon: e.latlng.lng });
},
});
return null;
};
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 isLogged={isLogged} logoutState={() => setIsLogged(false)} />
<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 Element).id,
},
})
}
className="section__photos__item"
>
<img
src={file.img as string}
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='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
/>
{mapPin.lat !== 0 ? (
<Marker
position={[mapPin.lat, mapPin.lon]}
></Marker>
) : null}
<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>
</>
);
};
render(<App />, document.getElementById("root"));

@ -0,0 +1,7 @@
export interface FileType {
lastModified: number;
name: string;
size: number;
type: string;
webkitRelativePath: string;
}

@ -0,0 +1,49 @@
export interface File {
img?: unknown;
id: string;
selected?: boolean;
}
interface FilesState {
files: Array<File>;
}
interface FilesAction {
type: string;
payload: File;
}
export const filesInitialState = {
files: [],
};
export function fileReducer(
state: FilesState,
action: FilesAction
): FilesState {
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: (Address | void | string)[];
comment: string;
my_address: string;
address: Address;
}
interface FormAction {
type: string;
payload: {
field: string;
value: Address | void | string;
};
}
export const fromInitialState = {
name: "",
email: "",
plate: "",
my_address: "",
offenses: [],
comment: "",
address: {
house_number: "",
road: "",
suburb: "",
neighbourhood: "",
hamlet: "",
},
};
export function formReducer(state: FormState, action: FormAction): FormState {
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,14 @@
<!DOCTYPE html>
<html>
<head>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css"
rel="stylesheet"
/>
<title>Zgłaszańsko</title>
</head>
<body>
<div id="root"></div>
<script src="./app.tsx"></script>
</body>
</html>

@ -0,0 +1,24 @@
import axios from "axios";
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 | void> {
const nominatim_url = `https://nominatim.openstreetmap.org/reverse?lat=${location.lat}&lon=${location.lon}&format=jsonv2`;
try {
const data = await axios.get(nominatim_url);
const nominatim_dat = data.data as { address: Address };
return nominatim_dat.address;
} catch (error) {
console.log(123);
}
}

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css"
rel="stylesheet"
/>
<title>Zgłaszańsko</title>
</head>
<body>
<div id="root"></div>
<script src="./login.tsx"></script>
</body>
</html>

@ -0,0 +1,94 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { ReactElement, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import Nav from "./Nav";
import "./styles/reset.css";
import "./styles/app.scss";
import "regenerator-runtime/runtime";
import axios from "axios";
export default function Login(): ReactElement {
const [isLogged, setIsLogged] = useState(false);
const [error, setError] = useState(null);
const [loginState, setLoginState] = useState({
username: "",
password: "",
});
const checkUser = async (): Promise<void> => {
try {
await axios.get(
"http://localhost:8080/api/v1/collections/users/me"
);
setIsLogged(true);
} catch (error) {
setIsLogged(false);
}
};
useEffect(() => {
void checkUser();
}, []);
const postLogin = async () => {
try {
await axios.post("http://localhost:8080/api/v1/sessions/", {
username: loginState.username,
password: loginState.password,
});
void checkUser();
} catch (e) {
console.log(e);
}
};
const notLoggedForm = () => {
return (
<form>
<label htmlFor="username">
Nazwa użytkownika
<input
type="text"
name="username"
value={loginState.username}
onChange={(e) =>
setLoginState({
...loginState,
username: e.target.value,
})
}
/>
</label>
<label htmlFor="password">
Hasło
<input
type="password"
name="password"
value={loginState.password}
onChange={(e) =>
setLoginState({
...loginState,
password: e.target.value,
})
}
/>
</label>
<button
type="submit"
onClick={(e) => {
e.preventDefault();
console.log(postLogin());
}}
>
Zaloguj
</button>
</form>
);
};
return (
<>
<Nav isLogged={isLogged} logoutState={() => setIsLogged(false)} />
{isLogged ? <h1>Jesteś zalogowany</h1> : notLoggedForm()}
</>
);
}
ReactDOM.render(<Login />, document.getElementById("root"));

@ -0,0 +1,110 @@
export type Offense = {
name: string;
car_is: string;
id: string;
implies?: string[];
};
export const offenses: Offense[] = [
{
id: "obszar_wyłączony",
name: "Parkowanie na obszarze wyłączonym z ruchu",
car_is: "jest zaparkowany na obszarze wyłączonym z ruchu",
},
{
id: "widoczność_pasy",
name: "Ograniczanie widoczności na pasach",
car_is:
"ogranicza widoczność na przejściu dla pieszych, powodując zagrożenie osób korzystających z przejścia",
},
{
id: "widoczność_skrz",
name: "Ograniczanie widoczności na skrzyżowaniu",
car_is:
"ogranicza widoczność na skrzyżowaniu, powodując zagrożenie dla uczestników ruchu",
},
{
id: "utrudnia_ruch_rowerom",
name: "Utrudnianie ruchu rowerowego",
car_is: "utrudnia ruch rowerowy",
},
{
id: "utrudnia_ruch_pieszym",
name: "Utrudnianie ruchu pieszego",
car_is: "utrudnia ruch pieszych",
},
{
id: "poza_wyzn",
name: "Parkowanie poza wyznaczonym miejscem",
car_is: "jest zaparkowany poza wyznaczonym miejscem parkingowym",
},
{
id: "przed_przejsc",
name: "Parkowanie <10m przed przejściem dla pieszych",
car_is: "jest zaparkowany mniej niż 10m przed przejściem dla pieszych",
implies: ["widoczność_pasy"],
},
{
id: "na_zakazie",
name: "Parkowanie za znakiem zakazu parkowania",
car_is: "jest zaparkowany za znakiem zakazu parkowania",
},
{
id: "brama",
name: "Blokowanie bramy wjazdowej",
car_is: "blokuje bramę wjazdową",
},
{
id: "na_chodzie",
name: "Postój na chodzie",
car_is: "ma włączony silnik podczas postoju",
},
{
id: "blk_chodnik",
name: "Blokowanie chodnika",
car_is:
"jest zaparkowany na chodniku pozostawiając mniej niż 1,5m dla pieszych",
implies: ["utrudnia_ruch_pieszym"],
},
{
id: "chyba2.5t",
name: "Duży samochód dostawczy na chodniku",
car_is:
"najprawdopodobniej przekracza dopuszczalną całkowitą masę 2,5t będąc zaparkowanym na chodniku",
},
{
id: "hydrant",
name: "Blokowanie hydrantu",
car_is: "blokuje dostęp do hydrantu",
},
{
id: "przystanek_15",
name: "Parkowanie <15m od przystanku",
car_is:
"jest zaparkowany w odległości mniejszej niż 15m od tablicy oznaczającej przystanek",
},
{
id: "utr_innym_zapark",
name: "Utrudnianie wyjazdu innym zaparkowanym samochodom",
car_is:
"dokonuje postoju w miejscu utrudniającym dostęp do innych, prawidłowo zaparkowanych pojazdów lub wyjazd tych pojazdów",
},
{
id: "pas_rowerow",
name: "blokuje pas rowerów",
car_is: "zaparkowany na pasie dla rowerów",
implies: ["utrudnia_ruch_rowerom"],
},
{
id: "t30",
name: "Parkowanie niezgodnie z rysunkiem na tabliczce (T-30)",
car_is:
"nie stosuje się do znaku T-30, wskazującego sposób ustawienia pojazdu względem krawędzi jezdni",
},
{
id: "blokuje_znak",
name: "Zasłanianie znaku drogowego",
car_is:
"zaparkowany w odległości mniejszej niż 10m od przedniej strony znaku drogowego, zasłaniając go (Art 49, punkt 1, ust. 6)",
},
];

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css"
rel="stylesheet"
/>
<title>Zgłaszańsko</title>
</head>
<body>
<div id="root"></div>
<script src="./register.tsx"></script>
</body>
</html>

@ -0,0 +1,78 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { ReactElement, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import Nav from "./Nav";
import "./styles/reset.css";
import "./styles/app.scss";
import "regenerator-runtime/runtime";
import axios from "axios";
export default function Register(): ReactElement {
const [isLogged, setIsLogged] = useState(false);
const [registerState, setRegisterState] = useState({
email: "",
});
const [error, setError] = useState(null);
const checkUser = async (): Promise<void> => {
try {
await axios.get(
"http://localhost:8080/api/v1/collections/users/me"
);
setIsLogged(true);
} catch (error) {
setIsLogged(false);
}
};
useEffect(() => {
void checkUser();
}, []);
const postRegister = async () => {
try {
await axios.post(
"http://localhost:8080/api/v1/collections/registration-intents",
{
email: registerState.email,
}
);
void checkUser();
} catch (e) {
console.log(e);
}
};
const notLoggedForm = () => {
return (
<form>
<label htmlFor="email">
Adres email
<input
type="email"
name="email"
value={registerState.email}
onChange={(e) =>
setRegisterState({
email: e.target.value,
})
}
/>
</label>
<button
type="submit"
onClick={(e) => {
e.preventDefault();
console.log(postRegister());
}}
>
Zaloguj
</button>
</form>
);
};
return (
<>
<Nav isLogged={isLogged} logoutState={() => setIsLogged(false)} />
{isLogged ? <h1>Jesteś zalogowany</h1> : notLoggedForm()}
</>
);
}
ReactDOM.render(<Register />, document.getElementById("root"));

@ -0,0 +1,147 @@
.button:hover {
filter: brightness(1.1);
}
.container {
width: 100%;
height: 100vh;
display: flex;
}
.button {
background-color: #999;
height: 2rem;
line-height: 2rem;
padding: 0 0.75rem;
text-decoration: none;
color: white;
border: none;
cursor: pointer;
margin: 0 15px;
}
.container__section {
flex: 1;
padding: 15px;
display: block;
height: auto;
}
.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.1rem;
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: 3rem;
}
.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;
}
.container__section__form {
width: 100%;
height: 600px;
}
.textarea {
font-size: 2rem;
}
.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,27 @@
.navbar {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: #000000 2px solid;
padding: 15px 0;
}
.navbar__title {
font-size: 1.5em;
margin: 0 15px;
}
.navbar__links {
display: flex;
}
.button-link {
background-color: #999;
height: 2rem;
line-height: 2rem;
padding: 0 0.75rem;
text-decoration: none;
color: white;
border: none;
cursor: pointer;
margin: 0 15px;
font-size: 0.9em;
}

@ -0,0 +1,127 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Zgłaszańsko</title>
</head>
<body>
<h1>Zgłaszańsko</h1>
</body>
</html>

@ -1 +0,0 @@
import { submit } from "poznan-sm-submit";

@ -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,14 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "esnext", "jsx": "react",
"module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"noImplicitAny": true, "noImplicitAny": false,
"noImplicitThis": true, "noImplicitThis": true,
"strictNullChecks": true, "strictNullChecks": true,
"target": "ES2020", "target": "ES6",
"declaration": true, "declaration": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["es6", "esnext"], "lib": ["dom"],
"outDir": "lib", "outDir": "lib",
"checkJs": true, "checkJs": true,
"allowJs": true, "allowJs": true,

Loading…
Cancel
Save