commit 8a842d364d7ad3b4ed86b741eac4b2565bb0747a Author: Faheed Date: Mon Dec 29 04:50:49 2025 +0300 tenhal landing page added diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..467ce30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# --- Python and Django --- +*.py[cod] +__pycache__/ +*.so +.Python +env/ +venv/ +staticfiles/ +media/ + +# --- Databases (IMPORTANT for SQLite) --- +# We ignore the database so your local data doesn't overwrite your AWS data +db.sqlite3 +db.sqlite3-journal +db.sqlite3-wal +db.sqlite3-shm + +# --- Secrets and Environment --- +.env +.venv +env/ +venv/ +.secret_key + +# --- Operating System --- +.DS_Store +Thumbs.db + +# --- GeoIP --- +# If you download these manually on AWS, ignore them here +geoip/*.mmdb \ No newline at end of file diff --git a/landing_page/__init__.py b/landing_page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/landing_page/admin.py b/landing_page/admin.py new file mode 100644 index 0000000..e26cdaa --- /dev/null +++ b/landing_page/admin.py @@ -0,0 +1,85 @@ +from django.contrib import admin +from .models import ( + LandingPageSettings, Partners, Expertise, + Product, Inquiry, Testimonial, TeamMember +) + +@admin.register(LandingPageSettings) +class LandingPageSettingsAdmin(admin.ModelAdmin): + list_display = ('logo', 'company_address_en') + +@admin.register(Partners) +class PartnersAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'order') + list_editable = ('order',) + ordering = ('order',) + +@admin.register(Expertise) +class ExpertiseAdmin(admin.ModelAdmin): + list_display = ('title_en', 'title_ar', 'order') + list_editable = ('order',) + ordering = ('order',) + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'order', 'link') + list_editable = ('order',) + ordering = ('order',) + +@admin.register(Inquiry) +class InquiryAdmin(admin.ModelAdmin): + list_display = ('name', 'email','message', 'created_at') + readonly_fields = ('created_at',) + +@admin.register(Testimonial) +class TestimonialAdmin(admin.ModelAdmin): + list_display = ('client_name_en', 'client_name_ar', 'order') + list_editable = ('order',) + ordering = ('order',) + +@admin.register(TeamMember) +class TeamMemberAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'role_en', 'order') + list_editable = ('order',) + ordering = ('order',) + + + +# admin.py +import json +from django.contrib import admin +from django.db.models import Count +from rangefilter.filters import DateRangeFilterBuilder # Improved range picker +from .models import VisitorLog + +@admin.register(VisitorLog) +class VisitorLogAdmin(admin.ModelAdmin): + list_display = ('ip_address', 'country', 'city', 'timestamp') + + # 1. Add the Date Range Filter to the sidebar + list_filter = ( + ("timestamp", DateRangeFilterBuilder()), + 'country', + ) + + change_list_template = 'admin/visitor_log_changelist.html' + + def changelist_view(self, request, extra_context=None): + # 2. Get the current filtered results based on your date selection + response = super().changelist_view(request, extra_context) + + try: + # This captures the exact filtered queryset being shown in the list + qs = response.context_data['cl'].queryset + except (AttributeError, KeyError): + return response + + # 3. Aggregate data from the FILTERED queryset + country_stats = list(qs.values('country').annotate(total=Count('id')).order_by('-total')[:10]) + city_stats = list(qs.values('city').annotate(total=Count('id')).order_by('-total')[:10]) + + extra_context = extra_context or {} + extra_context['country_data'] = json.dumps(country_stats) + extra_context['city_data'] = json.dumps(city_stats) + + return super().changelist_view(request, extra_context=extra_context) \ No newline at end of file diff --git a/landing_page/apps.py b/landing_page/apps.py new file mode 100644 index 0000000..547ffb0 --- /dev/null +++ b/landing_page/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LandingPageConfig(AppConfig): + name = 'landing_page' diff --git a/landing_page/middleware.py b/landing_page/middleware.py new file mode 100644 index 0000000..6658948 --- /dev/null +++ b/landing_page/middleware.py @@ -0,0 +1,20 @@ +# middleware.py +import logging +from django_q.tasks import async_task + +logger = logging.getLogger(__name__) + +class AnalyticsMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + ip = request.META.get('REMOTE_ADDR') + + # LOG 1: Check if Middleware is even being triggered + print(f"DEBUG: Middleware triggered for IP: {ip}") + + # Hand off to the queue + async_task('landing_page.tasks.log_visitor_location', ip) + + return self.get_response(request) \ No newline at end of file diff --git a/landing_page/migrations/0001_initial.py b/landing_page/migrations/0001_initial.py new file mode 100644 index 0000000..244ee6a --- /dev/null +++ b/landing_page/migrations/0001_initial.py @@ -0,0 +1,117 @@ +# Generated by Django 6.0 on 2025-12-28 16:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Expertise', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title_en', models.CharField(blank=True, max_length=100, null=True)), + ('title_ar', models.CharField(blank=True, max_length=100, null=True)), + ('description_en', models.TextField(blank=True, null=True)), + ('description_ar', models.TextField(blank=True, null=True)), + ('icon_svg', models.TextField(help_text='Paste SVG code here')), + ('order', models.IntegerField(default=0)), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Inquiry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='LandingPageSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('logo', models.ImageField(upload_to='logos/')), + ('company_address_en', models.TextField()), + ('company_address_ar', models.TextField()), + ('company_email', models.JSONField(help_text='Save as a list of emails')), + ('company_phone', models.JSONField(help_text='Save as a list of phone numbers')), + ('facebook_url', models.URLField(blank=True, null=True)), + ('twitter_url', models.URLField(blank=True, null=True)), + ('linkedin_url', models.URLField(blank=True, null=True)), + ('instagram_url', models.URLField(blank=True, null=True)), + ('hero_image', models.ImageField(upload_to='hero_images/')), + ], + ), + migrations.CreateModel( + name='Partners', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(blank=True, max_length=100, null=True)), + ('name_ar', models.CharField(blank=True, max_length=100, null=True)), + ('logo', models.ImageField(blank=True, null=True, upload_to='partners/')), + ('order', models.IntegerField(default=0)), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(blank=True, max_length=100, null=True)), + ('name_ar', models.CharField(blank=True, max_length=100, null=True)), + ('description_en', models.TextField(blank=True, null=True)), + ('description_ar', models.TextField(blank=True, null=True)), + ('features_en', models.JSONField(help_text='English features list')), + ('features_ar', models.JSONField(help_text='Arabic features list')), + ('order', models.IntegerField(default=0)), + ('link', models.URLField(blank=True, null=True)), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='TeamMember', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(blank=True, max_length=100, null=True)), + ('name_ar', models.CharField(blank=True, max_length=100, null=True)), + ('role_en', models.CharField(blank=True, max_length=100, null=True)), + ('role_ar', models.CharField(blank=True, max_length=100, null=True)), + ('photo', models.ImageField(upload_to='team_members/')), + ('bio_en', models.TextField(blank=True, null=True)), + ('bio_ar', models.TextField(blank=True, null=True)), + ('order', models.IntegerField(default=0)), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Testimonial', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('client_name_en', models.CharField(blank=True, max_length=100, null=True)), + ('client_name_ar', models.CharField(blank=True, max_length=100, null=True)), + ('feedback_en', models.TextField(blank=True, null=True)), + ('feedback_ar', models.TextField(blank=True, null=True)), + ('client_photo', models.ImageField(blank=True, null=True, upload_to='testimonials/')), + ('order', models.IntegerField(default=0)), + ], + options={ + 'ordering': ['order'], + }, + ), + ] diff --git a/landing_page/migrations/0002_alter_expertise_options_alter_inquiry_options_and_more.py b/landing_page/migrations/0002_alter_expertise_options_alter_inquiry_options_and_more.py new file mode 100644 index 0000000..cf3e8a2 --- /dev/null +++ b/landing_page/migrations/0002_alter_expertise_options_alter_inquiry_options_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 6.0 on 2025-12-28 20:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('landing_page', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='expertise', + options={'ordering': ['order'], 'verbose_name_plural': 'Expertise Areas'}, + ), + migrations.AlterModelOptions( + name='inquiry', + options={'ordering': ['-created_at'], 'verbose_name_plural': 'Inquiries'}, + ), + migrations.AlterModelOptions( + name='landingpagesettings', + options={'verbose_name_plural': 'Landing Page Settings'}, + ), + migrations.AlterModelOptions( + name='partners', + options={'ordering': ['order'], 'verbose_name_plural': 'Partners'}, + ), + migrations.AlterModelOptions( + name='product', + options={'ordering': ['order'], 'verbose_name_plural': 'Products'}, + ), + migrations.AlterModelOptions( + name='teammember', + options={'ordering': ['order'], 'verbose_name_plural': 'Team Members'}, + ), + migrations.AlterModelOptions( + name='testimonial', + options={'ordering': ['order'], 'verbose_name_plural': 'Testimonials'}, + ), + migrations.AlterField( + model_name='landingpagesettings', + name='company_email', + field=models.JSONField(default=list, help_text='Save as a list of emails as JSON i.e. ["", ""]'), + ), + migrations.AlterField( + model_name='landingpagesettings', + name='company_phone', + field=models.JSONField(default=list, help_text='Save as a list of phone numbers as JSON i.e. ["", ""]'), + ), + migrations.AlterField( + model_name='product', + name='features_ar', + field=models.JSONField(default=list, help_text='Arabic features list as JSON array i.e. ["الميزة 1", "الميزة 2"]'), + ), + migrations.AlterField( + model_name='product', + name='features_en', + field=models.JSONField(default=list, help_text='English features list as JSON array i.e. ["Feature 1", "Feature 2"]'), + ), + ] diff --git a/landing_page/migrations/0003_product_image.py b/landing_page/migrations/0003_product_image.py new file mode 100644 index 0000000..16811e3 --- /dev/null +++ b/landing_page/migrations/0003_product_image.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0 on 2025-12-28 20:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('landing_page', '0002_alter_expertise_options_alter_inquiry_options_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='products/'), + ), + ] diff --git a/landing_page/migrations/0004_visitorlog.py b/landing_page/migrations/0004_visitorlog.py new file mode 100644 index 0000000..393340e --- /dev/null +++ b/landing_page/migrations/0004_visitorlog.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0 on 2025-12-28 23:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('landing_page', '0003_product_image'), + ] + + operations = [ + migrations.CreateModel( + name='VisitorLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip_address', models.GenericIPAddressField()), + ('country', models.CharField(blank=True, max_length=100, null=True)), + ('region', models.CharField(blank=True, max_length=100, null=True)), + ('city', models.CharField(blank=True, max_length=100, null=True)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name_plural': 'Visitor Logs', + 'ordering': ['-timestamp'], + }, + ), + ] diff --git a/landing_page/migrations/__init__.py b/landing_page/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/landing_page/models.py b/landing_page/models.py new file mode 100644 index 0000000..d75e355 --- /dev/null +++ b/landing_page/models.py @@ -0,0 +1,120 @@ +from django.db import models + +class LandingPageSettings(models.Model): + logo = models.ImageField(upload_to='logos/') + company_address_en = models.TextField() + company_address_ar = models.TextField() + company_email = models.JSONField(help_text="Save as a list of emails as JSON i.e. [\"\", \"\"]",default=list) + company_phone = models.JSONField(help_text="Save as a list of phone numbers as JSON i.e. [\"\", \"\"]",default=list) + facebook_url = models.URLField(blank=True, null=True) + twitter_url = models.URLField(blank=True, null=True) + linkedin_url = models.URLField(blank=True, null=True) + instagram_url = models.URLField(blank=True, null=True) + hero_image = models.ImageField(upload_to='hero_images/') + + def __str__(self): + return "Landing Page Settings" + class Meta: + verbose_name_plural = "Landing Page Settings" + +class Partners(models.Model): + name_en = models.CharField(max_length=100, blank=True, null=True) + name_ar = models.CharField(max_length=100, blank=True, null=True) + logo = models.ImageField(upload_to='partners/', blank=True, null=True) + order = models.IntegerField(default=0) + + class Meta: + ordering = ['order'] + verbose_name_plural = "Partners" + def __str__(self): + return self.name_en or "Partner" + +class Expertise(models.Model): + title_en = models.CharField(max_length=100, blank=True, null=True) + title_ar = models.CharField(max_length=100, blank=True, null=True) + description_en = models.TextField(blank=True, null=True) + description_ar = models.TextField(blank=True, null=True) + icon_svg = models.TextField(help_text="Paste SVG code here") + order = models.IntegerField(default=0) + + class Meta: + ordering = ['order'] + verbose_name_plural = "Expertise Areas" + def __str__(self): + return self.title_en or "Expertise" + +class Product(models.Model): + name_en = models.CharField(max_length=100, blank=True, null=True) + name_ar = models.CharField(max_length=100, blank=True, null=True) + description_en = models.TextField(blank=True, null=True) + description_ar = models.TextField(blank=True, null=True) + features_en = models.JSONField(help_text="English features list as JSON array i.e. [\"Feature 1\", \"Feature 2\"]",default=list) + features_ar = models.JSONField(help_text="Arabic features list as JSON array i.e. [\"الميزة 1\", \"الميزة 2\"]",default=list) + order = models.IntegerField(default=0) + image = models.ImageField(upload_to='products/', blank=True, null=True) + link = models.URLField(blank=True, null=True) + + class Meta: + ordering = ['order'] + verbose_name_plural = "Products" + def __str__(self): + return self.name_en or "Product" + + +class Inquiry(models.Model): + name = models.CharField(max_length=100) + email = models.EmailField() + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.email} - {self.message[:20]}" + class Meta: + ordering = ['-created_at'] + verbose_name_plural = "Inquiries" + +class Testimonial(models.Model): + client_name_en = models.CharField(max_length=100, blank=True, null=True) + client_name_ar = models.CharField(max_length=100, blank=True, null=True) + feedback_en = models.TextField(null=True, blank=True) + feedback_ar = models.TextField(null=True, blank=True) + client_photo = models.ImageField(upload_to='testimonials/', blank=True, null=True) + order = models.IntegerField(default=0) + + class Meta: + ordering = ['order'] + verbose_name_plural = "Testimonials" + def __str__(self): + return self.client_name_en or "Client" + +class TeamMember(models.Model): + name_en = models.CharField(max_length=100, blank=True, null=True) + name_ar = models.CharField(max_length=100, blank=True, null=True) + role_en = models.CharField(max_length=100, blank=True, null=True) + role_ar = models.CharField(max_length=100, blank=True, null=True) + photo = models.ImageField(upload_to='team_members/') + bio_en = models.TextField(null=True, blank=True) + bio_ar = models.TextField(null=True, blank=True) + order = models.IntegerField(default=0) + + class Meta: + ordering = ['order'] + verbose_name_plural = "Team Members" + def __str__(self): + return self.name_en or "Team Member" + + + + +# models.py +class VisitorLog(models.Model): + ip_address = models.GenericIPAddressField() + country = models.CharField(max_length=100, null=True, blank=True) + region = models.CharField(max_length=100, null=True, blank=True) # State/Province + city = models.CharField(max_length=100, null=True, blank=True) + timestamp = models.DateTimeField(auto_now_add=True) + class Meta: + ordering = ['-timestamp'] + verbose_name_plural = "Visitor Logs" + def __str__(self): + return f"{self.ip_address} from {self.city}, {self.country}" \ No newline at end of file diff --git a/landing_page/tasks.py b/landing_page/tasks.py new file mode 100644 index 0000000..4603272 --- /dev/null +++ b/landing_page/tasks.py @@ -0,0 +1,33 @@ +# your_app/services.py +from django.contrib.gis.geoip2 import GeoIP2 +from .models import VisitorLog + +def log_visitor_location(ip_address): + # if ip_address in ['127.0.0.1', 'localhost']: + # return + if ip_address.startswith(('127.', '192.168.', '10.', '172.16.')): + VisitorLog.objects.create( + ip_address=ip_address, + country="Local Network", + city="Development Machine", + region="Home" + ) + print(f"DEBUG: Logged local IP {ip_address}") + return + + try: + g = GeoIP2() + loc = g.city(ip_address) + VisitorLog.objects.create( + ip_address=ip_address, + country=loc.get('country_name'), + region=loc.get('region'), + city=loc.get('city') + ) + except Exception: + # If the IP is not in the database (like your WiFi IP) + VisitorLog.objects.create( + ip_address=ip_address, + country="Anonymous/Private", + city="N/A" + ) \ No newline at end of file diff --git a/landing_page/tests.py b/landing_page/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/landing_page/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/landing_page/urls.py b/landing_page/urls.py new file mode 100644 index 0000000..d9ad700 --- /dev/null +++ b/landing_page/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.home, name='home'), + path('contact/submit/', views.submit_inquiry, name='submit_inquiry'), +] \ No newline at end of file diff --git a/landing_page/views.py b/landing_page/views.py new file mode 100644 index 0000000..58cdde1 --- /dev/null +++ b/landing_page/views.py @@ -0,0 +1,35 @@ +from django.shortcuts import render +from django.http import JsonResponse +from .models import ( + LandingPageSettings, Partners, Expertise, + Product, Testimonial, TeamMember, Inquiry +) + +def home(request): + # 1. Get current language + current_lang = request.LANGUAGE_CODE or 'en' + + # 2. Fetch data + context = { + 'settings': LandingPageSettings.objects.first(), + 'partners': Partners.objects.all(), + 'expertise': Expertise.objects.all(), + 'products': Product.objects.all(), + 'testimonials': Testimonial.objects.all(), + 'team': TeamMember.objects.all(), + 'current_lang': current_lang, + } + return render(request, 'index.html', context) + +def submit_inquiry(request): + if request.method == 'POST': + try: + Inquiry.objects.create( + name=request.POST.get('name'), + email=request.POST.get('email'), + message=request.POST.get('message') + ) + return JsonResponse({'status': 'success', 'message': 'Message sent successfully!'}) + except Exception as e: + return JsonResponse({'status': 'error', 'message': str(e)}, status=400) + return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) \ No newline at end of file diff --git a/locale/ar/LC_MESSAGES/django.mo b/locale/ar/LC_MESSAGES/django.mo new file mode 100644 index 0000000..d3fcb87 Binary files /dev/null and b/locale/ar/LC_MESSAGES/django.mo differ diff --git a/locale/ar/LC_MESSAGES/django.po b/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 0000000..700d573 --- /dev/null +++ b/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,199 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-12-28 22:58+0000\n" +"PO-Revision-Date: 2025-12-29 02:00+0300\n" +"Last-Translator: Gemini\n" +"Language-Team: Arabic\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: templates/index.html:7 +msgid "AI Product and Services" +msgstr "منتجات وخدمات الذكاء الاصطناعي" + +#: templates/index.html:279 templates/index.html:326 +msgid "Services" +msgstr "خدماتنا" + +#: templates/index.html:280 templates/index.html:327 +msgid "Team" +msgstr "الفريق" + +#: templates/index.html:281 templates/index.html:328 templates/index.html:681 +msgid "Products" +msgstr "المنتجات" + +#: templates/index.html:282 templates/index.html:329 +msgid "Testimonials" +msgstr "آراء العملاء" + +#: templates/index.html:296 +msgid "Arabic" +msgstr "العربية" + +#: templates/index.html:309 +msgid "English" +msgstr "الإنجليزية" + +#: templates/index.html:316 +msgid "Get Started" +msgstr "ابدأ الآن" + +#: templates/index.html:330 +msgid "Contact" +msgstr "اتصل بنا" + +#: templates/index.html:336 +msgid "Digital Transformation Partner" +msgstr "شريك التحول الرقمي" + +#: templates/index.html:344 +msgid "Our Services" +msgstr "خدماتنا" + +#: templates/index.html:345 +msgid "Contact Sales" +msgstr "اتصل بالمبيعات" + +#: templates/index.html:392 +msgid "Our Expertise" +msgstr "خبراتنا" + +#: templates/index.html:393 +msgid "Engineering Services" +msgstr "الخدمات الهندسية" + +#: templates/index.html:419 +msgid "No expertise listed yet." +msgstr "لا توجد خبرات مدرجة بعد." + +#: templates/index.html:430 +msgid "Software Products" +msgstr "المنتجات البرمجية" + +#: templates/index.html:431 +msgid "Proprietary Solutions" +msgstr "حلول مملوكة وحصرية" + +#: templates/index.html:454 +msgid "Enterprise Solution" +msgstr "حلول المؤسسات" + +#: templates/index.html:468 templates/index.html:470 +msgid "Request Demo" +msgstr "طلب عرض توضيحي" + +#: templates/index.html:501 +msgid "No products available." +msgstr "لا توجد منتجات متاحة حالياً." + +#: templates/index.html:513 +msgid "Leadership" +msgstr "القيادة" + +#: templates/index.html:514 +msgid "Meet Team" +msgstr "تعرف على الفريق" + +#: templates/index.html:555 +msgid "Success Stories" +msgstr "قصص النجاح" + +#: templates/index.html:556 +msgid "Trusted by Leaders" +msgstr "موثوقون من قبل قادة الصناعة" + +#: templates/index.html:595 +msgid "Ready to build?" +msgstr "هل أنت مستعد للتنفيذ؟" + +#: templates/index.html:596 +msgid "" +"Our solutions architects typically respond to project inquiries within 24 " +"business hours." +msgstr "عادة ما يقوم مهندسو الحلول لدينا بالرد على استفسارات المشاريع في غضون 24 ساعة عمل." + +#: templates/index.html:602 +msgid "Address" +msgstr "العنوان" + +#: templates/index.html:611 +msgid "Call Us" +msgstr "اتصل بنا" + +#: templates/index.html:620 +msgid "Email Us" +msgstr "راسلنا بريدياً" + +#: templates/index.html:628 +msgid "Working Hours" +msgstr "ساعات العمل" + +#: templates/index.html:629 +msgid "Sunday - Thursday" +msgstr "الأحد - الخميس" + +#: templates/index.html:639 +msgid "Full Name" +msgstr "الاسم الكامل" + +#: templates/index.html:640 +msgid "Your full name" +msgstr "اسمك الكامل" + +#: templates/index.html:641 +msgid "Work Email" +msgstr "بريد العمل الإلكتروني" + +#: templates/index.html:643 +msgid "Message" +msgstr "الرسالة" + +#: templates/index.html:644 +msgid "Tell us about your project..." +msgstr "أخبرنا عن مشروعك..." + +#: templates/index.html:645 +msgid "Send Message" +msgstr "إرسال الرسالة" + +#: templates/index.html:654 +msgid "Message sent successfully!" +msgstr "تم إرسال الرسالة بنجاح!" + +#: templates/index.html:662 +msgid "" +"Empowering global enterprises through elite engineering and AI products." +msgstr "تمكين الشركات العالمية من خلال هندسة متميزة ومنتجات الذكاء الاصطناعي." + +#: templates/index.html:665 +msgid "Expertise" +msgstr "الخبرات" + +#: templates/index.html:695 +msgid "Company" +msgstr "الشركة" + +#: templates/index.html:697 +msgid "About Us" +msgstr "من نحن" + +#: templates/index.html:743 +msgid "Built with Engineering Excellence" +msgstr "صُمم بامتياز هندسي" + +#: templates/index.html:872 +msgid "Sending..." +msgstr "جاري الإرسال..." \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..7873b74 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tenhal.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6b71e46 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +aiohappyeyeballs==2.6.1 +aiohttp==3.13.2 +aiosignal==1.4.0 +asgiref==3.11.0 +attrs==25.4.0 +certifi==2025.11.12 +charset-normalizer==3.4.4 +Django==6.0 +django-admin-rangefilter==0.13.5 +django-extensions==4.1 +django-picklefield==3.4.0 +django-q2==1.9.0 +frozenlist==1.8.0 +geoip2==5.2.0 +gunicorn==23.0.0 +idna==3.11 +maxminddb==3.0.0 +multidict==6.7.0 +packaging==25.0 +pillow==12.0.0 +propcache==0.4.1 +requests==2.32.5 +setproctitle==1.3.7 +sqlparse==0.5.5 +typing_extensions==4.15.0 +urllib3==2.6.2 +yarl==1.22.0 diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..1a4e1ad --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,244 @@ + + /* --- CSS STYLES --- */ + :root { + --background: #ffffff; + --foreground: #09090b; + --card: #ffffff; + --primary: #18181b; + --primary-foreground: #fafafa; + --secondary: #f4f4f5; + --secondary-foreground: #18181b; + --muted: #f4f4f5; + --muted-foreground: #71717a; + --border: #e4e4e7; + --input: #e4e4e7; + --radius: 0.5rem; + --section-spacing: clamp(60px, 10vw, 120px); + --container-max: 1200px; + --gutter: 20px; + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --font-arabic: "Cairo", ui-sans-serif, system-ui, sans-serif; + } + + /* Apply Arabic font if language is Arabic */ + html[lang="ar"] { --font-sans: "Cairo", ui-sans-serif, system-ui, sans-serif; } + + * { margin: 0; padding: 0; box-sizing: border-box; } + html { scroll-behavior: smooth; } + body { font-family: var(--font-sans); -webkit-font-smoothing: antialiased; color: var(--foreground); line-height: 1.6; overflow-x: hidden; letter-spacing: -0.01em; } + + /* Directionality helper */ + + .container { max-width: var(--container-max); margin: 0 auto; padding: 0 var(--gutter); } + a { text-decoration: none; color: inherit; transition: 0.2s; } + ul { list-style: none; } + + /* Typography */ + h1, h2, h3 { letter-spacing: -0.025em; line-height: 1.1; margin-bottom: 1rem; } + h1 { font-size: clamp(2.25rem, 8vw, 4rem); } + h2 { font-size: clamp(1.75rem, 5vw, 2.5rem); } + h3 { font-size: 1.25rem; font-weight: 600; } + p { color: var(--muted-foreground); } + .lead { font-size: clamp(1.1rem, 3vw, 1.25rem); } + + /* Navigation */ + header { position: fixed; top: 0; left: 0; right: 0; height: 6rem; background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); z-index: 1000; display: flex; align-items: center; } + .nav-inner { display: flex; justify-content: space-between; align-items: center; width: 100%; } + .logo { font-weight: 800; font-size: 1.25rem; letter-spacing: -0.05em; display: flex; align-items: center; gap: 0.5rem; text-decoration: none; color: inherit;} + .logo img { height: 70px; width: auto; } + .nav-links { display: flex; gap: 1.5rem; } + .nav-links a { font-size: 0.875rem; font-weight: 500; color: var(--muted-foreground); } + .nav-links a:hover { color: var(--foreground); } + .mobile-toggle { display: none; background: none; border: none; cursor: pointer; color: var(--foreground); } + + /* Language Switcher */ + .lang-switch { + display: flex; gap: 0.5rem; align-items: center; font-size: 0.8rem; font-weight: 600; + margin: 0 1rem; cursor: pointer; border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; + } + .lang-switch:hover { background: var(--secondary); } + + /* Mobile Overlay */ + .nav-overlay { position: fixed; top: 6rem; left: 0; width: 100%; height: 0; background: var(--background); overflow: hidden; transition: 0.3s cubic-bezier(0.16, 1, 0.3, 1); z-index: 999; display: flex; flex-direction: column; padding: 0 1.5rem; } + .nav-overlay.open { height: calc(100vh - 6rem); padding-top: 2rem; } + .nav-overlay a { font-size: 1.5rem; font-weight: 600; padding: 1.25rem 0; border-bottom: 1px solid var(--border); } + + /* Hero & Visuals */ + .hero { padding: 8rem 0 var(--section-spacing); background: radial-gradient(#e4e4e7 1px, transparent 1px); background-size: 24px 24px; } + .hero-grid { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 3rem; align-items: center; } + .hero-visual { border: 1px solid var(--border); border-radius: var(--radius); height: 320px; background: white; position: relative; overflow: hidden; box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.05); } + .hero-visual-bg { width: 100%; height: 100%; background: linear-gradient(to right, #f4f4f5 1px, transparent 1px), linear-gradient(to bottom, #f4f4f5 1px, transparent 1px); background-size: 20px 20px; } + .hero-img { width: 100%; height: 100%; object-fit: cover; display: block; } + + + /* --- Marquee --- */ + .partners-section { + padding: 2.5rem 0; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + overflow: hidden; + + } + + .marquee-track { + display: flex; + gap: 3rem; /* Reduced from 6rem to 3rem for tighter look with fewer items */ + width: max-content; + /* ADJUSTMENT 1: Added padding to ensure track has width even with few items */ + padding: 0 1rem; + /* ADJUSTMENT 2: Increased animation duration from 40s to 80s for smoother speed */ + animation: marquee 120s linear infinite; + } + + .partner-logo { + height: 40px; + width: auto; + object-fit: contain; + filter: grayscale(100%); + opacity: 0.5; + } + + /* Keep existing text style for names without logos */ + .partner-item { + font-weight: 700; + color: #d4d4d8; + font-size: 1.1rem; + /* ADJUSTMENT 3: Ensure text doesn't break or wrap */ + white-space: nowrap; + /* Flexbox to align text vertically with logos if they appear mixed */ + display: flex; + align-items: center; + /* Give text elements some space if mixed with logos */ + padding: 0 0.5rem; + } + + @keyframes marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } + } + + + + /* Cards & Badge */ + .badge { display: inline-block; padding: 0.25rem 0.75rem; background: var(--secondary); border-radius: 99px; font-size: 0.75rem; font-weight: 600; margin-bottom: 1.5rem; } + + /* Carousel Styles (Shared) */ + .section-header { display: flex; justify-content: space-between; align-items: end; margin-bottom: 2rem; } + .carousel-container { position: relative; } + + .carousel-track { + display: flex; + gap: 1.5rem; + overflow-x: auto; + scroll-behavior: smooth; + padding-bottom: 1rem; + -ms-overflow-style: none; + scrollbar-width: none; + scroll-snap-type: x mandatory; + } + .carousel-track::-webkit-scrollbar { display: none; } + + .carousel-item { flex: 0 0 auto; scroll-snap-align: start; width: 100%; } + + @media (min-width: 768px) { + .services-track .carousel-item { width: calc(33.333% - 1rem); } + .testimonials-track .carousel-item { width: calc(50% - 0.75rem); } + .team-track .carousel-item { width: calc(25% - 1.125rem); } + } + + .card { padding: 2rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); transition: transform 0.2s, box-shadow 0.2s; height: 100%; display: flex; flex-direction: column; } + .card:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(0,0,0,0.05); } + .card-icon { width: 3rem; height: 3rem; background: var(--secondary); border-radius: var(--radius); display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; color: var(--foreground); } + + /* Testimonial Specifics */ + .testimonial-img { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; border: 2px solid var(--secondary); margin: 0 auto 1.5rem auto; display: block; } + .testimonial-text { font-size: 1.125rem; font-weight: 500; color: var(--foreground); margin-bottom: auto; text-align: center; } + .testimonial-author { text-align: center; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--secondary); } + .testimonial-author-name { font-size: 0.9rem; font-weight: 700; color: var(--foreground); display: block; margin-bottom: 0.25rem; } + .testimonial-author-role { font-size: 0.8rem; color: var(--muted-foreground); } + + /* Team Specifics */ + .team-card { text-align: center; align-items: center; justify-content: center; } + .team-card:hover { border-color: var(--foreground); } + .team-img-wrapper { width: 96px; height: 96px; margin-bottom: 1.25rem; border-radius: 50%; background: var(--muted); overflow: hidden; flex-shrink: 0; } + .team-img-wrapper img { width: 100%; height: 100%; object-fit: cover; } + + /* Carousel Controls */ + .carousel-controls { display: flex; gap: 0.5rem; } + .slider-btn { + background: white; border: 1px solid var(--border); color: var(--foreground); + width: 40px; height: 40px; border-radius: 50%; cursor: pointer; + display: flex; align-items: center; justify-content: center; transition: 0.2s; flex-shrink: 0; + } + .slider-btn:hover { background: var(--secondary); border-color: var(--foreground); } + + /* Product Section (Dark) */ + .product-section { background: #18181b; color: white; padding: var(--section-spacing) 0; position: relative; } + + .product-slider-wrapper { position: relative; width: 100%; overflow: hidden; } + + .product-slide { + display: grid; grid-template-columns: 1fr 1fr; gap: 4rem; align-items: center; + opacity: 0; position: absolute; top: 0; left: 0; width: 100%; + transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1); + transform: translateY(30px); z-index: 0; pointer-events: none; visibility: hidden; + } + .product-slide.active { opacity: 1; position: relative; transform: translateY(0); z-index: 10; pointer-events: all; visibility: visible; } + + .product-ui-mockup { + background: #27272a; border: 1px solid #3f3f46; border-radius: var(--radius); aspect-ratio: 16/10; padding: 1.5rem; + display: flex; flex-direction: column; gap: 1rem; box-shadow: 0 20px 50px -10px rgba(0,0,0,0.5); + } + + @keyframes popIn { 0% { transform: scale(0.95); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } + .product-slide.active .product-ui-mockup { animation: popIn 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards; } + + .ui-block { background: #3f3f46; border-radius: 4px; } + .feature-list { list-style: none; margin-top: 1.5rem; } + .feature-list li { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; color: #d4d4d8; } + + /* Product Controls */ + .slider-controls { display: flex; justify-content: flex-end; align-items: center; gap: 1rem; margin-bottom: 2rem; } + .slider-controls .slider-btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; } + .slider-controls .slider-btn:hover { background: white; color: #18181b; } + .slider-indicators { display: flex; gap: 0.5rem; } + .indicator { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.2); cursor: pointer; transition: 0.2s; } + .indicator.active { background: white; width: 20px; border-radius: 4px; } + + /* Form Controls */ + .contact-wrapper { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: white; } + .contact-info { background: var(--secondary); padding: 3.5rem; } + .contact-form-box { padding: 3.5rem; } + .form-label { display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem; } + .form-control { width: 100%; padding: 0.75rem; border: 1px solid var(--input); border-radius: var(--radius); margin-bottom: 1.25rem; font-family: inherit; transition: 0.2s; } + .form-control:focus { outline: none; border-color: var(--foreground); } + textarea.form-control { min-height: 120px; resize: vertical; } + .btn { display: inline-flex; align-items: center; justify-content: center; padding: 0 1.5rem; height: 3rem; font-size: 0.875rem; font-weight: 500; border-radius: var(--radius); cursor: pointer; border: 1px solid transparent; transition: 0.2s; } + .btn-primary { background: var(--primary); color: var(--primary-foreground); } + .btn-primary:hover { opacity: 0.9; } + .btn-outline { border: 1px solid var(--border); background: transparent; } + .btn-outline:hover { background: var(--secondary); } + .btn-lg { height: 3.5rem; padding: 0 2rem; font-size: 1rem; } + + /* Reveal Animation */ + .reveal { opacity: 0; transform: translateY(20px); transition: 0.8s cubic-bezier(0.16, 1, 0.3, 1); } + .reveal.active { opacity: 1; transform: translateY(0); } + + /* Footer */ + footer { padding: 5rem 0 2.5rem; border-top: 1px solid var(--border); } + .footer-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 3rem; margin-bottom: 4rem; } + .footer-links li { margin-bottom: 0.75rem; font-size: 0.875rem; color: var(--muted-foreground); cursor: pointer; transition: 0.2s;} + .footer-links li:hover { color: var(--foreground); } + + @media (max-width: 1024px) { + .hero-grid, .product-slide, .contact-wrapper, .footer-grid { grid-template-columns: 1fr; } + .nav-links, .nav-cta { display: none; } + .mobile-toggle { display: block; } + .product-slide { gap: 2rem; text-align: center; } + .feature-list li { justify-content: center; } + .slider-controls { justify-content: center; } + .contact-info, .contact-form-box { padding: 2rem; } + .carousel-item { width: 85%; } + .section-header { flex-direction: column; align-items: flex-start; gap: 1rem; } + } + \ No newline at end of file diff --git a/static/images/android-chrome-192x192.png b/static/images/android-chrome-192x192.png new file mode 100644 index 0000000..4a0af6a Binary files /dev/null and b/static/images/android-chrome-192x192.png differ diff --git a/static/images/android-chrome-512x512.png b/static/images/android-chrome-512x512.png new file mode 100644 index 0000000..4bae178 Binary files /dev/null and b/static/images/android-chrome-512x512.png differ diff --git a/static/images/apple-touch-icon.png b/static/images/apple-touch-icon.png new file mode 100644 index 0000000..80678fc Binary files /dev/null and b/static/images/apple-touch-icon.png differ diff --git a/static/images/favicon-16x16.png b/static/images/favicon-16x16.png new file mode 100644 index 0000000..418be08 Binary files /dev/null and b/static/images/favicon-16x16.png differ diff --git a/static/images/favicon-32x32.png b/static/images/favicon-32x32.png new file mode 100644 index 0000000..c368791 Binary files /dev/null and b/static/images/favicon-32x32.png differ diff --git a/static/images/favicon.ico b/static/images/favicon.ico new file mode 100644 index 0000000..c92f1de Binary files /dev/null and b/static/images/favicon.ico differ diff --git a/static/images/site.webmanifest b/static/images/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/static/images/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..7ceba91 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,182 @@ +// --- Toggle Mobile Menu --- + // --- Mobile Navigation --- + function toggleMenu() { + document.getElementById('mobile-nav').classList.toggle('open'); + } + + // --- Intersection Observer for Scroll Animations --- + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) entry.target.classList.add('active'); + }); + }, { threshold: 0.1 }); + + document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); + + // --- Generic Horizontal Scroll Logic (Services, Team, Testimonials) --- + function scrollCarousel(trackId, direction) { + const track = document.getElementById(trackId); + if (!track) return; + const firstItem = track.querySelector('.carousel-item'); + if (!firstItem) return; + const itemWidth = firstItem.offsetWidth; + const gap = 24; + const scrollAmount = itemWidth + gap; + + // Adjust direction for RTL + let scrollDir = direction; + if (document.dir === 'rtl') { + scrollDir = direction * -1; + } + + if (scrollDir === -1) { + track.scrollBy({ left: -scrollAmount, behavior: 'smooth' }); + } else { + track.scrollBy({ left: scrollAmount, behavior: 'smooth' }); + } + } + + // --- Product Slider Logic (Fade) --- + const slides = document.querySelectorAll('.product-slide'); + const indicators = document.querySelectorAll('.indicator'); + const prevBtn = document.getElementById('prev-btn'); + const nextBtn = document.getElementById('next-btn'); + const productWrapper = document.querySelector('.product-slider-wrapper'); + + let currentSlide = 0; + const totalSlides = slides.length; + let slideInterval; + + if(totalSlides > 0) { + function showSlide(index) { + if (index >= totalSlides) currentSlide = 0; + else if (index < 0) currentSlide = totalSlides - 1; + else currentSlide = index; + + slides.forEach(slide => slide.classList.remove('active')); + indicators.forEach(ind => ind.classList.remove('active')); + + slides[currentSlide].classList.add('active'); + indicators[currentSlide].classList.add('active'); + } + + function nextSlide() { + showSlide(currentSlide + 1); + } + + function prevSlide() { + showSlide(currentSlide - 1); + } + + if(nextBtn) { + nextBtn.addEventListener('click', () => { + nextSlide(); + resetTimer(); + }); + } + + if(prevBtn) { + prevBtn.addEventListener('click', () => { + prevSlide(); + resetTimer(); + }); + } + + indicators.forEach((ind, index) => { + ind.addEventListener('click', () => { + showSlide(index); + resetTimer(); + }); + }); + + function startTimer() { + slideInterval = setInterval(nextSlide, 5000); + } + + function resetTimer() { + clearInterval(slideInterval); + startTimer(); + } + + if(productWrapper) { + productWrapper.addEventListener('mouseenter', () => clearInterval(slideInterval)); + productWrapper.addEventListener('mouseleave', startTimer); + startTimer(); + } + } + + // --- Form Submission (AJAX) --- + function showToast(message) { + const toast = document.getElementById('toast'); + toast.textContent = message; + toast.style.transform = 'translateY(0)'; + + setTimeout(() => { + toast.style.transform = 'translateY(150%)'; + }, 3000); + } + + document.getElementById('contactForm').addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(this); + const submitBtn = this.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + submitBtn.textContent = '{% trans "Sending..." %}'; + submitBtn.disabled = true; + + fetch("{% url 'submit_inquiry' %}", { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.json()) + .then(data => { + if(data.status === 'success') { + showToast(data.message); + document.getElementById('contactForm').reset(); + } else { + showToast('Error: ' + (data.message || 'Something went wrong')); + } + }) + .catch(error => { + showToast('Error: Could not submit form.'); + console.error('Error:', error); + }) + .finally(() => { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + }); + }); + + // --- Marquee Width Fix --- + // Ensures the partner list is always wide enough to scroll smoothly + function adjustMarqueeWidth() { + const track = document.getElementById('marquee-track'); + if (!track) return; + + // Get the rendered width of the single list + const currentWidth = track.scrollWidth; + // Target width: We want the track to be at least 200% of screen width + // so it has enough room to scroll off-screen before resetting. + const targetWidth = window.innerWidth * 2.0; + + // Calculate how many times we need to repeat the list to reach target width + const repeats = Math.max(2, Math.ceil(targetWidth / currentWidth)); + + // Get the raw HTML content (the first run of the loop) + const originalContent = track.innerHTML; + + // Rebuild the track with the repeated content + track.innerHTML = ''; + for (let i = 0; i < repeats; i++) { + track.innerHTML += originalContent; + } + } + + // Run on load and resize + window.addEventListener('load', adjustMarqueeWidth); + window.addEventListener('resize', adjustMarqueeWidth); + \ No newline at end of file diff --git a/templates/admin/visitor_log_changelist.html b/templates/admin/visitor_log_changelist.html new file mode 100644 index 0000000..39a5a41 --- /dev/null +++ b/templates/admin/visitor_log_changelist.html @@ -0,0 +1,43 @@ +{% extends "admin/change_list.html" %} + +{% block content %} +
+
+

