tenhal landing page added

This commit is contained in:
Faheed 2025-12-29 04:50:49 +03:00
commit 8a842d364d
35 changed files with 2044 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -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

0
landing_page/__init__.py Normal file
View File

85
landing_page/admin.py Normal file
View File

@ -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)

5
landing_page/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class LandingPageConfig(AppConfig):
name = 'landing_page'

View File

@ -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)

View File

@ -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'],
},
),
]

View File

@ -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. ["<email1>", "<email2>"]'),
),
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. ["<phone1>", "<phone2>"]'),
),
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"]'),
),
]

View File

@ -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/'),
),
]

View File

@ -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'],
},
),
]

View File

120
landing_page/models.py Normal file
View File

@ -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. [\"<email1>\", \"<email2>\"]",default=list)
company_phone = models.JSONField(help_text="Save as a list of phone numbers as JSON i.e. [\"<phone1>\", \"<phone2>\"]",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}"

33
landing_page/tasks.py Normal file
View File

@ -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"
)

3
landing_page/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

7
landing_page/urls.py Normal file
View File

@ -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'),
]

35
landing_page/views.py Normal file
View File

@ -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)

Binary file not shown.

View File

@ -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 <EMAIL@ADDRESS>, 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 "جاري الإرسال..."

22
manage.py Executable file
View File

@ -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()

27
requirements.txt Normal file
View File

@ -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

244
static/css/main.css Normal file
View File

@ -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; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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"}

182
static/js/main.js Normal file
View File

@ -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);

View File

@ -0,0 +1,43 @@
{% extends "admin/change_list.html" %}
{% block content %}
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px;">
<div style="flex: 1; min-width: 400px; background: white; padding: 15px; border: 1px solid #ccc; border-radius: 4px;">
<h3 style="margin-top:0">Top Countries (Filtered)</h3>
<canvas id="countryChart" height="150"></canvas>
</div>
<div style="flex: 1; min-width: 400px; background: white; padding: 15px; border: 1px solid #ccc; border-radius: 4px;">
<h3 style="margin-top:0">Top Cities (Filtered)</h3>
<canvas id="cityChart" height="150"></canvas>
</div>
</div>
{{ block.super }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const countryRaw = JSON.parse('{{ country_data|safe }}');
const cityRaw = JSON.parse('{{ city_data|safe }}');
const setupChart = (canvasId, data, label, color) => {
new Chart(document.getElementById(canvasId), {
type: 'bar',
data: {
labels: data.map(i => i.country || i.city || 'Unknown'),
datasets: [{
label: label,
data: data.map(i => i.total),
backgroundColor: color
}]
},
options: { indexAxis: 'y', responsive: true }
});
};
setupChart('countryChart', countryRaw, 'Visitors', '#3498db');
setupChart('cityChart', cityRaw, 'Visitors', '#e67e22');
});
</script>
{% endblock %}

507
templates/index.html Normal file
View File

