From 7efe09331b949d59c4b2e6326205dcff2664d5ed Mon Sep 17 00:00:00 2001 From: Kobe Date: Tue, 15 Jul 2025 11:33:04 +0200 Subject: [PATCH] First Commit --- .gitignore | 142 +++++++++++ Dockerfile | 25 ++ Makefile | 139 +++++++++++ README.md | 356 +++++++++++++++++++++++++++ app.py | 205 ++++++++++++++++ build_assets.py | 143 +++++++++++ docker-compose.dev.yml | 24 ++ docker-compose.yml | 26 ++ requirements.txt | 10 + robots.txt | 19 ++ static/css/main.css | 399 +++++++++++++++++++++++++++++++ static/css/main.d1a4575d.min.css | 1 + static/css/main.min.css | 1 + static/js/main.cbb94d6d.min.js | 18 ++ static/js/main.js | 291 ++++++++++++++++++++++ static/js/main.min.js | 18 ++ templates/about.html | 203 ++++++++++++++++ templates/base.html | 149 ++++++++++++ templates/contact.html | 216 +++++++++++++++++ templates/errors/400.html | 18 ++ templates/errors/401.html | 18 ++ templates/errors/403.html | 18 ++ templates/errors/404.html | 23 ++ templates/errors/405.html | 18 ++ templates/errors/500.html | 23 ++ templates/errors/502.html | 18 ++ templates/errors/503.html | 18 ++ templates/errors/504.html | 18 ++ templates/index.html | 165 +++++++++++++ templates/services.html | 205 ++++++++++++++++ templates/sitemap.xml | 11 + test_app.py | 37 +++ translation_manager.py | 184 ++++++++++++++ translations/de.json | 171 +++++++++++++ translations/en.json | 185 ++++++++++++++ translations/fr.json | 171 +++++++++++++ translations/nl.json | 171 +++++++++++++ 37 files changed, 3857 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.py create mode 100644 build_assets.py create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100644 robots.txt create mode 100644 static/css/main.css create mode 100644 static/css/main.d1a4575d.min.css create mode 100644 static/css/main.min.css create mode 100644 static/js/main.cbb94d6d.min.js create mode 100644 static/js/main.js create mode 100644 static/js/main.min.js create mode 100644 templates/about.html create mode 100644 templates/base.html create mode 100644 templates/contact.html create mode 100644 templates/errors/400.html create mode 100644 templates/errors/401.html create mode 100644 templates/errors/403.html create mode 100644 templates/errors/404.html create mode 100644 templates/errors/405.html create mode 100644 templates/errors/500.html create mode 100644 templates/errors/502.html create mode 100644 templates/errors/503.html create mode 100644 templates/errors/504.html create mode 100644 templates/index.html create mode 100644 templates/services.html create mode 100644 templates/sitemap.xml create mode 100644 test_app.py create mode 100644 translation_manager.py create mode 100644 translations/de.json create mode 100644 translations/en.json create mode 100644 translations/fr.json create mode 100644 translations/nl.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77a91be --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Docker +.dockerignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fbea9ff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy all application files +COPY . . + +# Build assets for production +RUN python build_assets.py + +# Set production environment +ENV FLASK_ENV=production +ENV FLASK_DEBUG=0 + +# Ensure proper permissions +RUN chmod +x app.py + +EXPOSE 5000 + +# Use python module syntax for better reliability +CMD ["python", "-u", "app.py"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9cb73f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,139 @@ +# Kobelly Base Website - Makefile + +.PHONY: help install build clean run run-prod deploy dev-setup docker-run docker-run-dev docker-stop docker-stop-dev docker-logs docker-logs-dev + +# Default target +help: + @echo "Kobelly Base Website - Available Commands:" + @echo "" + @echo "Development:" + @echo " install - Install Python dependencies" + @echo " dev-setup - Setup development environment" + @echo " run - Run development server" + @echo " run-prod - Run production server" + @echo "" + @echo "Build & Deploy:" + @echo " build - Build minified assets" + @echo " clean - Clean old assets" + @echo " deploy - Clean and build assets" + @echo "" + @echo "Docker:" + @echo " docker-run - Run production Docker container" + @echo " docker-run-dev - Run development Docker container" + @echo " docker-stop - Stop production container" + @echo " docker-stop-dev - Stop development container" + @echo " docker-logs - View production logs" + @echo " docker-logs-dev - View development logs" + +# Install dependencies +install: + @echo "📩 Installing Python dependencies..." + pip install -r requirements.txt + @echo "✅ Dependencies installed successfully!" + +# Setup development environment +dev-setup: install + @echo "🔧 Setting up development environment..." + @echo "✅ Development environment ready!" + @echo "💡 Run 'make run' to start the development server" + +# Build assets +build: + @echo "🔹 Building assets..." + python build_assets.py + @echo "✅ Assets built successfully!" + +# Clean old assets +clean: + @echo "đŸ§č Cleaning old assets..." + python build_assets.py clean + @echo "✅ Cleanup completed!" + +# Deploy (clean + build) +deploy: clean build + @echo "🚀 Deployment ready!" + +# Run development server +run: + @echo "🚀 Starting development server..." + @echo "🌐 Website will be available at: http://localhost:5000" + @echo "🔧 Debug mode: ON" + @echo "📝 Press Ctrl+C to stop" + FLASK_DEBUG=1 python app.py + +# Run production server +run-prod: + @echo "🚀 Starting production server..." + @echo "🌐 Website will be available at: http://localhost:5000" + @echo "🔧 Debug mode: OFF" + @echo "📝 Press Ctrl+C to stop" + FLASK_DEBUG=0 python app.py + +# Docker commands +docker-run: + @echo "🐳 Running production Docker container..." + docker-compose up --build -d + @echo "✅ Production container started!" + @echo "🌐 Website available at: http://localhost:10332" + +docker-run-dev: + @echo "🐳 Running development Docker container..." + docker-compose -f docker-compose.dev.yml up --build -d + @echo "✅ Development container started!" + @echo "🌐 Website available at: http://localhost:10333" + +docker-stop: + @echo "🛑 Stopping production Docker container..." + docker-compose down + @echo "✅ Production container stopped!" + +docker-stop-dev: + @echo "🛑 Stopping development Docker container..." + docker-compose -f docker-compose.dev.yml down + @echo "✅ Development container stopped!" + +docker-logs: + @echo "📋 Production container logs:" + docker-compose logs -f + +docker-logs-dev: + @echo "📋 Development container logs:" + docker-compose -f docker-compose.dev.yml logs -f + +# Additional utility commands +test: + @echo "đŸ§Ș Running tests..." + @echo "⚠ No tests configured yet" + @echo "💡 Add your test commands here" + +lint: + @echo "🔍 Running linting..." + @echo "⚠ No linting configured yet" + @echo "💡 Add your linting commands here" + +format: + @echo "🎹 Formatting code..." + @echo "⚠ No formatting configured yet" + @echo "💡 Add your formatting commands here" + +# Database commands (if needed in the future) +db-migrate: + @echo "đŸ—„ïž Running database migrations..." + @echo "⚠ No database configured yet" + @echo "💡 Add your migration commands here" + +db-seed: + @echo "đŸŒ± Seeding database..." + @echo "⚠ No database configured yet" + @echo "💡 Add your seeding commands here" + +# Backup and restore (if needed in the future) +backup: + @echo "đŸ’Ÿ Creating backup..." + @echo "⚠ No backup configured yet" + @echo "💡 Add your backup commands here" + +restore: + @echo "đŸ“„ Restoring from backup..." + @echo "⚠ No restore configured yet" + @echo "💡 Add your restore commands here" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dea3253 --- /dev/null +++ b/README.md @@ -0,0 +1,356 @@ +# Kobelly Base website + +A professional Flask-based website that will be used to create websites for small businesses. This is the base set of files all websites will build off. Features multilanguage support (English, Dutch, French, German) with a simple JSON-based translation system and is fully dockerized for easy deployment. + +## Features + +- 🌐 **Multilanguage Support**: English (EN), Dutch (NL), French (FR), German (DE) with JSON-based translations +- đŸ“± **Responsive Design**: Modern, mobile-friendly interface +- 🎹 **Professional UI**: Clean, modern design with Bootstrap 5 +- 🐳 **Docker Support**: Easy containerization and deployment +- ⚡ **Fast Performance**: Optimized for speed and performance with minified assets +- 🔧 **No Database Required**: Simple, lightweight setup +- 🚀 **Asset Optimization**: CSS/JS minification with cache busting +- 📧 **Contact Form**: Fully functional email contact form +- đŸ—ș **SEO Optimized**: Sitemap generation and robots.txt + +## Pages + +- **Home**: Hero section and other elements to promote a business +- **Services**: Detailed service offerings with pricing information +- **About**: Company story, team, and values +- **Contact**: Contact form and business information + +## Quick Start + +### Local Development (Recommended) + +1. **Install Python dependencies:** + ```bash + pip install -r requirements.txt + ``` + +2. **Build assets (optional, for production-like experience):** + ```bash + make build + ``` + +3. **Run the application:** + ```bash + python app.py + ``` + +4. **Access the website:** + Open your browser and go to `http://localhost:5000` + +### Using Make Commands + +The project includes a Makefile for common tasks: + +```bash +make help # Show all available commands +make install # Install dependencies +make build # Build minified assets +make clean # Clean old assets +make run # Run development server +make run-prod # Run production server +make deploy # Clean and build assets +make dev-setup # Setup development environment + +# Docker commands +make docker-run # Run production Docker container +make docker-run-dev # Run development Docker container +make docker-stop # Stop production container +make docker-stop-dev # Stop development container +make docker-logs # View production logs +make docker-logs-dev # View development logs +``` + +### Docker Deployment + +#### Production Mode (Default) +1. **Build and run with Docker Compose:** + ```bash + docker-compose up --build + ``` + +2. **Or build and run manually:** + ```bash + docker build -t kobelly-website . + docker run -p 5000:5000 kobelly-website + ``` + +#### Development Mode +For local development with Docker (with live reload and debug mode): + +1. **Build and run development container:** + ```bash + docker-compose -f docker-compose.dev.yml up --build + ``` + +2. **Or use Make commands:** + ```bash + make docker-run-dev # Start development container + make docker-logs-dev # View development logs + make docker-stop-dev # Stop development container + ``` + +**Development vs Production:** +- **Development**: Debug mode enabled, live code reloading, unminified assets +- **Production**: Debug disabled, minified assets, optimized performance + +## Project Structure + +``` +Kobelly/ +├── app.py # Main Flask application +├── requirements.txt # Python dependencies +├── Dockerfile # Docker configuration +├── docker-compose.yml # Docker Compose configuration +├── docker-compose.dev.yml # Development Docker Compose +├── Makefile # Build and deployment commands +├── build_assets.py # Asset minification script +├── translation_manager.py # JSON-based translation system +├── test_translations.py # Translation testing script +├── robots.txt # Search engine configuration +├── static/ # Static assets +│ ├── css/ # CSS files +│ │ ├── main.css # Main stylesheet +│ │ └── main.min.css # Minified CSS (generated) +│ ├── js/ # JavaScript files +│ │ ├── main.js # Main script +│ │ └── main.min.js # Minified JS (generated) +│ └── images/ # Images and icons +├── templates/ # HTML templates +│ ├── base.html # Base template with navigation +│ ├── index.html # Homepage +│ ├── services.html # Services page +│ ├── about.html # About page +│ ├── contact.html # Contact page +│ └── components/ # Reusable template components +│ ├── hero_section.html +│ ├── cta_section.html +└── translations/ # JSON translation files + ├── en.json # English translations + ├── nl.json # Dutch translations + ├── fr.json # French translations + └── de.json # German translations +``` + +## Multilanguage Support + +The website supports four languages using a simple JSON-based translation system: +- đŸ‡ș🇾 **English** (EN) - Default +- đŸ‡łđŸ‡± **Dutch** (NL) +- đŸ‡«đŸ‡· **French** (FR) +- đŸ‡©đŸ‡Ș **German** (DE) + +Users can switch languages using the language selector in the navigation bar or URL parameters (`?lang=de`). The language preference is stored in the session. + +### Translation System + +The project uses a custom JSON-based translation system that: +- **No compilation needed** - Just edit JSON files directly +- **Real-time updates** - Changes appear immediately after restart +- **Simple maintenance** - Clear JSON structure +- **Independent languages** - Edit English without breaking translations + +### Adding New Translations + +1. **Add the string to your template:** + ```html +

