25 Commits

Author SHA1 Message Date
48e98cbfc3 add more user input santitation 2024-02-14 00:04:00 +01:00
coja
6739e997bc Changed to relative path and updated the documentation 2024-01-21 13:16:44 +01:00
33cd9c202e Merge pull request 'Prebaceni *.html fajlovi za stranice u pages folder' (#5) from svitvojimilioni/taskmanager:better-templates into master
Reviewed-on: #5
2024-01-19 16:27:43 +00:00
efcab93460 Prebaceni *.html fajlovi za stranice u pages folder 2024-01-19 11:23:04 -05:00
b4344d31a8 Merge pull request 'Izmenjen templates folder tako da bude modularniji' (#4) from svitvojimilioni/taskmanager:better-templates into master
Reviewed-on: #4
2024-01-19 16:17:37 +00:00
2d4b88bc7a Vracen path za config na staro 2024-01-19 11:15:58 -05:00
2721d2a524 Izmenjen templates folder tako da bude modularniji
- dodati folderi layouts, includes, pages
- dodate base layout koji koriste ostale stranice
- dodat header.html i footer.html i includes koji koriste ostale stranice
- sav html refaktorisan tako da koristi base.html layout i header i footer iz includes
2024-01-19 10:53:12 -05:00
13d1e1674d fix taskuser delition after removing tasks and repsonse.html 2024-01-19 00:07:17 +01:00
44b7228982 fix errors when empty optinal values 2024-01-18 23:34:01 +01:00
673c04af19 add response.html and deleting user assignment to deleted tasks 2024-01-18 23:15:22 +01:00
4911842d3f fix check for already added users to task 2024-01-18 22:53:35 +01:00
d71cde1171 check if user already added to task 2024-01-18 22:41:26 +01:00
e615f774ad add user input sanitation 2024-01-18 15:00:13 +01:00
ed38156e77 add sql db and config file to install scripts for saving 2024-01-18 14:26:27 +01:00
4b82097fd9 update debian control info 2024-01-18 14:05:50 +01:00
d0d9529e41 add task deleting 2024-01-18 14:04:00 +01:00
a1c41349bd update README install instructions and add var to .gitignore 2024-01-18 12:37:07 +01:00
066bac721b add password to user registration and remove file copies from build dir 2024-01-18 12:32:09 +01:00
5964ef4963 Merge pull request 'front/app-design-init' (#3) from front/app-design-init into master
Reviewed-on: #3
2024-01-18 10:33:56 +00:00
coja
8acbb02a2f README update 2024-01-18 02:04:32 +01:00
coja
2c4c9a9802 [FE] Design init 2024-01-18 01:58:03 +01:00
8aa2675240 add deb files to gitignore and change permissions of install scripts 2024-01-17 15:35:47 +01:00
e5c424fff8 fix install script path in makefile 2024-01-17 15:13:56 +01:00
b3f257b7f4 fix build files 2024-01-17 15:12:42 +01:00
9f2817fcf8 implement addtask and addusers to a task features 2024-01-16 14:44:06 +01:00
27 changed files with 569 additions and 105 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,3 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,flask,vim,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=python,flask,vim,visualstudiocode
*.db
@@ -28,7 +27,7 @@ eggs/
lib64/
parts/
sdist/
#var/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
@@ -234,3 +233,5 @@ tags
# End of https://www.toptal.com/developers/gitignore/api/python,flask,vim,visualstudiocode
# deb files
*.deb

View File

@@ -1,3 +1,39 @@
# taskmanager
# Task Manager
Interactive TODO list web application
# Development Setup
Install python and pip on local machine
```bash
pip install virtualenv
python -m venv venv #/path/to/new/virtual/environment
source venv/bin/activate #activate virtual env
pip install -r requirments.txt
python3 ./init_db.py #initialize database
python3 ./run.py #run project
```
# On database changes
Delete file `/instance/taskmanager.db`
And reinit the db
```shell
python3 ./init_db.py
```
# Build app
```bash
cd build-deb/
make
```
# Install app
```bash
apt install ./build-deb/taskmanager.deb
```

View File

@@ -7,8 +7,12 @@ man: man/taskmanager.1.md
deb: man ../requirments.txt ../run.py ../taskmanager ../LICENSE
cp -r ../taskmanager/* taskmanager/var/taskmanager/taskmanager/
cp ../run.py taskmanager/var/taskmanager/
cp ../init_db.py taskmanager/var/taskmanager/
cp ../LICENSE taskmanager/var/taskmanager/
chmod -w taskmanager/DEBIAN/*
chmod +w taskmanager/DEBIAN/control
dpkg-deb --build taskmanager
chmod +w taskmanager/DEBIAN/*
clean:
rm -f taskmanager.deb
rm -f man/taskmanager.1

View File

@@ -1,15 +1,15 @@
% FLASKAPP(1) taskmanager 1.0.0
% TASKMANAGER(1) taskmanager 1.0.0
% Decentrala
% Jun 2023
# NAME
taskmanager - Web app
taskmanager - Interactive TODO list Web app
# SYNOPSIS
**python3 run.py**
# DESCRIPTION
Web app
Interactive TODO list Web app
# AUTHORS
Decentrala

View File

@@ -7,5 +7,5 @@ Installed-Size: 2000
Depends: gunicorn, python3-flask-sqlalchemy
Homepage: https://gitea.dmz.rs/Decentrala/taskmanager
Maintainer: Decentrala <dmz@dmz.rs>
Description: Web app
Version: 1.0.0
Description: Interactive TODO list Web app
Version: 1.0.10

View File

@@ -1,3 +1,12 @@
#!/bin/sh
/usr/bin/systemctl enable taskmanager.service
/var/taskmanager/init_db.py
/sbin/service taskmanager start
if [ -f /tmp/oldtaskmanagerconfig.ini ] ; then
cp /tmp/oldtaskmanagerconfig.ini /var/taskmanager/taskmanager/config.ini
rm /tmp/oldtaskmanagerconfig.ini
fi
if [ -f /tmp/oldtaskmanager.db ] ; then
cp /tmp/oldtaskmanager.db /var/taskmanager/instance/taskmanager.db
rm /tmp/oldtaskmanager.db
fi

View File

@@ -0,0 +1,6 @@
if [ -f /var/taskmanager/taskmanager/config.ini ] ; then
cp /var/taskmanager/taskmanager/config.ini /tmp/oldtaskmanagerconfig.ini
fi
if [ -f /var/taskmanager/instance/taskmanager.db ] ; then
cp /var/taskmanager/instance/taskmanager.db /tmp/oldtaskmanager.db
fi

View File

@@ -1,3 +1,9 @@
#!/bin/sh
/sbin/service taskmanager stop
/usr/bin/systemdctl disable taskmanager.service
/usr/bin/systemctl disable taskmanager.service
if [ -f /var/taskmanager/taskmanager/config.ini ] ; then
cp /var/taskmanager/taskmanager/config.ini /tmp/oldtaskmanagerconfig.ini
fi
if [ -f /var/taskmanager/instance/taskmanager.db ] ; then
cp /var/taskmanager/instance/taskmanager.db /tmp/oldtaskmanager.db
fi

View File

@@ -5,7 +5,7 @@ After=network.target nss-lookup.target
[Service]
WorkingDirectory=/var/taskmanager/
ExecStart=/usr/bin/gunicorn --workers 3 --bind 127.0.0.1:5000 run:app
ExecStart=/usr/bin/gunicorn --workers 3 --bind 0.0.0.0:80 run:app
[Install]
WantedBy=multi-user.target

2
taskmanager/config.ini Normal file
View File

@@ -0,0 +1,2 @@
[credentials]
ADMINPASS = defaultpassword

View File

@@ -0,0 +1,17 @@
from taskmanager.models import *
def gettaskusers(taskid):
users = list()
userids = list()
try:
taskusers = TaskUser.query.filter_by(taskid = taskid).all()
except:
taskusers = list()
for taskuser in taskusers:
userids.append(taskuser.userid)
for userid in userids:
users.append(User.query.get(userid))
return users

View File

@@ -4,11 +4,13 @@ class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
desc = db.Column(db.String, nullable=True)
creatorid = db.Column(db.Integer, nullable=True)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, nullable=False)
contact = db.Column(db.String, nullable=False)
password = db.Column(db.String, nullable=True)
class TaskUser(db.Model):
id = db.Column(db.Integer, primary_key=True)

View File

@@ -1,55 +1,184 @@
import configparser
import os
from flask import render_template, request, redirect
from taskmanager import app, db
from taskmanager.functions import *
from taskmanager.models import *
PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
CONFIG_PATH = os.path.join(PROJECT_PATH, "config.ini")
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
ADMINPASS = config.get('credentials', 'ADMINPASS')
@app.route('/', methods=['GET'])
def index():
tasks = Task.query.all()
return render_template('index.html', tasks = tasks)
return render_template('pages/index.html', tasks = tasks)
@app.route('/addtask', methods=['GET','POST'])
def addtask():
if request.method == 'GET':
return render_template('pages/addtask.html')
elif request.method == 'POST':
taskname = request.form['taskname']
taskdesc = request.form['taskdesc']
username = request.form['username']
# Input sanitation
# Task name
if not taskname.printable() or ("<" in taskname and ">" in taskname):
return render_template('pages/response.html', response = "Task name has to be made only of letters or numbers.")
if len(taskname) < 1 or len(taskname) > 40:
return render_template('pages/response.html', response = "Task name lenght invalid, only smaller then 40 charachters allowed")
# Username
if username == "":
creatorid = None
else:
try:
creatorid = User.query.filter_by(username = username).first().id
except:
return render_template('pages/response.html', response = 'No user with this username. Please register')
if creatorid is None:
return render_template('pages/response.html', response = 'No user with this username. Please register.')
# Task descripton
if taskdesc != '':
if not taskdesc.isprintable() or ("<" in taskdesc and ">" in taskdesc):
return render_template('pages/response.html', response = "Task description has to be made of printable characters.")
if len(taskdesc) > 2000:
return render_template('pages/response.html', response = "Task description lenght invalid, only smaller then 2000 charachters allowed")
sqladdtask = Task(name = taskname, desc = taskdesc, creatorid = creatorid)
try:
db.session.add(sqladdtask)
db.session.commit()
return render_template('pages/response.html', response = 'Task added')
except:
return render_template('pages/response.html', response = 'Adding task failed')
@app.route('/register', methods=['POST', 'GET'])
def register():
if request.method == 'GET':
return render_template('register.html')
return render_template('pages/register.html')
elif request.method == 'POST':
username = request.form['username']
contact = request.form['contact']
sqladduser = User(username = username, contact = contact)
password = request.form['password']
# Username
if not username.isalnum():
return render_template('pages/response.html', response = "Username has to be made only of letters or numbers.")
if len(username) < 1 or len(username) > 40:
return render_template('pages/response.html', response = "Username lenght invalid, only smaller then 40 charachters allowed")
# Contact
if contact != '':
if not contact.isprintable() or ("<" in contact and ">" in contact):
return render_template('pages/response.html', response = "Contact information has to be made of printable characters.")
if len(contact) > 100:
return render_template('pages/response.html', response = "Contact lenght invalid, only smaller then 100 charachters allowed")
# Password
if password != '':
if not password.isprintable():
return render_template('pages/response.html', response = "Password has to be made of printable characters.")
if len(password) > 500:
return render_template('pages/response.html', response = "Password lenght invalid, only smaller then 500 charachters allowed")
sqladduser = User(username = username, contact = contact, password = password)
try:
db.session.add(sqladduser)
db.session.commit()
return 'User added'
return render_template('pages/response.html', response = 'User added')
except:
return 'Adding user failed'
return render_template('pages/response.html', response = 'Adding user failed')
else:
return 'HTTP request method not recogniezed'
return render_template('pages/response.html', response = 'HTTP request method not recogniezed')
@app.route('/projects/<int:task_id>', methods=['GET','POST'])
def project(task_id:int):
try:
task = Task.query.get(task_id)
except:
return render_template('pages/response.html', response = 'Task not found, bad URL')
if task is None:
return render_template('pages/response.html', response = 'Task not found, bad URL')
users = gettaskusers(task_id)
if request.method == 'GET':
try:
task = Task.query.get(task_id)
except:
return 'Task not found, bad URL'
try:
userid = TaskUser.query.filter_by(taskid = task_id).first().userid
users = User.query.get(userid).username
except:
users = "No users added to this task"
return render_template("project.html", task = task, users = users)
return render_template("pages/project.html", task = task, users = users)
elif request.method == 'POST':
# Assigning user to task
username = request.form['username']
for user in users:
if username == user.username:
return render_template('pages/response.html', response = 'User already added to task')
try:
userid = User.query.filter_by(username = username).first().id
except:
return 'User not found, please <a href="/register">register</a>.'
return render_template('pages/response.html', response = 'User not found, please register.')
if userid is None:
return render_template('pages/response.html', response = 'User not found, please register.')
sqladduser = TaskUser(userid = userid, taskid = task_id)
try:
db.session.add(sqladduser)
db.session.commit()
return 'User added'
return render_template('pages/response.html', response = 'User added')
except:
return 'Adding user failed'
return render_template('pages/response.html', response = 'Adding user failed')
@app.route('/projects/<int:task_id>/del', methods=['GET','POST'])
def deltask(task_id:int):
try:
task = Task.query.get(task_id)
except:
return render_template('pages/response.html', response = 'Task not found, bad URL')
if task is None:
return render_template('pages/response.html', response = 'Task not found, bad URL')
try:
taskusers = TaskUser.query.filter_by(taskid = task_id).all()
except:
taskusers = None
creatorid = task.creatorid
if request.method == 'GET':
if creatorid is None:
try:
db.session.delete(task)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting task failed')
try:
if taskusers != None:
for taskuser in taskusers:
db.session.delete(taskuser)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting user assignment to task failed')
return render_template('pages/response.html', response = 'Task deleted')
else:
return render_template('pages/deltask.html', task = task)
if request.method == 'POST':
password = request.form['password']
if len(password) < 1 or len(password) > 500:
return render_template('pages/response.html', response = "Password lenght invalid, only smaller then 500 charachters allowed")
# Check password
if password != ADMINPASS and password != User.query.get(creatorid).password:
return render_template('pages/response.html', response = 'Wrong password')
# Delete task
try:
db.session.delete(task)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting task failed')
try:
if taskusers != None:
for taskuser in taskusers:
db.session.delete(taskuser)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting user assignment to task failed')
return render_template('pages/response.html', response = 'Task deleted')

View File

@@ -1,25 +1,36 @@
:root {
--border-radus: 1rem;
--background: #000000;
--header-background: #FFFFFF;
--background: #383840;
--header-background: #212121;
--header-height: 3rem;
--input-bar-height: 3rem;
--white: #FFF;
--primary: #d2d2d2;
--font-primary: #d2d2d2;
color: #FFF;
}
body{
background: var(--background);
font-family: sans-serif;
* {
margin: 0;
padding: 0;
}
a, a:visited {
color: var(--font-primary);
}
body {
background: var(--background);
font-family: sans-serif;
}
main {
display: flex;
flex-direction: column;
align-self: flex-start;
gap: 1rem;
margin: 0 auto;
padding: 30px 0;
height: calc(100vh - var(--header-height) - var(--input-bar-height));
box-sizing: border-box;
}
@@ -36,6 +47,12 @@ header {
box-sizing: border-box;
}
header ul {
display: flex;
list-style-type: none;
gap:15px
}
footer {
width: 100vw;
height: var(--input-bar-height);
@@ -47,3 +64,120 @@ footer {
box-sizing: border-box;
gap: 1rem;
}
/* global classes */
.container {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 20px 30px;
}
.page h1 {
margin-bottom: 15px;
}
.btn-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 15px 12px;
}
.btn {
display: inline-block;
padding: 5px 20px;
border: 1px solid var(--primary);
color: var(--primary);
background-color: transparent;
border-radius: 5px;
}
.btn:hover {
background-color: rgba(0,0,0,0.3);
cursor: pointer;
}
.btn a {
text-decoration: none;
}
.label {
text-transform: uppercase;
letter-spacing: 2px;
font-size: 14px;
font-weight: 600;
margin-bottom: 11px;
display: inline-block;
user-select: none;
}
.underline {
border-bottom: 1px solid var(--font-primary);
padding-bottom: 2px;
margin-top:20px;
}
.section-tasks > ul {
margin-left: 30px;
}
/* page index*/
.page-index .btn{
margin-bottom: 20px;
}
.task-wrap {
border: 1px solid var(--primary);
padding: 20px 40px 40px 40px;
border-radius: 5px;
box-shadow: inset 0 0 9px 4px rgba(255,255,255, 0.1);
}
.task {
display: flex;
border-bottom: 1px solid white;
}
.task a:hover {
background-color: rgba(255,255,255,0.1);
}
.task a {
display: flex;
gap: 15px;
width: 100%;
padding: 20px;
text-decoration: none;
}
/* Page projects */
.page-project h1 {
text-transform: uppercase;
}
.user-info-wrap{
margin-bottom: 10px;
}
/* form */
.form-wrap {
max-width: 400px;
}
.form-input {
display: flex;
flex-direction: column;
padding: 10px 0;
}
.form-input input {
height: 25px;
border-radius: 5px;
outline: none;
}

View File

@@ -0,0 +1,2 @@
<footer>
</footer>

View File

@@ -0,0 +1,8 @@
<header>
<nav class="container">
<ul>
<li class="current"><a href="/">Home</a></li>
<li><a href="/register">Register</a></li>
</ul>
</nav>
</header>

View File

@@ -0,0 +1,31 @@
{% extends "layouts/base.html" %}
{% block content %}
<header>
<nav class="container">
<ul>
<li class="current"><a href="/">Home</a></li>
<li><a href="/register">Register</a></li>
</ul>
</nav>
</header>
<main class="container page page-index">
<section>
<div class="btn">
<a href="/addtask">Add new task</a>
</div>
<div class="tasks-wrap">
<h1>Tasks</h1>
{% for task in tasks %}
<div class="task">
<a href="/projects/{{task.id}}">
<div>{{task.id}}.</div>
<div>{{task.name}}</div>
</a>
</div>
{% endfor %}
</div>
</section>
</main>
<footer>
</footer>
{% endblock content %}

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Decentrala</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
</header>
<main>
<section>
{% for task in tasks %}
<li><a href="/projects/{{task.id}}"> {{task.name}} </a></li>
{% endfor %}
</section>
</main>
<footer>
</footer>
</body>
</html>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/static/style.css" />
<title>Task manager</title>
</head>
<body>
{% include "includes/header.html" %}
{% block content %}
{% endblock content %}
{% include "includes/footer.html" %}
</body>
</html>

View File

@@ -0,0 +1,25 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-addtask">
<h1>Create new task</h1>
<div class="form-wrap">
<form action="/addtask" method="POST">
<div class="form-input">
<label for="taskname" class="label">Task name:</label>
<input type="text" name="taskname" id="taskname" required />
</div>
<div class="form-input">
<label for="taskdesc" class="label">Description:</label>
<input type="text" name="taskdesc" id="taskdesc" placeholder="optional"/>
</div>
<div class="form-input">
<label for="username" class="label">Username:</label>
<input type="text" name="username" id="username" placeholder="optional"/>
</div>
<div class="btn-wrap">
<button class="btn">Submit</button>
</div>
</div>
</form>
</main>
{% endblock content %}

View File

@@ -0,0 +1,19 @@
{% extends "layouts/base.html" %}
{% block content %}
<body>
<main class="container page page-addtask">
<h1>Create new task</h1>
<div class="form-wrap">
<form action="/projects/{{task.id}}/del" method="POST">
<p> Task creator's password <p>
<div class="form-input">
<label for="password" class="label">password:</label>
<input type="password" name="password" id="password" required />
</div>
<div class="btn-wrap">
<button class="btn">DELETE</button>
</div>
</div>
</form>
</main>
{% endblock content %}

View File

@@ -0,0 +1,21 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-index">
<section>
<div class="btn">
<a href="/addtask">Add new task</a>
</div>
<div class="tasks-wrap">
<h1>Tasks</h1>
{% for task in tasks %}
<div class="task">
<a href="/projects/{{task.id}}">
<div>{{task.id}}.</div>
<div>{{task.name}}</div>
</a>
</div>
{% endfor %}
</div>
</section>
</main>
{% endblock content %}

View File

@@ -0,0 +1,36 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-project">
<section >
<h1>{{task.name}}</h1>
<label class="label underline">Description</label>
<p>{{task.desc}}</p>
</section>
<section class="section-task">
<div>
<label class="label underline">Users added to this task</label>
{% for user in users %}
<div class="user-info-wrap">
<div><b>Username:</b> {{user.username}}</div>
<div><b>Contact info:</b> {{user.contact}}</div>
</div>
{% endfor %}
</div>
<div>
<label class="label underline"> Add person to task</label>
<div class="form-wrap">
<form action="/projects/{{task.id}}" method="POST">
<div class="form-input">
<label for="username" class="label">Username:</label>
<input type="text" name="username" id="username" required />
</div>
<div class="btn-wrap">
<button class="btn">Submit</button>
</div>
</form>
<p><a href="/projects/{{task.id}}/del">DELETE TASK</a><p>
</div>
</div>
</section>
</main>
{% endblock content %}

View File

@@ -0,0 +1,24 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-register">
<div class="form-wrap">
<form action="/register" method="POST">
<div class="form-input">
<label for="username">Username:</label>
<input type="text" name="username" id="username" required />
</div>
<div class="form-input">
<label for="contact">Contact:</label>
<input type="text" name="contact" id="contact" required />
</div>
<div class="form-input">
<label for="password">Password:</label>
<input type="password" name="password" placeholder="optional" id="password"/>
</div>
<div class="btn-wrap">
<button class="btn">Submit</button>
</div>
</form>
</div>
</main>
{% endblock content %}

View File

@@ -0,0 +1,8 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-project">
<section >
<p>{{response}}<p>
</section>
</main>
{% endblock content %}

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Decentrala</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
</header>
<main>
<section>
<li>{{task.name}}</li>
<li>{{task.desc}}</li>
<li>{{users}}</li>
</section>
<h1> Add yourself to task </h1>
<form action="/projects/{{task.id}}" method="POST">
<label for="username">username</label>
<input type="text" name="username" id="username" required>
<button> Submit </button>
</form>
</main>
<footer>
</footer>
</body>
</html>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Register</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<main>
<form action="/register" method="POST">
<label for="username">username</label>
<input type="text" name="username" id="username" required>
<label for="contact">contact</label>
<input type="text" name="contact" id="contact" required>
<button> Submit </button>
</form>
</main>
</body>
</html>