tenhal landing page added
This commit is contained in:
commit
8a842d364d
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal 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
0
landing_page/__init__.py
Normal file
85
landing_page/admin.py
Normal file
85
landing_page/admin.py
Normal 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
5
landing_page/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LandingPageConfig(AppConfig):
|
||||
name = 'landing_page'
|
||||
20
landing_page/middleware.py
Normal file
20
landing_page/middleware.py
Normal 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)
|
||||
117
landing_page/migrations/0001_initial.py
Normal file
117
landing_page/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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"]'),
|
||||
),
|
||||
]
|
||||
18
landing_page/migrations/0003_product_image.py
Normal file
18
landing_page/migrations/0003_product_image.py
Normal 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/'),
|
||||
),
|
||||
]
|
||||
28
landing_page/migrations/0004_visitorlog.py
Normal file
28
landing_page/migrations/0004_visitorlog.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
landing_page/migrations/__init__.py
Normal file
0
landing_page/migrations/__init__.py
Normal file
120
landing_page/models.py
Normal file
120
landing_page/models.py
Normal 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
33
landing_page/tasks.py
Normal 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
3
landing_page/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
7
landing_page/urls.py
Normal file
7
landing_page/urls.py
Normal 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
35
landing_page/views.py
Normal 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)
|
||||
BIN
locale/ar/LC_MESSAGES/django.mo
Normal file
BIN
locale/ar/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
199
locale/ar/LC_MESSAGES/django.po
Normal file
199
locale/ar/LC_MESSAGES/django.po
Normal 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
22
manage.py
Executable 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
27
requirements.txt
Normal 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
244
static/css/main.css
Normal 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; }
|
||||
}
|
||||
|
||||
BIN
static/images/android-chrome-192x192.png
Normal file
BIN
static/images/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
static/images/android-chrome-512x512.png
Normal file
BIN
static/images/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
static/images/apple-touch-icon.png
Normal file
BIN
static/images/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
static/images/favicon-16x16.png
Normal file
BIN
static/images/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 473 B |
BIN
static/images/favicon-32x32.png
Normal file
BIN
static/images/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/favicon.ico
Normal file
BIN
static/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
static/images/site.webmanifest
Normal file
1
static/images/site.webmanifest
Normal 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
182
static/js/main.js
Normal 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);
|
||||
|
||||
43
templates/admin/visitor_log_changelist.html
Normal file
43
templates/admin/visitor_log_changelist.html
Normal 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
507
templates/index.html
Normal 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 Kingdom’s 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
0
tenhal/__init__.py
Normal file
16
tenhal/asgi.py
Normal file
16
tenhal/asgi.py
Normal 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
203
tenhal/settings.py
Normal 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
21
tenhal/urls.py
Normal 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
16
tenhal/wsgi.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user