{{ t('New Section Title') }}

+ ``` + +2. **Add it to the translation files:** + ```json + // translations/en.json + { + "New Section Title": "New Section Title" + } + + // translations/de.json + { + "New Section Title": "Neuer Abschnittstitel" + } + ``` + +3. **Restart your Flask app** - Changes appear immediately! + +## Customization + +### Content Updates + +1. **Text Content**: Edit the HTML templates in the `templates/` directory +2. **Translations**: Update the `.json` files in the `translations/` directory +3. **Styling**: Modify the CSS in `static/css/main.css` + +### Adding New Pages + +1. Add a new route in `app.py` +2. Create a new template file in `templates/` +3. Add navigation link in `templates/base.html` + +### Updating Translations + +The JSON-based system makes translation updates simple: + +1. **Edit translation files directly:** + ```bash + # Open and edit the JSON files + nano translations/en.json + nano translations/de.json + # etc. + ``` + +2. **Restart the application:** + ```bash + python app.py + ``` + +3. **Test translations:** + ```bash + python test_translations.py + ``` + +## Asset Optimization + +The project includes automatic CSS and JavaScript minification with cache busting for optimal performance. + +### Asset Building + +**Development Mode:** +- Uses unminified assets for easier debugging +- Assets are served directly from source files + +**Production Mode:** +- Automatically serves minified assets +- Cache busting prevents browser caching issues +- Assets are optimized for faster loading + +### Building Assets + +```bash +# Build minified assets +make build + +# Clean old cache-busted assets +make clean + +# Full deployment (clean + build) +make deploy +``` + +### Asset Files + +- **CSS**: `static/css/main.css` → `static/css/main.min.css` +- **JavaScript**: `static/js/main.js` → `static/js/main.min.js` + +The build process creates cache-busted versions with MD5 hashes in the filename to ensure browsers always load the latest version. + +## Contact Form Setup + +The website includes a fully functional contact form that sends email notifications. To set up email functionality: + +### 1. Email Configuration + +The application uses environment variables for email configuration. You can set these in your environment or create a `.env` file: + +```bash +# For Gmail (recommended) +MAIL_SERVER=smtp.gmail.com +MAIL_PORT=587 +MAIL_USE_TLS=True +MAIL_USERNAME=contact@kobelly.be +MAIL_PASSWORD=your-app-password-here +MAIL_DEFAULT_SENDER=contact@kobelly.be +``` + +### 2. For Gmail users: +- Enable 2-factor authentication on your Google account +- Generate an "App Password" in Google Account settings +- Use the app password as `MAIL_PASSWORD` + +### 3. Alternative Email Providers + +**For other SMTP providers, update the settings accordingly:** +```bash +# Example for Outlook/Hotmail +MAIL_SERVER=smtp-mail.outlook.com +MAIL_PORT=587 +MAIL_USE_TLS=True + +# Example for custom SMTP server +MAIL_SERVER=your-smtp-server.com +MAIL_PORT=587 +MAIL_USE_TLS=True +``` + +### 4. Testing the Contact Form + +1. Start the application +2. Navigate to the Contact page +3. Fill out and submit the form +4. Check your email for the notification + +The form includes: +- **Form validation** (required fields, email format, phone format) +- **Loading states** (button shows spinner during submission) +- **Success/error notifications** (toast notifications) +- **Email notifications** (HTML and plain text formats) + +## Configuration + +### Environment Variables + +- `SECRET_KEY`: Secret key for Flask sessions (default: 'your-secret-key-change-in-production') +- `FLASK_ENV`: Flask environment (development/production) +- `FLASK_DEBUG`: Debug mode (0=disabled, 1=enabled, defaults to 1 in development) +- `MAIL_SERVER`: SMTP server for email (default: smtp.gmail.com) +- `MAIL_PORT`: SMTP port (default: 587) +- `MAIL_USE_TLS`: Use TLS encryption (default: True) +- `MAIL_USERNAME`: Email username +- `MAIL_PASSWORD`: Email password +- `MAIL_DEFAULT_SENDER`: Default sender email + +**Environment Modes:** + +**Development (Local):** +```bash +FLASK_ENV=development +FLASK_DEBUG=1 +``` + +**Production (Docker):** +```bash +FLASK_ENV=production +FLASK_DEBUG=0 +``` + +### Production Deployment + +For production deployment: + +1. Set a strong `SECRET_KEY` +2. Disable debug mode +3. Use a production WSGI server (e.g., Gunicorn) +4. Set up proper SSL/TLS certificates +5. Configure a reverse proxy (e.g., Nginx) + +## Technologies Used + +- **Backend**: Flask 2.3.3 +- **Frontend**: Bootstrap 5, Font Awesome +- **Translation**: Custom JSON-based system +- **Containerization**: Docker, Docker Compose +- **Styling**: Custom CSS with CSS variables +- **Asset Management**: Custom build script with CSS/JS minification +- **Email**: Flask-Mail + +## Browser Support + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) +- Mobile browsers (iOS Safari, Chrome Mobile) \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..9566cc0 --- /dev/null +++ b/app.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +Kobelly Base Website - Flask Application +A professional, multilanguage website template for small businesses +""" + +import os +import json +from datetime import datetime +from flask import Flask, render_template, request, session, redirect, url_for, flash, jsonify +from flask_mail import Mail, Message +from translation_manager import init_app as init_translations, translate, get_current_language, set_language + +# Initialize Flask app +app = Flask(__name__) + +# Configuration +app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production') +app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.gmail.com') +app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587)) +app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'True').lower() == 'true' +app.config['MAIL_USE_SSL'] = os.environ.get('MAIL_USE_SSL', 'False').lower() == 'true' +app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'your-email@gmail.com') +app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'your-password') +app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'your-email@gmail.com') + +# Initialize extensions +mail = Mail(app) + +# Initialize translation system +init_translations(app) + +# Development vs Production mode +DEBUG_MODE = os.environ.get('FLASK_DEBUG', '0') == '1' + +def get_asset_url(filename, asset_type='css'): + """Get the appropriate asset URL based on environment""" + if DEBUG_MODE: + return url_for('static', filename=f'{asset_type}/{filename}') + else: + # In production, use minified assets with cache busting + static_dir = 'static' + asset_dir = os.path.join(static_dir, asset_type) + + if asset_type == 'css': + base_name = 'main.min.css' + else: + base_name = 'main.min.js' + + # Look for cache-busted version + if os.path.exists(asset_dir): + for file in os.listdir(asset_dir): + if file.startswith('main.') and file.endswith(f'.min.{asset_type}'): + return url_for('static', filename=f'{asset_type}/{file}') + + # Fallback to regular minified version + return url_for('static', filename=f'{asset_type}/{base_name}') + +@app.context_processor +def inject_globals(): + """Inject global variables into templates""" + return { + 'get_asset_url': get_asset_url, + 'current_year': datetime.now().year, + 'debug_mode': DEBUG_MODE, + 't': translate + } + +@app.route('/') +def index(): + """Homepage""" + return render_template('index.html') + +@app.route('/services') +def services(): + """Services page""" + return render_template('services.html') + +@app.route('/about') +def about(): + """About page""" + return render_template('about.html') + +@app.route('/contact') +def contact(): + """Contact page""" + return render_template('contact.html') + +@app.route('/set-language/') +def set_language_route(lang): + """Set language and redirect back""" + if set_language(lang): + # Redirect back to the previous page or home + return redirect(request.referrer or url_for('index')) + return redirect(url_for('index')) + +@app.route('/contact', methods=['POST']) +def contact_submit(): + """Handle contact form submission""" + try: + name = request.form.get('name', '').strip() + email = request.form.get('email', '').strip() + subject = request.form.get('subject', '').strip() + message = request.form.get('message', '').strip() + + # Basic validation + if not all([name, email, subject, message]): + flash(translate('Please fill in all fields'), 'error') + return redirect(url_for('contact')) + + # Create email message + msg = Message( + subject=f"Contact Form: {subject}", + recipients=[app.config['MAIL_DEFAULT_SENDER']], + body=f""" +New contact form submission: + +Name: {name} +Email: {email} +Subject: {subject} + +Message: +{message} + +--- +Sent from the website contact form. + """.strip() + ) + + # Send email + mail.send(msg) + + flash(translate('Thank you! Your message has been sent successfully.'), 'success') + return redirect(url_for('contact')) + + except Exception as e: + print(f"Error sending email: {e}") + flash(translate('Sorry, there was an error sending your message. Please try again.'), 'error') + return redirect(url_for('contact')) + +@app.route('/sitemap.xml') +def sitemap(): + """Generate sitemap for SEO""" + pages = [ + {'url': url_for('index', _external=True), 'priority': '1.0'}, + {'url': url_for('services', _external=True), 'priority': '0.8'}, + {'url': url_for('about', _external=True), 'priority': '0.7'}, + {'url': url_for('contact', _external=True), 'priority': '0.6'}, + ] + + sitemap_xml = render_template('sitemap.xml', pages=pages) + response = app.make_response(sitemap_xml) + response.headers["Content-Type"] = "application/xml" + return response + +@app.errorhandler(400) +def bad_request(error): + return render_template('400.html'), 400 + +@app.errorhandler(401) +def unauthorized(error): + return render_template('401.html'), 401 + +@app.errorhandler(403) +def forbidden(error): + return render_template('403.html'), 403 + +@app.errorhandler(405) +def method_not_allowed(error): + return render_template('405.html'), 405 + +@app.errorhandler(502) +def bad_gateway(error): + return render_template('502.html'), 502 + +@app.errorhandler(503) +def service_unavailable(error): + return render_template('503.html'), 503 + +@app.errorhandler(504) +def gateway_timeout(error): + return render_template('504.html'), 504 + +@app.errorhandler(404) +def not_found(error): + """404 error handler""" + return render_template('404.html'), 404 + +@app.errorhandler(500) +def internal_error(error): + """500 error handler""" + return render_template('500.html'), 500 + +if __name__ == '__main__': + # Build assets if in production + if not DEBUG_MODE: + try: + from build_assets import create_cache_busted_assets + create_cache_busted_assets() + except Exception as e: + print(f"Warning: Could not build assets: {e}") + + # Run the application + port = int(os.environ.get('PORT', 5000)) + app.run(host='0.0.0.0', port=port, debug=DEBUG_MODE) \ No newline at end of file diff --git a/build_assets.py b/build_assets.py new file mode 100644 index 0000000..288d2de --- /dev/null +++ b/build_assets.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +""" +Asset build script for Kobelly's Base website +Handles CSS and JS minification with cache busting +""" + +import os +import hashlib +import shutil +from datetime import datetime +from cssmin import cssmin +from jsmin import jsmin + +def get_file_hash(filepath): + """Generate MD5 hash of file content for cache busting""" + with open(filepath, 'rb') as f: + return hashlib.md5(f.read()).hexdigest()[:8] + +def minify_css(input_file, output_file): + """Minify CSS file""" + try: + with open(input_file, 'r', encoding='utf-8') as f: + css_content = f.read() + + minified_css = cssmin(css_content) + + with open(output_file, 'w', encoding='utf-8') as f: + f.write(minified_css) + + print(f"✓ CSS minified: {input_file} → {output_file}") + return True + except Exception as e: + print(f"✗ Error minifying CSS {input_file}: {e}") + return False + +def minify_js(input_file, output_file): + """Minify JavaScript file""" + try: + with open(input_file, 'r', encoding='utf-8') as f: + js_content = f.read() + + minified_js = jsmin(js_content) + + with open(output_file, 'w', encoding='utf-8') as f: + f.write(minified_js) + + print(f"✓ JS minified: {input_file} → {output_file}") + return True + except Exception as e: + print(f"✗ Error minifying JS {input_file}: {e}") + return False + +def create_cache_busted_assets(): + """Create cache-busted versions of assets""" + static_dir = 'static' + css_dir = os.path.join(static_dir, 'css') + js_dir = os.path.join(static_dir, 'js') + + # Ensure directories exist + os.makedirs(css_dir, exist_ok=True) + os.makedirs(js_dir, exist_ok=True) + + # CSS files + css_files = [ + ('main.css', 'main.min.css') + ] + + # JS files + js_files = [ + ('main.js', 'main.min.js') + ] + + print("🔹 Building assets...") + print(f"📅 Build time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print() + + # Process CSS files + for input_file, output_file in css_files: + input_path = os.path.join(css_dir, input_file) + output_path = os.path.join(css_dir, output_file) + + if os.path.exists(input_path): + if minify_css(input_path, output_path): + # Generate cache-busted filename + file_hash = get_file_hash(output_path) + cache_busted_name = f"main.{file_hash}.min.css" + cache_busted_path = os.path.join(css_dir, cache_busted_name) + + # Copy minified file to cache-busted version + shutil.copy2(output_path, cache_busted_path) + print(f"✓ Cache-busted CSS created: {cache_busted_name}") + else: + print(f"⚠ CSS file not found: {input_path}") + + print() + + # Process JS files + for input_file, output_file in js_files: + input_path = os.path.join(js_dir, input_file) + output_path = os.path.join(js_dir, output_file) + + if os.path.exists(input_path): + if minify_js(input_path, output_path): + # Generate cache-busted filename + file_hash = get_file_hash(output_path) + cache_busted_name = f"main.{file_hash}.min.js" + cache_busted_path = os.path.join(js_dir, cache_busted_name) + + # Copy minified file to cache-busted version + shutil.copy2(output_path, cache_busted_path) + print(f"✓ Cache-busted JS created: {cache_busted_name}") + else: + print(f"⚠ JS file not found: {input_path}") + + print() + print("🎉 Asset build completed!") + +def clean_old_assets(): + """Clean old cache-busted assets""" + static_dir = 'static' + css_dir = os.path.join(static_dir, 'css') + js_dir = os.path.join(static_dir, 'js') + + # Remove old cache-busted files + for directory in [css_dir, js_dir]: + if os.path.exists(directory): + for filename in os.listdir(directory): + if filename.startswith('main.') and filename.endswith('.min.css') or filename.endswith('.min.js'): + if not filename in ['main.min.css', 'main.min.js']: # Keep the base minified files + filepath = os.path.join(directory, filename) + os.remove(filepath) + print(f"đŸ—‘ïž Removed old asset: {filename}") + +if __name__ == '__main__': + import sys + + if len(sys.argv) > 1 and sys.argv[1] == 'clean': + print("đŸ§č Cleaning old assets...") + clean_old_assets() + print("✓ Cleanup completed!") + else: + clean_old_assets() + create_cache_busted_assets() \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..92e62ee --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + web: + build: . + ports: + - "10333:5000" + environment: + - MAIL_SERVER=smtp.gmail.com + - MAIL_PORT=587 + - MAIL_USE_TLS=True + - MAIL_USE_SSL=False + - MAIL_USERNAME=dev@example.com + - MAIL_PASSWORD=devpassword + - MAIL_DEFAULT_SENDER=dev@example.com + - SECRET_KEY=dev-secret-key-change-in-production + - FLASK_ENV=development + - FLASK_DEBUG=1 + volumes: + - .:/app + restart: unless-stopped + networks: + - default + command: ["python", "-u", "app.py"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ad12d2c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + web: + build: . + ports: + - "10332:5000" + environment: + - MAIL_SERVER=smtppro.zoho.eu + - MAIL_PORT=587 + - MAIL_USE_TLS=True + - MAIL_USE_SSL=False + - MAIL_USERNAME=contact@kobelly.be + - MAIL_PASSWORD=Surface125300!? + - MAIL_DEFAULT_SENDER=contact@kobelly.be + - SECRET_KEY=AiDr591lSXZdVtp9UVV4WcBlae7bgu + - FLASK_ENV=production + - FLASK_DEBUG=0 + restart: unless-stopped + networks: + - default + - nginx-proxy-manager_default + +networks: + nginx-proxy-manager_default: + external: true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ee0ec5e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +Flask==2.3.3 +Werkzeug==2.3.7 +Flask-Assets==2.1.0 +cssmin==0.2.0 +jsmin==3.0.1 +click==8.1.7 +blinker==1.6.3 +# Translation system - JSON-based, no compilation needed +# Email functionality +Flask-Mail==0.9.1 \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..a0b6206 --- /dev/null +++ b/robots.txt @@ -0,0 +1,19 @@ +User-agent: * +Allow: / + +# Sitemap location +Sitemap: https://kobelly.com/sitemap.xml + +# Crawl delay (optional - be respectful to server) +Crawl-delay: 1 + +# Disallow admin areas (if any) +Disallow: /admin/ +Disallow: /private/ +Disallow: /temp/ + +# Allow important pages +Allow: /services/ +Allow: /about/ +Allow: /contact/ +Allow: /portfolio/ \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..48b6da7 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,399 @@ +/* Kobelly Base Website - Main CSS */ + +/* Custom Properties */ +:root { + --primary-color: #0d6efd; + --secondary-color: #6c757d; + --success-color: #198754; + --info-color: #0dcaf0; + --warning-color: #ffc107; + --danger-color: #dc3545; + --light-color: #f8f9fa; + --dark-color: #212529; + --white-color: #ffffff; + --body-bg: #ffffff; + --text-color: #212529; + --text-muted: #6c757d; + --border-color: #dee2e6; + --shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --border-radius: 0.375rem; + --border-radius-lg: 0.5rem; + --transition: all 0.15s ease-in-out; +} + +/* Base Styles */ +* { + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--body-bg); +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.2; + margin-bottom: 0.5rem; +} + +p { + margin-bottom: 1rem; +} + +.lead { + font-size: 1.125rem; + font-weight: 300; +} + +/* Navigation */ +.navbar { + transition: var(--transition); + backdrop-filter: blur(10px); + background-color: rgba(255, 255, 255, 0.95) !important; +} + +.navbar-brand { + font-size: 1.5rem; + font-weight: 700; +} + +.nav-link { + font-weight: 500; + transition: var(--transition); +} + +.nav-link:hover { + color: var(--primary-color) !important; +} + +/* Hero Section */ +.hero-section { + background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%); + min-height: 75vh; + display: flex; + align-items: center; +} + +.min-vh-75 { + min-height: 75vh; +} + +.hero-image { + animation: float 6s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-20px); } +} + +/* Cards */ +.card { + transition: var(--transition); + border: none; + box-shadow: var(--shadow); +} + +.card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); +} + +.card-body { + padding: 2rem; +} + +/* Buttons */ +.btn { + font-weight: 500; + border-radius: var(--border-radius); + transition: var(--transition); + padding: 0.75rem 1.5rem; +} + +.btn-lg { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-primary:hover { + background-color: #0056b3; + border-color: #0056b3; + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-outline-primary { + color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-outline-primary:hover { + background-color: var(--primary-color); + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-light { + background-color: var(--white-color); + border-color: var(--white-color); + color: var(--text-color); +} + +.btn-light:hover { + background-color: #f8f9fa; + border-color: #f8f9fa; + color: var(--text-color); + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +/* Icons */ +.feature-icon, +.service-icon, +.value-icon { + transition: var(--transition); +} + +.feature-icon:hover, +.service-icon:hover, +.value-icon:hover { + transform: scale(1.1); +} + +/* Process Steps */ +.process-step { + transition: var(--transition); +} + +.process-step:hover { + transform: scale(1.05); +} + +/* Contact Form */ +.form-control { + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + padding: 0.75rem 1rem; + transition: var(--transition); +} + +.form-control:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +.form-label { + font-weight: 500; + margin-bottom: 0.5rem; +} + +/* Alerts */ +.alert { + border-radius: var(--border-radius); + border: none; +} + +.alert-success { + background-color: #d1e7dd; + color: #0f5132; +} + +.alert-danger { + background-color: #f8d7da; + color: #721c24; +} + +/* Accordion */ +.accordion-button { + font-weight: 500; + border-radius: var(--border-radius); +} + +.accordion-button:not(.collapsed) { + background-color: var(--primary-color); + color: var(--white-color); +} + +.accordion-button:focus { + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Footer */ +footer { + background: linear-gradient(135deg, var(--dark-color) 0%, #343a40 100%); + color: var(--white-color) !important; +} + +footer .text-muted { + color: rgba(255, 255, 255, 0.7) !important; +} + +footer a { + color: rgba(255, 255, 255, 0.8) !important; +} + +footer a:hover { + color: var(--white-color) !important; +} + +.social-links a { + transition: var(--transition); + display: inline-block; +} + +.social-links a:hover { + transform: translateY(-3px); + color: var(--primary-color) !important; +} + +/* Language Selector */ +.dropdown-toggle { + border-radius: var(--border-radius); + font-weight: 500; +} + +.dropdown-item { + transition: var(--transition); +} + +.dropdown-item:hover { + background-color: var(--primary-color); + color: var(--white-color); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .hero-section { + min-height: 60vh; + text-align: center; + } + + .display-4 { + font-size: 2.5rem; + } + + .display-6 { + font-size: 1.75rem; + } + + .card-body { + padding: 1.5rem; + } + + .btn-lg { + padding: 0.75rem 1.5rem; + font-size: 1rem; + } +} + +@media (max-width: 576px) { + .hero-section { + min-height: 50vh; + } + + .display-4 { + font-size: 2rem; + } + + .display-6 { + font-size: 1.5rem; + } + + .card-body { + padding: 1rem; + } +} + +/* Animations */ +.fade-in { + animation: fadeIn 0.6s ease-in; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.slide-in-left { + animation: slideInLeft 0.6s ease-out; +} + +@keyframes slideInLeft { + from { opacity: 0; transform: translateX(-30px); } + to { opacity: 1; transform: translateX(0); } +} + +.slide-in-right { + animation: slideInRight 0.6s ease-out; +} + +@keyframes slideInRight { + from { opacity: 0; transform: translateX(30px); } + to { opacity: 1; transform: translateX(0); } +} + +/* Utility Classes */ +.text-primary { + color: var(--primary-color) !important; +} + +.bg-primary { + background-color: var(--primary-color) !important; +} + +.border-primary { + border-color: var(--primary-color) !important; +} + +.shadow-sm { + box-shadow: var(--shadow) !important; +} + +.shadow { + box-shadow: var(--shadow-lg) !important; +} + +/* Loading States */ +.loading { + opacity: 0.6; + pointer-events: none; +} + +/* Focus States */ +.btn:focus, +.form-control:focus, +.accordion-button:focus { + outline: none; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Print Styles */ +@media print { + .navbar, + .btn, + .social-links { + display: none !important; + } + + body { + background: white !important; + color: black !important; + } + + .card { + border: 1px solid #ccc !important; + box-shadow: none !important; + } +} \ No newline at end of file diff --git a/static/css/main.d1a4575d.min.css b/static/css/main.d1a4575d.min.css new file mode 100644 index 0000000..c899d23 --- /dev/null +++ b/static/css/main.d1a4575d.min.css @@ -0,0 +1 @@ +:root{--primary-color:#0d6efd;--secondary-color:#6c757d;--success-color:#198754;--info-color:#0dcaf0;--warning-color:#ffc107;--danger-color:#dc3545;--light-color:#f8f9fa;--dark-color:#212529;--white-color:#fff;--body-bg:#fff;--text-color:#212529;--text-muted:#6c757d;--border-color:#dee2e6;--shadow:0 .125rem .25rem rgba(0,0,0,0.075);--shadow-lg:0 .5rem 1rem rgba(0,0,0,0.15);--border-radius:.375rem;--border-radius-lg:.5rem;--transition:all .15s ease-in-out}*{box-sizing:border-box}body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;line-height:1.6;color:var(--text-color);background-color:var(--body-bg)}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.2;margin-bottom:.5rem}p{margin-bottom:1rem}.lead{font-size:1.125rem;font-weight:300}.navbar{transition:var(--transition);backdrop-filter:blur(10px);background-color:rgba(255,255,255,0.95)!important}.navbar-brand{font-size:1.5rem;font-weight:700}.nav-link{font-weight:500;transition:var(--transition)}.nav-link:hover{color:var(--primary-color)!important}.hero-section{background:linear-gradient(135deg,var(--primary-color) 0,#0056b3 100%);min-height:75vh;display:flex;align-items:center}.min-vh-75{min-height:75vh}.hero-image{animation:float 6s ease-in-out infinite}@keyframes float{0%,100%{transform:translateY(0px)}50%{transform:translateY(-20px)}}.card{transition:var(--transition);border:none;box-shadow:var(--shadow)}.card:hover{transform:translateY(-5px);box-shadow:var(--shadow-lg)}.card-body{padding:2rem}.btn{font-weight:500;border-radius:var(--border-radius);transition:var(--transition);padding:.75rem 1.5rem}.btn-lg{padding:1rem 2rem;font-size:1.125rem}.btn-primary{background-color:var(--primary-color);border-color:var(--primary-color)}.btn-primary:hover{background-color:#0056b3;border-color:#0056b3;transform:translateY(-2px);box-shadow:var(--shadow-lg)}.btn-outline-primary{color:var(--primary-color);border-color:var(--primary-color)}.btn-outline-primary:hover{background-color:var(--primary-color);border-color:var(--primary-color);transform:translateY(-2px);box-shadow:var(--shadow-lg)}.btn-light{background-color:var(--white-color);border-color:var(--white-color);color:var(--text-color)}.btn-light:hover{background-color:#f8f9fa;border-color:#f8f9fa;color:var(--text-color);transform:translateY(-2px);box-shadow:var(--shadow-lg)}.feature-icon,.service-icon,.value-icon{transition:var(--transition)}.feature-icon:hover,.service-icon:hover,.value-icon:hover{transform:scale(1.1)}.process-step{transition:var(--transition)}.process-step:hover{transform:scale(1.05)}.form-control{border-radius:var(--border-radius);border:1px solid var(--border-color);padding:.75rem 1rem;transition:var(--transition)}.form-control:focus{border-color:var(--primary-color);box-shadow:0 0 0 .2rem rgba(13,110,253,0.25)}.form-label{font-weight:500;margin-bottom:.5rem}.alert{border-radius:var(--border-radius);border:none}.alert-success{background-color:#d1e7dd;color:#0f5132}.alert-danger{background-color:#f8d7da;color:#721c24}.accordion-button{font-weight:500;border-radius:var(--border-radius)}.accordion-button:not(.collapsed){background-color:var(--primary-color);color:var(--white-color)}.accordion-button:focus{box-shadow:0 0 0 .2rem rgba(13,110,253,0.25)}footer{background:linear-gradient(135deg,var(--dark-color) 0,#343a40 100%);color:var(--white-color)!important}footer .text-muted{color:rgba(255,255,255,0.7)!important}footer a{color:rgba(255,255,255,0.8)!important}footer a:hover{color:var(--white-color)!important}.social-links a{transition:var(--transition);display:inline-block}.social-links a:hover{transform:translateY(-3px);color:var(--primary-color)!important}.dropdown-toggle{border-radius:var(--border-radius);font-weight:500}.dropdown-item{transition:var(--transition)}.dropdown-item:hover{background-color:var(--primary-color);color:var(--white-color)}@media(max-width:768px){.hero-section{min-height:60vh;text-align:center}.display-4{font-size:2.5rem}.display-6{font-size:1.75rem}.card-body{padding:1.5rem}.btn-lg{padding:.75rem 1.5rem;font-size:1rem}}@media(max-width:576px){.hero-section{min-height:50vh}.display-4{font-size:2rem}.display-6{font-size:1.5rem}.card-body{padding:1rem}}.fade-in{animation:fadeIn .6s ease-in}@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.slide-in-left{animation:slideInLeft .6s ease-out}@keyframes slideInLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}.slide-in-right{animation:slideInRight .6s ease-out}@keyframes slideInRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}.text-primary{color:var(--primary-color)!important}.bg-primary{background-color:var(--primary-color)!important}.border-primary{border-color:var(--primary-color)!important}.shadow-sm{box-shadow:var(--shadow)!important}.shadow{box-shadow:var(--shadow-lg)!important}.loading{opacity:.6;pointer-events:none}.btn:focus,.form-control:focus,.accordion-button:focus{outline:none;box-shadow:0 0 0 .2rem rgba(13,110,253,0.25)}@media print{.navbar,.btn,.social-links{display:none!important}body{background:white!important;color:black!important}.card{border:1px solid #ccc!important;box-shadow:none!important}} \ No newline at end of file diff --git a/static/css/main.min.css b/static/css/main.min.css new file mode 100644 index 0000000..c899d23 --- /dev/null +++ b/static/css/main.min.css @@ -0,0 +1 @@ +:root{--primary-color:#0d6efd;--secondary-color:#6c757d;--success-color:#198754;--info-color:#0dcaf0;--warning-color:#ffc107;--danger-color:#dc3545;--light-color:#f8f9fa;--dark-color:#212529;--white-color:#fff;--body-bg:#fff;--text-color:#212529;--text-muted:#6c757d;--border-color:#dee2e6;--shadow:0 .125rem .25rem rgba(0,0,0,0.075);--shadow-lg:0 .5rem 1rem rgba(0,0,0,0.15);--border-radius:.375rem;--border-radius-lg:.5rem;--transition:all .15s ease-in-out}*{box-sizing:border-box}body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;line-height:1.6;color:var(--text-color);background-color:var(--body-bg)}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.2;margin-bottom:.5rem}p{margin-bottom:1rem}.lead{font-size:1.125rem;font-weight:300}.navbar{transition:var(--transition);backdrop-filter:blur(10px);background-color:rgba(255,255,255,0.95)!important}.navbar-brand{font-size:1.5rem;font-weight:700}.nav-link{font-weight:500;transition:var(--transition)}.nav-link:hover{color:var(--primary-color)!important}.hero-section{background:linear-gradient(135deg,var(--primary-color) 0,#0056b3 100%);min-height:75vh;display:flex;align-items:center}.min-vh-75{min-height:75vh}.hero-image{animation:float 6s ease-in-out infinite}@keyframes float{0%,100%{transform:translateY(0px)}50%{transform:translateY(-20px)}}.card{transition:var(--transition);border:none;box-shadow:var(--shadow)}.card:hover{transform:translateY(-5px);box-shadow:var(--shadow-lg)}.card-body{padding:2rem}.btn{font-weight:500;border-radius:var(--border-radius);transition:var(--transition);padding:.75rem 1.5rem}.btn-lg{padding:1rem 2rem;font-size:1.125rem}.btn-primary{background-color:var(--primary-color);border-color:var(--primary-color)}.btn-primary:hover{background-color:#0056b3;border-color:#0056b3;transform:translateY(-2px);box-shadow:var(--shadow-lg)}.btn-outline-primary{color:var(--primary-color);border-color:var(--primary-color)}.btn-outline-primary:hover{background-color:var(--primary-color);border-color:var(--primary-color);transform:translateY(-2px);box-shadow:var(--shadow-lg)}.btn-light{background-color:var(--white-color);border-color:var(--white-color);color:var(--text-color)}.btn-light:hover{background-color:#f8f9fa;border-color:#f8f9fa;color:var(--text-color);transform:translateY(-2px);box-shadow:var(--shadow-lg)}.feature-icon,.service-icon,.value-icon{transition:var(--transition)}.feature-icon:hover,.service-icon:hover,.value-icon:hover{transform:scale(1.1)}.process-step{transition:var(--transition)}.process-step:hover{transform:scale(1.05)}.form-control{border-radius:var(--border-radius);border:1px solid var(--border-color);padding:.75rem 1rem;transition:var(--transition)}.form-control:focus{border-color:var(--primary-color);box-shadow:0 0 0 .2rem rgba(13,110,253,0.25)}.form-label{font-weight:500;margin-bottom:.5rem}.alert{border-radius:var(--border-radius);border:none}.alert-success{background-color:#d1e7dd;color:#0f5132}.alert-danger{background-color:#f8d7da;color:#721c24}.accordion-button{font-weight:500;border-radius:var(--border-radius)}.accordion-button:not(.collapsed){background-color:var(--primary-color);color:var(--white-color)}.accordion-button:focus{box-shadow:0 0 0 .2rem rgba(13,110,253,0.25)}footer{background:linear-gradient(135deg,var(--dark-color) 0,#343a40 100%);color:var(--white-color)!important}footer .text-muted{color:rgba(255,255,255,0.7)!important}footer a{color:rgba(255,255,255,0.8)!important}footer a:hover{color:var(--white-color)!important}.social-links a{transition:var(--transition);display:inline-block}.social-links a:hover{transform:translateY(-3px);color:var(--primary-color)!important}.dropdown-toggle{border-radius:var(--border-radius);font-weight:500}.dropdown-item{transition:var(--transition)}.dropdown-item:hover{background-color:var(--primary-color);color:var(--white-color)}@media(max-width:768px){.hero-section{min-height:60vh;text-align:center}.display-4{font-size:2.5rem}.display-6{font-size:1.75rem}.card-body{padding:1.5rem}.btn-lg{padding:.75rem 1.5rem;font-size:1rem}}@media(max-width:576px){.hero-section{min-height:50vh}.display-4{font-size:2rem}.display-6{font-size:1.5rem}.card-body{padding:1rem}}.fade-in{animation:fadeIn .6s ease-in}@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.slide-in-left{animation:slideInLeft .6s ease-out}@keyframes slideInLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}.slide-in-right{animation:slideInRight .6s ease-out}@keyframes slideInRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}.text-primary{color:var(--primary-color)!important}.bg-primary{background-color:var(--primary-color)!important}.border-primary{border-color:var(--primary-color)!important}.shadow-sm{box-shadow:var(--shadow)!important}.shadow{box-shadow:var(--shadow-lg)!important}.loading{opacity:.6;pointer-events:none}.btn:focus,.form-control:focus,.accordion-button:focus{outline:none;box-shadow:0 0 0 .2rem rgba(13,110,253,0.25)}@media print{.navbar,.btn,.social-links{display:none!important}body{background:white!important;color:black!important}.card{border:1px solid #ccc!important;box-shadow:none!important}} \ No newline at end of file diff --git a/static/js/main.cbb94d6d.min.js b/static/js/main.cbb94d6d.min.js new file mode 100644 index 0000000..d8be1b7 --- /dev/null +++ b/static/js/main.cbb94d6d.min.js @@ -0,0 +1,18 @@ +document.addEventListener('DOMContentLoaded',function(){'use strict';initNavbar();initAnimations();initFormValidation();initScrollEffects();initContactForm();initAccordion();initTooltips();initModals();console.log('Kobelly Base Website loaded successfully!');});function initNavbar(){const navbar=document.querySelector('.navbar');const navbarToggler=document.querySelector('.navbar-toggler');const navbarCollapse=document.querySelector('.navbar-collapse');window.addEventListener('scroll',function(){if(window.scrollY>50){navbar.classList.add('scrolled');}else{navbar.classList.remove('scrolled');}});const navLinks=document.querySelectorAll('.navbar-nav .nav-link');navLinks.forEach(link=>{link.addEventListener('click',function(){if(navbarCollapse.classList.contains('show')){navbarToggler.click();}});});} +function initAnimations(){const observerOptions={threshold:0.1,rootMargin:'0px 0px -50px 0px'};const observer=new IntersectionObserver(function(entries){entries.forEach(entry=>{if(entry.isIntersecting){entry.target.classList.add('fade-in');observer.unobserve(entry.target);}});},observerOptions);const animateElements=document.querySelectorAll('.card, .feature-icon, .service-icon, .value-icon');animateElements.forEach(el=>{observer.observe(el);});} +function initFormValidation(){const forms=document.querySelectorAll('form[data-validate]');forms.forEach(form=>{form.addEventListener('submit',function(e){if(!validateForm(form)){e.preventDefault();}});});} +function validateForm(form){let isValid=true;const inputs=form.querySelectorAll('input[required], textarea[required]');inputs.forEach(input=>{if(!input.value.trim()){showFieldError(input,'This field is required');isValid=false;}else if(input.type==='email'&&!isValidEmail(input.value)){showFieldError(input,'Please enter a valid email address');isValid=false;}else{clearFieldError(input);}});return isValid;} +function isValidEmail(email){const emailRegex=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return emailRegex.test(email);} +function showFieldError(input,message){clearFieldError(input);const errorDiv=document.createElement('div');errorDiv.className='invalid-feedback d-block';errorDiv.textContent=message;input.classList.add('is-invalid');input.parentNode.appendChild(errorDiv);} +function clearFieldError(input){input.classList.remove('is-invalid');const errorDiv=input.parentNode.querySelector('.invalid-feedback');if(errorDiv){errorDiv.remove();}} +function initScrollEffects(){const anchorLinks=document.querySelectorAll('a[href^="#"]');anchorLinks.forEach(link=>{link.addEventListener('click',function(e){e.preventDefault();const target=document.querySelector(this.getAttribute('href'));if(target){target.scrollIntoView({behavior:'smooth',block:'start'});}});});const backToTopBtn=document.createElement('button');backToTopBtn.innerHTML='';backToTopBtn.className='btn btn-primary position-fixed';backToTopBtn.style.cssText='bottom: 20px; right: 20px; z-index: 1000; display: none; border-radius: 50%; width: 50px; height: 50px;';backToTopBtn.setAttribute('aria-label','Back to top');document.body.appendChild(backToTopBtn);window.addEventListener('scroll',function(){if(window.scrollY>300){backToTopBtn.style.display='block';}else{backToTopBtn.style.display='none';}});backToTopBtn.addEventListener('click',function(){window.scrollTo({top:0,behavior:'smooth'});});} +function initContactForm(){const contactForm=document.querySelector('form[action*="contact"]');if(!contactForm)return;contactForm.addEventListener('submit',function(e){const submitBtn=this.querySelector('button[type="submit"]');const originalText=submitBtn.textContent;submitBtn.disabled=true;submitBtn.innerHTML='Sending...';submitBtn.classList.add('loading');});} +function initAccordion(){const accordionButtons=document.querySelectorAll('.accordion-button');accordionButtons.forEach(button=>{button.addEventListener('click',function(){const isExpanded=this.getAttribute('aria-expanded')==='true';const accordionCollapse=this.nextElementSibling;if(!isExpanded){accordionCollapse.classList.add('expanding');} +setTimeout(()=>{accordionCollapse.classList.remove('expanding');},300);});});} +function initTooltips(){const tooltipTriggerList=[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));tooltipTriggerList.map(function(tooltipTriggerEl){return new bootstrap.Tooltip(tooltipTriggerEl);});} +function initModals(){const modalTriggerList=[].slice.call(document.querySelectorAll('[data-bs-toggle="modal"]'));modalTriggerList.map(function(modalTriggerEl){return new bootstrap.Modal(modalTriggerEl);});} +function debounce(func,wait){let timeout;return function executedFunction(...args){const later=()=>{clearTimeout(timeout);func(...args);};clearTimeout(timeout);timeout=setTimeout(later,wait);};} +function throttle(func,limit){let inThrottle;return function(){const args=arguments;const context=this;if(!inThrottle){func.apply(context,args);inThrottle=true;setTimeout(()=>inThrottle=false,limit);}};} +const optimizedScrollHandler=throttle(function(){},16);window.addEventListener('scroll',optimizedScrollHandler);window.addEventListener('error',function(e){console.error('JavaScript error:',e.error);});if('serviceWorker'in navigator){window.addEventListener('load',function(){navigator.serviceWorker.register('/sw.js').then(function(registration){console.log('ServiceWorker registration successful');}).catch(function(err){console.log('ServiceWorker registration failed');});});} +function initAccessibility(){const skipLink=document.createElement('a');skipLink.href='#main-content';skipLink.textContent='Skip to main content';skipLink.className='sr-only sr-only-focusable position-absolute';skipLink.style.cssText='top: 10px; left: 10px; z-index: 1001; padding: 10px; background: white; border: 1px solid #ccc;';document.body.insertBefore(skipLink,document.body.firstChild);const mainContent=document.querySelector('main');if(mainContent){mainContent.id='main-content';}} +initAccessibility(); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..f6fea53 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,291 @@ +// Kobelly Base Website - Main JavaScript + +document.addEventListener('DOMContentLoaded', function() { + 'use strict'; + + // Initialize all components + initNavbar(); + initAnimations(); + initFormValidation(); + initScrollEffects(); + initContactForm(); + initAccordion(); + initTooltips(); + initModals(); + + console.log('Kobelly Base Website loaded successfully!'); +}); + +// Navbar functionality +function initNavbar() { + const navbar = document.querySelector('.navbar'); + const navbarToggler = document.querySelector('.navbar-toggler'); + const navbarCollapse = document.querySelector('.navbar-collapse'); + + // Navbar scroll effect + window.addEventListener('scroll', function() { + if (window.scrollY > 50) { + navbar.classList.add('scrolled'); + } else { + navbar.classList.remove('scrolled'); + } + }); + + // Close mobile menu when clicking on a link + const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (navbarCollapse.classList.contains('show')) { + navbarToggler.click(); + } + }); + }); +} + +// Animation initialization +function initAnimations() { + // Intersection Observer for fade-in animations + const observerOptions = { + threshold: 0.1, + rootMargin: '0px 0px -50px 0px' + }; + + const observer = new IntersectionObserver(function(entries) { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('fade-in'); + observer.unobserve(entry.target); + } + }); + }, observerOptions); + + // Observe elements for animation + const animateElements = document.querySelectorAll('.card, .feature-icon, .service-icon, .value-icon'); + animateElements.forEach(el => { + observer.observe(el); + }); +} + +// Form validation +function initFormValidation() { + const forms = document.querySelectorAll('form[data-validate]'); + + forms.forEach(form => { + form.addEventListener('submit', function(e) { + if (!validateForm(form)) { + e.preventDefault(); + } + }); + }); +} + +function validateForm(form) { + let isValid = true; + const inputs = form.querySelectorAll('input[required], textarea[required]'); + + inputs.forEach(input => { + if (!input.value.trim()) { + showFieldError(input, 'This field is required'); + isValid = false; + } else if (input.type === 'email' && !isValidEmail(input.value)) { + showFieldError(input, 'Please enter a valid email address'); + isValid = false; + } else { + clearFieldError(input); + } + }); + + return isValid; +} + +function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +function showFieldError(input, message) { + clearFieldError(input); + + const errorDiv = document.createElement('div'); + errorDiv.className = 'invalid-feedback d-block'; + errorDiv.textContent = message; + + input.classList.add('is-invalid'); + input.parentNode.appendChild(errorDiv); +} + +function clearFieldError(input) { + input.classList.remove('is-invalid'); + const errorDiv = input.parentNode.querySelector('.invalid-feedback'); + if (errorDiv) { + errorDiv.remove(); + } +} + +// Scroll effects +function initScrollEffects() { + // Smooth scrolling for anchor links + const anchorLinks = document.querySelectorAll('a[href^="#"]'); + anchorLinks.forEach(link => { + link.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); + }); + + // Back to top button + const backToTopBtn = document.createElement('button'); + backToTopBtn.innerHTML = ''; + backToTopBtn.className = 'btn btn-primary position-fixed'; + backToTopBtn.style.cssText = 'bottom: 20px; right: 20px; z-index: 1000; display: none; border-radius: 50%; width: 50px; height: 50px;'; + backToTopBtn.setAttribute('aria-label', 'Back to top'); + + document.body.appendChild(backToTopBtn); + + window.addEventListener('scroll', function() { + if (window.scrollY > 300) { + backToTopBtn.style.display = 'block'; + } else { + backToTopBtn.style.display = 'none'; + } + }); + + backToTopBtn.addEventListener('click', function() { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }); +} + +// Contact form handling +function initContactForm() { + const contactForm = document.querySelector('form[action*="contact"]'); + if (!contactForm) return; + + contactForm.addEventListener('submit', function(e) { + const submitBtn = this.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + // Show loading state + submitBtn.disabled = true; + submitBtn.innerHTML = 'Sending...'; + submitBtn.classList.add('loading'); + }); +} + +// Accordion functionality +function initAccordion() { + const accordionButtons = document.querySelectorAll('.accordion-button'); + + accordionButtons.forEach(button => { + button.addEventListener('click', function() { + const isExpanded = this.getAttribute('aria-expanded') === 'true'; + + // Add animation class + const accordionCollapse = this.nextElementSibling; + if (!isExpanded) { + accordionCollapse.classList.add('expanding'); + } + + // Remove animation class after transition + setTimeout(() => { + accordionCollapse.classList.remove('expanding'); + }, 300); + }); + }); +} + +// Tooltip initialization +function initTooltips() { + const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.map(function(tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); +} + +// Modal initialization +function initModals() { + const modalTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="modal"]')); + modalTriggerList.map(function(modalTriggerEl) { + return new bootstrap.Modal(modalTriggerEl); + }); +} + +// Utility functions +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +function throttle(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} + +// Performance optimization +const optimizedScrollHandler = throttle(function() { + // Handle scroll events efficiently +}, 16); // ~60fps + +window.addEventListener('scroll', optimizedScrollHandler); + +// Error handling +window.addEventListener('error', function(e) { + console.error('JavaScript error:', e.error); +}); + +// Service Worker registration (for PWA features) +if ('serviceWorker' in navigator) { + window.addEventListener('load', function() { + navigator.serviceWorker.register('/sw.js') + .then(function(registration) { + console.log('ServiceWorker registration successful'); + }) + .catch(function(err) { + console.log('ServiceWorker registration failed'); + }); + }); +} + +// Accessibility improvements +function initAccessibility() { + // Skip to main content link + const skipLink = document.createElement('a'); + skipLink.href = '#main-content'; + skipLink.textContent = 'Skip to main content'; + skipLink.className = 'sr-only sr-only-focusable position-absolute'; + skipLink.style.cssText = 'top: 10px; left: 10px; z-index: 1001; padding: 10px; background: white; border: 1px solid #ccc;'; + + document.body.insertBefore(skipLink, document.body.firstChild); + + // Add main content id + const mainContent = document.querySelector('main'); + if (mainContent) { + mainContent.id = 'main-content'; + } +} + +// Initialize accessibility features +initAccessibility(); \ No newline at end of file diff --git a/static/js/main.min.js b/static/js/main.min.js new file mode 100644 index 0000000..d8be1b7 --- /dev/null +++ b/static/js/main.min.js @@ -0,0 +1,18 @@ +document.addEventListener('DOMContentLoaded',function(){'use strict';initNavbar();initAnimations();initFormValidation();initScrollEffects();initContactForm();initAccordion();initTooltips();initModals();console.log('Kobelly Base Website loaded successfully!');});function initNavbar(){const navbar=document.querySelector('.navbar');const navbarToggler=document.querySelector('.navbar-toggler');const navbarCollapse=document.querySelector('.navbar-collapse');window.addEventListener('scroll',function(){if(window.scrollY>50){navbar.classList.add('scrolled');}else{navbar.classList.remove('scrolled');}});const navLinks=document.querySelectorAll('.navbar-nav .nav-link');navLinks.forEach(link=>{link.addEventListener('click',function(){if(navbarCollapse.classList.contains('show')){navbarToggler.click();}});});} +function initAnimations(){const observerOptions={threshold:0.1,rootMargin:'0px 0px -50px 0px'};const observer=new IntersectionObserver(function(entries){entries.forEach(entry=>{if(entry.isIntersecting){entry.target.classList.add('fade-in');observer.unobserve(entry.target);}});},observerOptions);const animateElements=document.querySelectorAll('.card, .feature-icon, .service-icon, .value-icon');animateElements.forEach(el=>{observer.observe(el);});} +function initFormValidation(){const forms=document.querySelectorAll('form[data-validate]');forms.forEach(form=>{form.addEventListener('submit',function(e){if(!validateForm(form)){e.preventDefault();}});});} +function validateForm(form){let isValid=true;const inputs=form.querySelectorAll('input[required], textarea[required]');inputs.forEach(input=>{if(!input.value.trim()){showFieldError(input,'This field is required');isValid=false;}else if(input.type==='email'&&!isValidEmail(input.value)){showFieldError(input,'Please enter a valid email address');isValid=false;}else{clearFieldError(input);}});return isValid;} +function isValidEmail(email){const emailRegex=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return emailRegex.test(email);} +function showFieldError(input,message){clearFieldError(input);const errorDiv=document.createElement('div');errorDiv.className='invalid-feedback d-block';errorDiv.textContent=message;input.classList.add('is-invalid');input.parentNode.appendChild(errorDiv);} +function clearFieldError(input){input.classList.remove('is-invalid');const errorDiv=input.parentNode.querySelector('.invalid-feedback');if(errorDiv){errorDiv.remove();}} +function initScrollEffects(){const anchorLinks=document.querySelectorAll('a[href^="#"]');anchorLinks.forEach(link=>{link.addEventListener('click',function(e){e.preventDefault();const target=document.querySelector(this.getAttribute('href'));if(target){target.scrollIntoView({behavior:'smooth',block:'start'});}});});const backToTopBtn=document.createElement('button');backToTopBtn.innerHTML='';backToTopBtn.className='btn btn-primary position-fixed';backToTopBtn.style.cssText='bottom: 20px; right: 20px; z-index: 1000; display: none; border-radius: 50%; width: 50px; height: 50px;';backToTopBtn.setAttribute('aria-label','Back to top');document.body.appendChild(backToTopBtn);window.addEventListener('scroll',function(){if(window.scrollY>300){backToTopBtn.style.display='block';}else{backToTopBtn.style.display='none';}});backToTopBtn.addEventListener('click',function(){window.scrollTo({top:0,behavior:'smooth'});});} +function initContactForm(){const contactForm=document.querySelector('form[action*="contact"]');if(!contactForm)return;contactForm.addEventListener('submit',function(e){const submitBtn=this.querySelector('button[type="submit"]');const originalText=submitBtn.textContent;submitBtn.disabled=true;submitBtn.innerHTML='Sending...';submitBtn.classList.add('loading');});} +function initAccordion(){const accordionButtons=document.querySelectorAll('.accordion-button');accordionButtons.forEach(button=>{button.addEventListener('click',function(){const isExpanded=this.getAttribute('aria-expanded')==='true';const accordionCollapse=this.nextElementSibling;if(!isExpanded){accordionCollapse.classList.add('expanding');} +setTimeout(()=>{accordionCollapse.classList.remove('expanding');},300);});});} +function initTooltips(){const tooltipTriggerList=[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));tooltipTriggerList.map(function(tooltipTriggerEl){return new bootstrap.Tooltip(tooltipTriggerEl);});} +function initModals(){const modalTriggerList=[].slice.call(document.querySelectorAll('[data-bs-toggle="modal"]'));modalTriggerList.map(function(modalTriggerEl){return new bootstrap.Modal(modalTriggerEl);});} +function debounce(func,wait){let timeout;return function executedFunction(...args){const later=()=>{clearTimeout(timeout);func(...args);};clearTimeout(timeout);timeout=setTimeout(later,wait);};} +function throttle(func,limit){let inThrottle;return function(){const args=arguments;const context=this;if(!inThrottle){func.apply(context,args);inThrottle=true;setTimeout(()=>inThrottle=false,limit);}};} +const optimizedScrollHandler=throttle(function(){},16);window.addEventListener('scroll',optimizedScrollHandler);window.addEventListener('error',function(e){console.error('JavaScript error:',e.error);});if('serviceWorker'in navigator){window.addEventListener('load',function(){navigator.serviceWorker.register('/sw.js').then(function(registration){console.log('ServiceWorker registration successful');}).catch(function(err){console.log('ServiceWorker registration failed');});});} +function initAccessibility(){const skipLink=document.createElement('a');skipLink.href='#main-content';skipLink.textContent='Skip to main content';skipLink.className='sr-only sr-only-focusable position-absolute';skipLink.style.cssText='top: 10px; left: 10px; z-index: 1001; padding: 10px; background: white; border: 1px solid #ccc;';document.body.insertBefore(skipLink,document.body.firstChild);const mainContent=document.querySelector('main');if(mainContent){mainContent.id='main-content';}} +initAccessibility(); \ No newline at end of file diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..1123827 --- /dev/null +++ b/templates/about.html @@ -0,0 +1,203 @@ +{% extends "base.html" %} + +{% block title %}{{ t('About') }} - {{ t('Your Business Name') }}{% endblock %} + +{% block content %} + +
+
+
+
+