Top Countries (Filtered)

+ +
+ +
+

Top Cities (Filtered)

+ +
+
+ +{{ block.super }} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..47fe4a1 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,507 @@ +{% load static i18n %} + + + + + + {% if settings %}{{ settings.company_name|default:"Tenhal" }}{% else %}Tenhal{% endif %} | {% trans 'AI Product and Services' %} + + + + + + + + + + + + + + + +
+ +
+ + + +
+
+
+
{% trans "Digital Transformation Partner" %}
+

+ {% if current_lang == 'ar' %}تمكين المستقبل الرقمي للمملكة{% else %}Empowering the Kingdom’s Digital Future.{% endif %} +

+

+ {% if current_lang == 'ar' %}نحن نقدم خبرة هندسية عالمية وحلول ذكاء اصطناعي محلية لمساعدة الشركات السعودية على النمو والابتكار.{% else %}We deliver world-class engineering expertise and localized AI solutions to help Saudi enterprises scale, innovate, and lead the next era of technology.{% endif %} +

+ +
+
+
+
+
+
+
+
+
+
+ {% if settings and settings.hero_image %} + Tenhal Software Platform + {% else %} + Placeholder + {% endif %} +
+
+
+
+
+ +
+ +
+ {% for partner in partners %} + {% if partner.logo %} + + {% else %} + {% if current_lang == 'ar' %}{{ partner.name_ar }}{% else %}{{ partner.name_en }}{% endif %} + {% endif %} + {% empty %} + KAAUHHammadi Hospital + {% endfor %} + + + +
+
+ + +
+
+
+
+
+
{% trans "Our Expertise" %}
+

