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