{{ t('About Us') }}

+

{{ t('Learn more about our company, our mission, and the team behind our success.') }}

+
+
+
+
+ + +
+
+
+
+

{{ t('Our Story') }}

+

{{ t('Founded with a vision to provide exceptional services and build lasting relationships with our clients.') }}

+

{{ t('We started as a small team with big dreams, determined to make a difference in the business world. Over the years, we have grown into a trusted partner for businesses of all sizes, helping them achieve their goals and overcome challenges.') }}

+

{{ t('Our commitment to quality, innovation, and customer satisfaction has remained at the core of everything we do. We believe in building long-term partnerships based on trust, transparency, and mutual success.') }}

+
+
+
+

10+

+

{{ t('Years Experience') }}

+
+
+
+
+

500+

+

{{ t('Happy Clients') }}

+
+
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+

{{ t('Our Mission & Values') }}

+

{{ t('We are guided by our core values and committed to our mission of delivering excellence.') }}

+
+
+ +
+
+
+
+
+ +
+

{{ t('Our Mission') }}

+

{{ t('To provide innovative, reliable, and cost-effective solutions that empower businesses to achieve their full potential and drive sustainable growth in an ever-evolving market.') }}

+
+
+
+ +
+
+
+
+ +
+

{{ t('Our Vision') }}