{% trans "Engineering Services" %}

+
+ +
+
+ + +
+
+ + +
+
+
+
{% trans "Software Products" %}
+

{% trans "Proprietary Solutions" %}

+
+ +
+
+ {% for product in products %} +
+ {% endfor %} +
+
+ + +
+ +
+ {% for product in products %} +
+
+

{% if current_lang == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}

+
{% trans "Enterprise Solution" %}
+

{% if current_lang == 'ar' %}{{ product.description_ar }}{% else %}{{ product.description_en }}{% endif %}

+
    + {% if current_lang == 'ar' %} + {% for feature in product.features_ar %} +
  • {{ feature }}
  • + {% endfor %} + {% else %} + {% for feature in product.features_en %} +
  • {{ feature }}
  • + {% endfor %} + {% endif %} +
+ {% if product.link %} + {% trans "Request Demo" %} + {% else %} + {% trans "Request Demo" %} + {% endif %} +
+ + {% if product.link %} + +
+
+
+
+
+ {% if product.image %} +
+
+
+
+
+
+ Mockup +
+
+
+
+ {% endif%} +
+
+ + {% endif%} + +
+ {% empty %} +
{% trans "No products available." %}
+ {% endfor %} +
+
+
+ + +
+
+
+
+
+
{% trans "Leadership" %}
+

