Compare commits
13 Commits
revert-2-r
...
root_serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b9e900cfe | ||
| bade1f11dd | |||
| b843849af9 | |||
| 29205de12f | |||
| a105372638 | |||
| 7e8266d735 | |||
| cc6c3699f6 | |||
| d84cc29edb | |||
|
|
2fcd7f4276 | ||
| 56ed26b3f0 | |||
| 18a9a65f70 | |||
|
|
d07f409426 | ||
| baa158fcd0 |
33
app.py
33
app.py
@@ -20,7 +20,7 @@ from config import app_config, Config
|
|||||||
from controllers.core import routes_core
|
from controllers.core import routes_core
|
||||||
from controllers.legal import routes_legal
|
from controllers.legal import routes_legal
|
||||||
from controllers.user import routes_user
|
from controllers.user import routes_user
|
||||||
from extensions import db, csrf, cors, mail, oauth
|
from extensions import db, csrf, mail, oauth
|
||||||
from helpers.helper_app import Helper_App
|
from helpers.helper_app import Helper_App
|
||||||
# external
|
# external
|
||||||
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session
|
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session
|
||||||
@@ -41,12 +41,20 @@ sys.path.insert(0, os.path.dirname(__file__)) # Todo: why?
|
|||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.getenv('KEY_SECRET_FLASK')
|
||||||
|
|
||||||
# AppConfig(app)
|
# AppConfig(app)
|
||||||
app.config.from_object(app_config) # for db init with required keys
|
app.config.from_object(app_config) # for db init with required keys
|
||||||
app.app_config = app_config
|
app.app_config = app_config
|
||||||
# app.config["config"] = app_config()
|
# app.config["config"] = app_config()
|
||||||
|
|
||||||
|
print('sql vars')
|
||||||
|
print(app.config['DB_PASSWORD'])
|
||||||
|
print(app.config['DB_USER'])
|
||||||
|
print(app.config['SQLALCHEMY_DATABASE_URI'])
|
||||||
|
print(app.config['SECRET_KEY'])
|
||||||
|
print(os.getenv('KEY_SECRET_FLASK'))
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
handler = RotatingFileHandler('app.log', maxBytes=10000, backupCount=3)
|
handler = RotatingFileHandler('app.log', maxBytes=10000, backupCount=3)
|
||||||
handler.setLevel(logging.DEBUG)
|
handler.setLevel(logging.DEBUG)
|
||||||
@@ -73,12 +81,13 @@ def make_session_permanent():
|
|||||||
session.permanent = True
|
session.permanent = True
|
||||||
|
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
"""
|
cors = CORS(app, resources={
|
||||||
cors = CORS()
|
r"/static/*": {
|
||||||
db = SQLAlchemy()
|
"origins": [app.config["URL_HOST"]],
|
||||||
mail = Mail()
|
"methods": ["GET"],
|
||||||
oauth = OAuth()
|
"max_age": 3600
|
||||||
"""
|
}
|
||||||
|
})
|
||||||
|
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
cors.init_app(app)
|
cors.init_app(app)
|
||||||
@@ -115,3 +124,13 @@ app.register_blueprint(routes_user)
|
|||||||
def console_log(value):
|
def console_log(value):
|
||||||
Helper_App.console_log(value)
|
Helper_App.console_log(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def add_cache_headers(response):
|
||||||
|
if request.path.startswith('/static/'):
|
||||||
|
# Cache static assets
|
||||||
|
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||||
|
else:
|
||||||
|
# No caching for dynamic content
|
||||||
|
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||||
|
return response
|
||||||
12
config.py
12
config.py
@@ -40,8 +40,8 @@ class Config:
|
|||||||
# Auth0
|
# Auth0
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
# SESSION_COOKIE_SAMESITE = 'Lax'
|
SESSION_COOKIE_SAMESITE = 'Strict'
|
||||||
# PERMANENT_SESSION_LIFETIME = 3600
|
REMEMBER_COOKIE_SECURE = True
|
||||||
WTF_CSRF_ENABLED = True
|
WTF_CSRF_ENABLED = True
|
||||||
# WTF_CSRF_CHECK_DEFAULT = False # We'll check it manually for API routes
|
# WTF_CSRF_CHECK_DEFAULT = False # We'll check it manually for API routes
|
||||||
# WTF_CSRF_HEADERS = ['X-CSRFToken'] # Accept CSRF token from this header
|
# WTF_CSRF_HEADERS = ['X-CSRFToken'] # Accept CSRF token from this header
|
||||||
@@ -52,7 +52,7 @@ class Config:
|
|||||||
DOMAIN_AUTH0 = os.getenv('DOMAIN_AUTH0')
|
DOMAIN_AUTH0 = os.getenv('DOMAIN_AUTH0')
|
||||||
ID_TOKEN_USER = 'user'
|
ID_TOKEN_USER = 'user'
|
||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
DB_NAME = os.getenv('partsltd')
|
DB_NAME = os.getenv('partsltd_prod')
|
||||||
DB_USER = os.getenv('DB_USER')
|
DB_USER = os.getenv('DB_USER')
|
||||||
DB_PASSWORD = os.getenv('DB_PASSWORD')
|
DB_PASSWORD = os.getenv('DB_PASSWORD')
|
||||||
DB_HOST = os.getenv('DB_HOST')
|
DB_HOST = os.getenv('DB_HOST')
|
||||||
@@ -80,9 +80,15 @@ class Config:
|
|||||||
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
||||||
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
|
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
|
||||||
MAIL_CONTACT_PUBLIC = os.getenv('MAIL_CONTACT_PUBLIC')
|
MAIL_CONTACT_PUBLIC = os.getenv('MAIL_CONTACT_PUBLIC')
|
||||||
|
"""
|
||||||
# Recaptcha
|
# Recaptcha
|
||||||
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
|
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
|
||||||
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
|
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
|
||||||
|
"""
|
||||||
|
# ALTCHA
|
||||||
|
ALTCHA_API_KEY = os.getenv('ALTCHA_API_KEY')
|
||||||
|
ALTCHA_SECRET_KEY = os.getenv('ALTCHA_SECRET_KEY')
|
||||||
|
ALTCHA_REGION = 'eu'
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
class DevelopmentConfig(Config):
|
||||||
is_development = True
|
is_development = True
|
||||||
|
|||||||
@@ -14,18 +14,24 @@ Initializes the Flask application, sets the configuration based on the environme
|
|||||||
# internal
|
# internal
|
||||||
from datastores.datastore_base import DataStore_Base
|
from datastores.datastore_base import DataStore_Base
|
||||||
from forms.contact import Form_Contact
|
from forms.contact import Form_Contact
|
||||||
|
from helpers.helper_app import Helper_App
|
||||||
from models.model_view_contact import Model_View_Contact
|
from models.model_view_contact import Model_View_Contact
|
||||||
from models.model_view_home import Model_View_Home
|
from models.model_view_home import Model_View_Home
|
||||||
import lib.argument_validation as av
|
import lib.argument_validation as av
|
||||||
# external
|
# external
|
||||||
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session, Blueprint, current_app
|
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session, Blueprint, current_app, flash
|
||||||
from flask_mail import Mail, Message
|
from flask_mail import Mail, Message
|
||||||
from extensions import db, oauth, mail
|
from extensions import db, oauth, mail
|
||||||
from urllib.parse import quote_plus, urlencode
|
from urllib.parse import quote_plus, urlencode
|
||||||
from authlib.integrations.flask_client import OAuth
|
from authlib.integrations.flask_client import OAuth
|
||||||
from authlib.integrations.base_client import OAuthError
|
from authlib.integrations.base_client import OAuthError
|
||||||
from urllib.parse import quote, urlparse, parse_qs
|
from urllib.parse import quote, urlparse, parse_qs
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import datetime
|
||||||
|
from altcha import ChallengeOptions, create_challenge, verify_solution
|
||||||
|
|
||||||
routes_core = Blueprint('routes_core', __name__)
|
routes_core = Blueprint('routes_core', __name__)
|
||||||
|
|
||||||
@@ -53,10 +59,12 @@ def contact():
|
|||||||
def contact_post():
|
def contact_post():
|
||||||
try:
|
try:
|
||||||
form = Form_Contact()
|
form = Form_Contact()
|
||||||
|
Helper_App.console_log(f"Form submitted: {request.form}")
|
||||||
|
Helper_App.console_log(f"ALTCHA data in request: {request.form.get('altcha')}")
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Handle form submission
|
try:
|
||||||
email = form.email.data
|
email = form.email.data
|
||||||
CC = form.CC.data # not in use
|
# CC = form.CC.data # not in use
|
||||||
contact_name = form.contact_name.data
|
contact_name = form.contact_name.data
|
||||||
company_name = form.company_name.data
|
company_name = form.company_name.data
|
||||||
message = form.message.data
|
message = form.message.data
|
||||||
@@ -66,8 +74,85 @@ def contact_post():
|
|||||||
mailItem = Message("PARTS Website Contact Us Message", recipients=[current_app.config['MAIL_CONTACT_PUBLIC']])
|
mailItem = Message("PARTS Website Contact Us Message", recipients=[current_app.config['MAIL_CONTACT_PUBLIC']])
|
||||||
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n{receive_marketing_text}\nKind regards,\n{contact_name}\n{company_name}\n{email}"
|
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n{receive_marketing_text}\nKind regards,\n{contact_name}\n{company_name}\n{email}"
|
||||||
mail.send(mailItem)
|
mail.send(mailItem)
|
||||||
|
flash('Thank you for your message. We will get back to you soon!', 'success')
|
||||||
return "Submitted."
|
return "Submitted."
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
|
print(f"Form validation errors: {form.errors}")
|
||||||
return "Invalid. Failed to submit."
|
return "Invalid. Failed to submit."
|
||||||
# html_body = render_template('pages/core/_contact.html', model = model)
|
# html_body = render_template('pages/core/_contact.html', model = model)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify(error=str(e)), 403
|
return jsonify(error=str(e)), 403
|
||||||
|
|
||||||
|
@routes_core.route(Model_View_Contact.HASH_ALTCHA_CREATE_CHALLENGE, methods=['GET'])
|
||||||
|
def create_altcha_challenge():
|
||||||
|
options = ChallengeOptions(
|
||||||
|
expires = datetime.datetime.now() + datetime.timedelta(hours=1),
|
||||||
|
max_number = 100000, # The maximum random number
|
||||||
|
hmac_key = current_app.config["ALTCHA_SECRET_KEY"],
|
||||||
|
)
|
||||||
|
challenge = create_challenge(options)
|
||||||
|
print("Challenge created:", challenge)
|
||||||
|
# return jsonify({"challenge": challenge})
|
||||||
|
return jsonify({
|
||||||
|
"algorithm": challenge.algorithm,
|
||||||
|
"challenge": challenge.challenge,
|
||||||
|
"salt": challenge.salt,
|
||||||
|
"signature": challenge.signature,
|
||||||
|
})
|
||||||
|
|
||||||
|
"""
|
||||||
|
def verify_altcha_signature(payload):
|
||||||
|
"" "Verify the ALTCHA signature"" "
|
||||||
|
if 'algorithm' not in payload or 'signature' not in payload or 'verificationData' not in payload:
|
||||||
|
return False
|
||||||
|
|
||||||
|
algorithm = payload['algorithm']
|
||||||
|
signature = payload['signature']
|
||||||
|
verification_data = payload['verificationData']
|
||||||
|
|
||||||
|
# Calculate SHA hash of the verification data
|
||||||
|
if algorithm == 'SHA-256':
|
||||||
|
hash_func = hashlib.sha256
|
||||||
|
else:
|
||||||
|
# Fallback to SHA-256 if algorithm not specified
|
||||||
|
hash_func = hashlib.sha256
|
||||||
|
|
||||||
|
# Calculate the hash of verification_data
|
||||||
|
data_hash = hash_func(verification_data.encode('utf-8')).digest()
|
||||||
|
|
||||||
|
# Calculate the HMAC signature
|
||||||
|
calculated_signature = hmac.new(
|
||||||
|
current_app.config["ALTCHA_SECRET_KEY"].encode('utf-8'),
|
||||||
|
data_hash,
|
||||||
|
hash_func
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
# Compare the calculated signature with the provided signature
|
||||||
|
return hmac.compare_digest(calculated_signature, signature)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_altcha_dummy_signature(challenge):
|
||||||
|
# Example payload to verify
|
||||||
|
payload = {
|
||||||
|
"algorithm": challenge.algorithm,
|
||||||
|
"challenge": challenge.challenge,
|
||||||
|
"number": 12345, # Example number
|
||||||
|
"salt": challenge.salt,
|
||||||
|
"signature": challenge.signature,
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
|
||||||
|
@routes_core.route(Model_View_Contact.HASH_ALTCHA_VERIFY_SOLUTION, methods=['POST'])
|
||||||
|
def verify_altcha_challenge():
|
||||||
|
payload = request.json
|
||||||
|
|
||||||
|
ok, err = verify_solution(payload, current_app.config["ALTCHA_SECRET_KEY"], check_expires=True)
|
||||||
|
if err:
|
||||||
|
return jsonify({"error": err}), 400
|
||||||
|
elif ok:
|
||||||
|
return jsonify({"verified": True})
|
||||||
|
else:
|
||||||
|
return jsonify({"verified": False}), 403
|
||||||
|
"""
|
||||||
@@ -8,7 +8,7 @@ from authlib.integrations.flask_client import OAuth
|
|||||||
|
|
||||||
|
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
cors = CORS()
|
# cors = CORS()
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
mail = Mail()
|
mail = Mail()
|
||||||
oauth = OAuth()
|
oauth = OAuth()
|
||||||
@@ -14,13 +14,65 @@ Defines Flask-WTF form for handling user input on Contact Us page.
|
|||||||
# internal
|
# internal
|
||||||
# from business_objects.store.product_category import Filters_Product_Category # circular
|
# from business_objects.store.product_category import Filters_Product_Category # circular
|
||||||
# from models.model_view_store import Model_View_Store # circular
|
# from models.model_view_store import Model_View_Store # circular
|
||||||
|
from models.model_view_base import Model_View_Base
|
||||||
from forms.base import Form_Base
|
from forms.base import Form_Base
|
||||||
# external
|
# external
|
||||||
|
from flask import Flask, render_template, request, flash, redirect, url_for, current_app
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, TextAreaField, SubmitField, BooleanField, IntegerField, SelectField, FloatField
|
from wtforms import StringField, TextAreaField, SubmitField, HiddenField, BooleanField, Field
|
||||||
from wtforms.validators import InputRequired, NumberRange, Regexp, DataRequired, Optional
|
from wtforms.validators import DataRequired, Email, ValidationError
|
||||||
|
import markupsafe
|
||||||
from flask_wtf.recaptcha import RecaptchaField
|
from flask_wtf.recaptcha import RecaptchaField
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
import json
|
||||||
|
from altcha import verify_solution
|
||||||
|
import base64
|
||||||
|
|
||||||
|
class ALTCHAValidator:
|
||||||
|
def __init__(self, message=None):
|
||||||
|
self.message = message or 'ALTCHA verification failed'
|
||||||
|
|
||||||
|
def __call__(self, form, field):
|
||||||
|
altcha_data = field.data
|
||||||
|
|
||||||
|
if not altcha_data:
|
||||||
|
raise ValidationError(self.message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# The data is base64 encoded JSON
|
||||||
|
try:
|
||||||
|
# First try to decode it as JSON directly (if it's not base64 encoded)
|
||||||
|
altcha_payload = json.loads(altcha_data)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# If direct JSON decoding fails, try base64 decoding first
|
||||||
|
decoded_data = base64.b64decode(altcha_data).decode('utf-8')
|
||||||
|
altcha_payload = json.loads(decoded_data)
|
||||||
|
|
||||||
|
ok, err = verify_solution(altcha_payload, current_app.config["ALTCHA_SECRET_KEY"], check_expires=True)
|
||||||
|
|
||||||
|
if err or not ok:
|
||||||
|
raise ValidationError(self.message + ': ' + (err or 'Invalid solution'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationError(f'Invalid ALTCHA data: {str(e)}')
|
||||||
|
|
||||||
|
class ALTCHAField(Field):
|
||||||
|
def __init__(self, label='', validators=None, **kwargs):
|
||||||
|
validators = validators or []
|
||||||
|
validators.append(ALTCHAValidator())
|
||||||
|
|
||||||
|
super(ALTCHAField, self).__init__(label, validators, **kwargs)
|
||||||
|
|
||||||
|
def __call__(self, **kwargs):
|
||||||
|
html = f"""
|
||||||
|
<altcha-widget
|
||||||
|
challengeurl="/get-challenge"
|
||||||
|
auto="onload"
|
||||||
|
id="{self.id}"
|
||||||
|
name="{self.name}">
|
||||||
|
</altcha-widget>
|
||||||
|
"""
|
||||||
|
return markupsafe.Markup(html)
|
||||||
|
|
||||||
|
|
||||||
class Form_Contact(FlaskForm):
|
class Form_Contact(FlaskForm):
|
||||||
@@ -29,5 +81,7 @@ class Form_Contact(FlaskForm):
|
|||||||
company_name = StringField('Company')
|
company_name = StringField('Company')
|
||||||
message = TextAreaField('Message')
|
message = TextAreaField('Message')
|
||||||
receive_marketing = BooleanField('I would like to receive marketing emails.')
|
receive_marketing = BooleanField('I would like to receive marketing emails.')
|
||||||
recaptcha = RecaptchaField()
|
# recaptcha = RecaptchaField()
|
||||||
|
# altcha = HiddenField('ALTCHA') # , validators=[validate_altcha]
|
||||||
|
altcha = ALTCHAField('Verification')
|
||||||
submit = SubmitField('Send Message')
|
submit = SubmitField('Send Message')
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ class Model_View_Base(BaseModel, ABC):
|
|||||||
FLAG_PAGE_BODY: ClassVar[str] = 'page-body'
|
FLAG_PAGE_BODY: ClassVar[str] = 'page-body'
|
||||||
FLAG_PHONE_NUMBER: ClassVar[str] = Base.FLAG_PHONE_NUMBER
|
FLAG_PHONE_NUMBER: ClassVar[str] = Base.FLAG_PHONE_NUMBER
|
||||||
FLAG_POSTCODE: ClassVar[str] = Base.FLAG_POSTCODE
|
FLAG_POSTCODE: ClassVar[str] = Base.FLAG_POSTCODE
|
||||||
|
FLAG_CAPTCHA: ClassVar[str] = 'recaptcha'
|
||||||
FLAG_RIGHT_HAND_SIDE: ClassVar[str] = 'rhs'
|
FLAG_RIGHT_HAND_SIDE: ClassVar[str] = 'rhs'
|
||||||
FLAG_ROW: ClassVar[str] = 'row'
|
FLAG_ROW: ClassVar[str] = 'row'
|
||||||
FLAG_ROW_NEW: ClassVar[str] = 'row-new'
|
FLAG_ROW_NEW: ClassVar[str] = 'row-new'
|
||||||
@@ -148,6 +149,8 @@ class Model_View_Base(BaseModel, ABC):
|
|||||||
FLAG_USER: ClassVar[str] = User.FLAG_USER
|
FLAG_USER: ClassVar[str] = User.FLAG_USER
|
||||||
FLAG_WEBSITE: ClassVar[str] = Base.FLAG_WEBSITE
|
FLAG_WEBSITE: ClassVar[str] = Base.FLAG_WEBSITE
|
||||||
# flagIsDatePicker: ClassVar[str] = 'is-date-picker'
|
# flagIsDatePicker: ClassVar[str] = 'is-date-picker'
|
||||||
|
HASH_ALTCHA_CREATE_CHALLENGE: ClassVar[str] = '/altcha/create-challenge'
|
||||||
|
# HASH_ALTCHA_VERIFY_SOLUTION: ClassVar[str] = '/altcha/verify-solution'
|
||||||
HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION: ClassVar[str] = '/store/permutation_filter'
|
HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION: ClassVar[str] = '/store/permutation_filter'
|
||||||
HASH_CALLBACK_LOGIN: ClassVar[str] = '/callback-login'
|
HASH_CALLBACK_LOGIN: ClassVar[str] = '/callback-login'
|
||||||
HASH_PAGE_ACCESSIBILITY_REPORT: ClassVar[str] = '/accessibility-report'
|
HASH_PAGE_ACCESSIBILITY_REPORT: ClassVar[str] = '/accessibility-report'
|
||||||
|
|||||||
@@ -22,18 +22,11 @@ from pydantic import BaseModel
|
|||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
class Model_View_Contact(Model_View_Base):
|
class Model_View_Contact(Model_View_Base):
|
||||||
# Attributes
|
FLAG_ALTCHA_WIDGET: ClassVar[str] = 'altcha-widget'
|
||||||
FLAG_COMPANY_NAME: ClassVar[str] = 'company_name'
|
FLAG_COMPANY_NAME: ClassVar[str] = 'company_name'
|
||||||
FLAG_CONTACT_NAME: ClassVar[str] = 'contact_name'
|
FLAG_CONTACT_NAME: ClassVar[str] = 'contact_name'
|
||||||
FLAG_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
|
FLAG_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
|
||||||
"""
|
ID_CONTACT_FORM: ClassVar[str] = 'contact-form'
|
||||||
ID_EMAIL: ClassVar[str] = 'email'
|
|
||||||
ID_COMPANY_NAME: ClassVar[str] = 'company_name'
|
|
||||||
ID_CONTACT_NAME: ClassVar[str] = 'contact_name'
|
|
||||||
ID_MESSAGE: ClassVar[str] = 'msg'
|
|
||||||
ID_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
|
|
||||||
ID_NAME: ClassVar[str] = 'name'
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_contact: Form_Contact
|
form_contact: Form_Contact
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
flask
|
Flask==3.0.3
|
||||||
|
gunicorn==23.0.0
|
||||||
flask_wtf
|
flask_wtf
|
||||||
flask_sqlalchemy
|
flask_sqlalchemy
|
||||||
flask_cors
|
flask_cors
|
||||||
@@ -13,3 +14,5 @@ authlib
|
|||||||
pydantic
|
pydantic
|
||||||
# psycopg2
|
# psycopg2
|
||||||
requests
|
requests
|
||||||
|
cryptography
|
||||||
|
altcha
|
||||||
@@ -41,6 +41,10 @@ textarea.form-input {
|
|||||||
padding-left: 200px;
|
padding-left: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.recaptcha {
|
||||||
|
margin-left: 15vw;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
margin-left: 40%;
|
margin-left: 40%;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
@@ -88,3 +92,9 @@ input[type="submit"]:hover {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.container.recaptcha {
|
||||||
|
margin-left: -12vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
static/dist/css/core_contact.bundle.css
vendored
95
static/dist/css/core_contact.bundle.css
vendored
@@ -1,89 +1,6 @@
|
|||||||
/*
|
|
||||||
h1, h2, h3 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-content: center;
|
|
||||||
}
|
|
||||||
.container-input {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.container-input label {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Contact Form Section * /
|
|
||||||
.contact-form {
|
|
||||||
padding: 8rem 0 4rem;
|
|
||||||
background: linear-gradient(45deg, #f8fafc, #eff6ff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form-content {
|
|
||||||
width: 50vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form p {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: var(--subheading);
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.lhs,
|
|
||||||
td.lhs {
|
|
||||||
min-width: 8vw;
|
|
||||||
max-width: 8vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.rhs,
|
|
||||||
td.rhs {
|
|
||||||
min-width: 36vw;
|
|
||||||
max-width: 36vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
td input,
|
|
||||||
td textarea {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-checkbox.receive_marketing {
|
|
||||||
margin-left: 22.25vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.contact-section {
|
.contact-section {
|
||||||
padding: 8rem 2rem 4rem;
|
padding: 2rem 2rem 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-form {
|
.contact-form {
|
||||||
@@ -124,6 +41,10 @@ textarea.form-input {
|
|||||||
padding-left: 200px;
|
padding-left: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.recaptcha {
|
||||||
|
margin-left: 15vw;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
margin-left: 40%;
|
margin-left: 40%;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
@@ -172,4 +93,10 @@ input[type="submit"]:hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.container.recaptcha {
|
||||||
|
margin-left: -12vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=core_contact.bundle.css.map*/
|
/*# sourceMappingURL=core_contact.bundle.css.map*/
|
||||||
4351
static/dist/js/main.bundle.js
vendored
4351
static/dist/js/main.bundle.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,5 @@
|
|||||||
import DOM from './dom.js';
|
import DOM from './dom.js';
|
||||||
|
|
||||||
// Module for API calls
|
|
||||||
export default class API {
|
export default class API {
|
||||||
|
|
||||||
static getCsrfToken() {
|
static getCsrfToken() {
|
||||||
@@ -151,7 +150,6 @@ export default class API {
|
|||||||
const api = new API();
|
const api = new API();
|
||||||
export default api;
|
export default api;
|
||||||
|
|
||||||
Example of using the API
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initializeApp();
|
initializeApp();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
|
|
||||||
// Main entry point for the application
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// import API from './api.js';
|
|
||||||
import DOM from './dom.js';
|
import DOM from './dom.js';
|
||||||
import Router from './router.js';
|
import Router from './router.js';
|
||||||
|
|
||||||
@@ -19,34 +17,24 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Global event listeners
|
|
||||||
// document.addEventListener('click', this.handleGlobalClick.bind(this));
|
// document.addEventListener('click', this.handleGlobalClick.bind(this));
|
||||||
// Add more global event listeners as needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGlobalClick(event) {
|
handleGlobalClick(event) {
|
||||||
// Handle global click events
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
// Additional startup logic
|
|
||||||
this.initPageCurrent();
|
this.initPageCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
initPageCurrent() {
|
initPageCurrent() {
|
||||||
/*
|
|
||||||
_pageCurrent = Router.getPageCurrent();
|
|
||||||
_pageCurrent.initialize();
|
|
||||||
*/
|
|
||||||
this.router.loadPageCurrent();
|
this.router.loadPageCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application instance
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
|
|
||||||
// DOM ready handler
|
|
||||||
function domReady(fn) {
|
function domReady(fn) {
|
||||||
if (document.readyState !== 'loading') {
|
if (document.readyState !== 'loading') {
|
||||||
fn();
|
fn();
|
||||||
@@ -55,13 +43,10 @@ function domReady(fn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize and start the app when DOM is ready
|
|
||||||
domReady(() => {
|
domReady(() => {
|
||||||
app.initialize();
|
app.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expose app to window for debugging (optional)
|
|
||||||
window.app = app;
|
window.app = app;
|
||||||
|
|
||||||
// Export app if using modules
|
|
||||||
export default app;
|
export default app;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import Validation from "./lib/validation.js";
|
import Validation from "./lib/validation.js";
|
||||||
|
|
||||||
// Module for DOM manipulation
|
|
||||||
export default class DOM {
|
export default class DOM {
|
||||||
static setElementAttributesValuesCurrentAndPrevious(element, data) {
|
static setElementAttributesValuesCurrentAndPrevious(element, data) {
|
||||||
DOM.setElementAttributeValueCurrent(element, data);
|
DOM.setElementAttributeValueCurrent(element, data);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
// internal
|
||||||
import BasePage from "../base.js";
|
import BasePage from "../base.js";
|
||||||
|
// vendor
|
||||||
|
import { Altcha } from "../../vendor/altcha.js";
|
||||||
|
|
||||||
export default class PageContact extends BasePage {
|
export default class PageContact extends BasePage {
|
||||||
static hash = hashPageContact;
|
static hash = hashPageContact;
|
||||||
@@ -10,9 +12,49 @@ export default class PageContact extends BasePage {
|
|||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.sharedInitialize();
|
this.sharedInitialize();
|
||||||
|
// this.hookupALTCHAByLocalServer();
|
||||||
this.hookupButtonSubmitFormContactUs();
|
this.hookupButtonSubmitFormContactUs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
hookupALTCHAByAPI() {
|
||||||
|
const form = document.querySelector(idContactForm);
|
||||||
|
const altchaWidget = form.querySelector('altcha-widget');
|
||||||
|
|
||||||
|
// Listen for verification events from the ALTCHA widget
|
||||||
|
if (altchaWidget) {
|
||||||
|
altchaWidget.addEventListener('serververification', function(event) {
|
||||||
|
// Create or update the hidden input for ALTCHA
|
||||||
|
let altchaInput = form.querySelector('input[name="altcha"]');
|
||||||
|
if (!altchaInput) {
|
||||||
|
altchaInput = document.createElement('input');
|
||||||
|
altchaInput.type = 'hidden';
|
||||||
|
altchaInput.name = 'altcha';
|
||||||
|
form.appendChild(altchaInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the verification payload
|
||||||
|
altchaInput.value = event.detail.payload;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
hookupALTCHAByLocalServer() {
|
||||||
|
window.ALTCHA = { init: (config) => {
|
||||||
|
document.querySelectorAll(config.selector).forEach(el => {
|
||||||
|
new Altcha({
|
||||||
|
target: el,
|
||||||
|
props: {
|
||||||
|
challengeurl: config.challenge.url,
|
||||||
|
auto: 'onload'
|
||||||
|
}
|
||||||
|
}).$on('verified', (e) => {
|
||||||
|
config.challenge.onSuccess(e.detail.payload, el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
hookupButtonSubmitFormContactUs() {
|
hookupButtonSubmitFormContactUs() {
|
||||||
const button = document.querySelector('form input[type="submit"]');
|
const button = document.querySelector('form input[type="submit"]');
|
||||||
button.classList.add(flagButton);
|
button.classList.add(flagButton);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// internal
|
// internal
|
||||||
import BasePage from "../base.js";
|
import BasePage from "../base.js";
|
||||||
// external
|
// external
|
||||||
import AOS from 'aos';
|
|
||||||
|
|
||||||
|
|
||||||
export default class PageHome extends BasePage {
|
export default class PageHome extends BasePage {
|
||||||
@@ -15,46 +14,10 @@ export default class PageHome extends BasePage {
|
|||||||
initialize() {
|
initialize() {
|
||||||
this.sharedInitialize();
|
this.sharedInitialize();
|
||||||
this.hookupButtonsNavContact();
|
this.hookupButtonsNavContact();
|
||||||
// this.initialiseAOS();
|
|
||||||
this.initialiseAnimations();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AOS */
|
|
||||||
initialiseAOS() {
|
|
||||||
AOS.init({
|
|
||||||
duration: 1000,
|
|
||||||
once: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/* Manual animations *
|
|
||||||
initialiseAnimations() {
|
|
||||||
// Check if IntersectionObserver is supported
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
threshold: 0.1,
|
|
||||||
rootMargin: '50px'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Observe all elements with 'reveal' class
|
|
||||||
document.querySelectorAll('.reveal').forEach((element) => {
|
|
||||||
observer.observe(element);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// If IntersectionObserver is not supported, make all elements visible
|
|
||||||
document.querySelectorAll('.reveal').forEach((element) => {
|
|
||||||
element.style.opacity = 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
leave() {
|
leave() {
|
||||||
super.leave();
|
super.leave();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,30 +12,11 @@ import PageLicense from './pages/legal/license.js';
|
|||||||
// import PageUserLogout from './pages/user/logout.js';
|
// import PageUserLogout from './pages/user/logout.js';
|
||||||
// import PageUserAccount from './pages/user/account.js';
|
// import PageUserAccount from './pages/user/account.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import API from './api.js';
|
import API from './api.js';
|
||||||
import DOM from './dom.js';
|
import DOM from './dom.js';
|
||||||
import PagePrivacyPolicy from './pages/legal/privacy_policy.js';
|
import PagePrivacyPolicy from './pages/legal/privacy_policy.js';
|
||||||
import PageRetentionSchedule from './pages/legal/retention_schedule.js';
|
import PageRetentionSchedule from './pages/legal/retention_schedule.js';
|
||||||
|
|
||||||
// Create a context for the pages
|
|
||||||
// const pagesContext = require.context('./pages', true, /\.js$/);
|
|
||||||
|
|
||||||
/*
|
|
||||||
const pageModules = {
|
|
||||||
// Core
|
|
||||||
[hashPageHome]: () => import('./pages/core/home.js'),
|
|
||||||
[hashPageContact]: () => import('./pages/core/contact.js'),
|
|
||||||
[hashPageServices]: () => import('./pages/core/services.js'),
|
|
||||||
[hashPageAdminHome]: () => import('./pages/core/admin_home.js'),
|
|
||||||
// Legal
|
|
||||||
[hashPageAccessibilityStatement]: () => import('./pages/legal/accessibility_statement.js'),
|
|
||||||
[hashPageLicense]: () => import('./pages/legal/license.js'),
|
|
||||||
// User
|
|
||||||
// Add other pages here...
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default class Router {
|
export default class Router {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -146,11 +127,8 @@ export default class Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and export a singleton instance
|
|
||||||
export const router = new Router();
|
export const router = new Router();
|
||||||
// import this for navigation
|
|
||||||
|
|
||||||
// Usage example (you can put this in your main.js or app.js)
|
|
||||||
/*
|
/*
|
||||||
router.addRoute('/', () => {
|
router.addRoute('/', () => {
|
||||||
console.log('Home page');
|
console.log('Home page');
|
||||||
@@ -162,7 +140,6 @@ router.addRoute('/about', () => {
|
|||||||
// Load about page content
|
// Load about page content
|
||||||
});
|
});
|
||||||
|
|
||||||
// Example of how to use the router in other parts of your application
|
|
||||||
export function setupNavigationEvents() {
|
export function setupNavigationEvents() {
|
||||||
document.querySelectorAll('a[data-nav]').forEach(link => {
|
document.querySelectorAll('a[data-nav]').forEach(link => {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
|
|||||||
2636
static/js/vendor/altcha.js
vendored
Normal file
2636
static/js/vendor/altcha.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -117,6 +117,7 @@
|
|||||||
var flagPageBody = "{{ model.FLAG_PAGE_BODY }}";
|
var flagPageBody = "{{ model.FLAG_PAGE_BODY }}";
|
||||||
var flagPhoneNumber = "{{ model.FLAG_PHONE_NUMBER }}";
|
var flagPhoneNumber = "{{ model.FLAG_PHONE_NUMBER }}";
|
||||||
var flagPostcode = "{{ model.FLAG_POSTCODE }}";
|
var flagPostcode = "{{ model.FLAG_POSTCODE }}";
|
||||||
|
var flagCaptcha = "{{ model.FLAG_CAPTCHA }}";
|
||||||
var flagRightHandSide = "{{ model.FLAG_RIGHT_HAND_SIDE }}";
|
var flagRightHandSide = "{{ model.FLAG_RIGHT_HAND_SIDE }}";
|
||||||
var flagRow = "{{ model.FLAG_ROW }}";
|
var flagRow = "{{ model.FLAG_ROW }}";
|
||||||
var flagRowNew = "{{ model.FLAG_ROW_NEW }}";
|
var flagRowNew = "{{ model.FLAG_ROW_NEW }}";
|
||||||
@@ -131,6 +132,7 @@
|
|||||||
var flagTemporaryElement = "{{ model.FLAG_TEMPORARY_ELEMENT }}";
|
var flagTemporaryElement = "{{ model.FLAG_TEMPORARY_ELEMENT }}";
|
||||||
var flagUser = "{{ model.FLAG_USER }}";
|
var flagUser = "{{ model.FLAG_USER }}";
|
||||||
var flagWebsite = "{{ model.FLAG_WEBSITE }}";
|
var flagWebsite = "{{ model.FLAG_WEBSITE }}";
|
||||||
|
var hashALTCHACreateChallenge = "{{ model.HASH_ALTCHA_CREATE_CHALLENGE }}";
|
||||||
var hashApplyFiltersStoreProductPermutation = "{{ model.HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION }}";
|
var hashApplyFiltersStoreProductPermutation = "{{ model.HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION }}";
|
||||||
var hashPageAccessibilityReport = "{{ model.HASH_PAGE_ACCESSIBILITY_REPORT }}";
|
var hashPageAccessibilityReport = "{{ model.HASH_PAGE_ACCESSIBILITY_REPORT }}";
|
||||||
var hashPageAccessibilityStatement = "{{ model.HASH_PAGE_ACCESSIBILITY_STATEMENT }}";
|
var hashPageAccessibilityStatement = "{{ model.HASH_PAGE_ACCESSIBILITY_STATEMENT }}";
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
{% block page_head %}
|
{% block page_head %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='dist/css/core_contact.bundle.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='dist/css/core_contact.bundle.css') }}">
|
||||||
|
{#
|
||||||
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@altcha/browser@latest/dist/index.js" defer></script>
|
||||||
|
#}
|
||||||
|
{# with CDN
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@altcha/browser@1.1.0/dist/altcha.min.js"></script>
|
||||||
|
<style>
|
||||||
|
.altcha-widget {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
#}
|
||||||
|
{# with locally stored vendor project - this is imported into contact.js
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/vendor/altcha.js')}}"></script>
|
||||||
|
#}
|
||||||
|
<style>
|
||||||
|
.altcha-widget {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_nav_links %}
|
{% block page_nav_links %}
|
||||||
@@ -12,6 +31,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_body %}
|
{% block page_body %}
|
||||||
|
{#
|
||||||
<script>
|
<script>
|
||||||
function loadRecaptcha() {
|
function loadRecaptcha() {
|
||||||
var script = document.createElement('script');
|
var script = document.createElement('script');
|
||||||
@@ -22,15 +42,25 @@
|
|||||||
|
|
||||||
window.addEventListener('load', loadRecaptcha);
|
window.addEventListener('load', loadRecaptcha);
|
||||||
</script>
|
</script>
|
||||||
|
#}
|
||||||
|
|
||||||
<!-- Divs -->
|
|
||||||
{% set form = model.form_contact %}
|
{% set form = model.form_contact %}
|
||||||
<section class="contact-section">
|
<section class="contact-section">
|
||||||
<div class="contact-form">
|
<div class="contact-form">
|
||||||
<h1>Contact Us</h1>
|
<h1>Contact Us</h1>
|
||||||
<p>Please fill in the form below and we'll get back to you as soon as possible.</p>
|
<p>Please fill in the form below and we'll get back to you as soon as possible.</p>
|
||||||
|
|
||||||
<form id="contact-form" method="POST" action="{{ url_for('routes_core.contact') }}">
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="flashes">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form id="{{ model.ID_CONTACT_FORM }}" method="POST" action="{{ url_for('routes_core.contact') }}">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
@@ -59,8 +89,28 @@
|
|||||||
{{ model.form_contact.receive_marketing() }}
|
{{ model.form_contact.receive_marketing() }}
|
||||||
{{ model.form_contact.receive_marketing.label }}
|
{{ model.form_contact.receive_marketing.label }}
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ model.FLAG_CONTAINER }}">
|
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_CAPTCHA }}">
|
||||||
{{ model.form_contact.recaptcha() }}
|
{# {{ model.form_contact.recaptcha() }} #}
|
||||||
|
{#
|
||||||
|
<altcha-widget
|
||||||
|
challengeurl="https://eu.altcha.org/api/v1/challenge?apiKey={{ model.app.app_config.ALTCHA_API_KEY }}"
|
||||||
|
spamfilter
|
||||||
|
></altcha-widget>
|
||||||
|
#}
|
||||||
|
<div>
|
||||||
|
{{ form.altcha.label }}
|
||||||
|
{#
|
||||||
|
{{ form.altcha }}
|
||||||
|
{{ form.altcha.hidden() }}
|
||||||
|
#}
|
||||||
|
<altcha-widget
|
||||||
|
class="altcha-widget"
|
||||||
|
challengeurl="{{ url_for('routes_core.create_altcha_challenge') }}"
|
||||||
|
auto="onload"
|
||||||
|
id="{{ form.altcha.id }}"
|
||||||
|
name="{{ form.altcha.name }}"
|
||||||
|
></altcha-widget>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ model.FLAG_CONTAINER_INPUT }}">
|
<div class="{{ model.FLAG_CONTAINER_INPUT }}">
|
||||||
{{ model.form_contact.submit() }}
|
{{ model.form_contact.submit() }}
|
||||||
@@ -100,7 +150,46 @@
|
|||||||
</section>
|
</section>
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
{# with CDN
|
||||||
<script>
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize ALTCHA widget
|
||||||
|
ALTCHA.init({
|
||||||
|
selector: '.altcha-widget',
|
||||||
|
challenge: {
|
||||||
|
url: '/get-challenge',
|
||||||
|
onSuccess: function(result, element) {
|
||||||
|
// Store the result in the hidden input field
|
||||||
|
const hiddenInput = element.parentNode.querySelector('input[type="hidden"]');
|
||||||
|
hiddenInput.value = JSON.stringify(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
#}
|
||||||
|
|
||||||
|
{# with locally stored vendor project - this is now in contact.js
|
||||||
|
<script type="module">
|
||||||
|
import { Altcha } from "{{ url_for('static', filename='js/vendor/altcha.js') }}";
|
||||||
|
window.ALTCHA = { init: (config) => {
|
||||||
|
document.querySelectorAll(config.selector).forEach(el => {
|
||||||
|
new Altcha({
|
||||||
|
target: el,
|
||||||
|
props: {
|
||||||
|
challengeurl: config.challenge.url,
|
||||||
|
auto: 'onload'
|
||||||
|
}
|
||||||
|
}).$on('verified', (e) => {
|
||||||
|
config.challenge.onSuccess(e.detail.payload, el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}};
|
||||||
|
</script>
|
||||||
|
#}
|
||||||
|
<script>
|
||||||
|
var flagALTCHAWidget = "{{ model.FLAG_ALTCHA_WIDGET }}";
|
||||||
|
var idContactForm = "#{{ model.ID_CONTACT_FORM }}";
|
||||||
var idEmail = "#{{ model.ID_EMAIL }}";
|
var idEmail = "#{{ model.ID_EMAIL }}";
|
||||||
var idMessage = "#{{ model.ID_MESSAGE }}";
|
var idMessage = "#{{ model.ID_MESSAGE }}";
|
||||||
var idContactName = "#{{ model.ID_CONTACT_NAME }}";
|
var idContactName = "#{{ model.ID_CONTACT_NAME }}";
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="hero-content" data-aos="fade-up">
|
<div class="hero-content" data-aos="fade-up">
|
||||||
<h1>Transform Your Business with Modern ERP Solutions</h1>
|
<h1>Transform Your Business with Modern ERP Solutions</h1>
|
||||||
<p>UK-based ERPNext implementation experts helping SMBs streamline operations with customized, cost-effective solutions.</p>
|
<p>UK-based ERPNext specialist providing integrated ERP and e-commerce solutions. 5+ years experience implementing systems for builders merchants and automotive companies.</p>
|
||||||
<a href="{{ model.HASH_PAGE_CONTACT }}" class="{{ model.FLAG_BUTTON }} {{ model.FLAG_BUTTON_PRIMARY }}">Get Started</a>
|
<a href="{{ model.HASH_PAGE_CONTACT }}" class="{{ model.FLAG_BUTTON }} {{ model.FLAG_BUTTON_PRIMARY }}">Book Consultation</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -71,30 +71,29 @@
|
|||||||
<section id="pricing" class="pricing">
|
<section id="pricing" class="pricing">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title text-center">Simple, Transparent Pricing</h2>
|
<h2 class="section-title text-center">Simple, Transparent Pricing</h2>
|
||||||
<p class="section-subtitle text-center">Everything you need to run your business efficiently</p>
|
<p class="section-subtitle text-center">Enterprise-grade solutions at SMB-friendly prices</p>
|
||||||
|
|
||||||
<div class="pricing-card" data-aos="fade-up">
|
<div class="pricing-card" data-aos="fade-up">
|
||||||
<h3>Implementation</h3>
|
<h3>Implementation</h3>
|
||||||
<div class="price">From £10,000</div>
|
<div class="price">From £10,000</div>
|
||||||
<p>One-time implementation fee</p>
|
<p>One-time setup fee based on business size</p>
|
||||||
<ul style="list-style: none; margin: 2rem 0;">
|
<ul style="list-style: none; margin: 2rem 0;">
|
||||||
<li>✓ Full system setup</li>
|
<li>✓ Full ERP & e-commerce setup</li>
|
||||||
|
<li>✓ Product catalogue configuration</li>
|
||||||
|
<li>✓ Staff training</li>
|
||||||
<li>✓ Data migration</li>
|
<li>✓ Data migration</li>
|
||||||
<li>✓ User training</li>
|
|
||||||
<li>✓ Custom configurations</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Monthly Support & Hosting</h3>
|
<h3>Monthly Support & Hosting</h3>
|
||||||
<div class="price">£200</div>
|
<div class="price">£200</div>
|
||||||
<p>Fixed monthly fee</p>
|
<p>12-month minimum term, billed annually at £2,400</p>
|
||||||
<ul style="list-style: none; margin: 2rem 0;">
|
<ul style="list-style: none; margin: 2rem 0;">
|
||||||
<li>✓ Unlimited support tickets</li>
|
<li>✓ Unlimited support tickets</li>
|
||||||
<li>✓ Cloud hosting</li>
|
<li>✓ Cloud hosting (99.9% uptime)</li>
|
||||||
<li>✓ Regular maintenance</li>
|
<li>✓ Regular maintenance and system updates</li>
|
||||||
<li>✓ System updates</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<a href="{{ model.HASH_PAGE_CONTACT }}" class="{{ model.FLAG_BUTTON }} {{ model.FLAG_BUTTON_PRIMARY }}">Get Started</a>
|
<a href="{{ model.HASH_PAGE_CONTACT }}" class="{{ model.FLAG_BUTTON }} {{ model.FLAG_BUTTON_PRIMARY }}">Get Custom Quote</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -103,7 +102,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">Ready to Transform Your Business?</h2>
|
<h2 class="section-title">Ready to Transform Your Business?</h2>
|
||||||
<p class="section-subtitle">Contact us today to discuss your ERP needs</p>
|
<p class="section-subtitle">Contact us today to discuss your ERP needs</p>
|
||||||
<a href="{{ model.HASH_PAGE_CONTACT }}" class="{{ model.FLAG_BUTTON }} {{ model.FLAG_BUTTON_LIGHT }}">Contact Us</a>
|
<a href="{{ model.HASH_PAGE_CONTACT }}" class="{{ model.FLAG_BUTTON }} {{ model.FLAG_BUTTON_LIGHT }}">Book Consultation</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user