+

{{ t('To be the leading provider of business solutions, recognized for our expertise, integrity, and commitment to client success across all industries.') }}

+
+
+
+
+ +
+
+
+
+ +
+
{{ t('Excellence') }}
+

{{ t('We strive for excellence in everything we do, maintaining the highest standards of quality and professionalism.') }}

+
+
+ +
+
+
+ +
+
{{ t('Integrity') }}
+

{{ t('We conduct business with honesty, transparency, and ethical practices, building trust with our clients and partners.') }}

+
+
+ +
+
+
+ +
+
{{ t('Innovation') }}
+

{{ t('We embrace new ideas and technologies to provide cutting-edge solutions that meet evolving business needs.') }}

+
+
+
+
+
+ + +
+
+
+
+

{{ t('Our Team') }}

+

{{ t('Meet the dedicated professionals who make our company successful.') }}

+
+
+ +
+
+
+
+
+
+ +
+
+
{{ t('John Doe') }}
+

{{ t('CEO & Founder') }}

+

{{ t('Experienced leader with a passion for innovation and business growth.') }}

+
+
+
+ +
+
+
+
+
+ +
+
+
{{ t('Jane Smith') }}
+

{{ t('Operations Manager') }}

+

{{ t('Dedicated professional ensuring smooth operations and client satisfaction.') }}

