Add initial project structure with Docker setup and frontend/backend files
This commit is contained in:
9
backend/Dockerfile
Executable file
9
backend/Dockerfile
Executable file
@@ -0,0 +1,9 @@
|
||||
FROM python:3.9-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
ENV DATABASE_URL=postgresql://username:password@db/workout_buddy
|
||||
ENV JWT_SECRET_KEY=super-secret
|
||||
EXPOSE 5000
|
||||
CMD ["python", "app.py"]
|
||||
145
backend/app.py
Executable file
145
backend/app.py
Executable file
@@ -0,0 +1,145 @@
|
||||
import os
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_restful import Api, Resource
|
||||
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from marshmallow import Schema, fields, validate
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from flask_cors import CORS
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# PostgreSQL database configuration
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY', 'super-secret') # Change this in production!
|
||||
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 3600 # 1 hour
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'postgresql://workout_user:workout_password@db/workout_buddy')
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
api = Api(app)
|
||||
jwt = JWTManager(app)
|
||||
limiter = Limiter(app, key_func=get_remote_address)
|
||||
|
||||
# Set up logging
|
||||
if not app.debug:
|
||||
if not os.path.exists('logs'):
|
||||
os.mkdir('logs')
|
||||
file_handler = RotatingFileHandler('logs/workout_buddy.log', maxBytes=10240, backupCount=10)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
|
||||
file_handler.setLevel(logging.INFO)
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.info('Workout Buddy startup')
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
age = db.Column(db.Integer, nullable=False)
|
||||
location = db.Column(db.String(100), nullable=False)
|
||||
gender = db.Column(db.String(10), nullable=False)
|
||||
password_hash = db.Column(db.String(128))
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
updated_at = db.Column(db.DateTime(timezone=True), server_default=db.func.now(), onupdate=db.func.now())
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
class Message(db.Model):
|
||||
__tablename__ = 'messages'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
receiver_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
content = db.Column(db.String(500), nullable=False)
|
||||
created_at = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
sender = db.relationship('User', foreign_keys=[sender_id], backref=db.backref('sent_messages', lazy=True))
|
||||
receiver = db.relationship('User', foreign_keys=[receiver_id], backref=db.backref('received_messages', lazy=True))
|
||||
|
||||
class UserSchema(Schema):
|
||||
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
|
||||
email = fields.Email(required=True)
|
||||
age = fields.Int(required=True, validate=validate.Range(min=18, max=120))
|
||||
location = fields.Str(required=True, validate=validate.Length(min=1, max=100))
|
||||
password = fields.Str(required=True, validate=validate.Length(min=8))
|
||||
|
||||
class MessageSchema(Schema):
|
||||
content = fields.Str(required=True, validate=validate.Length(min=1, max=500))
|
||||
|
||||
user_schema = UserSchema()
|
||||
message_schema = MessageSchema()
|
||||
|
||||
# Define the resource classes
|
||||
class UserResource(Resource):
|
||||
def post(self):
|
||||
try:
|
||||
data = request.get_json()
|
||||
errors = user_schema.validate(data)
|
||||
if errors:
|
||||
return {'message': 'Validation error', 'errors': errors}, 400
|
||||
|
||||
existing_user = User.query.filter_by(email=data['email']).first()
|
||||
if existing_user:
|
||||
return {'message': 'User with this email already exists'}, 409
|
||||
|
||||
user = User(
|
||||
name=data['name'],
|
||||
email=data['email'],
|
||||
age=data['age'],
|
||||
location=data['location'],
|
||||
gender=data['gender']
|
||||
)
|
||||
user.set_password(data['password'])
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user_schema.dump(user), 201
|
||||
except Exception as e:
|
||||
app.logger.error(f'Error creating user: {str(e)}')
|
||||
app.logger.info(f'Attempting to create user: {data["email"]}')
|
||||
return {'message': 'An error occurred while creating the user'}, 500
|
||||
class MessageResource(Resource):
|
||||
@jwt_required()
|
||||
def get(self):
|
||||
messages = Message.query.all()
|
||||
return message_schema.dump(messages, many=True), 200
|
||||
|
||||
@jwt_required()
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
message = Message(
|
||||
sender_id=get_jwt_identity(), # Assuming JWT identity is user ID
|
||||
receiver_id=data['receiver_id'],
|
||||
content=data['content']
|
||||
)
|
||||
db.session.add(message)
|
||||
db.session.commit()
|
||||
return message_schema.dump(message), 201
|
||||
|
||||
class AuthResource(Resource):
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
user = User.query.filter_by(email=data['email']).first()
|
||||
if user and user.check_password(data['password']):
|
||||
access_token = create_access_token(identity=user.id)
|
||||
return {'access_token': access_token}, 200
|
||||
return {'message': 'Invalid credentials'}, 401
|
||||
|
||||
# Add resource endpoints
|
||||
api.add_resource(UserResource, '/users')
|
||||
api.add_resource(MessageResource, '/messages')
|
||||
api.add_resource(AuthResource, '/auth')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
9
backend/requirements.txt
Executable file
9
backend/requirements.txt
Executable file
@@ -0,0 +1,9 @@
|
||||
Flask==2.2.5
|
||||
Flask-SQLAlchemy==3.0.4
|
||||
Flask-RESTful==0.3.9
|
||||
Flask-JWT-Extended==4.4.4
|
||||
Werkzeug==2.3.0
|
||||
marshmallow==3.19.0
|
||||
Flask-Limiter==2.7.0
|
||||
psycopg2-binary==2.9.7
|
||||
flask-cors==3.0.10
|
||||
50
db/init.sql
Executable file
50
db/init.sql
Executable file
@@ -0,0 +1,50 @@
|
||||
-- Create Users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(120) UNIQUE NOT NULL,
|
||||
age INTEGER NOT NULL,
|
||||
location VARCHAR(100) NOT NULL,
|
||||
gender VARCHAR(10) NOT NULL,
|
||||
password_hash VARCHAR(128) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create Messages table
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sender_id INTEGER NOT NULL REFERENCES users(id),
|
||||
receiver_id INTEGER NOT NULL REFERENCES users(id),
|
||||
content VARCHAR(500) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index for faster queries on location
|
||||
CREATE INDEX IF NOT EXISTS idx_users_location ON users(location);
|
||||
|
||||
-- Create indexes for faster message retrieval
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages(receiver_id);
|
||||
|
||||
-- Function to update the updated_at column
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Drop the trigger if it already exists to avoid conflicts
|
||||
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
|
||||
|
||||
-- Create trigger to automatically update the updated_at column
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Grant necessary permissions to workout_user
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO workout_user;
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO workout_user;
|
||||
41
docker-compose.yaml
Executable file
41
docker-compose.yaml
Executable file
@@ -0,0 +1,41 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:13
|
||||
environment:
|
||||
- POSTGRES_DB=workout_buddy
|
||||
- POSTGRES_USER=workout_user
|
||||
- POSTGRES_PASSWORD=workout_password
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
- ./db/:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U workout_user -d workout_buddy"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://workout_user:workout_password@db/workout_buddy
|
||||
- JWT_SECRET_KEY=super-secret
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- SECRET_KEY=super-secret
|
||||
- API_URL=http://backend:5000
|
||||
depends_on:
|
||||
- backend
|
||||
#
|
||||
volumes:
|
||||
db-data:
|
||||
9
frontend/Dockerfile
Executable file
9
frontend/Dockerfile
Executable file
@@ -0,0 +1,9 @@
|
||||
FROM python:3.9-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
ENV SECRET_KEY=super-secret
|
||||
ENV API_URL=http://backend:5000
|
||||
EXPOSE 8000
|
||||
CMD ["python", "app.py"]
|
||||
101
frontend/app.py
Executable file
101
frontend/app.py
Executable file
@@ -0,0 +1,101 @@
|
||||
|
||||
import os
|
||||
from flask import Flask, render_template, redirect, url_for, request, session, jsonify
|
||||
import requests
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'super-secret')
|
||||
|
||||
API_URL = os.environ.get('API_URL', 'http://localhost:5000')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if 'access_token' in session:
|
||||
return redirect(url_for('dashboard'))
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
data = {
|
||||
'name': request.form['name'],
|
||||
'email': request.form['email'],
|
||||
'age': request.form['age'],
|
||||
'location': request.form['location'],
|
||||
'gender': request.form['gender'],
|
||||
'password': request.form['password']
|
||||
}
|
||||
response = requests.post(f"{API_URL}/users", json=data)
|
||||
if response.status_code == 201:
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
return render_template('register.html', error=response.json().get('message', 'Error during registration'))
|
||||
return render_template('register.html')
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
response = requests.post(f"{API_URL}/auth", json={'email': email, 'password': password})
|
||||
if response.status_code == 200:
|
||||
session['access_token'] = response.json()['access_token']
|
||||
# Save the user ID from the access token or fetch it from the API
|
||||
session['user_id'] = email # Or any unique identifier like user ID
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
return render_template('login.html', error=response.json().get('message', 'Invalid credentials'))
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/dashboard')
|
||||
def dashboard():
|
||||
if 'access_token' not in session:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# Fetch the current user's details
|
||||
headers = {'Authorization': f'Bearer {session["access_token"]}'}
|
||||
user_response = requests.get(f"{API_URL}/users/me", headers=headers) # Adjust the API URL as necessary
|
||||
user = user_response.json() if user_response.status_code == 200 else None
|
||||
|
||||
# Handle search filters
|
||||
location = request.args.get('location', '')
|
||||
age = request.args.get('age', '')
|
||||
gender = request.args.get('gender', 'Any')
|
||||
|
||||
filters = {}
|
||||
if location:
|
||||
filters['location'] = location
|
||||
if age:
|
||||
filters['age'] = age
|
||||
if gender and gender != "Any":
|
||||
filters['gender'] = gender
|
||||
|
||||
# Fetch users based on filters
|
||||
response = requests.get(f"{API_URL}/users", params=filters, headers=headers)
|
||||
users = response.json() if response.status_code == 200 else []
|
||||
|
||||
# Fetch messages for the current user
|
||||
messages_response = requests.get(f"{API_URL}/messages", headers=headers)
|
||||
messages = messages_response.json() if messages_response.status_code == 200 else []
|
||||
|
||||
return render_template('dashboard.html', user=user, users=users, messages=messages)
|
||||
|
||||
@app.route('/messages', methods=['POST'])
|
||||
def send_message():
|
||||
if 'access_token' not in session:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
data = {
|
||||
'receiver_id': request.form['receiver_id'],
|
||||
'content': request.form['message_content']
|
||||
}
|
||||
headers = {'Authorization': f'Bearer {session["access_token"]}', 'Content-Type': 'application/json'}
|
||||
response = requests.post(f"{API_URL}/messages", headers=headers, json=data)
|
||||
|
||||
if response.status_code == 201:
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
return jsonify({'message': 'Error sending message'}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
4
frontend/requirements.txt
Executable file
4
frontend/requirements.txt
Executable file
@@ -0,0 +1,4 @@
|
||||
Flask==2.2.5
|
||||
requests==2.31.0
|
||||
Werkzeug==2.3.0
|
||||
Flask-JWT-Extended==4.4.4
|
||||
51
frontend/templates/dashboard.html
Executable file
51
frontend/templates/dashboard.html
Executable file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome, {{ user.name }}</h1>
|
||||
<a href="{{ url_for('logout') }}">Logout</a>
|
||||
|
||||
<h2>Search Users</h2>
|
||||
<form method="GET">
|
||||
<input type="text" name="location" placeholder="Location">
|
||||
<input type="number" name="age" placeholder="Age">
|
||||
<select name="gender">
|
||||
<option value="Any">Any</option>
|
||||
<option value="Male">Male</option>
|
||||
<option value="Female">Female</option>
|
||||
</select>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
<h2>Users</h2>
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>{{ user.name }} - {{ user.age }} years old - {{ user.location }} - {{ user.gender }}</li>
|
||||
{% else %}
|
||||
<li>No users found.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Messages</h2>
|
||||
<form method="POST" action="{{ url_for('send_message') }}">
|
||||
<input type="hidden" name="receiver_id" value="{{ user.id }}">
|
||||
<textarea name="message_content" placeholder="Type your message here..."></textarea>
|
||||
<button type="submit">Send Message</button>
|
||||
</form>
|
||||
|
||||
<ul>
|
||||
{% for message in messages %}
|
||||
<li>{{ message.content }}</li>
|
||||
{% else %}
|
||||
<li>No messages found.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
13
frontend/templates/eula.html
Executable file
13
frontend/templates/eula.html
Executable file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>End User License Agreement</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>End User License Agreement (EULA)</h1>
|
||||
<p>[Insert your EULA text here]</p>
|
||||
<a href="{{ url_for('register') }}">Back to Registration</a>
|
||||
</body>
|
||||
</html>
|
||||
18
frontend/templates/index.html
Executable file
18
frontend/templates/index.html
Executable file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Home</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to the Workout Buddy</h1>
|
||||
<div>
|
||||
<a href="{{ url_for('login') }}">Login</a>
|
||||
<a href="{{ url_for('register') }}">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
frontend/templates/login.html
Executable file
26
frontend/templates/login.html
Executable file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Login</h1>
|
||||
<form method="POST">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" name="email" required>
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" name="password" required>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
50
frontend/templates/register.html
Executable file
50
frontend/templates/register.html
Executable file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Register</h1>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="age">Age:</label>
|
||||
<input type="number" name="age" min="18" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="location">Location:</label>
|
||||
<input type="text" name="location" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gender">Gender:</label>
|
||||
<select name="gender" required>
|
||||
<option value="Male">Male</option>
|
||||
<option value="Female">Female</option>
|
||||
<option value="Prefer Not to Say">Prefer Not to Say</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<p>By registering, you agree to our <a href="/eula" target="_blank">End User License Agreement (EULA)</a>.</p>
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
40
frontend/templates/static/styles.css
Executable file
40
frontend/templates/static/styles.css
Executable file
@@ -0,0 +1,40 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4; /* Light background color for better visibility */
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px; /* Limit the width of the form container */
|
||||
margin: 50px auto; /* Center the form horizontally and add vertical margin */
|
||||
padding: 20px;
|
||||
background-color: white; /* White background for the form */
|
||||
border-radius: 8px; /* Rounded corners for the form */
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center; /* Center the heading */
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 500px; /* Increase space between form groups */
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%; /* Make the button full-width */
|
||||
padding: 10px; /* Add padding to the button */
|
||||
background-color: #28a745; /* Green background color */
|
||||
color: white; /* White text color */
|
||||
border: none; /* Remove default border */
|
||||
border-radius: 5px; /* Rounded corners for the button */
|
||||
cursor: pointer; /* Change cursor to pointer on hover */
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #218838; /* Darker green on hover */
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red; /* Red color for error messages */
|
||||
text-align: center; /* Center error messages */
|
||||
}
|
||||
40
frontend/templates/styles.css
Executable file
40
frontend/templates/styles.css
Executable file
@@ -0,0 +1,40 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4; /* Light background color for better visibility */
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px; /* Limit the width of the form container */
|
||||
margin: 50px auto; /* Center the form horizontally and add vertical margin */
|
||||
padding: 20px;
|
||||
background-color: white; /* White background for the form */
|
||||
border-radius: 8px; /* Rounded corners for the form */
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center; /* Center the heading */
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 500px; /* Increase space between form groups */
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%; /* Make the button full-width */
|
||||
padding: 10px; /* Add padding to the button */
|
||||
background-color: #28a745; /* Green background color */
|
||||
color: white; /* White text color */
|
||||
border: none; /* Remove default border */
|
||||
border-radius: 5px; /* Rounded corners for the button */
|
||||
cursor: pointer; /* Change cursor to pointer on hover */
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #218838; /* Darker green on hover */
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red; /* Red color for error messages */
|
||||
text-align: center; /* Center error messages */
|
||||
}
|
||||
Reference in New Issue
Block a user