{% trans "Meet Team" %}

+
+ +
+
+ + +
+
+ + +
+
+
+
+
+
{% trans "Success Stories" %}
+

{% trans "Trusted by Leaders" %}

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

{% trans "Ready to build?" %}

+

{% trans "Our solutions architects typically respond to project inquiries within 24 business hours." %}

+ +
+ {% if settings %} + {% if settings.company_address_en %} +
+
{% trans "Address" %}
+
+ {% if current_lang == 'ar' %}{{ settings.company_address_ar }}{% else %}{{ settings.company_address_en }}{% endif %} +
+
+ {% endif %} + + {% if settings.company_phone %} +
+
{% trans "Call Us" %}
+ {% for phone in settings.company_phone %} +
{{ phone }}
+ {% endfor %} +
+ {% endif %} + + {% if settings.company_email %} +
+
{% trans "Email Us" %}
+ {% for email in settings.company_email %} +
{{ email }}
+ {% endfor %} +
+ {% endif %} + +
+
{% trans "Working Hours" %}
+
{% trans "Sunday - Thursday" %}
+
9:00AM - 05:00PM
+
+ {% endif %} +
+
+ +
+
+ {% csrf_token %} + + + + + + + +
+
+
+
+
+ + +
+ {% trans "Message sent successfully!" %} +
+ +
+
+ +
+