+
+
+
+ +
+
+
+
+
+ +
+
+
{{ t('Mike Johnson') }}
+

{{ t('Technical Lead') }}

+

{{ t('Expert in technical solutions and innovative problem-solving approaches.') }}

+
+
+
+ +
+
+
+
+
+ +
+
+
{{ t('Sarah Wilson') }}
+

{{ t('Client Relations') }}

+

{{ t('Committed to building strong relationships and ensuring client success.') }}

+
+
+
+
+
+
+ + +
+
+

{{ t('Ready to Work With Us?') }}

+

{{ t('Let us help you achieve your business goals with our professional services and dedicated support.') }}

+ {{ t('Get Started Today') }} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..108597a --- /dev/null +++ b/templates/base.html @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + {% block title %}{{ t('Your Business Name') }}{% endblock %} + + + + + + + + + + + + {% block extra_head %}{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + + + + + + + + + {% block extra_scripts %}{% endblock %} + + \ No newline at end of file diff --git a/templates/contact.html b/templates/contact.html new file mode 100644 index 0000000..e610678 --- /dev/null +++ b/templates/contact.html @@ -0,0 +1,216 @@ +{% extends "base.html" %} + +{% block title %}{{ t('Contact') }} - {{ t('Your Business Name') }}{% endblock %} + +{% block content %} + +
+
+
+
+

{{ t('Contact Us') }}

+

{{ t('Get in touch with us today. We are here to help you with all your business needs.') }}

+
+
+
+
+ + +
+
+
+ +
+
+
+

{{ t('Send Us a Message') }}

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+ + +
+
+
+

{{ t('Get in Touch') }}

+ +
+
+
+ +
+
+
{{ t('Address') }}
+

{{ t('123 Business Street') }}
{{ t('City, State 12345') }}
{{ t('Country') }}

+
+
+ +
+
+ +
+
+
{{ t('Phone') }}
+

+1 234 567 890

+
+
+ +
+
+ +
+
+
{{ t('Email') }}
+

info@yourbusiness.com

+
+
+ +
+
+ +
+
+
{{ t('Business Hours') }}
+

{{ t('Monday - Friday') }}
{{ t('9:00 AM - 6:00 PM') }}
{{ t('Saturday') }}
{{ t('10:00 AM - 4:00 PM') }}

+
+
+
+ +
+ +
{{ t('Follow Us') }}
+ +
+
+
+
+
+
+ + +
+
+
+
+

{{ t('Frequently Asked Questions') }}

+

{{ t('Find answers to common questions about our services and processes.') }}

+
+
+ +
+
+
+
+

+ +

+
+
+ {{ t('We offer a comprehensive range of professional services including consulting, implementation, support, and specialized solutions tailored to meet your specific business needs.') }} +
+
+
+ +
+

+ +

+
+
+ {{ t('Project timelines vary depending on the scope and complexity. We typically complete projects within 2-8 weeks, but we always provide detailed timelines during our initial consultation.') }} +
+
+
+ +
+

+ +

+
+
+ {{ t('Yes, we provide comprehensive ongoing support and maintenance services to ensure your continued success and satisfaction with our solutions.') }} +
+
+
+
+
+ +
+
+
+

{{ t('Why Choose Us?') }}

+
    +
  • + + {{ t('Experience') }}: {{ t('Years of industry experience and expertise') }} +
  • +
  • + + {{ t('Quality') }}: {{ t('Commitment to delivering high-quality solutions') }} +
  • +
  • + + {{ t('Support') }}: {{ t('Dedicated customer support and maintenance') }} +
  • +
  • + + {{ t('Flexibility') }}: {{ t('Customized solutions to meet your specific needs') }} +
  • +
  • + + {{ t('Reliability') }}: {{ t('Proven track record of successful project delivery') }} +
  • +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/400.html b/templates/errors/400.html new file mode 100644 index 0000000..ef471a0 --- /dev/null +++ b/templates/errors/400.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ t('Bad Request') }} - {{ t('Your Business Name') }}{% endblock %} +{% block content %} +