@ -0,0 +1,507 @@
{% load static i18n %}
<!DOCTYPE html>
<html lang="{{ current_lang }}" dir="{% if current_lang == 'ar' %}rtl{% else %}ltr{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if settings %}{{ settings.company_name|default:"Tenhal" }}{% else %}Tenhal{% endif %} | {% trans 'AI Product and Services' %}</title>
<!-- Fonts: Cairo for Arabic, Inter for English -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700;800&family=Inter:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<!-- Favicon & Touch Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'images/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'images/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'images/favicon-16x16.png' %}">
<link rel="manifest" href="{% static 'images/site.webmanifest' %}">
<link rel="stylesheet" href="{% static 'css/main.css' %}">
</head>
<body>
<header>
<div class="container nav-inner">
<a href="#" class="logo">
{% if settings and settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo">
{% else %}
<div style="width:32px; height:32px; background:var(--primary); border-radius:6px; display:flex; align-items:center; justify-content:center; color:white; font-weight:900;">T</div>
{% endif %}
</a>
<div style="display: flex; align-items: center;">
<nav class="nav-links">
<a href="#services">{% trans "Services" %}</a>
<a href="#team">{% trans "Team" %}</a>
<a href="#products">{% trans "Products" %}</a>
<a href="#testimonials">{% trans "Testimonials" %}</a>
</nav>
<!-- LANGUAGE TOGGLE FORM -->
{% if current_lang == 'en' %}
<form action="{% url 'set_language' %}" method="post" style="display:inline;">{% csrf_token %}
<input name="language" type="hidden" value="ar">
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button class="lang-switch" type="submit" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6" height="20" width="20" style="color: inherit;">
<path fill-rule="evenodd" d="M9 2.25a.75.75 0 0 1 .75.75v1.506a49.384 49.384 0 0 1 5.343.371.75.75 0 1 1-.186 1.489c-.66-.083-1.323-.151-1.99-.206a18.67 18.67 0 0 1-2.97 6.323c.318.384.65.753 1 1.107a.75.75 0 0 1-1.07 1.052A18.902 18.902 0 0 1 9 13.687a18.823 18.823 0 0 1-5.656 4.482.75.75 0 0 1-.688-1.333 17.323 17.323 0 0 0 5.396-4.353A18.72 18.72 0 0 1 5.89 8.598a.75.75 0 0 1 1.388-.568A17.21 17.21 0 0 0 9 11.224a17.168 17.168 0 0 0 2.391-5.165 48.04 48.04 0 0 0-8.298.307.75.75 0 0 1-.186-1.489 49.159 49.159 0 0 1 5.343-.371V3A.75.75 0 0 1 9 2.25ZM15.75 9a.75.75 0 0 1 .68.433l5.25 11.25a.75.75 0 1 1-1.36.634l-1.198-2.567h-6.744l-1.198 2.567a.75.75 0 0 1-1.36-.634l5.25-11.25A.75.75 0 0 1 15.75 9Zm-2.672 8.25h5.344l-2.672-5.726-2.672 5.726Z" clip-rule="evenodd" />
</svg>
{% trans "Arabic" %}
</button>
</form>
{% else %}
<form action="{% url 'set_language' %}" method="post" style="display:inline;">{% csrf_token %}
<input name="language" type="hidden" value="en">
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button class="lang-switch" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6" height="20" width="20" style="color: inherit;">
<path fill-rule="evenodd" d="M9 2.25a.75.75 0 0 1 .75.75v1.506a49.384 49.384 0 0 1 5.343.371.75.75 0 1 1-.186 1.489c-.66-.083-1.323-.151-1.99-.206a18.67 18.67 0 0 1-2.97 6.323c.318.384.65.753 1 1.107a.75.75 0 0 1-1.07 1.052A18.902 18.902 0 0 1 9 13.687a18.823 18.823 0 0 1-5.656 4.482.75.75 0 0 1-.688-1.333 17.323 17.323 0 0 0 5.396-4.353A18.72 18.72 0 0 1 5.89 8.598a.75.75 0 0 1 1.388-.568A17.21 17.21 0 0 0 9 11.224a17.168 17.168 0 0 0 2.391-5.165 48.04 48.04 0 0 0-8.298.307.75.75 0 0 1-.186-1.489 49.159 49.159 0 0 1 5.343-.371V3A.75.75 0 0 1 9 2.25ZM15.75 9a.75.75 0 0 1 .68.433l5.25 11.25a.75.75 0 1 1-1.36.634l-1.198-2.567h-6.744l-1.198 2.567a.75.75 0 0 1-1.36-.634l5.25-11.25A.75.75 0 0 1 15.75 9Zm-2.672 8.25h5.344l-2.672-5.726-2.672 5.726Z" clip-rule="evenodd" />
</svg>
{% trans "English" %}
</button>
</form>
{% endif %}
<!-- END LANGUAGE TOGGLE -->
<div class="nav-cta">
<a href="#contact" class="btn btn-primary">{% trans "Get Started" %}</a>
</div>
<button class="mobile-toggle" onclick="toggleMenu()" aria-label="Toggle Menu">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
</button>
</div>
</div>
</header>
<div class="nav-overlay" id="mobile-nav">
<a href="#services" onclick="toggleMenu()">{% trans "Services" %}</a>
<a href="#team" onclick="toggleMenu()">{% trans "Team" %}</a>
<a href="#products" onclick="toggleMenu()">{% trans "Products" %}</a>
<a href="#testimonials" onclick="toggleMenu()">{% trans "Testimonials" %}</a>
<a href="#contact" onclick="toggleMenu()">{% trans "Contact" %}</a>
</div>
<section class="hero">
<div class="container hero-grid">
<div class="reveal">
<div class="badge">{% trans "Digital Transformation Partner" %}</div>
<h1>
{% if current_lang == 'ar' %}تمكين المستقبل الرقمي للمملكة{% else %}Empowering the Kingdoms Digital Future.{% endif %}
</h1>
<p class="lead" style="margin-bottom: 2.5rem;">
{% 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 %}
</p>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<a href="#services" class="btn btn-primary btn-lg">{% trans "Our Services" %}</a>
<a href="#contact" class="btn btn-outline btn-lg">{% trans "Contact Sales" %}</a>
</div>
</div>
<div class="hero-visual reveal">
<div class="hero-visual-bg"></div>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 85%; height: 75%; background: white; border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); overflow: hidden; display: flex; flex-direction: column;">
<div style="height: 30px; background: #fdfdfd; border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 12px; gap: 6px;">
<div style="width: 8px; height: 8px; border-radius: 50%; background: #09090b;"></div>
<div style="width: 8px; height: 8px; border-radius: 50%; background: #09090b;"></div>
<div style="width: 8px; height: 8px; border-radius: 50%; background: #09090b;"></div>
</div>
<div style="flex: 1; position: relative;">
{% if settings and settings.hero_image %}
<img src="{{ settings.hero_image.url }}" alt="Tenhal Software Platform" class="hero-img">
{% else %}
<img src="https://picsum.photos/seed/tech/600/400" alt="Placeholder" class="hero-img">
{% endif %}
</div>
</div>
</div>
</div>
</section>
<div class="partners-section">
<!-- Added ID 'marquee-track' to this div -->
<div id="marquee-track" class="marquee-track">
{% for partner in partners %}
{% if partner.logo %}
<img src="{{ partner.logo.url }}" alt="{{ partner.name_en }}" class="partner-logo">
{% else %}
<span class="partner-item">{% if current_lang == 'ar' %}{{ partner.name_ar }}{% else %}{{ partner.name_en }}{% endif %}</span>
{% endif %}
{% empty %}
<span class="partner-item">KAAUH</span><span class="partner-item">Hammadi Hospital</span>
{% endfor %}
<!-- Remove the manual repeat block below -->
<!-- Javascript will now handle repeating the content to fill the width -->
</div>
</div>
<!-- Services Section -->
<section id="services" style="padding: var(--section-spacing) 0;">
<div class="container">
<div class="reveal">
<div class="section-header">
<div>
<div class="badge">{% trans "Our Expertise" %}</div>
<h2>{% trans "Engineering Services" %}</h2>
</div>
<div class="carousel-controls">
<button class="slider-btn" onclick="scrollCarousel('services-track', -1)" aria-label="Previous Service">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<button class="slider-btn" onclick="scrollCarousel('services-track', 1)" aria-label="Next Service">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
</div>
</div>
<div class="carousel-container reveal">
<div id="services-track" class="carousel-track services-track">
{% for service in expertise %}
<div class="carousel-item">
<div class="card">
<div class="card-icon">
{{ service.icon_svg|safe }}
</div>
<h3>{% if current_lang == 'ar' %}{{ service.title_ar }}{% else %}{{ service.title_en }}{% endif %}</h3>
<p>{% if current_lang == 'ar' %}{{ service.description_ar }}{% else %}{{ service.description_en }}{% endif %}</p>
</div>
</div>
{% empty %}
<p>{% trans "No expertise listed yet." %}</p>
{% endfor %}
</div>
</div>
</div>
</section>
<!-- Products Section -->
<section id="products" class="product-section">
<div class="container">
<div class="reveal" style="margin-bottom: 3rem; text-align: center;">
<div class="badge" style="background: #27272a; color: white; border: 1px solid #3f3f46;">{% trans "Software Products" %}</div>
<h2 style="margin-top: 1rem;">{% trans "Proprietary Solutions" %}</h2>
</div>
<div class="slider-controls reveal">
<div class="slider-indicators" id="slider-indicators">
{% for product in products %}
<div class="indicator {% if forloop.first %}active{% endif %}" data-index="{{ forloop.counter0 }}"></div>
{% endfor %}
</div>
<div style="width: 1rem;"></div>
<button class="slider-btn" id="prev-btn" aria-label="Previous Product">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<button class="slider-btn" id="next-btn" aria-label="Next Product">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
<div class="product-slider-wrapper">
{% for product in products %}
<div class="product-slide {% if forloop.first %}active{% endif %}">
<div class="product-text">
<h2 style="color: white; margin-bottom: 0.5rem;">{% if current_lang == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}</h2>
<div class="badge" style="background: #3f3f46; color: white; margin-bottom: 1.5rem;">{% trans "Enterprise Solution" %}</div>
<p style="color: #a1a1aa; margin-bottom: 2rem;">{% if current_lang == 'ar' %}{{ product.description_ar }}{% else %}{{ product.description_en }}{% endif %}</p>
<ul class="feature-list">
{% if current_lang == 'ar' %}
{% for feature in product.features_ar %}
<li><svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg> {{ feature }}</li>
{% endfor %}
{% else %}
{% for feature in product.features_en %}
<li><svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg> {{ feature }}</li>
{% endfor %}
{% endif %}
</ul>
{% if product.link %}
<a href="{{ product.link }}" target="_blank" class="btn" style="background: white; color: black; margin-top: 2rem;">{% trans "Request Demo" %}</a>
{% else %}
<a href="#contact" class="btn" style="background: white; color: black; margin-top: 2rem;">{% trans "Request Demo" %}</a>
{% endif %}
</div>
{% if product.link %}
<a href="{{ product.link }}" target="_blank">
<div class="product-ui-mockup">
<div style="display: flex; gap: 6px; margin-bottom: 0.5rem;">
<div style="width: 8px; height: 8px; border-radius: 50%; background: #52525b;"></div>
<div style="width: 8px; height: 8px; border-radius: 50%; background: #52525b;"></div>
</div>
{% if product.image %}
<div style="display: flex; flex-direction: column; gap: 10px; flex: 1;">
<div style="display: flex; gap: 10px; height: 100%;">
<div style="width: 30%; background: #3f3f46; border-radius: 4px;"></div>
<div style="width: 70%; display: flex; flex-direction: column; gap: 8px;">
<div style="height: 40px; background: #52525b; border-radius: 4px;"></div>
<div style="flex: 1; background: #3f3f46; border-radius: 4px; overflow: hidden;">
<img src="{{ product.image.url }}" style="width: 100%; height: 100%; object-fit: cover; opacity: 0.8;" alt="Mockup">
</div>
</div>
</div>
</div>
{% endif%}
</div>
</a>
{% endif%}
</div>
{% empty %}
<div class="product-slide active" style="text-align:center;">{% trans "No products available." %}</div>
{% endfor %}
</div>
</div>
</section>
<!-- Team Section -->
<section id="team" style="padding: var(--section-spacing) 0; background: var(--secondary);">
<div class="container">
<div class="reveal">
<div class="section-header">
<div>
<div class="badge">{% trans "Leadership" %}</div>
<h2>{% trans "Meet Team" %}</h2>
</div>
<div class="carousel-controls">
<button class="slider-btn" onclick="scrollCarousel('team-track', -1)" aria-label="Previous Team Member">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<button class="slider-btn" onclick="scrollCarousel('team-track', 1)" aria-label="Next Team Member">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
</div>
</div>
<div class="carousel-container reveal">
<div id="team-track" class="carousel-track team-track">
{% for member in team %}
<div class="carousel-item">
<div class="team-card card">
<div class="team-img-wrapper">
{% if member.photo %}
<img src="{{ member.photo.url }}" alt="{% if current_lang == 'ar' %}{{ member.name_ar }}{% else %}{{ member.name_en }}{% endif %}">
{% else %}
<img src="https://picsum.photos/seed/{{ member.id }}/200/200" alt="Placeholder">
{% endif %}
</div>
<p style="font-size: 0.75rem; font-weight: 700; color: #71717a; text-transform: uppercase;">{% if current_lang == 'ar' %}{{ member.role_ar }}{% else %}{{ member.role_en }}{% endif %}</p>
<h3>{% if current_lang == 'ar' %}{{ member.name_ar }}{% else %}{{ member.name_en }}{% endif %}</h3>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</section>
<!-- Testimonials Section -->
<section id="testimonials" style="padding: var(--section-spacing) 0;">
<div class="container">
<div class="reveal">
<div class="section-header">
<div>
<div class="badge">{% trans "Success Stories" %}</div>
<h2>{% trans "Trusted by Leaders" %}</h2>
</div>
<div class="carousel-controls">
<button class="slider-btn" onclick="scrollCarousel('testimonials-track', -1)" aria-label="Previous Testimonial">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<button class="slider-btn" onclick="scrollCarousel('testimonials-track', 1)" aria-label="Next Testimonial">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
</div>
</div>
<div class="carousel-container reveal">
<div id="testimonials-track" class="carousel-track testimonials-track">
{% for testimonial in testimonials %}
<div class="carousel-item">
<div class="card">
{% if testimonial.client_photo %}
<img src="{{ testimonial.client_photo.url }}" alt="{% if current_lang == 'ar' %}{{ testimonial.client_name_ar }}{% else %}{{ testimonial.client_name_en }}{% endif %}" class="testimonial-img">
{% else %}
<img src="https://picsum.photos/seed/{{ testimonial.id }}/150/150" alt="Placeholder" class="testimonial-img">
{% endif %}
<p class="testimonial-text">"{{ testimonial.feedback_en }}"</p>
<div class="testimonial-author">
<span class="testimonial-author-name">{% if current_lang == 'ar' %}{{ testimonial.client_name_ar }}{% else %}{{ testimonial.client_name_en }}{% endif %}</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</section>
<section id="contact" style="padding: var(--section-spacing) 0;">
<div class="container">
<div class="contact-wrapper reveal">
<div class="contact-info">
<h2 style="margin-bottom: 1rem;">{% trans "Ready to build?" %}</h2>
<p style="margin-bottom: 2rem; color: var(--muted-foreground);">{% trans "Our solutions architects typically respond to project inquiries within 24 business hours." %}</p>
<div class="contact-details-grid" style="display: grid; gap: 1.5rem; margin-top: 2rem;">
{% if settings %}
{% if settings.company_address_en %}
<div>
<div style="font-size: 0.75rem; font-weight: 700; color: var(--primary); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.25rem;">{% trans "Address" %}</div>
<div style="font-size: 0.9rem; white-space: pre-line;">
{% if current_lang == 'ar' %}{{ settings.company_address_ar }}{% else %}{{ settings.company_address_en }}{% endif %}
</div>
</div>
{% endif %}
{% if settings.company_phone %}
<div>
<div style="font-size: 0.75rem; font-weight: 700; color: var(--primary); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.25rem;">{% trans "Call Us" %}</div>
{% for phone in settings.company_phone %}
<div style="font-size: 0.9rem;">{{ phone }}</div>
{% endfor %}
</div>
{% endif %}
{% if settings.company_email %}
<div>
<div style="font-size: 0.75rem; font-weight: 700; color: var(--primary); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.25rem;">{% trans "Email Us" %}</div>
{% for email in settings.company_email %}
<div style="font-size: 0.9rem;">{{ email }}</div>
{% endfor %}
</div>
{% endif %}
<div>
<div style="font-size: 0.75rem; font-weight: 700; color: var(--primary); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.25rem;">{% trans "Working Hours" %}</div>
<div style="font-size: 0.9rem;">{% trans "Sunday - Thursday" %}</div>
<div style="font-size: 0.9rem;">9:00AM - 05:00PM</div>
</div>
{% endif %}
</div>
</div>
<div class="contact-form-box">
<form id="contactForm" method="POST" action="{% url 'submit_inquiry' %}">
{% csrf_token %}
<label class="form-label">{% trans "Full Name" %}</label>
<input type="text" name="name" class="form-control" placeholder="{% trans 'Your full name' %}" required>
<label class="form-label">{% trans "Work Email" %}</label>
<input type="email" name="email" class="form-control" placeholder="name@company.com" required>
<label class="form-label">{% trans "Message" %}</label>
<textarea name="message" class="form-control" placeholder="{% trans 'Tell us about your project...' %}" style="min-height: 120px;"></textarea>
<button type="submit" class="btn btn-primary" style="width: 100%; height: 3.5rem; font-weight: 600;">{% trans "Send Message" %}</button>
</form>
</div>
</div>
</div>
</section>
<!-- Toast Notification -->
<div id="toast" style="position: fixed; bottom: 20px; right: 20px; background: #18181b; color: white; padding: 1rem 2rem; border-radius: var(--radius); transform: translateY(150%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 2000; box-shadow: 0 10px 30px rgba(0,0,0,0.1);">
{% trans "Message sent successfully!" %}
</div>
<footer>
<div class="container">
<div class="footer-grid">
<div>
<a href="#" class="logo" style="display: block; margin-bottom: 1.5rem;">TENHAL</a>
<p style="font-size: 0.875rem; max-width: 300px;">{% trans "Empowering global enterprises through elite engineering and AI products." %}</p>
</div>
<div>
<h4 style="font-size: 0.875rem; margin-bottom: 1.25rem;">{% trans "Expertise" %}</h4>
{% if expertise %}
<ul style="list-style: none;" class="footer-links">
{% if current_lang == "en" %}
{% for expertise in expertise %}
<li><a href="#services">{{ expertise.title_en |title}}</a></li>
{% endfor %}
{% else %}
<li><a href="#services">{{ expertise.title_ar }}</a></li>
{% endif %}
</ul>
{% endif %}
</div>
<div>
<h4 style="font-size: 0.875rem; margin-bottom: 1.25rem;">{% trans "Products" %}</h4>
<ul style="list-style: none;" class="footer-links">
{% if current_lang == "en" %}
{% for product in products %}
<li><a href="{{ product.link }}" target="_blank">{{ product.name_en }}</a></li>
{% endfor %}
{% else %}
{% for product in products %}
<li><a href="{{ product.link }}" target="_blank">{{ product.name_ar }}</a></li>
{% endfor %}
{% endif %}
</ul>
</div>
<div>
<h4 style="font-size: 0.875rem; margin-bottom: 1.25rem;">{% trans "Company" %}</h4>
<ul style="list-style: none; display: flex; align-items: center; gap: 1rem; padding: 0;" class="footer-links">
<li style="margin-inline-end: 0.5rem;"><a href="#team">{% trans "About Us" %}</a></li>
{% if settings.linkedin_url %}
<li>
<a href="{{ settings.linkedin_url }}" target="_blank" aria-label="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
</svg>
</a>
</li>
{% endif %}
{% if settings.twitter_url %}
<li>
<a href="{{ settings.twitter_url }}" target="_blank" aria-label="Twitter">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>
</a>
</li>
{% endif %}
{% if settings.facebook_url %}
<li>
<a href="{{ settings.facebook_url }}" target="_blank" aria-label="Facebook">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.675 0h-21.35c-.732 0-1.325.593-1.325 1.325v21.351c0 .731.593 1.324 1.325 1.324h11.495v-9.294h-3.128v-3.622h3.128v-2.671c0-3.1 1.893-4.788 4.659-4.788 1.325 0 2.463.099 2.795.143v3.24l-1.918.001c-1.504 0-1.795.715-1.795 1.763v2.313h3.587l-.467 3.622h-3.12v9.293h6.116c.73 0 1.323-.592 1.323-1.324v-21.35c0-.732-.593-1.325-1.325-1.325z"/>
</svg>
</a>
</li>
{% endif %}
{% if settings.instagram_url %}
<li>
<a href="{{ settings.instagram_url }}" target="_blank" aria-label="Instagram">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
<div style="padding-top: 2rem; border-top: 1px solid var(--border); font-size: 0.75rem; color: var(--muted-foreground); display: flex; justify-content: space-between;">
<p>© {% now "Y" %} tenhal.sa</p>
<p>{% trans "Built with Engineering Excellence" %}</p>
</div>
</div>
</footer>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

0
tenhal/__init__.py Normal file
View File

16
tenhal/asgi.py Normal file
View File

@ -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()

203
tenhal/settings.py Normal file
View File

@ -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

21
tenhal/urls.py Normal file
View File

@ -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)

16
tenhal/wsgi.py Normal file
View File

@ -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()