© {% now "Y" %} tenhal.sa

+

{% trans "Built with Engineering Excellence" %}

+
+
+
+ + + + \ No newline at end of file diff --git a/tenhal/__init__.py b/tenhal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tenhal/asgi.py b/tenhal/asgi.py new file mode 100644 index 0000000..e377ce5 --- /dev/null +++ b/tenhal/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for tenhal project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tenhal.settings') + +application = get_asgi_application() diff --git a/tenhal/settings.py b/tenhal/settings.py new file mode 100644 index 0000000..13cfd2e --- /dev/null +++ b/tenhal/settings.py @@ -0,0 +1,203 @@ +""" +Django settings for tenhal project. + +Generated by 'django-admin startproject' using Django 6.0. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +from pathlib import Path +import environ +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +env = environ.Env( + # set casting, default value + DEBUG=(bool, False) +) +environ.Env.read_env(os.path.join(BASE_DIR, '.env')) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env('DEBUG') +# ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rangefilter', + 'landing_page', + 'django_extensions', + 'django_q', + +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'landing_page.middleware.AnalyticsMiddleware', # Custom Analytics Middleware +] + +ROOT_URLCONF = 'tenhal.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'tenhal.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': BASE_DIR / 'db.sqlite3', +# } +# } + +# settings.py + +# 1. Database Optimization (Django 5.1+) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + 'OPTIONS': { + 'timeout': 20, # Wait 20s for locks + 'transaction_mode': 'IMMEDIATE', # Prevents deadlocks + 'init_command': ( + 'PRAGMA journal_mode=WAL;' # Read while writing + 'PRAGMA synchronous=NORMAL;' # Fast writes + 'PRAGMA mmap_size=134217728;' # 128MB Memory mapping for speed + 'PRAGMA cache_size=2000;' # Increase page cache + ), + }, + } +} + +# 2. Django-Q2 Settings +Q_CLUSTER = { + 'name': 'LandingPageQueue', + 'workers': 2, # Keep workers low for SQLite (1-2 is best) + 'recycle': 500, # Restart workers occasionally to free memory + 'timeout': 60, # Kill task if it hangs + 'retry': 120, # Retry task after 2 mins if it failed + 'orm': 'default', # USE SQLITE AS THE QUEUE + 'save_limit': 100, # Only keep last 100 successful tasks (Prevents DB bloat) + 'ack_failures': True, # Remove failed tasks from queue after retry + 'poll': 0.5, # How often to check for new tasks (seconds) +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = 'static/' + + +STATICFILES_DIRS = [ + BASE_DIR / 'static', +] + +# The absolute path to the directory where collectstatic will gather files for deployment +STATIC_ROOT = BASE_DIR / 'staticfiles' + + +MEDIA_URL = '/media/' # The URL prefix for user-uploaded media files +MEDIA_ROOT = BASE_DIR / 'media' + + + +# 3. Define Languages (English and Arabic) +LANGUAGE_CODE = 'en' # Default language +LANGUAGES = [ + ('en', 'English'), + ('ar', 'Arabic'), +] + +# 4. Use cookies to remember language choice +LANGUAGE_COOKIE_NAME = 'django_language' + +LOCALE_PATHS = [ + BASE_DIR / 'locale' +] + +# Path to the directory containing GeoLite2-City.mmdb and GeoLite2-Country.mmdb +GEOIP_PATH = BASE_DIR / 'geoip' + + + +# 3. Security Headers (Required for 10k visitors) +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True \ No newline at end of file diff --git a/tenhal/urls.py b/tenhal/urls.py new file mode 100644 index 0000000..23a59c4 --- /dev/null +++ b/tenhal/urls.py @@ -0,0 +1,21 @@ +from django.contrib import admin +from django.urls import path, include +# Import this for /en/ and /ar/ prefixes +from django.conf.urls.i18n import i18n_patterns +# Import the built-in view +from django.views.i18n import set_language +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), +] + i18n_patterns( + path('', include('landing_page.urls')), # Replace 'tenhal' with your app name + + # Add this URL for the form to work + path('set_language/', set_language, name='set_language'), +) + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/tenhal/wsgi.py b/tenhal/wsgi.py new file mode 100644 index 0000000..378644b --- /dev/null +++ b/tenhal/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for tenhal project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tenhal.settings') + +application = get_wsgi_application()