+
+
+
+
+

400

+

{{ t('Bad Request') }}

+

{{ t('The server could not understand your request. Please check your input and try again.') }}

+ {{ t('Go to Homepage') }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/401.html b/templates/errors/401.html new file mode 100644 index 0000000..95f7c75 --- /dev/null +++ b/templates/errors/401.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ t('Unauthorized') }} - {{ t('Your Business Name') }}{% endblock %} +{% block content %} +
+
+
+
+
+

401

+

{{ t('Unauthorized') }}

+

{{ t('You are not authorized to view this page. Please log in or contact support if you believe this is an error.') }}

+ {{ t('Go to Homepage') }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/403.html b/templates/errors/403.html new file mode 100644 index 0000000..64210ff --- /dev/null +++ b/templates/errors/403.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ t('Forbidden') }} - {{ t('Your Business Name') }}{% endblock %} +{% block content %} +
+
+
+
+
+

403

+

{{ t('Forbidden') }}

+

{{ t('You do not have permission to access this page.') }}

+ {{ t('Go to Homepage') }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/404.html b/templates/errors/404.html new file mode 100644 index 0000000..40d7a0a --- /dev/null +++ b/templates/errors/404.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}{{ t('Page Not Found') }} - {{ t('Your Business Name') }}{% endblock %} + +{% block content %} +
+
+
+
+
+

404

+

{{ t('Page Not Found') }}

+

{{ t('The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.') }}

+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/405.html b/templates/errors/405.html new file mode 100644 index 0000000..7e5b9ca --- /dev/null +++ b/templates/errors/405.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ t('Method Not Allowed') }} - {{ t('Your Business Name') }}{% endblock %} +{% block content %} +
+
+
+
+
+

405

+

{{ t('Method Not Allowed') }}

+

{{ t('The method used for this request is not allowed.') }}

+ {{ t('Go to Homepage') }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/500.html b/templates/errors/500.html new file mode 100644 index 0000000..8700196 --- /dev/null +++ b/templates/errors/500.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}{{ t('Server Error') }} - {{ t('Your Business Name') }}{% endblock %} + +{% block content %} +
+
+
+
+
+

500

+

{{ t('Server Error') }}

+

{{ t('Something went wrong on our end. We are working to fix the issue. Please try again later.') }}

+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/502.html b/templates/errors/502.html new file mode 100644 index 0000000..66825f1 --- /dev/null +++ b/templates/errors/502.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ t('Bad Gateway') }} - {{ t('Your Business Name') }}{% endblock %} +{% block content %} +
+
+
+
+
+

502

+

{{ t('Bad Gateway') }}

+

{{ t('The server received an invalid response from the upstream server.') }}

+ {{ t('Go to Homepage') }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/503.html b/templates/errors/503.html new file mode 100644 index 0000000..1574093 --- /dev/null +++ b/templates/errors/503.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ t('Service Unavailable') }} - {{ t('Your Business Name') }}{% endblock %} +{% block content %} +
+
+
+
+
+

503

+

{{ t('Service Unavailable') }}

+

{{ t('The server is currently unavailable. Please try again later.') }}

+ {{ t('Go to Homepage') }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/errors/504.html b/templates/errors/504.html new file mode 100644 index 0000000..bd1ca6f --- /dev/null +++ b/templates/errors/504.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ t('Gateway Timeout') }} - {{ t('Your Business Name') }}{% endblock %} +{% block content %} +
+
+
+
+
+

504

+

{{ t('Gateway Timeout') }}

+

{{ t('The server did not receive a timely response from the upstream server.') }}

+ {{ t('Go to Homepage') }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..61e6a72 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,165 @@ +{% extends "base.html" %} + +{% block title %}{{ t('Your Business Name') }} - {{ t('Professional Services') }}{% endblock %} + +{% block content %} + +
+
+
+
+

{{ t('Professional Solutions for Your Business') }}

+

{{ t('We provide high-quality services tailored to meet your business needs. Our experienced team is dedicated to delivering exceptional results.') }}

+ +
+
+
+ +
+
+
+
+
+ + +
+
+
+
+

{{ t('Why Choose Us') }}

+

{{ t('We offer comprehensive solutions with a focus on quality, reliability, and customer satisfaction.') }}

+
+
+ +
+
+
+
+ +
+
{{ t('Quality Service') }}
+

{{ t('We maintain the highest standards in all our services to ensure your complete satisfaction.') }}

+
+
+ +
+
+
+ +
+
{{ t('Timely Delivery') }}
+

{{ t('We understand the importance of deadlines and always deliver our services on time.') }}

+
+
+ +
+
+
+ +
+
{{ t('Expert Team') }}
+

{{ t('Our experienced professionals are dedicated to providing the best solutions for your business.') }}

+
+
+ +
+
+
+ +
+
{{ t('Customer Focus') }}
+

{{ t('Your success is our priority. We work closely with you to understand your specific needs.') }}

+
+
+
+
+
+ + +
+
+
+
+

{{ t('Our Services') }}

+

{{ t('Discover our comprehensive range of professional services designed to help your business grow.') }}

+
+
+ +
+
+
+
+
+ +
+
{{ t('Service 1') }}
+

{{ t('Professional service description that highlights the key benefits and features of this offering.') }}

+ {{ t('Learn More') }} +
+
+
+ +
+
+
+
+ +
+
{{ t('Service 2') }}
+

{{ t('Comprehensive solution that addresses specific business challenges and delivers measurable results.') }}

+ {{ t('Learn More') }} +
+
+
+ +
+
+
+
+ +
+
{{ t('Service 3') }}
+

{{ t('Reliable and secure service that provides peace of mind and protects your business interests.') }}

+ {{ t('Learn More') }} +
+
+
+
+ + +
+
+ + +
+
+
+
+

{{ t('About Our Company') }}

+

{{ t('We are a dedicated team of professionals committed to delivering exceptional services and building long-term relationships with our clients.') }}

+

{{ t('With years of experience in the industry, we understand the unique challenges that businesses face and provide tailored solutions to help them succeed.') }}

+ {{ t('Learn More About Us') }} +
+
+
+ +
+
+
+
+
+ + +
+
+

{{ t('Ready to Get Started?') }}

+

{{ t('Contact us today to discuss your needs and discover how we can help your business grow.') }}

+ {{ t('Contact Us Now') }} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/services.html b/templates/services.html new file mode 100644 index 0000000..6cc109f --- /dev/null +++ b/templates/services.html @@ -0,0 +1,205 @@ +{% extends "base.html" %} + +{% block title %}{{ t('Services') }} - {{ t('Your Business Name') }}{% endblock %} + +{% block content %} + +
+
+
+
+

{{ t('Our Services') }}

+

{{ t('Discover our comprehensive range of professional services designed to help your business grow and succeed.') }}

+
+
+
+
+ + +
+
+
+
+

{{ t('What We Offer') }}

+

{{ t('We provide a wide range of services tailored to meet the diverse needs of modern businesses.') }}

+
+
+ +
+
+
+
+
+
+ +
+
+

{{ t('Service 1') }}

+

{{ t('Professional service category') }}

+
+
+

{{ t('Comprehensive service description that explains the benefits, features, and value proposition of this offering. We ensure high quality and reliable delivery.') }}

+
    +
  • {{ t('Feature 1 description') }}
  • +
  • {{ t('Feature 2 description') }}
  • +
  • {{ t('Feature 3 description') }}
  • +
+
+ {{ t('Starting from $99') }} +
+
+
+
+ +
+
+
+
+
+ +
+
+

{{ t('Service 2') }}

+

{{ t('Advanced service category') }}

+
+
+

{{ t('Advanced service offering that provides comprehensive solutions for complex business challenges. Includes detailed analysis and customized implementation.') }}

+
    +
  • {{ t('Advanced feature 1') }}
  • +
  • {{ t('Advanced feature 2') }}
  • +
  • {{ t('Advanced feature 3') }}
  • +
+
+ {{ t('Starting from $199') }} +
+
+
+
+ +
+
+
+
+
+ +
+
+

{{ t('Service 3') }}

+

{{ t('Premium service category') }}

+
+
+

{{ t('Premium service that offers the highest level of support and features. Includes dedicated resources and priority support for critical business needs.') }}

+
    +
  • {{ t('Premium feature 1') }}
  • +
  • {{ t('Premium feature 2') }}
  • +
  • {{ t('Premium feature 3') }}
  • +
+
+ {{ t('Starting from $299') }} +
+
+
+
+ +
+
+
+
+
+ +
+
+

{{ t('Service 4') }}

+

{{ t('Specialized service category') }}

+
+
+

{{ t('Specialized service designed for specific industry needs. Provides targeted solutions with deep expertise in particular business domains.') }}

+
    +
  • {{ t('Specialized feature 1') }}
  • +
  • {{ t('Specialized feature 2') }}
  • +
  • {{ t('Specialized feature 3') }}
  • +
+
+ {{ t('Starting from $149') }} +
+
+
+
+
+
+
+ + +
+
+
+
+

{{ t('Our Process') }}

+

{{ t('We follow a proven methodology to ensure successful project delivery and client satisfaction.') }}

+
+
+ +
+
+
+
+
+ 1 +
+
+
{{ t('Consultation') }}
+

{{ t('We begin with a thorough consultation to understand your specific needs and requirements.') }}

+
+
+ +
+
+
+
+ 2 +
+
+
{{ t('Planning') }}
+

{{ t('We develop a comprehensive plan tailored to your business objectives and timeline.') }}

+
+
+ +
+
+
+
+ 3 +
+
+
{{ t('Implementation') }}
+

{{ t('Our expert team executes the plan with precision and attention to detail.') }}

+
+
+ +
+
+
+
+ 4 +
+
+
{{ t('Support') }}
+

{{ t('We provide ongoing support and maintenance to ensure continued success.') }}

+
+
+
+
+
+ + +
+
+

{{ t('Ready to Get Started?') }}

+

{{ t('Contact us today to discuss your specific needs and get a customized quote for our services.') }}

+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/sitemap.xml b/templates/sitemap.xml new file mode 100644 index 0000000..bd34c8c --- /dev/null +++ b/templates/sitemap.xml @@ -0,0 +1,11 @@ + + + {% for page in pages %} + + {{ page.url }} + {{ current_year }}-01-01 + weekly + {{ page.priority }} + + {% endfor %} + \ No newline at end of file diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000..9674aaa --- /dev/null +++ b/test_app.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +""" +Simple test script to debug Flask app issues +""" + +import os +import sys + +# Add current directory to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +try: + print("Testing imports...") + from flask import Flask + print("✓ Flask imported successfully") + + from translation_manager import load_translations, translate, get_current_language + print("✓ Translation manager imported successfully") + + print("\nTesting translation loading...") + load_translations() + print("✓ Translations loaded successfully") + + print("\nTesting translation function...") + result = translate("Home") + print(f"✓ Translation test: 'Home' -> '{result}'") + + print("\nTesting current language...") + lang = get_current_language() + print(f"✓ Current language: {lang}") + + print("\nAll tests passed! The issue might be elsewhere.") + +except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/translation_manager.py b/translation_manager.py new file mode 100644 index 0000000..bc80f73 --- /dev/null +++ b/translation_manager.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +Simple JSON-based Translation Manager for Kobelly's Base website +No compilation needed - just edit JSON files directly! +""" + +import os +import json +import glob +from flask import request, session, url_for +from functools import wraps + +# Supported languages +SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'nl'] + +# Default language +DEFAULT_LANGUAGE = 'en' + +# Translation cache +_translations = {} + +def load_translations(): + """Load all translation files into memory.""" + global _translations + + if not os.path.exists('translations'): + os.makedirs('translations') + + for lang in SUPPORTED_LANGUAGES: + lang_file = f'translations/{lang}.json' + + if os.path.exists(lang_file): + try: + with open(lang_file, 'r', encoding='utf-8') as f: + _translations[lang] = json.load(f) + print(f"✓ Loaded {lang} translations ({len(_translations[lang])} keys)") + except Exception as e: + print(f"✗ Error loading {lang} translations: {e}") + _translations[lang] = {} + else: + print(f"⚠ {lang} translation file not found, creating empty one") + _translations[lang] = {} + save_translations(lang, {}) + +def save_translations(lang, translations): + """Save translations for a specific language.""" + if not os.path.exists('translations'): + os.makedirs('translations') + + lang_file = f'translations/{lang}.json' + try: + with open(lang_file, 'w', encoding='utf-8') as f: + json.dump(translations, f, indent=2, ensure_ascii=False) + _translations[lang] = translations + print(f"✓ Saved {lang} translations") + return True + except Exception as e: + print(f"✗ Error saving {lang} translations: {e}") + return False + +def get_current_language(): + """Get the current language from session or request.""" + try: + # Check session first + if 'language' in session: + return session['language'] + + # Check request parameter + if request.args.get('lang') in SUPPORTED_LANGUAGES: + return request.args.get('lang') + + # Check Accept-Language header + if request.accept_languages: + for lang in request.accept_languages: + if lang[0] in SUPPORTED_LANGUAGES: + return lang[0] + except RuntimeError: + # Working outside of request context + pass + + return DEFAULT_LANGUAGE + +def set_language(lang): + """Set the current language in session.""" + if lang in SUPPORTED_LANGUAGES: + session['language'] = lang + print(f"✓ Language set to {lang}") + return True + print(f"✗ Invalid language: {lang}") + return False + +def translate(key, lang=None, **kwargs): + """Translate a key to the specified language.""" + if lang is None: + lang = get_current_language() + + # Get translation + translation = _translations.get(lang, {}).get(key, key) + + # Format with kwargs if provided + if kwargs: + try: + translation = translation.format(**kwargs) + except (KeyError, ValueError): + # If formatting fails, return the key + translation = key + + return translation + +def extract_strings(): + """Extract all translatable strings from templates and create translation files.""" + print("đŸ“€ Extracting translatable strings...") + + # Find all template files + template_files = glob.glob('templates/**/*.html', recursive=True) + + # Extract strings (this is a simplified version) + # In practice, you'd want to parse the templates more thoroughly + extracted_strings = set() + + for template_file in template_files: + try: + with open(template_file, 'r', encoding='utf-8') as f: + content = f.read() + # Look for common patterns (this is simplified) + # You might want to use a proper template parser + lines = content.split('\n') + for line in lines: + line = line.strip() + if line and not line.startswith('{') and not line.startswith('