diff --git a/.gitignore b/.gitignore index cee70fb6..eda06f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,59 @@ +# Python *.pyc *~ *.swo *.swp +__pycache__/ +*.py[cod] +*$py.class + +# Django db.sqlite3 -media -media-images -cache -.idea +*.log +media/ +media-images/ +cache/ static/CACHE -.sass-cache/ -main.css -main.css.map -ref.css -ref.css.map -.coverage -crm/dev_settings\.py -env -.env -docs/build +staticfiles/ celerybeat-schedule local_settings.py -media -.sass-cache +.env server.log -.vscode -staticfiles -.venv \ No newline at end of file +dump.rdb + +# Virtual Environment +env/ +venv/ +.venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Testing +.coverage +.pytest_cache/ +htmlcov/ + +# Documentation +docs/build/ + +# Node.js / Frontend +frontend/node_modules/ +frontend/.svelte-kit/ +frontend/build/ +frontend/.env +frontend/.env.local +frontend/package-lock.json +frontend/pnpm-lock.yaml + +# Logs +logs/ +*.log + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index a354b16b..69797928 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,249 +1,145 @@ -# CLAUDE.md - AI Assistant Guide for BottleCRM +# CLAUDE.md -## Project Overview +BottleCRM is a SaaS CRM platform: Django REST API backend (`backend/`) + SvelteKit frontend (`frontend/`). -BottleCRM (Django-CRM) is an open-source CRM system built with Django and Django REST Framework. It provides customer relationship management for startups and small businesses. +## Quick Reference -**Status**: Maintenance mode - active development has moved to [MicroPyramid/opensource-startup-crm](https://github.com/MicroPyramid/opensource-startup-crm) (SvelteKit version). +```bash +# Backend +cd backend && source venv/bin/activate && python manage.py runserver +cd backend && source venv/bin/activate && celery -A crm worker --loglevel=INFO + +# Frontend +cd frontend && pnpm run dev +``` + +**API Docs**: http://localhost:8000/swagger-ui/ ## Tech Stack -- **Backend**: Django 4.2.1, Django REST Framework 3.14.0 -- **Database**: PostgreSQL -- **Authentication**: JWT (djangorestframework-simplejwt) -- **Task Queue**: Celery 5.2.7 with Redis -- **CMS**: Wagtail 5.0.1 -- **API Docs**: drf-spectacular (Swagger/OpenAPI) -- **Deployment**: Docker, Gunicorn +**Backend**: Django 5.x, DRF, PostgreSQL, Redis/Celery, JWT auth, AWS S3/SES +**Frontend**: SvelteKit, Svelte 5 (runes), TailwindCSS, shadcn-svelte, Zod, Axios, Lucide icons -## Project Structure +## Multi-Tenancy & RLS (CRITICAL) -``` -crm/ # Django project settings -common/ # Shared models, auth, utilities -accounts/ # Account/Company management -contacts/ # Contact management -leads/ # Lead management -opportunity/ # Sales pipeline -tasks/ # Task management -cases/ # Support/issue tracking -events/ # Calendar/events -teams/ # Team management -invoices/ # Invoice management -emails/ # Email management -cms/ # Wagtail CMS -templates/ # HTML templates -static/ # Static assets -docs/ # Sphinx documentation -scripts/ # Deployment scripts +Every request operates within an organization context. RLS provides database-level isolation, but **always filter at ORM level too** (defense-in-depth). + +### Required Patterns + +```python +# ALWAYS filter by org - never use .all() on org-scoped models +queryset = Model.objects.filter(org=request.profile.org) + +# ALWAYS include org in get_object +def get_object(self, pk): + return get_object_or_404(Lead, id=pk, org=self.request.profile.org) + +# ALWAYS set org when creating +obj = Model.objects.create(name="Example", org=request.profile.org) +serializer.save(org=request.profile.org) ``` -## Key Files +### RLS-Protected Tables (24) -| File | Purpose | -|------|---------| -| `manage.py` | Django CLI entry point | -| `crm/settings.py` | Main configuration | -| `crm/urls.py` | URL routing | -| `crm/celery.py` | Celery task queue setup | -| `common/models.py` | User, Org, Profile models | -| `common/utils.py` | Shared utilities and constants | -| `requirements.txt` | Python dependencies | +`lead`, `accounts`, `contacts`, `opportunity`, `case`, `task`, `invoice`, `comment`, `attachments`, `document`, `teams`, `activity`, `tags`, `address`, `solution`, `board`, `board_column`, `board_task`, `board_member`, `apiSettings`, `account_email`, `emailLogs`, `invoice_history`, `security_audit_log` -## Development Setup +### Adding New Org-Scoped Models -### Prerequisites -- Python 3.x -- PostgreSQL 9.4+ -- Redis server +1. Inherit from `BaseOrgModel` (includes org field automatically) +2. Add table to `ORG_SCOPED_TABLES` in `common/rls/__init__.py` +3. Create RLS migration using `get_enable_policy_sql('table_name')` +4. Always filter by org in views -### Quick Start +### Celery Tasks + +```python +from common.tasks import set_rls_context + +@app.task +def my_task(data_id, org_id): + set_rls_context(org_id) # REQUIRED before any query + obj = MyModel.objects.get(id=data_id) +``` + +### RLS Commands ```bash -# Install dependencies -pip install -r requirements.txt - -# Configure environment -cp ENV.md .env -# Edit .env with your database credentials - -# Run migrations -python manage.py migrate - -# Start server -python manage.py runserver - -# Start Celery worker (separate terminal) -celery -A crm worker --loglevel=INFO +python manage.py manage_rls --status # Check status +python manage.py manage_rls --verify-user # Verify non-superuser (superusers bypass RLS!) +python manage.py manage_rls --test # Test isolation ``` -### Docker Setup +## Architecture -```bash -docker build -t djcrm:1 -f docker/dockerfile . -docker-compose -f docker/docker-compose.yml up +### User Model + +User → Profile → Org (one user can have profiles in multiple orgs) +- Profile includes: role (ADMIN/USER), sales/marketing access flags +- Super Admin: @micropyramid.com email domain + +### BaseModel (`common/base.py`) + +All models inherit from `BaseModel`: UUID PKs, timestamps (`created_at`, `updated_at`), audit (`created_by`, `updated_by`) + +### AssignableMixin + +For models with team/user assignment: Account, Lead, Contact, Opportunity, Case, Task + +### Django Apps + +- `common` - User, Profile, Org, Teams, Tags, Comment, Attachments, Document, Activity +- `accounts`, `leads`, `contacts`, `opportunity`, `cases`, `tasks`, `invoices`, `marketing` + +### API Pattern + +``` +GET/POST /api// # List/Create +GET/PUT/DELETE /api/// # Detail/Update/Delete +GET/POST /api//comment// # Comments +GET/POST /api//attachment// # Attachments ``` -## Common Commands +### Celery Tasks (`common/tasks.py`) + +Email tasks: `send_email_to_new_user`, `send_email_user_mentions`, `send_email_user_status`, `send_email_user_delete`, `send_email_to_reset_password` +Team tasks: `remove_users`, `update_team_users` + +## Frontend + +### Structure + +- `src/routes/(app)/` - Main CRM (authenticated) +- `src/routes/(no-layout)/` - Auth pages +- `src/lib/` - Components and utilities + +### Key Files + +- `src/hooks.server.js` - Auth, org validation, route protection +- `src/lib/stores/auth.js` - Auth state + +### Standards + +- **Svelte 5**: Use runes (`$state`, `$derived`, `$effect`) +- **NO TypeScript**: JavaScript with JSDoc annotations only +- **Validation**: Zod for forms +- **Icons**: Lucide Svelte ```bash -# Run development server -python manage.py runserver - -# Run migrations -python manage.py migrate - -# Create migrations -python manage.py makemigrations - -# Run tests -pytest - -# Run tests with coverage -coverage run -m pytest -coverage report - -# Start Celery worker -celery -A crm worker --loglevel=INFO - -# Format code -black . +pnpm run check # Type check via JSDoc +pnpm run lint # Lint +pnpm run format # Format ``` ## Testing -- **Framework**: pytest with Django plugin -- **Config**: `pytest.ini` -- **Coverage**: `.coveragerc` - -### Test Locations -- `*/tests_celery_tasks.py` - Celery task tests (per app) -- `*/tests.py` - Unit tests - -### Run Tests ```bash -pytest # All tests -pytest accounts/ # Single app -pytest -x # Stop on first failure -coverage run -m pytest # With coverage +# Backend +cd backend && pytest && black . && isort . + +# Frontend +cd frontend && pnpm run check && pnpm run lint ``` -## API Structure +## Related Docs -### Base URL Patterns -- `/api/` - Common endpoints -- `/api/accounts/` - Account management -- `/api/contacts/` - Contact management -- `/api/leads/` - Lead management -- `/api/opportunities/` - Sales pipeline -- `/api/teams/` - Team management -- `/api/tasks/` - Task management -- `/api/events/` - Events -- `/api/cases/` - Case management - -### API Documentation -- `/swagger-ui/` - Swagger UI -- `/api/schema/redoc/` - ReDoc -- `/schema/` - OpenAPI schema - -## Code Conventions - -### Django App Pattern -Each app follows this structure: -- `models.py` - Database models -- `views.py` - API views (class-based) -- `serializer.py` - DRF serializers -- `tasks.py` - Celery async tasks -- `tests_celery_tasks.py` - Task tests - -### Model Pattern -Models inherit from `common.base.BaseModel` for timestamp fields: -```python -from common.base import BaseModel - -class MyModel(BaseModel): - # created_at and updated_at are automatic - name = models.CharField(max_length=255) -``` - -### View Pattern -Views use DRF class-based views with permissions: -```python -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated - -class MyView(APIView): - permission_classes = [IsAuthenticated] -``` - -### Serializer Naming -- Read serializers: `ModelSerializer` -- Write serializers: `CreateModelSerializer` - -### Code Quality -- **Formatter**: Black (configured in `.pre-commit-config.yaml`) -- **Pre-commit**: `pre-commit install` - -## Environment Variables - -Key variables (see `ENV.md` for full list): - -``` -SECRET_KEY # Django secret key -ENV_TYPE # dev/stage/live -DBNAME # PostgreSQL database name -DBUSER # Database user -DBPASSWORD # Database password -DBHOST # Database host -CELERY_BROKER_URL # Redis URL for Celery -``` - -## Multi-tenancy - -The app supports multiple organizations via the `Org` model in `common/models.py`. Middleware in `common/middleware/get_company.py` handles organization context. - -## Important Patterns - -### Authentication -JWT tokens via `djangorestframework-simplejwt`. Custom auth in `common/external_auth.py`. - -### Async Tasks -Celery tasks defined in `*/tasks.py` files. Example: -```python -from celery import shared_task - -@shared_task -def send_email_task(email_data): - # Task implementation - pass -``` - -### Tags and Relationships -Many-to-many relationships use through models. Tags are stored in `accounts/models.py`. - -## CI/CD - -- **GitHub Actions**: CodeQL security scanning -- **Travis CI**: Test execution and coverage -- **Jenkins**: Docker build and deployment - -## External Documentation - -- ReadTheDocs: http://django-crm.readthedocs.io -- Demo: https://bottlecrm.io/ -- Sphinx docs: `docs/source/` - -## When Making Changes - -1. **Follow existing patterns** - Check similar files in the same app -2. **Add tests** - Create tests in `tests_celery_tasks.py` or `tests.py` -3. **Run formatter** - Use `black .` before committing -4. **Update migrations** - Run `makemigrations` for model changes -5. **Check API docs** - Ensure serializers are documented with drf-spectacular - -## Common Issues - -- **Database connection**: Ensure PostgreSQL is running and `.env` is configured -- **Celery tasks not running**: Start Redis and Celery worker -- **Static files 404**: Run `python manage.py collectstatic` -- **Migration conflicts**: Check for unapplied migrations with `python manage.py showmigrations` +- `QUICKSTART.md` - Setup guide diff --git a/Dockerfile b/Dockerfile index 33b08500..41983bac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu:24.04 # invalidate cache ARG APP_NAME diff --git a/README.md b/README.md index 5f55f927..5f70c00f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ and feature requests via github. ### Moving Forward -To align with the project’s vision and address these challenges, I’ve shifted development to a new repository using **SvelteKit** and **Prisma** for a robust, fast, and feature-rich framework. A Minimum Viable Product (MVP) was released last week at [MicroPyramid/opensource-startup-crm](https://github.com/MicroPyramid/opensource-startup-crm). +To align with the project’s vision and address these challenges, I’ve shifted development to a new repository using **SvelteKit** and **Prisma** for a robust, fast, and feature-rich framework. A Minimum Viable Product (MVP) was released last week at [MicroPyramid/Django-CRM](https://github.com/MicroPyramid/Django-CRM). #### Key Updates: @@ -151,10 +151,10 @@ isort # to sort imports in python **Note: This repository is no longer actively maintained.** For the latest development and community support, please visit our new SvelteKit-based CRM project: -- **New Project**: [MicroPyramid/opensource-startup-crm](https://github.com/MicroPyramid/opensource-startup-crm) +- **New Project**: [MicroPyramid/Django-CRM](https://github.com/MicroPyramid/Django-CRM) - Follow [@micropyramid]() on Twitter - For questions about the legacy Django CRM, check [past issues]() -- For new feature requests and active development, visit the [new repository](https://github.com/MicroPyramid/opensource-startup-crm) +- For new feature requests and active development, visit the [new repository](https://github.com/MicroPyramid/Django-CRM) - For commercial support [Contact us](https://micropyramid.com/contact-us/) ## Credits @@ -167,7 +167,7 @@ This project exists thanks to all the people who contributed during its active d ### Legacy Project Notice -This Django CRM repository is now in maintenance mode. While we welcome your feedback and appreciate past contributions, active development has moved to our new SvelteKit-based CRM solution at [MicroPyramid/opensource-startup-crm](https://github.com/MicroPyramid/opensource-startup-crm). +This Django CRM repository is now in maintenance mode. While we welcome your feedback and appreciate past contributions, active development has moved to our new SvelteKit-based CRM solution at [MicroPyramid/Django-CRM](https://github.com/MicroPyramid/Django-CRM). For commercial support [Contact us](https://micropyramid.com/contact-us/) diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py deleted file mode 100644 index 830bf379..00000000 --- a/accounts/migrations/0001_initial.py +++ /dev/null @@ -1,97 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.db import migrations, models -import phonenumber_field.modelfields -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Account', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=64, verbose_name='Name')), - ('email', models.EmailField(max_length=254)), - ('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None)), - ('industry', models.CharField(blank=True, choices=[('ADVERTISING', 'ADVERTISING'), ('AGRICULTURE', 'AGRICULTURE'), ('APPAREL & ACCESSORIES', 'APPAREL & ACCESSORIES'), ('AUTOMOTIVE', 'AUTOMOTIVE'), ('BANKING', 'BANKING'), ('BIOTECHNOLOGY', 'BIOTECHNOLOGY'), ('BUILDING MATERIALS & EQUIPMENT', 'BUILDING MATERIALS & EQUIPMENT'), ('CHEMICAL', 'CHEMICAL'), ('COMPUTER', 'COMPUTER'), ('EDUCATION', 'EDUCATION'), ('ELECTRONICS', 'ELECTRONICS'), ('ENERGY', 'ENERGY'), ('ENTERTAINMENT & LEISURE', 'ENTERTAINMENT & LEISURE'), ('FINANCE', 'FINANCE'), ('FOOD & BEVERAGE', 'FOOD & BEVERAGE'), ('GROCERY', 'GROCERY'), ('HEALTHCARE', 'HEALTHCARE'), ('INSURANCE', 'INSURANCE'), ('LEGAL', 'LEGAL'), ('MANUFACTURING', 'MANUFACTURING'), ('PUBLISHING', 'PUBLISHING'), ('REAL ESTATE', 'REAL ESTATE'), ('SERVICE', 'SERVICE'), ('SOFTWARE', 'SOFTWARE'), ('SPORTS', 'SPORTS'), ('TECHNOLOGY', 'TECHNOLOGY'), ('TELECOMMUNICATIONS', 'TELECOMMUNICATIONS'), ('TELEVISION', 'TELEVISION'), ('TRANSPORTATION', 'TRANSPORTATION'), ('VENTURE CAPITAL', 'VENTURE CAPITAL')], max_length=255, null=True, verbose_name='Industry Type')), - ('billing_address_line', models.CharField(blank=True, max_length=255, null=True, verbose_name='Address')), - ('billing_street', models.CharField(blank=True, max_length=55, null=True, verbose_name='Street')), - ('billing_city', models.CharField(blank=True, max_length=255, null=True, verbose_name='City')), - ('billing_state', models.CharField(blank=True, max_length=255, null=True, verbose_name='State')), - ('billing_postcode', models.CharField(blank=True, max_length=64, null=True, verbose_name='Post/Zip-code')), - ('billing_country', models.CharField(blank=True, choices=[('GB', 'United Kingdom'), ('AF', 'Afghanistan'), ('AX', 'Aland Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('CV', 'Cape Verde'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo, The Democratic Republic of the'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "Cote d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands (Malvinas)'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See (Vatican City State)'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran, Islamic Republic of'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', "Lao People's Democratic Republic"), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libyan Arab Jamahiriya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia, The Former Yugoslav Republic of'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia, Federated States of'), ('MD', 'Moldova'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('AN', 'Netherlands Antilles'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestinian Territory, Occupied'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('BL', 'Saint Barthelemy'), ('SH', 'Saint Helena'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syrian Arab Republic'), ('TW', 'Taiwan, Province of China'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania, United Republic of'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('US', 'United States'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Viet Nam'), ('VG', 'Virgin Islands, British'), ('VI', 'Virgin Islands, U.S.'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], max_length=3, null=True)), - ('website', models.URLField(blank=True, null=True, verbose_name='Website')), - ('description', models.TextField(blank=True, null=True)), - ('is_active', models.BooleanField(default=False)), - ('status', models.CharField(choices=[('open', 'Open'), ('close', 'Close')], default='open', max_length=64)), - ('contact_name', models.CharField(max_length=120, verbose_name='Contact Name')), - ], - options={ - 'verbose_name': 'Account', - 'verbose_name_plural': 'Accounts', - 'db_table': 'accounts', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='AccountEmail', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('message_subject', models.TextField(null=True)), - ('message_body', models.TextField(null=True)), - ('timezone', models.CharField(default='UTC', max_length=100)), - ('scheduled_date_time', models.DateTimeField(null=True)), - ('scheduled_later', models.BooleanField(default=False)), - ('from_email', models.EmailField(max_length=254)), - ('rendered_message_body', models.TextField(null=True)), - ], - options={ - 'verbose_name': 'Account Email', - 'verbose_name_plural': 'Account Emails', - 'db_table': 'account_email', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='AccountEmailLog', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('is_sent', models.BooleanField(default=False)), - ], - options={ - 'verbose_name': 'EmailLog', - 'verbose_name_plural': 'EmailLogs', - 'db_table': 'emailLogs', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='Tags', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=20)), - ('slug', models.CharField(blank=True, max_length=20, unique=True)), - ], - options={ - 'verbose_name': 'Tag', - 'verbose_name_plural': 'Tags', - 'db_table': 'tags', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/accounts/migrations/0002_initial.py b/accounts/migrations/0002_initial.py deleted file mode 100644 index 580d3659..00000000 --- a/accounts/migrations/0002_initial.py +++ /dev/null @@ -1,112 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('leads', '0001_initial'), - ('teams', '0001_initial'), - ('accounts', '0001_initial'), - ('contacts', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('common', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='tags', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - migrations.AddField( - model_name='tags', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='accountemaillog', - name='contact', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contact_email_log', to='contacts.contact'), - ), - migrations.AddField( - model_name='accountemaillog', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - migrations.AddField( - model_name='accountemaillog', - name='email', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='email_log', to='accounts.accountemail'), - ), - migrations.AddField( - model_name='accountemaillog', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='accountemail', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - migrations.AddField( - model_name='accountemail', - name='from_account', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_email', to='accounts.account'), - ), - migrations.AddField( - model_name='accountemail', - name='recipients', - field=models.ManyToManyField(related_name='recieved_email', to='contacts.contact'), - ), - migrations.AddField( - model_name='accountemail', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='account', - name='assigned_to', - field=models.ManyToManyField(related_name='account_assigned_users', to='common.profile'), - ), - migrations.AddField( - model_name='account', - name='contacts', - field=models.ManyToManyField(related_name='account_contacts', to='contacts.contact'), - ), - migrations.AddField( - model_name='account', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='account_created_by', to='common.profile'), - ), - migrations.AddField( - model_name='account', - name='lead', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='account_leads', to='leads.lead'), - ), - migrations.AddField( - model_name='account', - name='org', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='account_org', to='common.org'), - ), - migrations.AddField( - model_name='account', - name='tags', - field=models.ManyToManyField(blank=True, to='accounts.tags'), - ), - migrations.AddField( - model_name='account', - name='teams', - field=models.ManyToManyField(related_name='account_teams', to='teams.teams'), - ), - migrations.AddField( - model_name='account', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - ] diff --git a/accounts/migrations/0003_alter_account_created_by.py b/accounts/migrations/0003_alter_account_created_by.py deleted file mode 100644 index d13a2698..00000000 --- a/accounts/migrations/0003_alter_account_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-13 08:16 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('accounts', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='account', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/accounts/models.py b/accounts/models.py deleted file mode 100644 index 8b42ad14..00000000 --- a/accounts/models.py +++ /dev/null @@ -1,183 +0,0 @@ -import arrow -from django.db import models -from django.utils.text import slugify -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import pgettext_lazy -from phonenumber_field.modelfields import PhoneNumberField - -from common import utils -from common.models import Org, Profile -from common.utils import COUNTRIES, INDCHOICES -from contacts.models import Contact -from teams.models import Teams -from common.base import BaseModel - - -class Tags(BaseModel): - name = models.CharField(max_length=20) - slug = models.CharField(max_length=20, unique=True, blank=True) - - - class Meta: - verbose_name = "Tag" - verbose_name_plural = "Tags" - db_table = "tags" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.name}" - - - def save(self, *args, **kwargs): - self.slug = slugify(self.name) - super().save(*args, **kwargs) - - -class Account(BaseModel): - - ACCOUNT_STATUS_CHOICE = (("open", "Open"), ("close", "Close")) - - name = models.CharField(pgettext_lazy("Name of Account", "Name"), max_length=64) - email = models.EmailField() - phone = PhoneNumberField(null=True) - industry = models.CharField( - _("Industry Type"), max_length=255, choices=INDCHOICES, blank=True, null=True - ) - # billing_address = models.ForeignKey( - # Address, related_name='account_billing_address', on_delete=models.CASCADE, blank=True, null=True) - # shipping_address = models.ForeignKey( - # Address, related_name='account_shipping_address', on_delete=models.CASCADE, blank=True, null=True) - billing_address_line = models.CharField( - _("Address"), max_length=255, blank=True, null=True - ) - billing_street = models.CharField(_("Street"), max_length=55, blank=True, null=True) - billing_city = models.CharField(_("City"), max_length=255, blank=True, null=True) - billing_state = models.CharField(_("State"), max_length=255, blank=True, null=True) - billing_postcode = models.CharField( - _("Post/Zip-code"), max_length=64, blank=True, null=True - ) - billing_country = models.CharField( - max_length=3, choices=COUNTRIES, blank=True, null=True - ) - website = models.URLField(_("Website"), blank=True, null=True) - description = models.TextField(blank=True, null=True) - # created_by = models.ForeignKey( - # Profile, related_name="account_created_by", on_delete=models.SET_NULL, null=True - # ) - is_active = models.BooleanField(default=False) - tags = models.ManyToManyField(Tags, blank=True) - status = models.CharField( - choices=ACCOUNT_STATUS_CHOICE, max_length=64, default="open" - ) - lead = models.ForeignKey( - "leads.Lead", related_name="account_leads", on_delete=models.SET_NULL, null=True - ) - contact_name = models.CharField( - pgettext_lazy("Name of Contact", "Contact Name"), max_length=120 - ) - contacts = models.ManyToManyField( - "contacts.Contact", related_name="account_contacts" - ) - assigned_to = models.ManyToManyField(Profile, related_name="account_assigned_users") - teams = models.ManyToManyField(Teams, related_name="account_teams") - org = models.ForeignKey( - Org, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="account_org", - ) - - class Meta: - verbose_name = "Account" - verbose_name_plural = "Accounts" - db_table = "accounts" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.name}" - - def get_complete_address(self): - """Concatenates complete address.""" - address = "" - add_to_address = [ - self.billing_street, - self.billing_city, - self.billing_state, - self.billing_postcode, - self.get_billing_country_display(), - ] - address = utils.append_str_to(address, *add_to_address) - - return address - - @property - def created_on_arrow(self): - return arrow.get(self.created_at).humanize() - - @property - def contact_values(self): - contacts = list(self.contacts.values_list("id", flat=True)) - return ",".join(str(contact) for contact in contacts) - - @property - def get_team_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return Profile.objects.filter(id__in=team_user_ids) - - @property - def get_team_and_assigned_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = team_user_ids + assigned_user_ids - return Profile.objects.filter(id__in=user_ids) - - @property - def get_assigned_users_not_in_teams(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = set(assigned_user_ids) - set(team_user_ids) - return Profile.objects.filter(id__in=list(user_ids)) - - -class AccountEmail(BaseModel): - from_account = models.ForeignKey( - Account, related_name="sent_email", on_delete=models.SET_NULL, null=True - ) - recipients = models.ManyToManyField(Contact, related_name="recieved_email") - message_subject = models.TextField(null=True) - message_body = models.TextField(null=True) - timezone = models.CharField(max_length=100, default="UTC") - scheduled_date_time = models.DateTimeField(null=True) - scheduled_later = models.BooleanField(default=False) - from_email = models.EmailField() - rendered_message_body = models.TextField(null=True) - - class Meta: - verbose_name = "Account Email" - verbose_name_plural = "Account Emails" - db_table = "account_email" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.message_subject}" - -class AccountEmailLog(BaseModel): - """this model is used to track if the email is sent or not""" - - email = models.ForeignKey( - AccountEmail, related_name="email_log", on_delete=models.SET_NULL, null=True - ) - contact = models.ForeignKey( - Contact, related_name="contact_email_log", on_delete=models.SET_NULL, null=True - ) - is_sent = models.BooleanField(default=False) - - class Meta: - verbose_name = "EmailLog" - verbose_name_plural = "EmailLogs" - db_table = "emailLogs" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.email.message_subject}" diff --git a/ENV.md b/backend/.env.example similarity index 59% rename from ENV.md rename to backend/.env.example index 67ab5b9c..e2fe8f25 100644 --- a/ENV.md +++ b/backend/.env.example @@ -1,6 +1,6 @@ # Environment variables -SECRET_KEY="mco934$@)NHUYTC%6789" +SECRET_KEY="" ENV_TYPE="dev" DOMAIN_NAME="" @@ -14,24 +14,25 @@ AWS_SES_REGION_ENDPOINT="" # DB -DBNAME="bottlecrm" -DBUSER="postgres" -DBPASSWORD="root" -DBHOST="localhost" +DBNAME="" +DBUSER="" +DBPASSWORD="" +DBHOST="" DBPORT="" # Sentry SENTRY_DSN="" # Celery -CELERY_BROKER_URL="" -CELERY_RESULT_BACKEND="" +CELERY_BROKER_URL="redis://localhost:6379/0" +CELERY_RESULT_BACKEND="redis://localhost:6379/0" # Swagger SWAGGER_ROOT_URL="" -#CACHES -MEMCACHELOCATION="" +# Google OAuth +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" # Email DEFAULT_FROM_EMAIL="" diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..6a501449 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,407 @@ +# BottleCRM Backend - Django REST API + +The backend for BottleCRM, a multi-tenant CRM platform built with Django REST Framework. + +## Tech Stack + +- **Django 4.2.1** - Web framework +- **Django REST Framework 3.14.0** - API toolkit +- **PostgreSQL** - Database (psycopg2-binary 2.9.11) +- **Celery 5.5.3** - Async task queue +- **Redis 4.6.0** - Message broker for Celery +- **djangorestframework-simplejwt 5.2.2** - JWT authentication +- **drf-spectacular 0.26.2** - OpenAPI/Swagger documentation +- **django-ses 3.5.0** - AWS SES email backend +- **Sentry SDK 1.24.0** - Error tracking + +## Django Apps + +| App | Description | +|-----|-------------| +| `common` | User, Organization, Profile, Comments, Attachments, Document models | +| `accounts` | Customer account management | +| `leads` | Lead tracking and conversion | +| `contacts` | Contact management | +| `opportunity` | Sales pipeline and deal tracking | +| `cases` | Customer support tickets | +| `tasks` | Task management | +| `invoices` | Invoicing system | +| `events` | Calendar events | +| `teams` | Team management | +| `emails` | Email handling | +| `planner` | Planner events | +| `boards` | Kanban board system | +| `marketing` | Newsletter and contact forms | + +## Prerequisites + +- **Python 3.8+** +- **PostgreSQL** +- **Redis** (for Celery) +- **virtualenv** + +## Installation + +### 1. Create and activate virtual environment + +```bash +cd backend +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +### 2. Install dependencies + +```bash +pip install -r requirements.txt +``` + +### 3. Configure environment variables + +Create a `.env` file in the `backend/` directory: + +```env +# Django +SECRET_KEY=your-secret-key-here +ENV_TYPE=dev + +# Database +DBNAME=bottlecrm +DBUSER=postgres +DBPASSWORD=root +DBHOST=localhost +DBPORT=5432 + +# Email +DEFAULT_FROM_EMAIL=noreply@bottlecrm.com +ADMIN_EMAIL=admin@bottlecrm.com + +# Celery +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 + +# Domain +DOMAIN_NAME=http://localhost:8000 +SWAGGER_ROOT_URL=http://localhost:8000 +``` + +### 4. Set up database + +```bash +# Create PostgreSQL database +sudo -u postgres psql +CREATE DATABASE bottlecrm WITH OWNER = postgres; +ALTER USER postgres WITH PASSWORD 'root'; +\q + +# Run migrations +python manage.py migrate + +# Create superuser (optional) +python manage.py createsuperuser +``` + +### 5. Run the development server + +```bash +python manage.py runserver +``` + +The API will be available at `http://localhost:8000` + +## Running Celery + +For background tasks (emails, notifications), run the Celery worker: + +```bash +celery -A crm worker --loglevel=INFO +``` + +## API Documentation + +- **Swagger UI**: http://localhost:8000/swagger-ui/ +- **ReDoc**: http://localhost:8000/api/schema/redoc/ +- **Django Admin**: http://localhost:8000/admin/ + +## Architecture + +### Multi-Tenancy + +Every request operates within an organization context: + +- **Organization (Org)**: Top-level tenant container +- **Users**: Regular members with USER role +- **Admins**: Organization administrators with ADMIN role +- **Super Admin**: Users with @micropyramid.com email domain have platform-wide access + +### Authentication + +JWT-based authentication: + +``` +Authorization: Bearer +``` + +- Organization ID is embedded in the JWT token (not sent as header) +- Access token lifetime: 1 day +- Refresh token lifetime: 365 days + +### Middleware + +The middleware chain provides security: + +1. **`GetProfileAndOrg`** (`common.middleware.get_company`): + - Extracts org_id from JWT token claims (not headers - prevents spoofing) + - Validates user has active membership in the organization + - Sets `request.profile` and `request.org` + +2. **`RequireOrgContext`** (`common.middleware.rls_context`): + - Sets PostgreSQL session variable `app.current_org` for RLS + - Resets context after each request + +### Row-Level Security (RLS) + +PostgreSQL RLS provides database-level tenant isolation as defense-in-depth. + +#### How It Works + +1. **Middleware sets context**: `SET app.current_org = ''` +2. **RLS policies filter queries**: Only rows matching `org_id` are visible +3. **Fail-safe design**: Empty context returns zero rows (NULLIF pattern) + +#### Protected Tables (24 total) + +| Category | Tables | +|----------|--------| +| Core Business | `lead`, `accounts`, `contacts`, `opportunity`, `case`, `task`, `invoice` | +| Supporting | `comment`, `attachments`, `document`, `teams`, `activity`, `tags`, `address`, `solution` | +| Boards | `board`, `board_column`, `board_task`, `board_member` | +| Other | `apiSettings`, `account_email`, `emailLogs`, `invoice_history`, `security_audit_log` | + +#### Configuration + +RLS is configured in `common/rls/__init__.py`: + +```python +from common.rls import RLS_CONFIG, get_enable_policy_sql + +# List of protected tables +tables = RLS_CONFIG['tables'] + +# Enable RLS on a table +cursor.execute(get_enable_policy_sql('my_table')) +``` + +#### Management Commands + +```bash +# Check RLS status on all tables +python manage.py manage_rls --status + +# Verify database user is non-superuser (required for RLS) +python manage.py manage_rls --verify-user + +# Test RLS isolation between organizations +python manage.py manage_rls --test + +# Enable RLS on all configured tables +python manage.py manage_rls --enable + +# Disable RLS (for debugging only) +python manage.py manage_rls --disable +``` + +#### Critical: Database User Setup + +**PostgreSQL superusers bypass ALL RLS policies.** You must use a non-superuser: + +```sql +-- Create application user +CREATE USER crm_app WITH PASSWORD 'your_secure_password'; + +-- Grant permissions +GRANT CONNECT ON DATABASE bottlecrm TO crm_app; +GRANT USAGE ON SCHEMA public TO crm_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO crm_app; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_app; + +-- Future tables inherit permissions +ALTER DEFAULT PRIVILEGES IN SCHEMA public +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO crm_app; +``` + +Update `.env`: +```env +DBUSER=crm_app +DBPASSWORD=your_secure_password +``` + +#### Celery Tasks & RLS + +Background tasks don't go through middleware, so set RLS context manually: + +```python +from common.tasks import set_rls_context + +@app.task +def my_background_task(data_id, org_id): + set_rls_context(org_id) # Required! + obj = MyModel.objects.get(id=data_id) + # ... process +``` + +#### Adding RLS to New Tables + +1. Add table name to `ORG_SCOPED_TABLES` in `common/rls/__init__.py` +2. Create migration using `get_enable_policy_sql()` +3. Ensure model has `org = models.ForeignKey(Org, ...)` + +### BaseModel Pattern + +All models inherit from `BaseModel` (`common.base.BaseModel`): + +- UUID primary keys (not integer IDs) +- Automatic timestamps: `created_at`, `updated_at` +- Audit trail: `created_by`, `updated_by` +- Organization isolation: `org = models.ForeignKey(Org)` + +### API Endpoint Pattern + +``` +GET/POST /api// # List/Create +GET/PUT/DELETE /api/// # Detail/Update/Delete +GET/POST /api//comment// # Comments +GET/POST /api//attachment// # Attachments +``` + +## Project Structure + +``` +backend/ +├── manage.py +├── requirements.txt +├── crm/ # Django project settings +│ ├── settings.py +│ ├── urls.py +│ ├── celery.py +│ └── wsgi.py +├── common/ # Core models and utilities +│ ├── models.py # User, Org, Profile, etc. +│ ├── base.py # BaseModel +│ ├── middleware/ +│ └── tasks.py # Celery tasks +├── accounts/ +├── leads/ +├── contacts/ +├── opportunity/ +├── cases/ +├── tasks/ +├── invoices/ +├── events/ +├── teams/ +├── emails/ +├── planner/ +├── boards/ +├── marketing/ +├── templates/ +└── static/ +``` + +## Development + +### Code Quality + +```bash +# Format code +black . + +# Sort imports +isort . + +# Run tests +pytest +``` + +### Creating a New App + +1. Create the app: + ```bash + python manage.py startapp myapp + ``` + +2. Add to `INSTALLED_APPS` in `crm/settings.py` + +3. Create models inheriting from `BaseModel`: + ```python + from common.base import BaseModel + from common.models import Org + + class MyModel(BaseModel): + org = models.ForeignKey(Org, on_delete=models.CASCADE) + # ... other fields + ``` + +4. Always filter queries by organization: + ```python + queryset = MyModel.objects.filter(org=request.profile.org) + ``` + +5. Run migrations: + ```bash + python manage.py makemigrations + python manage.py migrate + ``` + +## Environment Variables Reference + +| Variable | Description | +|----------|-------------| +| `SECRET_KEY` | Django secret key | +| `ENV_TYPE` | Environment type (`dev` or `prod`) | +| `DBNAME` | PostgreSQL database name | +| `DBUSER` | PostgreSQL username | +| `DBPASSWORD` | PostgreSQL password | +| `DBHOST` | PostgreSQL host | +| `DBPORT` | PostgreSQL port | +| `DEFAULT_FROM_EMAIL` | Default sender email | +| `ADMIN_EMAIL` | Admin notification email | +| `CELERY_BROKER_URL` | Redis URL for Celery broker | +| `CELERY_RESULT_BACKEND` | Redis URL for Celery results | +| `DOMAIN_NAME` | Application domain | +| `SWAGGER_ROOT_URL` | Swagger documentation root URL | + +## Troubleshooting + +### Database Connection Issues + +```bash +# Check PostgreSQL is running +sudo systemctl status postgresql + +# Verify database exists +sudo -u postgres psql -l +``` + +### Migration Issues + +```bash +# Show migration status +python manage.py showmigrations + +# Reset migrations (development only) +python manage.py migrate --fake zero +``` + +### Celery Not Processing Tasks + +```bash +# Check Redis is running +redis-cli ping + +# Check Celery worker logs +celery -A crm worker --loglevel=DEBUG +``` + +## License + +MIT License - see [LICENSE](../LICENSE) for details. diff --git a/accounts/__init__.py b/backend/accounts/__init__.py similarity index 100% rename from accounts/__init__.py rename to backend/accounts/__init__.py diff --git a/accounts/apps.py b/backend/accounts/apps.py similarity index 100% rename from accounts/apps.py rename to backend/accounts/apps.py diff --git a/backend/accounts/migrations/0001_initial.py b/backend/accounts/migrations/0001_initial.py new file mode 100644 index 00000000..0f4e87fe --- /dev/null +++ b/backend/accounts/migrations/0001_initial.py @@ -0,0 +1,487 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import phonenumber_field.modelfields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Account", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255, verbose_name="Account Name")), + ( + "email", + models.EmailField( + blank=True, max_length=254, null=True, verbose_name="Email" + ), + ), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, + max_length=128, + null=True, + region=None, + verbose_name="Phone", + ), + ), + ( + "website", + models.URLField(blank=True, null=True, verbose_name="Website"), + ), + ( + "industry", + models.CharField( + blank=True, + choices=[ + ("ADVERTISING", "ADVERTISING"), + ("AGRICULTURE", "AGRICULTURE"), + ("APPAREL & ACCESSORIES", "APPAREL & ACCESSORIES"), + ("AUTOMOTIVE", "AUTOMOTIVE"), + ("BANKING", "BANKING"), + ("BIOTECHNOLOGY", "BIOTECHNOLOGY"), + ( + "BUILDING MATERIALS & EQUIPMENT", + "BUILDING MATERIALS & EQUIPMENT", + ), + ("CHEMICAL", "CHEMICAL"), + ("COMPUTER", "COMPUTER"), + ("EDUCATION", "EDUCATION"), + ("ELECTRONICS", "ELECTRONICS"), + ("ENERGY", "ENERGY"), + ("ENTERTAINMENT & LEISURE", "ENTERTAINMENT & LEISURE"), + ("FINANCE", "FINANCE"), + ("FOOD & BEVERAGE", "FOOD & BEVERAGE"), + ("GROCERY", "GROCERY"), + ("HEALTHCARE", "HEALTHCARE"), + ("INSURANCE", "INSURANCE"), + ("LEGAL", "LEGAL"), + ("MANUFACTURING", "MANUFACTURING"), + ("PUBLISHING", "PUBLISHING"), + ("REAL ESTATE", "REAL ESTATE"), + ("SERVICE", "SERVICE"), + ("SOFTWARE", "SOFTWARE"), + ("SPORTS", "SPORTS"), + ("TECHNOLOGY", "TECHNOLOGY"), + ("TELECOMMUNICATIONS", "TELECOMMUNICATIONS"), + ("TELEVISION", "TELEVISION"), + ("TRANSPORTATION", "TRANSPORTATION"), + ("VENTURE CAPITAL", "VENTURE CAPITAL"), + ], + max_length=255, + null=True, + verbose_name="Industry", + ), + ), + ( + "number_of_employees", + models.PositiveIntegerField( + blank=True, null=True, verbose_name="Number of Employees" + ), + ), + ( + "annual_revenue", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=15, + null=True, + verbose_name="Annual Revenue", + ), + ), + ( + "address_line", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Address" + ), + ), + ( + "city", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="City" + ), + ), + ( + "state", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="State" + ), + ), + ( + "postcode", + models.CharField( + blank=True, max_length=64, null=True, verbose_name="Postal Code" + ), + ), + ( + "country", + models.CharField( + blank=True, + choices=[ + ("GB", "United Kingdom"), + ("AF", "Afghanistan"), + ("AX", "Aland Islands"), + ("AL", "Albania"), + ("DZ", "Algeria"), + ("AS", "American Samoa"), + ("AD", "Andorra"), + ("AO", "Angola"), + ("AI", "Anguilla"), + ("AQ", "Antarctica"), + ("AG", "Antigua and Barbuda"), + ("AR", "Argentina"), + ("AM", "Armenia"), + ("AW", "Aruba"), + ("AU", "Australia"), + ("AT", "Austria"), + ("AZ", "Azerbaijan"), + ("BS", "Bahamas"), + ("BH", "Bahrain"), + ("BD", "Bangladesh"), + ("BB", "Barbados"), + ("BY", "Belarus"), + ("BE", "Belgium"), + ("BZ", "Belize"), + ("BJ", "Benin"), + ("BM", "Bermuda"), + ("BT", "Bhutan"), + ("BO", "Bolivia"), + ("BA", "Bosnia and Herzegovina"), + ("BW", "Botswana"), + ("BV", "Bouvet Island"), + ("BR", "Brazil"), + ("IO", "British Indian Ocean Territory"), + ("BN", "Brunei Darussalam"), + ("BG", "Bulgaria"), + ("BF", "Burkina Faso"), + ("BI", "Burundi"), + ("KH", "Cambodia"), + ("CM", "Cameroon"), + ("CA", "Canada"), + ("CV", "Cape Verde"), + ("KY", "Cayman Islands"), + ("CF", "Central African Republic"), + ("TD", "Chad"), + ("CL", "Chile"), + ("CN", "China"), + ("CX", "Christmas Island"), + ("CC", "Cocos (Keeling) Islands"), + ("CO", "Colombia"), + ("KM", "Comoros"), + ("CG", "Congo"), + ("CD", "Congo, The Democratic Republic of the"), + ("CK", "Cook Islands"), + ("CR", "Costa Rica"), + ("CI", "Cote d'Ivoire"), + ("HR", "Croatia"), + ("CU", "Cuba"), + ("CY", "Cyprus"), + ("CZ", "Czech Republic"), + ("DK", "Denmark"), + ("DJ", "Djibouti"), + ("DM", "Dominica"), + ("DO", "Dominican Republic"), + ("EC", "Ecuador"), + ("EG", "Egypt"), + ("SV", "El Salvador"), + ("GQ", "Equatorial Guinea"), + ("ER", "Eritrea"), + ("EE", "Estonia"), + ("ET", "Ethiopia"), + ("FK", "Falkland Islands (Malvinas)"), + ("FO", "Faroe Islands"), + ("FJ", "Fiji"), + ("FI", "Finland"), + ("FR", "France"), + ("GF", "French Guiana"), + ("PF", "French Polynesia"), + ("TF", "French Southern Territories"), + ("GA", "Gabon"), + ("GM", "Gambia"), + ("GE", "Georgia"), + ("DE", "Germany"), + ("GH", "Ghana"), + ("GI", "Gibraltar"), + ("GR", "Greece"), + ("GL", "Greenland"), + ("GD", "Grenada"), + ("GP", "Guadeloupe"), + ("GU", "Guam"), + ("GT", "Guatemala"), + ("GG", "Guernsey"), + ("GN", "Guinea"), + ("GW", "Guinea-Bissau"), + ("GY", "Guyana"), + ("HT", "Haiti"), + ("HM", "Heard Island and McDonald Islands"), + ("VA", "Holy See (Vatican City State)"), + ("HN", "Honduras"), + ("HK", "Hong Kong"), + ("HU", "Hungary"), + ("IS", "Iceland"), + ("IN", "India"), + ("ID", "Indonesia"), + ("IR", "Iran, Islamic Republic of"), + ("IQ", "Iraq"), + ("IE", "Ireland"), + ("IM", "Isle of Man"), + ("IL", "Israel"), + ("IT", "Italy"), + ("JM", "Jamaica"), + ("JP", "Japan"), + ("JE", "Jersey"), + ("JO", "Jordan"), + ("KZ", "Kazakhstan"), + ("KE", "Kenya"), + ("KI", "Kiribati"), + ("KP", "Korea, Democratic People's Republic of"), + ("KR", "Korea, Republic of"), + ("KW", "Kuwait"), + ("KG", "Kyrgyzstan"), + ("LA", "Lao People's Democratic Republic"), + ("LV", "Latvia"), + ("LB", "Lebanon"), + ("LS", "Lesotho"), + ("LR", "Liberia"), + ("LY", "Libyan Arab Jamahiriya"), + ("LI", "Liechtenstein"), + ("LT", "Lithuania"), + ("LU", "Luxembourg"), + ("MO", "Macao"), + ("MK", "Macedonia, The Former Yugoslav Republic of"), + ("MG", "Madagascar"), + ("MW", "Malawi"), + ("MY", "Malaysia"), + ("MV", "Maldives"), + ("ML", "Mali"), + ("MT", "Malta"), + ("MH", "Marshall Islands"), + ("MQ", "Martinique"), + ("MR", "Mauritania"), + ("MU", "Mauritius"), + ("YT", "Mayotte"), + ("MX", "Mexico"), + ("FM", "Micronesia, Federated States of"), + ("MD", "Moldova"), + ("MC", "Monaco"), + ("MN", "Mongolia"), + ("ME", "Montenegro"), + ("MS", "Montserrat"), + ("MA", "Morocco"), + ("MZ", "Mozambique"), + ("MM", "Myanmar"), + ("NA", "Namibia"), + ("NR", "Nauru"), + ("NP", "Nepal"), + ("NL", "Netherlands"), + ("AN", "Netherlands Antilles"), + ("NC", "New Caledonia"), + ("NZ", "New Zealand"), + ("NI", "Nicaragua"), + ("NE", "Niger"), + ("NG", "Nigeria"), + ("NU", "Niue"), + ("NF", "Norfolk Island"), + ("MP", "Northern Mariana Islands"), + ("NO", "Norway"), + ("OM", "Oman"), + ("PK", "Pakistan"), + ("PW", "Palau"), + ("PS", "Palestinian Territory, Occupied"), + ("PA", "Panama"), + ("PG", "Papua New Guinea"), + ("PY", "Paraguay"), + ("PE", "Peru"), + ("PH", "Philippines"), + ("PN", "Pitcairn"), + ("PL", "Poland"), + ("PT", "Portugal"), + ("PR", "Puerto Rico"), + ("QA", "Qatar"), + ("RE", "Reunion"), + ("RO", "Romania"), + ("RU", "Russian Federation"), + ("RW", "Rwanda"), + ("BL", "Saint Barthelemy"), + ("SH", "Saint Helena"), + ("KN", "Saint Kitts and Nevis"), + ("LC", "Saint Lucia"), + ("MF", "Saint Martin"), + ("PM", "Saint Pierre and Miquelon"), + ("VC", "Saint Vincent and the Grenadines"), + ("WS", "Samoa"), + ("SM", "San Marino"), + ("ST", "Sao Tome and Principe"), + ("SA", "Saudi Arabia"), + ("SN", "Senegal"), + ("RS", "Serbia"), + ("SC", "Seychelles"), + ("SL", "Sierra Leone"), + ("SG", "Singapore"), + ("SK", "Slovakia"), + ("SI", "Slovenia"), + ("SB", "Solomon Islands"), + ("SO", "Somalia"), + ("ZA", "South Africa"), + ("GS", "South Georgia and the South Sandwich Islands"), + ("ES", "Spain"), + ("LK", "Sri Lanka"), + ("SD", "Sudan"), + ("SR", "Suriname"), + ("SJ", "Svalbard and Jan Mayen"), + ("SZ", "Swaziland"), + ("SE", "Sweden"), + ("CH", "Switzerland"), + ("SY", "Syrian Arab Republic"), + ("TW", "Taiwan, Province of China"), + ("TJ", "Tajikistan"), + ("TZ", "Tanzania, United Republic of"), + ("TH", "Thailand"), + ("TL", "Timor-Leste"), + ("TG", "Togo"), + ("TK", "Tokelau"), + ("TO", "Tonga"), + ("TT", "Trinidad and Tobago"), + ("TN", "Tunisia"), + ("TR", "Turkey"), + ("TM", "Turkmenistan"), + ("TC", "Turks and Caicos Islands"), + ("TV", "Tuvalu"), + ("UG", "Uganda"), + ("UA", "Ukraine"), + ("AE", "United Arab Emirates"), + ("US", "United States"), + ("UM", "United States Minor Outlying Islands"), + ("UY", "Uruguay"), + ("UZ", "Uzbekistan"), + ("VU", "Vanuatu"), + ("VE", "Venezuela"), + ("VN", "Viet Nam"), + ("VG", "Virgin Islands, British"), + ("VI", "Virgin Islands, U.S."), + ("WF", "Wallis and Futuna"), + ("EH", "Western Sahara"), + ("YE", "Yemen"), + ("ZM", "Zambia"), + ("ZW", "Zimbabwe"), + ], + max_length=3, + null=True, + verbose_name="Country", + ), + ), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Notes"), + ), + ("is_active", models.BooleanField(default=True)), + ], + options={ + "verbose_name": "Account", + "verbose_name_plural": "Accounts", + "db_table": "accounts", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="AccountEmail", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("message_subject", models.TextField(null=True)), + ("message_body", models.TextField(null=True)), + ("timezone", models.CharField(default="UTC", max_length=100)), + ("scheduled_date_time", models.DateTimeField(null=True)), + ("scheduled_later", models.BooleanField(default=False)), + ("from_email", models.EmailField(max_length=254)), + ("rendered_message_body", models.TextField(null=True)), + ], + options={ + "verbose_name": "Account Email", + "verbose_name_plural": "Account Emails", + "db_table": "account_email", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="AccountEmailLog", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("is_sent", models.BooleanField(default=False)), + ], + options={ + "verbose_name": "EmailLog", + "verbose_name_plural": "EmailLogs", + "db_table": "emailLogs", + "ordering": ("-created_at",), + }, + ), + ] diff --git a/backend/accounts/migrations/0002_initial.py b/backend/accounts/migrations/0002_initial.py new file mode 100644 index 00000000..cd492c60 --- /dev/null +++ b/backend/accounts/migrations/0002_initial.py @@ -0,0 +1,202 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("accounts", "0001_initial"), + ("common", "0001_initial"), + ("contacts", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="account", + name="assigned_to", + field=models.ManyToManyField( + related_name="account_assigned_users", to="common.profile" + ), + ), + migrations.AddField( + model_name="account", + name="contacts", + field=models.ManyToManyField( + related_name="account_contacts", to="contacts.contact" + ), + ), + migrations.AddField( + model_name="account", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + migrations.AddField( + model_name="account", + name="org", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="accounts", + to="common.org", + ), + ), + migrations.AddField( + model_name="account", + name="tags", + field=models.ManyToManyField(blank=True, to="common.tags"), + ), + migrations.AddField( + model_name="account", + name="teams", + field=models.ManyToManyField( + related_name="account_teams", to="common.teams" + ), + ), + migrations.AddField( + model_name="account", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + migrations.AddField( + model_name="accountemail", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + migrations.AddField( + model_name="accountemail", + name="from_account", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="sent_email", + to="accounts.account", + ), + ), + migrations.AddField( + model_name="accountemail", + name="org", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="account_emails", + to="common.org", + ), + ), + migrations.AddField( + model_name="accountemail", + name="recipients", + field=models.ManyToManyField( + related_name="recieved_email", to="contacts.contact" + ), + ), + migrations.AddField( + model_name="accountemail", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + migrations.AddField( + model_name="accountemaillog", + name="contact", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="contact_email_log", + to="contacts.contact", + ), + ), + migrations.AddField( + model_name="accountemaillog", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + migrations.AddField( + model_name="accountemaillog", + name="email", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="email_log", + to="accounts.accountemail", + ), + ), + migrations.AddField( + model_name="accountemaillog", + name="org", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="account_email_logs", + to="common.org", + ), + ), + migrations.AddField( + model_name="accountemaillog", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + migrations.AddIndex( + model_name="account", + index=models.Index(fields=["name"], name="accounts_name_94ecef_idx"), + ), + migrations.AddIndex( + model_name="account", + index=models.Index(fields=["industry"], name="accounts_industr_6498ca_idx"), + ), + migrations.AddIndex( + model_name="account", + index=models.Index( + fields=["org", "-created_at"], name="accounts_org_id_6f0889_idx" + ), + ), + migrations.AddIndex( + model_name="accountemail", + index=models.Index( + fields=["org", "-created_at"], name="account_ema_org_id_3bbcd2_idx" + ), + ), + migrations.AddIndex( + model_name="accountemaillog", + index=models.Index( + fields=["org", "-created_at"], name="emailLogs_org_id_84814f_idx" + ), + ), + ] diff --git a/accounts/migrations/__init__.py b/backend/accounts/migrations/__init__.py similarity index 100% rename from accounts/migrations/__init__.py rename to backend/accounts/migrations/__init__.py diff --git a/backend/accounts/models.py b/backend/accounts/models.py new file mode 100644 index 00000000..a40d151f --- /dev/null +++ b/backend/accounts/models.py @@ -0,0 +1,190 @@ +import arrow +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import pgettext_lazy +from phonenumber_field.modelfields import PhoneNumberField + +from common.base import AssignableMixin, BaseModel +from common.models import Org, Profile, Tags, Teams +from common.utils import COUNTRIES, INDCHOICES +from contacts.models import Contact + + +class Account(AssignableMixin, BaseModel): + """ + Account model for CRM - Streamlined for modern sales workflow + Based on Twenty CRM and Salesforce patterns + """ + + # Core Account Information + name = models.CharField(_("Account Name"), max_length=255) + email = models.EmailField(_("Email"), blank=True, null=True) + phone = PhoneNumberField(_("Phone"), null=True, blank=True) + website = models.URLField(_("Website"), blank=True, null=True) + + # Business Information + industry = models.CharField( + _("Industry"), max_length=255, choices=INDCHOICES, blank=True, null=True + ) + number_of_employees = models.PositiveIntegerField( + _("Number of Employees"), blank=True, null=True + ) + annual_revenue = models.DecimalField( + _("Annual Revenue"), max_digits=15, decimal_places=2, blank=True, null=True + ) + + # Address (flat fields like Lead and Contact models) + address_line = models.CharField(_("Address"), max_length=255, blank=True, null=True) + city = models.CharField(_("City"), max_length=255, blank=True, null=True) + state = models.CharField(_("State"), max_length=255, blank=True, null=True) + postcode = models.CharField(_("Postal Code"), max_length=64, blank=True, null=True) + country = models.CharField( + _("Country"), max_length=3, choices=COUNTRIES, blank=True, null=True + ) + + # Assignment + assigned_to = models.ManyToManyField(Profile, related_name="account_assigned_users") + teams = models.ManyToManyField(Teams, related_name="account_teams") + contacts = models.ManyToManyField( + "contacts.Contact", related_name="account_contacts" + ) + + # Tags + tags = models.ManyToManyField(Tags, blank=True) + + # Notes + description = models.TextField(_("Notes"), blank=True, null=True) + + # System Fields + is_active = models.BooleanField(default=True) + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="accounts", + ) + + class Meta: + verbose_name = "Account" + verbose_name_plural = "Accounts" + db_table = "accounts" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["name"]), + models.Index(fields=["industry"]), + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return f"{self.name}" + + def get_complete_address(self): + """Concatenates complete address.""" + address_parts = [ + self.address_line, + self.city, + self.state, + self.postcode, + self.get_country_display() if self.country else None, + ] + return ", ".join(part for part in address_parts if part) + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() + + @property + def contact_values(self): + contacts = list(self.contacts.values_list("id", flat=True)) + return ",".join(str(contact) for contact in contacts) + + +class AccountEmail(BaseModel): + from_account = models.ForeignKey( + Account, related_name="sent_email", on_delete=models.SET_NULL, null=True + ) + recipients = models.ManyToManyField(Contact, related_name="recieved_email") + message_subject = models.TextField(null=True) + message_body = models.TextField(null=True) + timezone = models.CharField(max_length=100, default="UTC") + scheduled_date_time = models.DateTimeField(null=True) + scheduled_later = models.BooleanField(default=False) + from_email = models.EmailField() + rendered_message_body = models.TextField(null=True) + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="account_emails", + ) + + class Meta: + verbose_name = "Account Email" + verbose_name_plural = "Account Emails" + db_table = "account_email" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return f"{self.message_subject}" + + def save(self, *args, **kwargs): + """ + Ensure org follows the parent account; fallback to the first recipient's org. + + This guards against missing org assignment, which would violate multi-tenant isolation. + """ + if not self.org_id: + if self.from_account and self.from_account.org_id: + self.org_id = self.from_account.org_id + else: + # For unsaved m2m, recipients may not be available until after save + recipient = self.recipients.first() if self.pk else None + if recipient and recipient.org_id: + self.org_id = recipient.org_id + if not self.org_id: + raise ValidationError("Organization is required for AccountEmail") + super().save(*args, **kwargs) + + +class AccountEmailLog(BaseModel): + """this model is used to track if the email is sent or not""" + + email = models.ForeignKey( + AccountEmail, related_name="email_log", on_delete=models.SET_NULL, null=True + ) + contact = models.ForeignKey( + Contact, related_name="contact_email_log", on_delete=models.SET_NULL, null=True + ) + is_sent = models.BooleanField(default=False) + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="account_email_logs", + ) + + class Meta: + verbose_name = "EmailLog" + verbose_name_plural = "EmailLogs" + db_table = "emailLogs" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return f"{self.email.message_subject}" + + def save(self, *args, **kwargs): + """ + Align log org with parent email (preferred) or contact org. + """ + if not self.org_id: + if self.email and self.email.org_id: + self.org_id = self.email.org_id + elif self.contact and self.contact.org_id: + self.org_id = self.contact.org_id + if not self.org_id: + raise ValidationError("Organization is required for AccountEmailLog") + super().save(*args, **kwargs) diff --git a/accounts/serializer.py b/backend/accounts/serializer.py similarity index 62% rename from accounts/serializer.py rename to backend/accounts/serializer.py index 922247d1..659e496f 100644 --- a/accounts/serializer.py +++ b/backend/accounts/serializer.py @@ -1,15 +1,15 @@ from rest_framework import serializers -from accounts.models import Account, AccountEmail, Tags, AccountEmailLog +from accounts.models import Account, AccountEmail, AccountEmailLog +from common.models import Tags from common.serializer import ( AttachmentsSerializer, OrganizationSerializer, ProfileSerializer, - UserSerializer + TeamsSerializer, + UserSerializer, ) from contacts.serializer import ContactSerializer -from leads.serializer import LeadSerializer -from teams.serializer import TeamsSerializer class TagsSerailizer(serializers.ModelSerializer): @@ -19,44 +19,61 @@ class TagsSerailizer(serializers.ModelSerializer): class AccountSerializer(serializers.ModelSerializer): + """Serializer for reading Account data""" + created_by = UserSerializer() - lead = LeadSerializer() org = OrganizationSerializer() tags = TagsSerailizer(read_only=True, many=True) assigned_to = ProfileSerializer(read_only=True, many=True) contacts = ContactSerializer(read_only=True, many=True) teams = TeamsSerializer(read_only=True, many=True) account_attachment = AttachmentsSerializer(read_only=True, many=True) + get_team_users = ProfileSerializer(read_only=True, many=True) + get_team_and_assigned_users = ProfileSerializer(read_only=True, many=True) + get_assigned_users_not_in_teams = ProfileSerializer(read_only=True, many=True) + country = serializers.SerializerMethodField() + + def get_country(self, obj): + return obj.get_country_display() if obj.country else None class Meta: model = Account - # fields = ‘__all__’ fields = ( "id", + # Core Account Information "name", "email", "phone", - "industry", - "billing_address_line", - "billing_street", - "billing_city", - "billing_state", - "billing_postcode", - "billing_country", "website", + # Business Information + "industry", + "number_of_employees", + "annual_revenue", + # Address + "address_line", + "city", + "state", + "postcode", + "country", + # Assignment + "assigned_to", + "teams", + "contacts", + # Tags + "tags", + # Notes "description", + # Related "account_attachment", + # System "created_by", "created_at", "is_active", - "tags", - "status", - "lead", - "contact_name", - "contacts", - "assigned_to", - "teams", "org", + "created_on_arrow", + "get_team_users", + "get_team_and_assigned_users", + "get_assigned_users_not_in_teams", ) @@ -107,33 +124,44 @@ class AccountReadSerializer(serializers.ModelSerializer): class Meta: model = Account - fields = ["name", "billing_city", "tags"] + fields = ["name", "city", "tags"] + class AccountWriteSerializer(serializers.ModelSerializer): - + class Meta: model = Account - fields = ["name","phone", "email", "billing_address_line","billing_street","billing_city", "billing_state", "billing_postcode","billing_country","contacts", "teams", "assigned_to","tags","account_attachment", "website", "status","lead"] + fields = [ + "name", + "phone", + "email", + "website", + "industry", + "number_of_employees", + "annual_revenue", + "address_line", + "city", + "state", + "postcode", + "country", + "contacts", + "teams", + "assigned_to", + "tags", + "account_attachment", + "description", + ] class AccountCreateSerializer(serializers.ModelSerializer): - def __init__(self, *args, **kwargs): - account_view = kwargs.pop("account", False) - request_obj = kwargs.pop("request_obj", None) - super().__init__(*args, **kwargs) - self.fields["status"].required = False - if account_view: - self.fields["billing_address_line"].required = True - self.fields["billing_street"].required = True - self.fields["billing_city"].required = True - self.fields["billing_state"].required = True - self.fields["billing_postcode"].required = True - self.fields["billing_country"].required = True + """Serializer for creating/updating Account data""" - if self.instance: - self.fields["lead"].required = False - self.fields["lead"].required = False - self.org = request_obj.profile.org + def __init__(self, *args, **kwargs): + request_obj = kwargs.pop("request_obj", None) + kwargs.pop("account", None) # Remove unused 'account' parameter passed by views + super().__init__(*args, **kwargs) + if request_obj: + self.org = request_obj.profile.org def validate_name(self, name): if self.instance: @@ -151,31 +179,44 @@ class AccountCreateSerializer(serializers.ModelSerializer): class Meta: model = Account fields = ( + # Core Account Information "name", - "phone", "email", + "phone", "website", + # Business Information "industry", + "number_of_employees", + "annual_revenue", + # Address + "address_line", + "city", + "state", + "postcode", + "country", + # Notes "description", - "status", - "billing_address_line", - "billing_street", - "billing_city", - "billing_state", - "billing_postcode", - "billing_country", - "lead", - "contact_name", ) + class AccountDetailEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() account_attachment = serializers.FileField() + class AccountCommentEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() + class EmailWriteSerializer(serializers.ModelSerializer): class Meta: model = AccountEmail - fields = ("from_email", "recipients", "message_subject","scheduled_later","timezone","scheduled_date_time","message_body") \ No newline at end of file + fields = ( + "from_email", + "recipients", + "message_subject", + "scheduled_later", + "timezone", + "scheduled_date_time", + "message_body", + ) diff --git a/accounts/swagger_params1.py b/backend/accounts/swagger_params.py similarity index 99% rename from accounts/swagger_params1.py rename to backend/accounts/swagger_params.py index 297e81dd..91714642 100644 --- a/accounts/swagger_params1.py +++ b/backend/accounts/swagger_params.py @@ -15,5 +15,3 @@ account_get_params = [ OpenApiParameter("city", OpenApiTypes.STR, OpenApiParameter.QUERY), OpenApiParameter("tags", OpenApiTypes.STR, OpenApiParameter.QUERY), ] - - diff --git a/accounts/tasks.py b/backend/accounts/tasks.py similarity index 89% rename from accounts/tasks.py rename to backend/accounts/tasks.py index 8834beb5..5dea1ef1 100644 --- a/accounts/tasks.py +++ b/backend/accounts/tasks.py @@ -26,14 +26,16 @@ def send_email(email_obj_id): ).exists(): html = email_obj.message_body context_data = { - "email": contact_obj.primary_email - if contact_obj.primary_email - else "", - "name": contact_obj.first_name - if contact_obj.first_name - else "" + " " + contact_obj.last_name - if contact_obj.last_name - else "", + "email": contact_obj.email if contact_obj.email else "", + "name": ( + contact_obj.first_name + if contact_obj.first_name + else ( + "" + " " + contact_obj.last_name + if contact_obj.last_name + else "" + ) + ), } try: html_content = Template(html).render(Context(context_data)) @@ -43,7 +45,7 @@ def send_email(email_obj_id): html_content, from_email=from_email, to=[ - contact_obj.primary_email, + contact_obj.email, ], ) msg.content_subtype = "html" diff --git a/accounts/tests_celery_tasks.py b/backend/accounts/tests_celery_tasks.py similarity index 100% rename from accounts/tests_celery_tasks.py rename to backend/accounts/tests_celery_tasks.py diff --git a/accounts/urls.py b/backend/accounts/urls.py similarity index 100% rename from accounts/urls.py rename to backend/accounts/urls.py diff --git a/accounts/views.py b/backend/accounts/views.py similarity index 79% rename from accounts/views.py rename to backend/accounts/views.py index c8b90889..2ddee10b 100644 --- a/accounts/views.py +++ b/backend/accounts/views.py @@ -1,43 +1,39 @@ import json +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.shortcuts import get_object_or_404 - -from drf_rw_serializers import generics - from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema from rest_framework import status +from rest_framework.generics import GenericAPIView from rest_framework.pagination import LimitOffsetPagination from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.generics import GenericAPIView -from accounts import swagger_params1 -from accounts.models import Account, Tags +from accounts import swagger_params +from accounts.models import Account from accounts.serializer import ( - AccountCreateSerializer, - AccountSerializer, - EmailSerializer, - TagsSerailizer, - AccountReadSerializer, - AccountWriteSerializer, - AccountDetailEditSwaggerSerializer, AccountCommentEditSwaggerSerializer, - EmailWriteSerializer + AccountCreateSerializer, + AccountDetailEditSwaggerSerializer, + AccountReadSerializer, + AccountSerializer, + AccountWriteSerializer, + EmailSerializer, + EmailWriteSerializer, + TagsSerailizer, ) -from teams.serializer import TeamsSerializer from accounts.tasks import send_email, send_email_to_assigned_user from cases.serializer import CaseSerializer -from common.models import Attachments, Comment, Profile -from leads.models import Lead -from leads.serializer import LeadSerializer +from common.models import Attachments, Comment, Profile, Tags, Teams -#from common.external_auth import CustomDualAuthentication +# from common.external_auth import CustomDualAuthentication from common.serializer import ( AttachmentsSerializer, CommentSerializer, ProfileSerializer, + TeamsSerializer, ) from common.utils import ( CASE_TYPE, @@ -50,71 +46,79 @@ from common.utils import ( from contacts.models import Contact from contacts.serializer import ContactSerializer from invoices.serializer import InvoiceSerailizer +from leads.models import Lead +from leads.serializer import LeadSerializer from opportunity.models import SOURCES, STAGES, Opportunity from opportunity.serializer import OpportunitySerializer from tasks.serializer import TaskSerializer -from teams.models import Teams class AccountsListView(APIView, LimitOffsetPagination): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Account serializer_class = AccountReadSerializer def get_context_data(self, **kwargs): params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") + queryset = self.model.objects.filter(org=self.request.profile.org).order_by( + "-id" + ) if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: queryset = queryset.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) ).distinct() if params: if params.get("name"): queryset = queryset.filter(name__icontains=params.get("name")) if params.get("city"): - queryset = queryset.filter(billing_city__contains=params.get("city")) + queryset = queryset.filter(city__icontains=params.get("city")) if params.get("industry"): queryset = queryset.filter(industry__icontains=params.get("industry")) if params.get("tags"): - queryset = queryset.filter( - tags__in=params.get("tags") - ).distinct() + queryset = queryset.filter(tags__in=params.get("tags")).distinct() context = {} - queryset_open = queryset.filter(status="open") - results_accounts_open = self.paginate_queryset( - queryset_open.distinct(), self.request, view=self + + # Account model no longer has status field, return all accounts + # Filter by is_active instead + queryset_active = queryset.filter(is_active=True) + results_accounts_active = self.paginate_queryset( + queryset_active.distinct(), self.request, view=self ) - if results_accounts_open: - offset = queryset_open.filter(id__gte=results_accounts_open[-1].id).count() - if offset == queryset_open.count(): + if results_accounts_active: + offset = queryset_active.filter( + id__gte=results_accounts_active[-1].id + ).count() + if offset == queryset_active.count(): offset = None else: offset = 0 - accounts_open = AccountSerializer(results_accounts_open, many=True).data + accounts_active = AccountSerializer(results_accounts_active, many=True).data context["per_page"] = 10 page_number = (int(self.offset / 10) + 1,) context["page_number"] = page_number context["active_accounts"] = { "offset": offset, - "open_accounts": accounts_open, + "open_accounts": accounts_active, } - queryset_close = queryset.filter(status="close") - results_accounts_close = self.paginate_queryset( - queryset_close.distinct(), self.request, view=self + # Inactive accounts + queryset_inactive = queryset.filter(is_active=False) + results_accounts_inactive = self.paginate_queryset( + queryset_inactive.distinct(), self.request, view=self ) - if results_accounts_close: - offset = queryset_close.filter( - id__gte=results_accounts_close[-1].id + if results_accounts_inactive: + offset = queryset_inactive.filter( + id__gte=results_accounts_inactive[-1].id ).count() - if offset == queryset_close.count(): + if offset == queryset_inactive.count(): offset = None else: offset = 0 - accounts_close = AccountSerializer(results_accounts_close, many=True).data + accounts_inactive = AccountSerializer(results_accounts_inactive, many=True).data contacts = Contact.objects.filter(org=self.request.profile.org).values( "id", "first_name" @@ -122,7 +126,7 @@ class AccountsListView(APIView, LimitOffsetPagination): context["contacts"] = contacts context["closed_accounts"] = { "offset": offset, - "close_accounts": accounts_close, + "close_accounts": accounts_inactive, } context["teams"] = TeamsSerializer( Teams.objects.filter(org=self.request.profile.org), many=True @@ -130,28 +134,32 @@ class AccountsListView(APIView, LimitOffsetPagination): context["countries"] = COUNTRIES context["industries"] = INDCHOICES - tags = Tags.objects.all() + tags = Tags.objects.filter(org=self.request.profile.org) tags = TagsSerailizer(tags, many=True).data context["tags"] = tags - users = Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "id", "user__email" - ) + users = Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("id", "user__email") context["users"] = users leads = Lead.objects.filter(org=self.request.profile.org).exclude( Q(status="converted") | Q(status="closed") ) context["users"] = users context["leads"] = LeadSerializer(leads, many=True).data - context["status"] = ["open","close"] + context["status"] = ["active", "inactive"] # Maps to is_active field return context - @extend_schema(tags=["Accounts"], parameters=swagger_params1.account_get_params) + @extend_schema(tags=["Accounts"], parameters=swagger_params.account_get_params) def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return Response(context) - @extend_schema(tags=["Accounts"], parameters=swagger_params1.organization_params,request=AccountWriteSerializer) + @extend_schema( + tags=["Accounts"], + parameters=swagger_params.organization_params, + request=AccountWriteSerializer, + ) def post(self, request, *args, **kwargs): data = request.data serializer = AccountCreateSerializer( @@ -159,12 +167,12 @@ class AccountsListView(APIView, LimitOffsetPagination): ) # Save Account if serializer.is_valid(): - account_object = serializer.save( - org=request.profile.org - ) + account_object = serializer.save(org=request.profile.org) if data.get("contacts"): contacts_list = json.loads(data.get("contacts")) - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) if contacts: account_object.contacts.add(*contacts) if data.get("tags"): @@ -215,14 +223,18 @@ class AccountsListView(APIView, LimitOffsetPagination): class AccountDetailView(APIView): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) serializer_class = AccountReadSerializer def get_object(self, pk): - return get_object_or_404(Account, id=pk) + return get_object_or_404(Account, id=pk, org=self.request.profile.org) - @extend_schema(tags=["Accounts"], parameters=swagger_params1.organization_params,request=AccountWriteSerializer) + @extend_schema( + tags=["Accounts"], + parameters=swagger_params.organization_params, + request=AccountWriteSerializer, + ) def put(self, request, pk, format=None): data = request.data account_object = self.get_object(pk=pk) @@ -259,7 +271,9 @@ class AccountDetailView(APIView): account_object.contacts.clear() if data.get("contacts"): contacts_list = json.loads(data.get("contacts")) - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) if contacts: account_object.contacts.add(*contacts) @@ -315,7 +329,7 @@ class AccountDetailView(APIView): status=status.HTTP_400_BAD_REQUEST, ) - @extend_schema(tags=["Accounts"], parameters=swagger_params1.organization_params) + @extend_schema(tags=["Accounts"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if self.object.org != request.profile.org: @@ -338,7 +352,7 @@ class AccountDetailView(APIView): status=status.HTTP_200_OK, ) - @extend_schema(tags=["Accounts"], parameters=swagger_params1.organization_params) + @extend_schema(tags=["Accounts"], parameters=swagger_params.organization_params) def get(self, request, pk, format=None): self.account = self.get_object(pk=pk) if self.account.org != request.profile.org: @@ -371,9 +385,9 @@ class AccountDetailView(APIView): if self.request.profile.is_admin or self.request.profile.role == "ADMIN": users_mention = list( - Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "user__email" - ) + Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("user__email") ) elif self.request.profile != self.account.created_by: if self.account.created_by: @@ -385,14 +399,21 @@ class AccountDetailView(APIView): leads = Lead.objects.filter(org=self.request.profile.org).exclude( Q(status="converted") | Q(status="closed") ) + account_content_type = ContentType.objects.get_for_model(Account) + comments = Comment.objects.filter( + content_type=account_content_type, + object_id=self.account.id, + org=self.request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=account_content_type, + object_id=self.account.id, + org=self.request.profile.org, + ).order_by("-id") context.update( { - "attachments": AttachmentsSerializer( - self.account.account_attachment.all(), many=True - ).data, - "comments": CommentSerializer( - self.account.accounts_comments.all(), many=True - ).data, + "attachments": AttachmentsSerializer(attachments, many=True).data, + "comments": CommentSerializer(comments, many=True).data, "contacts": ContactSerializer( self.account.contacts.all(), many=True ).data, @@ -408,7 +429,7 @@ class AccountDetailView(APIView): "cases": CaseSerializer( self.account.accounts_cases.all(), many=True ).data, - "teams" : TeamsSerializer( + "teams": TeamsSerializer( Teams.objects.filter(org=self.request.profile.org), many=True ).data, "stages": STAGES, @@ -429,19 +450,21 @@ class AccountDetailView(APIView): self.account.sent_email.all(), many=True ).data, "users_mention": users_mention, - "leads" : LeadSerializer(leads, many=True).data, - "status" : ["open","close"] + "leads": LeadSerializer(leads, many=True).data, + "status": ["open", "close"], } ) return Response(context) @extend_schema( - tags=["Accounts"], parameters=swagger_params1.organization_params,request=AccountDetailEditSwaggerSerializer + tags=["Accounts"], + parameters=swagger_params.organization_params, + request=AccountDetailEditSwaggerSerializer, ) def post(self, request, pk, **kwargs): data = request.data context = {} - self.account_obj = Account.objects.get(pk=pk) + self.account_obj = Account.objects.get(pk=pk, org=request.profile.org) if self.account_obj.org != request.profile.org: return Response( { @@ -478,11 +501,16 @@ class AccountDetailView(APIView): attachment.attachment = self.request.FILES.get("account_attachment") attachment.save() - comments = Comment.objects.filter(account__id=self.account_obj.id).order_by( - "-id" - ) + account_content_type = ContentType.objects.get_for_model(Account) + comments = Comment.objects.filter( + content_type=account_content_type, + object_id=self.account_obj.id, + org=request.profile.org, + ).order_by("-id") attachments = Attachments.objects.filter( - account__id=self.account_obj.id + content_type=account_content_type, + object_id=self.account_obj.id, + org=request.profile.org, ).order_by("-id") context.update( { @@ -496,15 +524,17 @@ class AccountDetailView(APIView): class AccountCommentView(APIView): model = Comment - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) serializer_class = AccountCommentEditSwaggerSerializer def get_object(self, pk): - return self.model.objects.get(pk=pk) + return self.model.objects.get(pk=pk, org=self.request.profile.org) @extend_schema( - tags=["Accounts"], parameters=swagger_params1.organization_params,request=AccountCommentEditSwaggerSerializer + tags=["Accounts"], + parameters=swagger_params.organization_params, + request=AccountCommentEditSwaggerSerializer, ) def put(self, request, pk, format=None): data = request.data @@ -534,7 +564,7 @@ class AccountCommentView(APIView): status=status.HTTP_403_FORBIDDEN, ) - @extend_schema(tags=["Accounts"], parameters=swagger_params1.organization_params) + @extend_schema(tags=["Accounts"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if ( @@ -558,11 +588,11 @@ class AccountCommentView(APIView): class AccountAttachmentView(APIView): model = Attachments - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) serializer_class = AccountDetailEditSwaggerSerializer - @extend_schema(tags=["Accounts"], parameters=swagger_params1.organization_params) + @extend_schema(tags=["Accounts"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.model.objects.get(pk=pk) if ( @@ -585,12 +615,16 @@ class AccountAttachmentView(APIView): class AccountCreateMailView(APIView): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Account serializer_class = EmailWriteSerializer - @extend_schema(tags=["Accounts"], parameters=swagger_params1.organization_params,request=EmailWriteSerializer) + @extend_schema( + tags=["Accounts"], + parameters=swagger_params.organization_params, + request=EmailWriteSerializer, + ) def post(self, request, pk, *args, **kwargs): data = request.data scheduled_later = data.get("scheduled_later") @@ -614,7 +648,9 @@ class AccountCreateMailView(APIView): if data.get("recipients"): contacts = json.loads(data.get("recipients")) for contact in contacts: - obj_contact = Contact.objects.filter(id=contact, org=request.profile.org) + obj_contact = Contact.objects.filter( + id=contact, org=request.profile.org + ) if obj_contact.exists(): email_obj.recipients.add(contact) else: diff --git a/cases/__init__.py b/backend/cases/__init__.py similarity index 100% rename from cases/__init__.py rename to backend/cases/__init__.py diff --git a/cases/apps.py b/backend/cases/apps.py similarity index 100% rename from cases/apps.py rename to backend/cases/apps.py diff --git a/backend/cases/migrations/0001_initial.py b/backend/cases/migrations/0001_initial.py new file mode 100644 index 00000000..10a0a713 --- /dev/null +++ b/backend/cases/migrations/0001_initial.py @@ -0,0 +1,151 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("accounts", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Solution", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.CharField(max_length=255)), + ("description", models.TextField()), + ( + "status", + models.CharField( + choices=[ + ("draft", "Draft"), + ("reviewed", "Reviewed"), + ("approved", "Approved"), + ], + default="draft", + max_length=20, + ), + ), + ("is_published", models.BooleanField(default=False)), + ], + options={ + "verbose_name": "Solution", + "verbose_name_plural": "Solutions", + "db_table": "solution", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Case", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=64, verbose_name="Name")), + ( + "status", + models.CharField( + choices=[ + ("New", "New"), + ("Assigned", "Assigned"), + ("Pending", "Pending"), + ("Closed", "Closed"), + ("Rejected", "Rejected"), + ("Duplicate", "Duplicate"), + ], + max_length=64, + ), + ), + ( + "priority", + models.CharField( + choices=[ + ("Low", "Low"), + ("Normal", "Normal"), + ("High", "High"), + ("Urgent", "Urgent"), + ], + max_length=64, + ), + ), + ( + "case_type", + models.CharField( + blank=True, + choices=[ + ("Question", "Question"), + ("Incident", "Incident"), + ("Problem", "Problem"), + ], + default="", + max_length=255, + null=True, + ), + ), + ("closed_on", models.DateField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("is_active", models.BooleanField(default=True)), + ( + "account", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="accounts_cases", + to="accounts.account", + ), + ), + ], + options={ + "verbose_name": "Case", + "verbose_name_plural": "Cases", + "db_table": "case", + "ordering": ("-created_at",), + }, + ), + ] diff --git a/backend/cases/migrations/0002_initial.py b/backend/cases/migrations/0002_initial.py new file mode 100644 index 00000000..cdb26a7f --- /dev/null +++ b/backend/cases/migrations/0002_initial.py @@ -0,0 +1,134 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("cases", "0001_initial"), + ("common", "0001_initial"), + ("contacts", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="case", + name="assigned_to", + field=models.ManyToManyField( + related_name="case_assigned_users", to="common.profile" + ), + ), + migrations.AddField( + model_name="case", + name="contacts", + field=models.ManyToManyField(to="contacts.contact"), + ), + migrations.AddField( + model_name="case", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + migrations.AddField( + model_name="case", + name="org", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cases", + to="common.org", + ), + ), + migrations.AddField( + model_name="case", + name="teams", + field=models.ManyToManyField(related_name="cases_teams", to="common.teams"), + ), + migrations.AddField( + model_name="case", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + migrations.AddField( + model_name="solution", + name="cases", + field=models.ManyToManyField( + blank=True, related_name="solutions", to="cases.case" + ), + ), + migrations.AddField( + model_name="solution", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + migrations.AddField( + model_name="solution", + name="org", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="solutions", + to="common.org", + ), + ), + migrations.AddField( + model_name="solution", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + migrations.AddIndex( + model_name="case", + index=models.Index(fields=["status"], name="case_status_deb121_idx"), + ), + migrations.AddIndex( + model_name="case", + index=models.Index(fields=["priority"], name="case_priorit_bccbdf_idx"), + ), + migrations.AddIndex( + model_name="case", + index=models.Index( + fields=["org", "-created_at"], name="case_org_id_9c4eb6_idx" + ), + ), + migrations.AddIndex( + model_name="solution", + index=models.Index(fields=["status"], name="solution_status_c9b160_idx"), + ), + migrations.AddIndex( + model_name="solution", + index=models.Index( + fields=["is_published"], name="solution_is_publ_2f7331_idx" + ), + ), + migrations.AddIndex( + model_name="solution", + index=models.Index(fields=["org"], name="solution_org_id_577c5c_idx"), + ), + ] diff --git a/cases/migrations/__init__.py b/backend/cases/migrations/__init__.py similarity index 100% rename from cases/migrations/__init__.py rename to backend/cases/migrations/__init__.py diff --git a/backend/cases/models.py b/backend/cases/models.py new file mode 100644 index 00000000..a46df4ae --- /dev/null +++ b/backend/cases/models.py @@ -0,0 +1,110 @@ +import arrow +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import pgettext_lazy + +from accounts.models import Account +from common.base import AssignableMixin, BaseModel +from common.models import Org, Profile, Teams +from common.utils import CASE_TYPE, PRIORITY_CHOICE, STATUS_CHOICE +from contacts.models import Contact + + +class Case(AssignableMixin, BaseModel): + name = models.CharField(pgettext_lazy("Name of the case", "Name"), max_length=64) + status = models.CharField(choices=STATUS_CHOICE, max_length=64) + priority = models.CharField(choices=PRIORITY_CHOICE, max_length=64) + case_type = models.CharField( + choices=CASE_TYPE, max_length=255, blank=True, null=True, default="" + ) + account = models.ForeignKey( + Account, + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="accounts_cases", + ) + contacts = models.ManyToManyField(Contact) + # closed_on = models.DateTimeField() + closed_on = models.DateField(blank=True, null=True) + description = models.TextField(blank=True, null=True) + assigned_to = models.ManyToManyField(Profile, related_name="case_assigned_users") + # created_by = models.ForeignKey( + # Profile, related_name="case_created_by", on_delete=models.SET_NULL, null=True + # ) + is_active = models.BooleanField(default=True) + teams = models.ManyToManyField(Teams, related_name="cases_teams") + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="cases") + + class Meta: + verbose_name = "Case" + verbose_name_plural = "Cases" + db_table = "case" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["status"]), + models.Index(fields=["priority"]), + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return f"{self.name}" + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() + + +class Solution(BaseModel): + """ + Knowledge Base Solution + + Solutions are reusable answers/guides that can be linked to cases. + They form a knowledge base for common issues and their resolutions. + """ + + title = models.CharField(max_length=255) + description = models.TextField() + + STATUS_CHOICES = [ + ("draft", "Draft"), + ("reviewed", "Reviewed"), + ("approved", "Approved"), + ] + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft") + is_published = models.BooleanField(default=False) + + # Organization relation + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="solutions") + + # Cases that use this solution + cases = models.ManyToManyField(Case, related_name="solutions", blank=True) + + class Meta: + verbose_name = "Solution" + verbose_name_plural = "Solutions" + db_table = "solution" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["status"]), + models.Index(fields=["is_published"]), + models.Index(fields=["org"]), + ] + + def __str__(self): + return self.title + + def publish(self): + """Publish the solution (must be approved first)""" + if self.status == "approved": + self.is_published = True + self.save() + + def unpublish(self): + """Unpublish the solution""" + self.is_published = False + self.save() + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() diff --git a/cases/serializer.py b/backend/cases/serializer.py similarity index 95% rename from cases/serializer.py rename to backend/cases/serializer.py index b0926249..b1b5a1c8 100644 --- a/cases/serializer.py +++ b/backend/cases/serializer.py @@ -2,9 +2,13 @@ from rest_framework import serializers from accounts.serializer import AccountSerializer from cases.models import Case -from common.serializer import OrganizationSerializer, ProfileSerializer,UserSerializer +from common.serializer import ( + OrganizationSerializer, + ProfileSerializer, + TeamsSerializer, + UserSerializer, +) from contacts.serializer import ContactSerializer -from teams.serializer import TeamsSerializer class CaseSerializer(serializers.ModelSerializer): @@ -74,6 +78,7 @@ class CaseCreateSerializer(serializers.ModelSerializer): "created_on_arrow", ) + class CaseCreateSwaggerSerializer(serializers.ModelSerializer): class Meta: model = Case @@ -91,10 +96,11 @@ class CaseCreateSwaggerSerializer(serializers.ModelSerializer): "description", ) + class CaseDetailEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() case_attachment = serializers.FileField() + class CaseCommentEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() - diff --git a/backend/cases/solution_serializers.py b/backend/cases/solution_serializers.py new file mode 100644 index 00000000..4cf45577 --- /dev/null +++ b/backend/cases/solution_serializers.py @@ -0,0 +1,94 @@ +""" +Solution (Knowledge Base) Serializers +""" + +from rest_framework import serializers + +from cases.models import Case, Solution +from common.serializer import OrganizationSerializer + + +class SolutionSerializer(serializers.ModelSerializer): + """Serializer for Solution model""" + + org = OrganizationSerializer(read_only=True) + case_count = serializers.SerializerMethodField() + + class Meta: + model = Solution + fields = [ + "id", + "title", + "description", + "status", + "is_published", + "org", + "case_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + ] + read_only_fields = [ + "id", + "org", + "created_at", + "updated_at", + "created_by", + "updated_by", + ] + + def get_case_count(self, obj): + """Get number of cases using this solution""" + return obj.cases.count() + + +class SolutionCreateUpdateSerializer(serializers.ModelSerializer): + """Serializer for creating/updating solutions""" + + class Meta: + model = Solution + fields = [ + "title", + "description", + "status", + "is_published", + ] + + def validate_is_published(self, value): + """Only approved solutions can be published""" + if value and self.instance: + if self.instance.status != "approved": + raise serializers.ValidationError( + "Only approved solutions can be published. Please set status to 'approved' first." + ) + return value + + +class SolutionDetailSerializer(serializers.ModelSerializer): + """Detailed serializer for solution with linked cases""" + + org = OrganizationSerializer(read_only=True) + linked_cases = serializers.SerializerMethodField() + + class Meta: + model = Solution + fields = [ + "id", + "title", + "description", + "status", + "is_published", + "org", + "linked_cases", + "created_at", + "updated_at", + "created_by", + "updated_by", + ] + + def get_linked_cases(self, obj): + """Get cases that use this solution""" + from cases.serializer import CaseSerializer + + return CaseSerializer(obj.cases.all()[:10], many=True).data diff --git a/backend/cases/solution_views.py b/backend/cases/solution_views.py new file mode 100644 index 00000000..b6e97067 --- /dev/null +++ b/backend/cases/solution_views.py @@ -0,0 +1,236 @@ +""" +Solution (Knowledge Base) Views +""" + +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import status +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from cases.models import Solution +from cases.solution_serializers import ( + SolutionCreateUpdateSerializer, + SolutionDetailSerializer, + SolutionSerializer, +) + + +class SolutionListView(APIView, LimitOffsetPagination): + """ + List and create solutions (Knowledge Base) + """ + + permission_classes = [IsAuthenticated] + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="List all solutions with filters", + parameters=[ + OpenApiParameter( + "status", str, description="Filter by status (draft/reviewed/approved)" + ), + OpenApiParameter( + "is_published", bool, description="Filter by published status" + ), + OpenApiParameter( + "search", str, description="Search in title and description" + ), + ], + responses={200: SolutionSerializer(many=True)}, + ) + def get(self, request): + """List solutions with filters""" + queryset = Solution.objects.filter(org=request.profile.org).order_by( + "-created_at" + ) + + # Filters + status_filter = request.query_params.get("status") + is_published = request.query_params.get("is_published") + search = request.query_params.get("search") + + if status_filter: + queryset = queryset.filter(status=status_filter) + + if is_published is not None: + queryset = queryset.filter(is_published=is_published.lower() == "true") + + if search: + queryset = queryset.filter(title__icontains=search) | queryset.filter( + description__icontains=search + ) + + # Paginate + results = self.paginate_queryset(queryset, request, view=self) + serializer = SolutionSerializer(results, many=True) + + return self.get_paginated_response(serializer.data) + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="Create a new solution", + request=SolutionCreateUpdateSerializer, + responses={201: SolutionSerializer}, + ) + def post(self, request): + """Create a new solution""" + serializer = SolutionCreateUpdateSerializer(data=request.data) + + if serializer.is_valid(): + solution = serializer.save(org=request.profile.org, created_by=request.user) + + response_serializer = SolutionSerializer(solution) + return Response(response_serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class SolutionDetailView(APIView): + """ + Get, update, or delete a solution + """ + + permission_classes = [IsAuthenticated] + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="Get solution details", + responses={200: SolutionDetailSerializer}, + ) + def get(self, request, pk): + """Get solution details""" + try: + solution = Solution.objects.get(pk=pk, org=request.profile.org) + serializer = SolutionDetailSerializer(solution) + return Response(serializer.data) + except Solution.DoesNotExist: + return Response( + {"error": "Solution not found"}, status=status.HTTP_404_NOT_FOUND + ) + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="Update solution", + request=SolutionCreateUpdateSerializer, + responses={200: SolutionSerializer}, + ) + def put(self, request, pk): + """Update solution""" + try: + solution = Solution.objects.get(pk=pk, org=request.profile.org) + + serializer = SolutionCreateUpdateSerializer(solution, data=request.data) + if serializer.is_valid(): + solution = serializer.save(updated_by=request.user) + response_serializer = SolutionSerializer(solution) + return Response(response_serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Solution.DoesNotExist: + return Response( + {"error": "Solution not found"}, status=status.HTTP_404_NOT_FOUND + ) + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="Partially update solution", + request=SolutionCreateUpdateSerializer, + responses={200: SolutionSerializer}, + ) + def patch(self, request, pk): + """Partially update solution""" + try: + solution = Solution.objects.get(pk=pk, org=request.profile.org) + + serializer = SolutionCreateUpdateSerializer( + solution, data=request.data, partial=True + ) + if serializer.is_valid(): + solution = serializer.save(updated_by=request.user) + response_serializer = SolutionSerializer(solution) + return Response(response_serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Solution.DoesNotExist: + return Response( + {"error": "Solution not found"}, status=status.HTTP_404_NOT_FOUND + ) + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="Delete solution", + responses={204: None}, + ) + def delete(self, request, pk): + """Delete solution""" + try: + solution = Solution.objects.get(pk=pk, org=request.profile.org) + solution.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except Solution.DoesNotExist: + return Response( + {"error": "Solution not found"}, status=status.HTTP_404_NOT_FOUND + ) + + +class SolutionPublishView(APIView): + """ + Publish or unpublish a solution + """ + + permission_classes = [IsAuthenticated] + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="Publish an approved solution", + responses={200: SolutionSerializer}, + ) + def post(self, request, pk): + """Publish solution""" + try: + solution = Solution.objects.get(pk=pk, org=request.profile.org) + + if solution.status != "approved": + return Response( + { + "error": "Only approved solutions can be published. Please approve it first." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + solution.publish() + serializer = SolutionSerializer(solution) + return Response(serializer.data) + + except Solution.DoesNotExist: + return Response( + {"error": "Solution not found"}, status=status.HTTP_404_NOT_FOUND + ) + + +class SolutionUnpublishView(APIView): + """ + Unpublish a solution + """ + + permission_classes = [IsAuthenticated] + + @extend_schema( + tags=["Solutions (Knowledge Base)"], + description="Unpublish a solution", + responses={200: SolutionSerializer}, + ) + def post(self, request, pk): + """Unpublish solution""" + try: + solution = Solution.objects.get(pk=pk, org=request.profile.org) + solution.unpublish() + serializer = SolutionSerializer(solution) + return Response(serializer.data) + + except Solution.DoesNotExist: + return Response( + {"error": "Solution not found"}, status=status.HTTP_404_NOT_FOUND + ) diff --git a/cases/swagger_params1.py b/backend/cases/swagger_params.py similarity index 58% rename from cases/swagger_params1.py rename to backend/cases/swagger_params.py index 13b1dc67..4380edaf 100644 --- a/cases/swagger_params1.py +++ b/backend/cases/swagger_params.py @@ -11,8 +11,8 @@ organization_params = [ cases_list_get_params = [ organization_params_in_header, - OpenApiParameter("name", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("status", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("priority", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("account", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("name", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("status", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("priority", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("account", OpenApiTypes.STR, OpenApiParameter.QUERY), ] diff --git a/cases/tasks.py b/backend/cases/tasks.py similarity index 100% rename from cases/tasks.py rename to backend/cases/tasks.py diff --git a/cases/tests_celery_tasks.py b/backend/cases/tests_celery_tasks.py similarity index 100% rename from cases/tests_celery_tasks.py rename to backend/cases/tests_celery_tasks.py diff --git a/backend/cases/urls.py b/backend/cases/urls.py new file mode 100644 index 00000000..42430090 --- /dev/null +++ b/backend/cases/urls.py @@ -0,0 +1,32 @@ +from django.urls import path + +from cases import solution_views, views + +app_name = "api_cases" + +urlpatterns = [ + # Cases endpoints + path("", views.CaseListView.as_view()), + path("/", views.CaseDetailView.as_view()), + path("comment//", views.CaseCommentView.as_view()), + path("attachment//", views.CaseAttachmentView.as_view()), + # Solutions (Knowledge Base) endpoints + path( + "solutions/", solution_views.SolutionListView.as_view(), name="solutions_list" + ), + path( + "solutions//", + solution_views.SolutionDetailView.as_view(), + name="solution_detail", + ), + path( + "solutions//publish/", + solution_views.SolutionPublishView.as_view(), + name="solution_publish", + ), + path( + "solutions//unpublish/", + solution_views.SolutionUnpublishView.as_view(), + name="solution_unpublish", + ), +] diff --git a/cases/views.py b/backend/cases/views.py similarity index 79% rename from cases/views.py rename to backend/cases/views.py index 9d1d1bb9..8b47022a 100644 --- a/cases/views.py +++ b/backend/cases/views.py @@ -1,5 +1,6 @@ import json +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema from rest_framework import status @@ -10,40 +11,50 @@ from rest_framework.views import APIView from accounts.models import Account from accounts.serializer import AccountSerializer -from cases import swagger_params1 +from cases import swagger_params from cases.models import Case -from cases.serializer import CaseCreateSerializer, CaseSerializer,CaseCreateSwaggerSerializer,CaseDetailEditSwaggerSerializer,CaseCommentEditSwaggerSerializer +from cases.serializer import ( + CaseCommentEditSwaggerSerializer, + CaseCreateSerializer, + CaseCreateSwaggerSerializer, + CaseDetailEditSwaggerSerializer, + CaseSerializer, +) from cases.tasks import send_email_to_assigned_user -from common.models import Attachments, Comment, Profile +from common.models import Attachments, Comment, Profile, Teams -#from common.external_auth import CustomDualAuthentication +# from common.external_auth import CustomDualAuthentication from common.serializer import AttachmentsSerializer, CommentSerializer from common.utils import CASE_TYPE, PRIORITY_CHOICE, STATUS_CHOICE from contacts.models import Contact from contacts.serializer import ContactSerializer -from teams.models import Teams class CaseListView(APIView, LimitOffsetPagination): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Case def get_context_data(self, **kwargs): params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") + queryset = self.model.objects.filter(org=self.request.profile.org).order_by( + "-id" + ) accounts = Account.objects.filter(org=self.request.profile.org).order_by("-id") contacts = Contact.objects.filter(org=self.request.profile.org).order_by("-id") profiles = Profile.objects.filter(is_active=True, org=self.request.profile.org) if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: queryset = queryset.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) ).distinct() accounts = accounts.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) ).distinct() contacts = contacts.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) ).distinct() profiles = profiles.filter(role="ADMIN") @@ -82,15 +93,15 @@ class CaseListView(APIView, LimitOffsetPagination): context["contacts_list"] = ContactSerializer(contacts, many=True).data return context - @extend_schema( - tags=["Cases"], parameters=swagger_params1.cases_list_get_params - ) + @extend_schema(tags=["Cases"], parameters=swagger_params.cases_list_get_params) def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return Response(context) @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params,request=CaseCreateSwaggerSerializer + tags=["Cases"], + parameters=swagger_params.organization_params, + request=CaseCreateSwaggerSerializer, ) def post(self, request, *args, **kwargs): params = request.data @@ -105,7 +116,9 @@ class CaseListView(APIView, LimitOffsetPagination): if params.get("contacts"): contacts_list = params.get("contacts") - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) if contacts: cases_obj.contacts.add(*contacts) @@ -137,7 +150,12 @@ class CaseListView(APIView, LimitOffsetPagination): cases_obj.id, ) return Response( - {"error": False, "message": "Case Created Successfully"}, + { + "error": False, + "message": "Case Created Successfully", + "id": str(cases_obj.id), + "cases_obj": CaseSerializer(cases_obj).data, + }, status=status.HTTP_200_OK, ) @@ -148,15 +166,17 @@ class CaseListView(APIView, LimitOffsetPagination): class CaseDetailView(APIView): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Case def get_object(self, pk): - return self.model.objects.filter(id=pk).first() + return self.model.objects.filter(id=pk, org=self.request.profile.org).first() @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params,request=CaseCreateSwaggerSerializer + tags=["Cases"], + parameters=swagger_params.organization_params, + request=CaseCreateSwaggerSerializer, ) def put(self, request, pk, format=None): params = request.data @@ -168,7 +188,7 @@ class CaseDetailView(APIView): ) if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: if not ( - (self.request.profile == cases_object.created_by) + (self.request.profile.user == cases_object.created_by) or (self.request.profile in cases_object.assigned_to.all()) ): return Response( @@ -195,7 +215,9 @@ class CaseDetailView(APIView): cases_object.contacts.clear() if params.get("contacts"): contacts_list = params.get("contacts") - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) if contacts: cases_object.contacts.add(*contacts) @@ -240,9 +262,7 @@ class CaseDetailView(APIView): status=status.HTTP_400_BAD_REQUEST, ) - @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params - ) + @extend_schema(tags=["Cases"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if self.object.org != request.profile.org: @@ -265,11 +285,14 @@ class CaseDetailView(APIView): status=status.HTTP_200_OK, ) - @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params - ) + @extend_schema(tags=["Cases"], parameters=swagger_params.organization_params) def get(self, request, pk, format=None): self.cases = self.get_object(pk=pk) + if not self.cases: + return Response( + {"error": True, "errors": "Case not found"}, + status=status.HTTP_404_NOT_FOUND, + ) if self.cases.org != request.profile.org: return Response( {"error": True, "errors": "User company doesnot match with header...."}, @@ -279,7 +302,7 @@ class CaseDetailView(APIView): context["cases_obj"] = CaseSerializer(self.cases).data if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: if not ( - (self.request.profile == self.cases.created_by) + (self.request.profile.user == self.cases.created_by) or (self.request.profile in self.cases.assigned_to.all()) ): return Response( @@ -293,7 +316,7 @@ class CaseDetailView(APIView): comment_permission = False if ( - self.request.profile == self.cases.created_by + self.request.profile.user == self.cases.created_by or self.request.profile.is_admin or self.request.profile.role == "ADMIN" ): @@ -301,9 +324,9 @@ class CaseDetailView(APIView): if self.request.profile.is_admin or self.request.profile.role == "ADMIN": users_mention = list( - Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "user__email" - ) + Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("user__email") ) elif self.request.profile != self.cases.created_by: if self.cases.created_by: @@ -313,8 +336,17 @@ class CaseDetailView(APIView): else: users_mention = [] - attachments = Attachments.objects.filter(case=self.cases).order_by("-id") - comments = Comment.objects.filter(case=self.cases).order_by("-id") + case_content_type = ContentType.objects.get_for_model(Case) + attachments = Attachments.objects.filter( + content_type=case_content_type, + object_id=self.cases.id, + org=self.request.profile.org, + ).order_by("-id") + comments = Comment.objects.filter( + content_type=case_content_type, + object_id=self.cases.id, + org=self.request.profile.org, + ).order_by("-id") context.update( { @@ -333,11 +365,13 @@ class CaseDetailView(APIView): return Response(context) @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params,request=CaseDetailEditSwaggerSerializer + tags=["Cases"], + parameters=swagger_params.organization_params, + request=CaseDetailEditSwaggerSerializer, ) def post(self, request, pk, **kwargs): params = request.data - self.cases_obj = Case.objects.get(pk=pk) + self.cases_obj = Case.objects.get(pk=pk, org=request.profile.org) if self.cases_obj.org != request.profile.org: return Response( {"error": True, "errors": "User company doesnot match with header...."}, @@ -347,7 +381,7 @@ class CaseDetailView(APIView): comment_serializer = CommentSerializer(data=params) if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: if not ( - (self.request.profile == self.cases_obj.created_by) + (self.request.profile.user == self.cases_obj.created_by) or (self.request.profile in self.cases_obj.assigned_to.all()) ): return Response( @@ -372,8 +406,17 @@ class CaseDetailView(APIView): attachment.attachment = self.request.FILES.get("case_attachment") attachment.save() - attachments = Attachments.objects.filter(case=self.cases_obj).order_by("-id") - comments = Comment.objects.filter(case=self.cases_obj).order_by("-id") + case_content_type = ContentType.objects.get_for_model(Case) + attachments = Attachments.objects.filter( + content_type=case_content_type, + object_id=self.cases_obj.id, + org=request.profile.org, + ).order_by("-id") + comments = Comment.objects.filter( + content_type=case_content_type, + object_id=self.cases_obj.id, + org=request.profile.org, + ).order_by("-id") context.update( { @@ -387,14 +430,16 @@ class CaseDetailView(APIView): class CaseCommentView(APIView): model = Comment - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) def get_object(self, pk): - return self.model.objects.get(pk=pk) + return self.model.objects.get(pk=pk, org=self.request.profile.org) @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params,request=CaseCommentEditSwaggerSerializer + tags=["Cases"], + parameters=swagger_params.organization_params, + request=CaseCommentEditSwaggerSerializer, ) def put(self, request, pk, format=None): params = request.data @@ -424,9 +469,7 @@ class CaseCommentView(APIView): status=status.HTTP_403_FORBIDDEN, ) - @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params - ) + @extend_schema(tags=["Cases"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if ( @@ -450,12 +493,10 @@ class CaseCommentView(APIView): class CaseAttachmentView(APIView): model = Attachments - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) - @extend_schema( - tags=["Cases"], parameters=swagger_params1.organization_params - ) + @extend_schema(tags=["Cases"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.model.objects.get(pk=pk) if ( diff --git a/cms/__init__.py b/backend/common/__init__.py similarity index 100% rename from cms/__init__.py rename to backend/common/__init__.py diff --git a/common/access_decorators_mixins.py b/backend/common/access_decorators_mixins.py similarity index 100% rename from common/access_decorators_mixins.py rename to backend/common/access_decorators_mixins.py diff --git a/backend/common/admin.py b/backend/common/admin.py new file mode 100644 index 00000000..2b4118ba --- /dev/null +++ b/backend/common/admin.py @@ -0,0 +1,53 @@ +from django.contrib import admin + +from common.models import Address, Comment, CommentFiles, SessionToken, User + +# Register your models here. + +admin.site.register(User) +admin.site.register(Address) +admin.site.register(Comment) +admin.site.register(CommentFiles) + + +@admin.register(SessionToken) +class SessionTokenAdmin(admin.ModelAdmin): + list_display = ( + "user", + "token_jti_short", + "is_active", + "expires_at", + "last_used_at", + "created_at", + ) + list_filter = ("is_active", "expires_at", "created_at") + search_fields = ("user__email", "token_jti", "ip_address") + raw_id_fields = ("user",) + readonly_fields = ( + "token_jti", + "refresh_token_jti", + "created_at", + "last_used_at", + "revoked_at", + ) + date_hierarchy = "created_at" + ordering = ("-created_at",) + actions = ["revoke_tokens", "cleanup_expired"] + + def token_jti_short(self, obj): + return f"{obj.token_jti[:16]}..." + + token_jti_short.short_description = "Token JTI" + + def revoke_tokens(self, request, queryset): + for token in queryset: + token.revoke() + self.message_user(request, f"{queryset.count()} tokens revoked successfully.") + + revoke_tokens.short_description = "Revoke selected tokens" + + def cleanup_expired(self, request, queryset): + count, _ = SessionToken.cleanup_expired() + self.message_user(request, f"{count} expired tokens cleaned up.") + + cleanup_expired.short_description = "Cleanup expired tokens" diff --git a/common/app_urls/__init__.py b/backend/common/app_urls/__init__.py similarity index 65% rename from common/app_urls/__init__.py rename to backend/common/app_urls/__init__.py index 87267c87..2ef9fd50 100644 --- a/common/app_urls/__init__.py +++ b/backend/common/app_urls/__init__.py @@ -1,5 +1,7 @@ from django.urls import include, path +from tasks.urls import board_urlpatterns + app_name = "common_urls" urlpatterns = [ path("", include(("common.urls"))), @@ -7,8 +9,11 @@ urlpatterns = [ path("contacts/", include("contacts.urls", namespace="api_contacts")), path("leads/", include("leads.urls", namespace="api_leads")), path("opportunities/", include("opportunity.urls", namespace="api_opportunities")), - path("teams/", include("teams.urls", namespace="api_teams")), + # Teams URLs are now in common app at /api/teams/ path("tasks/", include("tasks.urls", namespace="api_tasks")), - path("events/", include("events.urls", namespace="api_events")), path("cases/", include("cases.urls", namespace="api_cases")), + path("invoices/", include("invoices.api_urls", namespace="api_invoices")), + path( + "boards/", include((board_urlpatterns, "api_boards")) + ), # Kanban Boards (merged into tasks app) ] diff --git a/backend/common/apps.py b/backend/common/apps.py new file mode 100644 index 00000000..5447e19c --- /dev/null +++ b/backend/common/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + name = "common" + + def ready(self): + # Import signals to register them + import common.signals # noqa: F401 diff --git a/backend/common/audit_log.py b/backend/common/audit_log.py new file mode 100644 index 00000000..70f69000 --- /dev/null +++ b/backend/common/audit_log.py @@ -0,0 +1,307 @@ +""" +Security Audit Logging for Multi-Tenancy + +Tracks security-relevant events for compliance and incident investigation: +- Login attempts (success/failure) +- Org switches +- Permission denials +- API key usage +- Suspicious activities + +Usage: + from common.audit_log import audit_log + + audit_log.login_success(user, org, request) + audit_log.org_switch(user, from_org, to_org, request) + audit_log.permission_denied(user, org, action, resource, request) +""" + +import logging + +from django.db import models +from django.utils import timezone + +from common.base import BaseModel + +logger = logging.getLogger("security.audit") + + +class SecurityAuditLog(BaseModel): + """ + Model to store security audit events. + + This is separate from Activity model which tracks business operations. + SecurityAuditLog tracks authentication and authorization events. + """ + + EVENT_TYPES = ( + ("LOGIN_SUCCESS", "Login Success"), + ("LOGIN_FAILURE", "Login Failure"), + ("LOGOUT", "Logout"), + ("ORG_SWITCH", "Organization Switch"), + ("TOKEN_REFRESH", "Token Refresh"), + ("TOKEN_REVOKED", "Token Revoked"), + ("PERMISSION_DENIED", "Permission Denied"), + ("CROSS_ORG_ATTEMPT", "Cross-Org Access Attempt"), + ("API_KEY_USED", "API Key Used"), + ("API_KEY_INVALID", "Invalid API Key"), + ("MEMBERSHIP_REVOKED", "Membership Revoked"), + ("SUSPICIOUS_ACTIVITY", "Suspicious Activity"), + ) + + event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True) + user = models.ForeignKey( + "common.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="security_audit_logs", + ) + org = models.ForeignKey( + "common.Org", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="security_audit_logs", + ) + + # Event details + description = models.TextField(blank=True, default="") + metadata = models.JSONField(default=dict, blank=True) + + # Request information + ip_address = models.GenericIPAddressField(null=True, blank=True) + user_agent = models.TextField(blank=True, default="") + request_path = models.CharField(max_length=500, blank=True, default="") + request_method = models.CharField(max_length=10, blank=True, default="") + + # Outcome + success = models.BooleanField(default=True) + + class Meta: + verbose_name = "Security Audit Log" + verbose_name_plural = "Security Audit Logs" + db_table = "security_audit_log" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["event_type", "-created_at"]), + models.Index(fields=["user", "-created_at"]), + models.Index(fields=["org", "-created_at"]), + models.Index(fields=["ip_address"]), + models.Index(fields=["success", "-created_at"]), + ] + + def __str__(self): + return f"{self.event_type} - {self.user} - {self.created_at}" + + +class AuditLogger: + """ + Helper class for logging security events. + + Logs to both database (SecurityAuditLog) and Python logger. + """ + + def _get_request_info(self, request): + """Extract request information for logging.""" + if not request: + return {} + + # Get IP address + ip = request.META.get("HTTP_X_FORWARDED_FOR") + if ip: + ip = ip.split(",")[0].strip() + else: + ip = request.META.get("REMOTE_ADDR") + + return { + "ip_address": ip, + "user_agent": request.META.get("HTTP_USER_AGENT", "")[:500], + "request_path": request.path[:500], + "request_method": request.method, + } + + def _log( + self, + event_type, + user=None, + org=None, + description="", + metadata=None, + success=True, + request=None, + ): + """ + Internal method to create audit log entry. + """ + request_info = self._get_request_info(request) + + # Log to database + try: + SecurityAuditLog.objects.create( + event_type=event_type, + user=user, + org=org, + description=description, + metadata=metadata or {}, + success=success, + **request_info, + ) + except Exception as e: + logger.error(f"Failed to create audit log: {e}") + + # Log to Python logger + log_level = logging.INFO if success else logging.WARNING + user_email = user.email if user else "anonymous" + org_name = org.name if org else "none" + + logger.log( + log_level, + f"{event_type} | user={user_email} | org={org_name} | " + f"ip={request_info.get('ip_address')} | {description}", + ) + + def login_success(self, user, org, request=None): + """Log successful login.""" + self._log( + "LOGIN_SUCCESS", + user=user, + org=org, + description=f"User logged in to {org.name if org else 'platform'}", + request=request, + ) + + def login_failure(self, email, reason, request=None): + """Log failed login attempt.""" + from common.models import User + + user = User.objects.filter(email=email).first() + + self._log( + "LOGIN_FAILURE", + user=user, + description=f"Login failed for {email}: {reason}", + metadata={"email": email, "reason": reason}, + success=False, + request=request, + ) + + def logout(self, user, org, request=None): + """Log user logout.""" + self._log( + "LOGOUT", + user=user, + org=org, + description=f"User logged out", + request=request, + ) + + def org_switch(self, user, from_org, to_org, request=None): + """Log organization switch.""" + self._log( + "ORG_SWITCH", + user=user, + org=to_org, + description=f"Switched from {from_org.name if from_org else 'none'} to {to_org.name}", + metadata={ + "from_org_id": str(from_org.id) if from_org else None, + "to_org_id": str(to_org.id), + }, + request=request, + ) + + def token_refresh(self, user, org, request=None): + """Log token refresh.""" + self._log( + "TOKEN_REFRESH", + user=user, + org=org, + description="Token refreshed", + request=request, + ) + + def token_revoked(self, user, org, reason, request=None): + """Log token revocation.""" + self._log( + "TOKEN_REVOKED", + user=user, + org=org, + description=f"Token revoked: {reason}", + metadata={"reason": reason}, + request=request, + ) + + def permission_denied(self, user, org, action, resource, request=None): + """Log permission denial.""" + self._log( + "PERMISSION_DENIED", + user=user, + org=org, + description=f"Permission denied for {action} on {resource}", + metadata={"action": action, "resource": resource}, + success=False, + request=request, + ) + + def cross_org_attempt(self, user, user_org, target_org, resource, request=None): + """Log attempted cross-org data access.""" + self._log( + "CROSS_ORG_ATTEMPT", + user=user, + org=user_org, + description=f"Attempted to access {resource} in org {target_org.name if target_org else 'unknown'}", + metadata={ + "user_org_id": str(user_org.id) if user_org else None, + "target_org_id": str(target_org.id) if target_org else None, + "resource": resource, + }, + success=False, + request=request, + ) + + def api_key_used(self, org, endpoint, request=None): + """Log API key usage.""" + self._log( + "API_KEY_USED", + org=org, + description=f"API key used for {endpoint}", + metadata={"endpoint": endpoint}, + request=request, + ) + + def api_key_invalid(self, api_key_prefix, request=None): + """Log invalid API key attempt.""" + self._log( + "API_KEY_INVALID", + description=f"Invalid API key attempted: {api_key_prefix}...", + metadata={"api_key_prefix": api_key_prefix}, + success=False, + request=request, + ) + + def membership_revoked(self, user, org, revoked_by=None, request=None): + """Log when user's org membership is revoked.""" + self._log( + "MEMBERSHIP_REVOKED", + user=user, + org=org, + description=f"Membership revoked by {revoked_by.email if revoked_by else 'system'}", + metadata={"revoked_by": str(revoked_by.id) if revoked_by else None}, + request=request, + ) + + def suspicious_activity(self, user, org, activity_type, details, request=None): + """Log suspicious activity for security review.""" + self._log( + "SUSPICIOUS_ACTIVITY", + user=user, + org=org, + description=f"Suspicious activity: {activity_type}", + metadata={"activity_type": activity_type, "details": details}, + success=False, + request=request, + ) + + +# Global audit logger instance +audit_log = AuditLogger() diff --git a/backend/common/base.py b/backend/common/base.py new file mode 100644 index 00000000..baaf49ef --- /dev/null +++ b/backend/common/base.py @@ -0,0 +1,153 @@ +import uuid + +# Third party imports +from crum import get_current_user + +# Django imports +from django.db import models + +# Module imports +from common.mixins import AuditModel + + +class AssignableMixin(models.Model): + """ + Mixin for models that have assigned_to (Profile) and teams (Teams) fields. + + Provides helper methods for getting users from teams and assignments. + Models using this mixin must define: + - assigned_to = ManyToManyField(Profile, ...) + - teams = ManyToManyField(Teams, ...) + """ + + class Meta: + abstract = True + + @property + def get_team_users(self): + """Get all users who are members of assigned teams.""" + from common.models import Profile + + team_user_ids = list(self.teams.values_list("users__id", flat=True)) + return Profile.objects.filter(id__in=team_user_ids) + + @property + def get_team_and_assigned_users(self): + """Get all users from both teams and direct assignments.""" + from common.models import Profile + + team_user_ids = list(self.teams.values_list("users__id", flat=True)) + assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) + user_ids = team_user_ids + assigned_user_ids + return Profile.objects.filter(id__in=user_ids) + + @property + def get_assigned_users_not_in_teams(self): + """Get users who are directly assigned but not part of any assigned team.""" + from common.models import Profile + + team_user_ids = list(self.teams.values_list("users__id", flat=True)) + assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) + user_ids = set(assigned_user_ids) - set(team_user_ids) + return Profile.objects.filter(id__in=list(user_ids)) + + +class BaseModel(AuditModel): + id = models.UUIDField( + default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + user = get_current_user() + if user is None or user.is_anonymous: + self.created_by = None + self.updated_by = None + super(BaseModel, self).save(*args, **kwargs) + else: + # Check if the model is being created or updated + if self._state.adding: + # If created only set created_by value: set updated_by to None + self.created_by = user + self.updated_by = None + # If updated only set updated_by value don't touch created_by + self.updated_by = user + super(BaseModel, self).save(*args, **kwargs) + + def __str__(self): + return str(self.id) + + +class OrgScopedQuerySet(models.QuerySet): + """QuerySet with automatic organization filtering support""" + + def for_org(self, org): + """Filter queryset by organization""" + return self.filter(org=org) + + def for_request(self, request): + """Filter queryset by organization from request""" + if hasattr(request, "profile") and request.profile: + return self.filter(org=request.profile.org) + return self.none() + + +class OrgScopedManager(models.Manager): + """Manager that uses OrgScopedQuerySet""" + + def get_queryset(self): + return OrgScopedQuerySet(self.model, using=self._db) + + def for_org(self, org): + return self.get_queryset().for_org(org) + + def for_request(self, request): + return self.get_queryset().for_request(request) + + +class BaseOrgModel(BaseModel): + """ + Base model for all organization-scoped entities. + + All tenant-owned models should inherit from this instead of BaseModel. + This ensures: + - Required org field (no null values) + - Per-org indexes for performance + - OrgScopedManager for easier filtering + + RLS Protection: + --------------- + Models inheriting from BaseOrgModel are protected by PostgreSQL Row-Level + Security (RLS) policies. The database automatically filters rows by the + current organization context (app.current_org session variable). + + RLS Configuration: See common/rls/__init__.py for policy definitions. + Management: Use `python manage.py manage_rls --status` to check status. + + IMPORTANT: RLS is bypassed if the database user is a superuser. + """ + + org = models.ForeignKey( + "common.Org", + on_delete=models.CASCADE, + related_name="%(class)s_set", + help_text="Organization this record belongs to", + ) + + objects = OrgScopedManager() + + class Meta: + abstract = True + indexes = [ + models.Index(fields=["org", "-created_at"]), + ] + + def save(self, *args, **kwargs): + # Validate org is set + if not self.org_id: + from django.core.exceptions import ValidationError + + raise ValidationError("Organization (org) is required for this model") + super().save(*args, **kwargs) diff --git a/backend/common/celery_utils.py b/backend/common/celery_utils.py new file mode 100644 index 00000000..64bd2001 --- /dev/null +++ b/backend/common/celery_utils.py @@ -0,0 +1,280 @@ +""" +Celery utilities for multi-tenancy support. + +Provides org-aware task execution to ensure background tasks +respect organization context and data isolation. +""" + +import logging +from functools import wraps + +from celery import shared_task +from django.db import connection + +logger = logging.getLogger(__name__) + + +class OrgContext: + """ + Context manager for setting organization context in background tasks. + + Usage: + with OrgContext(org_id): + # All queries here will have org context + leads = Lead.objects.filter(org_id=org_id) + + For RLS (when implemented): + Sets PostgreSQL app.current_org parameter for Row-Level Security. + """ + + def __init__(self, org_id): + self.org_id = str(org_id) if org_id else None + + def __enter__(self): + if self.org_id: + # Set PostgreSQL session variable for RLS (future use) + # This will be used when Row-Level Security is enabled + try: + with connection.cursor() as cursor: + cursor.execute("SET app.current_org = %s", [self.org_id]) + except Exception as e: + # RLS might not be configured yet - this is expected + logger.debug(f"Could not set app.current_org: {e}") + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Reset the context + if self.org_id: + try: + with connection.cursor() as cursor: + cursor.execute("RESET app.current_org") + except Exception: + pass + + return False + + +def org_aware_task(func): + """ + Decorator for Celery tasks that require organization context. + + The first argument to the task must be org_id. + + Usage: + @shared_task + @org_aware_task + def my_task(org_id, other_arg): + # org_id is automatically validated and context is set + pass + + Example: + @shared_task + @org_aware_task + def send_org_report(org_id, report_type): + org = Org.objects.get(id=org_id) + # Generate report for this org + pass + """ + + @wraps(func) + def wrapper(org_id, *args, **kwargs): + from common.models import Org + + # Validate org exists + try: + org = Org.objects.get(id=org_id, is_active=True) + except Org.DoesNotExist: + logger.error( + f"Task {func.__name__} failed: Org {org_id} not found or inactive" + ) + raise ValueError(f"Invalid org_id: {org_id}") + + # Execute with org context + with OrgContext(org_id): + logger.info(f"Executing {func.__name__} for org {org_id}") + return func(org_id, *args, **kwargs) + + return wrapper + + +def require_org_id(func): + """ + Simple decorator that ensures org_id is provided to a task. + + Usage: + @shared_task + @require_org_id + def my_task(org_id, data): + pass + """ + + @wraps(func) + def wrapper(*args, **kwargs): + org_id = kwargs.get("org_id") or (args[0] if args else None) + + if not org_id: + raise ValueError(f"Task {func.__name__} requires org_id parameter") + + return func(*args, **kwargs) + + return wrapper + + +# Example org-aware tasks + + +@shared_task +@org_aware_task +def cleanup_expired_sessions(org_id): + """ + Clean up expired session tokens for an organization. + + Usage: + cleanup_expired_sessions.delay(org_id=str(org.id)) + """ + from common.models import SessionToken + + deleted_count, _ = SessionToken.objects.filter( + user__profiles__org_id=org_id, is_active=False + ).delete() + + logger.info(f"Cleaned up {deleted_count} expired sessions for org {org_id}") + return deleted_count + + +@shared_task +@org_aware_task +def generate_org_activity_report(org_id, days=30): + """ + Generate activity report for an organization. + + Usage: + generate_org_activity_report.delay(org_id=str(org.id), days=30) + """ + from datetime import timedelta + + from django.utils import timezone + + from common.models import Activity + + start_date = timezone.now() - timedelta(days=days) + + activities = Activity.objects.filter(org_id=org_id, created_at__gte=start_date) + + report = { + "org_id": org_id, + "period_days": days, + "total_activities": activities.count(), + "by_action": {}, + "by_entity": {}, + } + + for activity in activities: + # Count by action + action = activity.action + report["by_action"][action] = report["by_action"].get(action, 0) + 1 + + # Count by entity type + entity = activity.entity_type + report["by_entity"][entity] = report["by_entity"].get(entity, 0) + 1 + + logger.info( + f"Generated activity report for org {org_id}: {report['total_activities']} activities" + ) + return report + + +@shared_task +def send_email_with_org_context(org_id, user_id, template_name, subject, context): + """ + Send email with organization branding/context. + + Usage: + send_email_with_org_context.delay( + org_id=str(org.id), + user_id=str(user.id), + template_name='welcome_email.html', + subject='Welcome to Our CRM', + context={'custom_key': 'value'} + ) + """ + from django.conf import settings + from django.core.mail import EmailMessage + from django.template.loader import render_to_string + + from common.models import Org, User + + try: + org = Org.objects.get(id=org_id) + user = User.objects.get(id=user_id) + except (Org.DoesNotExist, User.DoesNotExist) as e: + logger.error(f"Email send failed: {e}") + raise + + # Add org context to email template + email_context = { + "org_name": org.name, + "user_email": user.email, + "domain": settings.DOMAIN_NAME, + **context, + } + + html_content = render_to_string(template_name, context=email_context) + + msg = EmailMessage( + subject, + html_content, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[user.email], + ) + msg.content_subtype = "html" + msg.send() + + logger.info(f"Sent email '{subject}' to {user.email} for org {org.name}") + + +# Batch processing utilities + + +@shared_task +@org_aware_task +def batch_process_org_data(org_id, model_name, processor_func, batch_size=100): + """ + Process org data in batches to avoid memory issues. + + Usage: + batch_process_org_data.delay( + org_id=str(org.id), + model_name='leads.Lead', + processor_func='leads.tasks.process_lead', + batch_size=100 + ) + """ + from importlib import import_module + + from django.apps import apps + + # Get model + app_label, model_class = model_name.rsplit(".", 1) + Model = apps.get_model(app_label, model_class) + + # Get processor function + module_path, func_name = processor_func.rsplit(".", 1) + module = import_module(module_path) + processor = getattr(module, func_name) + + # Process in batches + queryset = Model.objects.filter(org_id=org_id) + total = queryset.count() + processed = 0 + + for i in range(0, total, batch_size): + batch = queryset[i : i + batch_size] + for obj in batch: + processor(obj) + processed += 1 + + logger.info(f"Processed {processed}/{total} {model_name} for org {org_id}") + + return processed diff --git a/cms/migrations/__init__.py b/backend/common/context_processors/__init__.py similarity index 100% rename from cms/migrations/__init__.py rename to backend/common/context_processors/__init__.py diff --git a/common/context_processors/common.py b/backend/common/context_processors/common.py similarity index 100% rename from common/context_processors/common.py rename to backend/common/context_processors/common.py diff --git a/common/custom_auth.py b/backend/common/custom_auth.py similarity index 100% rename from common/custom_auth.py rename to backend/common/custom_auth.py diff --git a/common/custom_openapi.py b/backend/common/custom_openapi.py similarity index 79% rename from common/custom_openapi.py rename to backend/common/custom_openapi.py index 8cef92a0..31a9c28d 100644 --- a/common/custom_openapi.py +++ b/backend/common/custom_openapi.py @@ -1,6 +1,6 @@ def preprocessing_filter_spec(endpoints): filtered = [] - for (path, path_regex, method, callback) in endpoints: + for path, path_regex, method, callback in endpoints: # Remove all but DRF API endpoints if path.startswith("/api/"): filtered.append((path, path_regex, method, callback)) diff --git a/common/external_auth.py b/backend/common/external_auth.py similarity index 62% rename from common/external_auth.py rename to backend/common/external_auth.py index 1a9e499f..2813cdba 100644 --- a/common/external_auth.py +++ b/backend/common/external_auth.py @@ -1,20 +1,27 @@ import jwt +from django.conf import settings from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request -from common.models import Org,Profile,User -from django.conf import settings + +from common.models import Org, Profile, User + def verify_jwt_token(token): - secret_key = (settings.SECRET_KEY) # Replace with your secret key used for token encoding/decoding + secret_key = ( + settings.SECRET_KEY + ) # Replace with your secret key used for token encoding/decoding try: - payload = jwt.decode(token, (settings.SECRET_KEY), algorithms=[settings.JWT_ALGO]) + payload = jwt.decode( + token, (settings.SECRET_KEY), algorithms=[settings.JWT_ALGO] + ) return True, payload except jwt.ExpiredSignatureError: return False, "Token is expired" except jwt.InvalidTokenError: return False, "Invalid token" + class CustomDualAuthentication(BaseAuthentication): def authenticate(self, request): @@ -22,31 +29,37 @@ class CustomDualAuthentication(BaseAuthentication): profile = None # Check JWT authentication - jwt_token = request.headers.get('Authorization', '').split(' ')[1] if 'Authorization' in request.headers else None + jwt_token = ( + request.headers.get("Authorization", "").split(" ")[1] + if "Authorization" in request.headers + else None + ) if jwt_token: is_valid, jwt_payload = verify_jwt_token(jwt_token) if is_valid: - jwt_user = (User.objects.get(id=jwt_payload['user_id']), True) - if jwt_payload['user_id'] is not None: + jwt_user = (User.objects.get(id=jwt_payload["user_id"]), True) + if jwt_payload["user_id"] is not None: if request.headers.get("org"): profile = Profile.objects.get( - user_id=jwt_payload['user_id'], org=request.headers.get("org"), is_active=True + user_id=jwt_payload["user_id"], + org=request.headers.get("org"), + is_active=True, ) if profile: request.profile = profile # Check API key authentication - api_key = request.headers.get('Token') # Get API key from request query params + api_key = request.headers.get("Token") # Get API key from request query params if api_key: try: organization = Org.objects.get(api_key=api_key) api_key_user = organization - request.META['org'] = api_key_user.id + request.META["org"] = api_key_user.id profile = Profile.objects.filter(org=api_key_user, role="ADMIN").first() request.profile = profile profile = (profile.user, True) except Org.DoesNotExist: - raise AuthenticationFailed('Invalid API Key') + raise AuthenticationFailed("Invalid API Key") # Select the appropriate user based on authentication method # Return the user if any authentication method succeeded diff --git a/common/__init__.py b/backend/common/management/__init__.py similarity index 100% rename from common/__init__.py rename to backend/common/management/__init__.py diff --git a/common/context_processors/__init__.py b/backend/common/management/commands/__init__.py similarity index 100% rename from common/context_processors/__init__.py rename to backend/common/management/commands/__init__.py diff --git a/backend/common/management/commands/audit_org_fields.py b/backend/common/management/commands/audit_org_fields.py new file mode 100644 index 00000000..53182fa1 --- /dev/null +++ b/backend/common/management/commands/audit_org_fields.py @@ -0,0 +1,227 @@ +""" +Management command to audit and fix nullable org fields. + +This is part of Phase 1 multi-tenancy hardening - ensures all org-scoped +models have proper org field values before adding NOT NULL constraints. + +Usage: + python manage.py audit_org_fields --check # Just check for issues + python manage.py audit_org_fields --fix # Attempt to fix issues + python manage.py audit_org_fields --verbose # Show detailed output +""" + +from django.apps import apps +from django.core.management.base import BaseCommand +from django.db import connection + + +class Command(BaseCommand): + help = "Audit and report on models with nullable org fields" + + def add_arguments(self, parser): + parser.add_argument( + "--check", + action="store_true", + help="Check for null org values without making changes", + ) + parser.add_argument( + "--fix", + action="store_true", + help="Attempt to backfill null org values from related objects", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Show detailed information about each model", + ) + + def handle(self, *args, **options): + check_only = options["check"] + fix = options["fix"] + verbose = options["verbose"] + + self.stdout.write( + self.style.NOTICE("\n=== Multi-Tenancy Org Field Audit ===\n") + ) + + # Models that should have org but currently allow null + models_with_nullable_org = [ + ("common", "Address"), + ("common", "Tags"), + ] + + # Models with proper org fields (for reference) + models_with_required_org = [ + ("common", "Comment"), + ("common", "Attachments"), + ("common", "Document"), + ("common", "Activity"), + ("common", "Teams"), + ("common", "APISettings"), + ("accounts", "Account"), + ("leads", "Lead"), + ("contacts", "Contact"), + ("opportunity", "Opportunity"), + ("cases", "Case"), + ("tasks", "Task"), + ("invoices", "Invoice"), + ] + + issues_found = [] + + # Check models with nullable org + self.stdout.write( + self.style.WARNING("Checking models with nullable org fields:\n") + ) + + for app_label, model_name in models_with_nullable_org: + try: + model = apps.get_model(app_label, model_name) + total_count = model.objects.count() + null_count = model.objects.filter(org__isnull=True).count() + + if null_count > 0: + issues_found.append( + (app_label, model_name, null_count, total_count) + ) + self.stdout.write( + f" ❌ {app_label}.{model_name}: " + f"{null_count}/{total_count} records have NULL org" + ) + else: + self.stdout.write( + f" ✅ {app_label}.{model_name}: " + f"All {total_count} records have org set" + ) + + if verbose: + self._show_model_details(model) + + except LookupError: + self.stdout.write( + self.style.ERROR(f" ⚠️ {app_label}.{model_name}: Model not found") + ) + + # Check models with required org + self.stdout.write( + self.style.SUCCESS("\nChecking models with required org fields:\n") + ) + + for app_label, model_name in models_with_required_org: + try: + model = apps.get_model(app_label, model_name) + total_count = model.objects.count() + + # Check if any null orgs exist (shouldn't happen but let's verify) + if hasattr(model, "org"): + null_count = model.objects.filter(org__isnull=True).count() + if null_count > 0: + issues_found.append( + (app_label, model_name, null_count, total_count) + ) + self.stdout.write( + f" ❌ {app_label}.{model_name}: " + f"{null_count}/{total_count} records have NULL org (UNEXPECTED)" + ) + else: + self.stdout.write( + f" ✅ {app_label}.{model_name}: {total_count} records OK" + ) + + except LookupError: + if verbose: + self.stdout.write( + self.style.WARNING( + f" ⚠️ {app_label}.{model_name}: Model not found" + ) + ) + + # Summary + self.stdout.write(self.style.NOTICE("\n=== Summary ===\n")) + + if issues_found: + self.stdout.write( + self.style.ERROR( + f"Found {len(issues_found)} models with NULL org values:\n" + ) + ) + for app_label, model_name, null_count, total_count in issues_found: + self.stdout.write(f" - {app_label}.{model_name}: {null_count} records") + + if fix: + self.stdout.write(self.style.WARNING("\nAttempting to fix issues...\n")) + self._fix_null_orgs(issues_found) + else: + self.stdout.write( + self.style.NOTICE( + "\nRun with --fix to attempt automatic backfill, " + "or manually update records before adding NOT NULL constraints." + ) + ) + else: + self.stdout.write( + self.style.SUCCESS( + "All models have proper org values! Ready for NOT NULL migration." + ) + ) + + # Recommendations + self.stdout.write(self.style.NOTICE("\n=== Next Steps ===\n")) + self.stdout.write( + "1. Fix any NULL org values using --fix or manually\n" + "2. Create migration to set org fields to NOT NULL:\n" + " - Address.org: null=False, blank=False\n" + " - Tags.org: null=False, blank=False\n" + "3. Run: python manage.py makemigrations common\n" + "4. Run: python manage.py migrate\n" + ) + + def _show_model_details(self, model): + """Show detailed information about a model's org field configuration.""" + org_field = model._meta.get_field("org") + self.stdout.write( + f" Field config: null={org_field.null}, blank={org_field.blank}, " + f"related_name='{org_field.related_query_name()}'" + ) + + def _fix_null_orgs(self, issues): + """Attempt to fix NULL org values by inferring from related objects.""" + from common.models import Address, Profile, Tags + + for app_label, model_name, null_count, total_count in issues: + model = apps.get_model(app_label, model_name) + + if model_name == "Address": + # Address can be inferred from Profile.address relationship + fixed = 0 + for address in Address.objects.filter(org__isnull=True): + # Find profile that references this address + profile = Profile.objects.filter(address=address).first() + if profile and profile.org: + address.org = profile.org + address.save() + fixed += 1 + + self.stdout.write(f" Fixed {fixed}/{null_count} Address records") + if fixed < null_count: + self.stdout.write( + self.style.WARNING( + f" {null_count - fixed} records couldn't be fixed - " + "no related Profile found. Consider deleting orphaned records." + ) + ) + + elif model_name == "Tags": + # Tags are harder - might need to check which entities use them + self.stdout.write( + self.style.WARNING( + f" Tags with NULL org need manual review. " + "Consider checking Account.tags, Lead.tags, etc. for usage." + ) + ) + + else: + self.stdout.write( + f" {model_name}: Automatic fix not implemented. " + "Please fix manually." + ) diff --git a/backend/common/management/commands/manage_rls.py b/backend/common/management/commands/manage_rls.py new file mode 100644 index 00000000..f11cbda6 --- /dev/null +++ b/backend/common/management/commands/manage_rls.py @@ -0,0 +1,189 @@ +""" +Management command to check Row-Level Security (RLS) status. + +Usage: + python manage.py manage_rls --status # Check RLS status + python manage.py manage_rls --test # Test RLS is working + python manage.py manage_rls --verify-user # Verify DB user is not superuser + +RLS Configuration: See common/rls/__init__.py for centralized policy definitions. +RLS is enabled/disabled via Django migrations, not this command. +""" + +from django.core.management.base import BaseCommand +from django.db import connection + +from common.rls import RLS_CONFIG, get_check_rls_status_sql, get_set_context_sql + + +class Command(BaseCommand): + help = "Check Row-Level Security (RLS) status for multi-tenancy" + + # Use centralized RLS configuration + ORG_SCOPED_TABLES = RLS_CONFIG["tables"] + + def add_arguments(self, parser): + parser.add_argument( + "--status", action="store_true", help="Check RLS status for all tables" + ) + parser.add_argument( + "--test", action="store_true", help="Test that RLS is working correctly" + ) + parser.add_argument( + "--verify-user", + action="store_true", + help="Verify database user is not a superuser", + ) + + def handle(self, *args, **options): + if connection.vendor != "postgresql": + self.stderr.write(self.style.ERROR("RLS is only supported on PostgreSQL")) + return + + if options["status"]: + self.check_status() + elif options["test"]: + self.test_rls() + elif options["verify_user"]: + self.verify_user() + else: + self.check_status() + + def check_status(self): + """Check RLS status for all org-scoped tables.""" + self.stdout.write(self.style.MIGRATE_HEADING("RLS Status:")) + self.stdout.write("") + + with connection.cursor() as cursor: + # Check if current user is superuser + cursor.execute( + "SELECT current_user, usesuper FROM pg_user WHERE usename = current_user" + ) + user, is_super = cursor.fetchone() + + if is_super: + self.stdout.write( + self.style.WARNING( + f' Database user "{user}" is a SUPERUSER - RLS will be bypassed!' + ) + ) + else: + self.stdout.write( + self.style.SUCCESS( + f' Database user "{user}" is not a superuser - RLS will be enforced' + ) + ) + + self.stdout.write("") + + enabled_count = 0 + disabled_count = 0 + + for table in self.ORG_SCOPED_TABLES: + cursor.execute(get_check_rls_status_sql(), [table]) + + result = cursor.fetchone() + if result: + rls_enabled, rls_forced = result + if rls_enabled: + status = self.style.SUCCESS("ENABLED") + if rls_forced: + status += " (forced)" + enabled_count += 1 + else: + status = self.style.WARNING("disabled") + disabled_count += 1 + else: + status = self.style.ERROR("TABLE NOT FOUND") + + self.stdout.write(f" {table}: {status}") + + self.stdout.write("") + self.stdout.write(f" Enabled: {enabled_count}, Disabled: {disabled_count}") + + def test_rls(self): + """Test that RLS is working correctly.""" + self.stdout.write(self.style.MIGRATE_HEADING("Testing RLS...")) + + set_context_sql = get_set_context_sql() + + with connection.cursor() as cursor: + # Get two different orgs + cursor.execute("SELECT id FROM company LIMIT 2") + orgs = cursor.fetchall() + + if len(orgs) < 2: + self.stdout.write( + self.style.WARNING("Need at least 2 orgs to test RLS. Skipping.") + ) + return + + org_a = str(orgs[0][0]) + org_b = str(orgs[1][0]) + + # Test with org_a context + cursor.execute(set_context_sql, [org_a]) + cursor.execute("SELECT COUNT(*) FROM lead") + count_a = cursor.fetchone()[0] + + # Test with org_b context + cursor.execute(set_context_sql, [org_b]) + cursor.execute("SELECT COUNT(*) FROM lead") + count_b = cursor.fetchone()[0] + + # Test with no context + cursor.execute(set_context_sql, [""]) + cursor.execute("SELECT COUNT(*) FROM lead") + count_none = cursor.fetchone()[0] + + self.stdout.write(f" Leads with org_a context: {count_a}") + self.stdout.write(f" Leads with org_b context: {count_b}") + self.stdout.write(f" Leads with no context: {count_none}") + + if count_none == 0 and (count_a > 0 or count_b > 0): + self.stdout.write( + self.style.SUCCESS("RLS is working - no data without context") + ) + elif count_none > 0: + self.stdout.write( + self.style.WARNING( + "RLS may not be fully enabled - data visible without context. " + "This is expected if the policy allows empty context." + ) + ) + + def verify_user(self): + """Verify the database user is not a superuser.""" + self.stdout.write(self.style.MIGRATE_HEADING("Verifying database user...")) + + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT usename, usesuper, usecreatedb + FROM pg_user + WHERE usename = current_user + """ + ) + user, is_super, can_create_db = cursor.fetchone() + + self.stdout.write(f" Current user: {user}") + self.stdout.write(f" Is superuser: {is_super}") + self.stdout.write(f" Can create DB: {can_create_db}") + + if is_super: + self.stdout.write("") + self.stdout.write( + self.style.ERROR( + "WARNING: Superusers bypass RLS!\n" + "Create a non-superuser for the application:\n\n" + " CREATE USER crm_app WITH PASSWORD 'secure_password';\n" + " GRANT CONNECT ON DATABASE bottlecrm TO crm_app;\n" + " GRANT USAGE ON SCHEMA public TO crm_app;\n" + " GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO crm_app;\n" + " GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_app;\n" + ) + ) + else: + self.stdout.write( + self.style.SUCCESS("Database user is properly configured for RLS") + ) diff --git a/backend/common/management/commands/migrate_from_prisma.py b/backend/common/management/commands/migrate_from_prisma.py new file mode 100644 index 00000000..dba696c0 --- /dev/null +++ b/backend/common/management/commands/migrate_from_prisma.py @@ -0,0 +1,1502 @@ +""" +Django management command to migrate data from Prisma/PostgreSQL to Django. + +Usage: + python manage.py migrate_from_prisma --db-host= --db-name= --db-user= --db-password= + + Options: + --dry-run: Validate data without inserting + --batch-size: Number of records per batch (default: 1000) + --skip-existing: Skip records that already exist + --only: Comma-separated list of models to migrate (e.g., "User,Account,Contact") +""" + +import logging +import uuid +from datetime import datetime +from decimal import Decimal + +import psycopg2 +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand +from django.db import connection, transaction +from django.utils import timezone +from psycopg2.extras import RealDictCursor + +from accounts.models import Account +from cases.models import Case, Solution + +# Import Django models +from common.models import ( + Activity, + Address, + Attachments, + Comment, + CommentFiles, + ContactFormSubmission, + Document, + Org, + Profile, + SessionToken, + Tags, + Teams, + User, +) +from contacts.models import Contact +from invoices.models import Invoice, Product +from leads.models import Company, Lead +from opportunity.models import Opportunity +from tasks.models import Board, BoardColumn, BoardMember, BoardTask, Task + +logger = logging.getLogger(__name__) + + +# ============================================================================= +# ENUM MAPPINGS: Prisma → Django +# ============================================================================= + +LEAD_STATUS_MAP = { + "NEW": "assigned", + "PENDING": "assigned", + "CONTACTED": "in process", + "QUALIFIED": "converted", + "UNQUALIFIED": "recycled", + "CONVERTED": "closed", +} + +LEAD_SOURCE_MAP = { + "WEB": "email", + "PHONE_INQUIRY": "call", + "PARTNER_REFERRAL": "partner", + "COLD_CALL": "call", + "TRADE_SHOW": "compaign", + "EMPLOYEE_REFERRAL": "existing customer", + "ADVERTISEMENT": "compaign", + "OTHER": "other", +} + +OPPORTUNITY_STAGE_MAP = { + "PROSPECTING": "PROSPECTING", + "QUALIFICATION": "QUALIFICATION", + "PROPOSAL": "PROPOSAL", + "NEGOTIATION": "NEGOTIATION", + "CLOSED_WON": "CLOSED_WON", + "CLOSED_LOST": "CLOSED_LOST", +} + +CASE_STATUS_MAP = { + "OPEN": "New", + "IN_PROGRESS": "Pending", + "CLOSED": "Closed", +} + +TASK_STATUS_MAP = { + "Not Started": "New", + "In Progress": "In Progress", + "Completed": "Completed", +} + +TASK_PRIORITY_MAP = { + "High": "High", + "Normal": "Medium", + "Low": "Low", +} + + +class Command(BaseCommand): + help = "Migrate data from Prisma PostgreSQL database to Django" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.prisma_conn = None + self.dry_run = False + self.batch_size = 1000 + self.skip_existing = False + self.stats = { + "migrated": {}, + "skipped": {}, + "errors": {}, + } + # ID mappings for relationship resolution + self.user_to_profile = {} # user_id -> {org_id: profile_id} + self.org_map = {} # old_org_id -> new_org_id + + def add_arguments(self, parser): + parser.add_argument( + "--db-host", type=str, required=True, help="Prisma database host" + ) + parser.add_argument( + "--db-port", type=int, default=5432, help="Prisma database port" + ) + parser.add_argument( + "--db-name", type=str, required=True, help="Prisma database name" + ) + parser.add_argument( + "--db-user", type=str, required=True, help="Prisma database user" + ) + parser.add_argument( + "--db-password", type=str, required=True, help="Prisma database password" + ) + parser.add_argument( + "--dry-run", action="store_true", help="Validate without inserting" + ) + parser.add_argument( + "--batch-size", type=int, default=1000, help="Batch size for inserts" + ) + parser.add_argument( + "--skip-existing", action="store_true", help="Skip existing records" + ) + parser.add_argument( + "--only", type=str, help="Comma-separated list of models to migrate" + ) + + def handle(self, *args, **options): + self.dry_run = options["dry_run"] + self.batch_size = options["batch_size"] + self.skip_existing = options["skip_existing"] + only_models = options.get("only") + + if only_models: + only_models = [m.strip().lower() for m in only_models.split(",")] + + self.stdout.write(self.style.NOTICE("Starting Prisma → Django migration")) + + if self.dry_run: + self.stdout.write( + self.style.WARNING("DRY RUN MODE - No data will be inserted") + ) + + # Connect to Prisma database + try: + self.prisma_conn = psycopg2.connect( + host=options["db_host"], + port=options["db_port"], + database=options["db_name"], + user=options["db_user"], + password=options["db_password"], + cursor_factory=RealDictCursor, + ) + self.stdout.write(self.style.SUCCESS("Connected to Prisma database")) + except Exception as e: + self.stdout.write( + self.style.ERROR(f"Failed to connect to Prisma database: {e}") + ) + return + + try: + # Migration order (respects dependencies) + migrations = [ + ("organization", self.migrate_organizations), + ("user", self.migrate_users), + ("profile", self.migrate_profiles), + ("teams", self.migrate_teams), + ("account", self.migrate_accounts), + ("contact", self.migrate_contacts), + ("accountcontacts", self.migrate_account_contacts), + ("lead", self.migrate_leads), + ("opportunity", self.migrate_opportunities), + ("case", self.migrate_cases), + ("solution", self.migrate_solutions), + ("task", self.migrate_tasks), + ("product", self.migrate_products), + ("comment", self.migrate_comments), + ("board", self.migrate_boards), + ("boardmember", self.migrate_board_members), + ("boardcolumn", self.migrate_board_columns), + ("boardtask", self.migrate_board_tasks), + ("contactsubmission", self.migrate_contact_submissions), + ] + + for model_name, migration_func in migrations: + if only_models and model_name not in only_models: + continue + + self.stdout.write(f"\nMigrating {model_name}...") + try: + migration_func() + except Exception as e: + self.stdout.write( + self.style.ERROR(f"Error migrating {model_name}: {e}") + ) + logger.exception(f"Error migrating {model_name}") + self.stats["errors"][model_name] = str(e) + + # Print summary + self.print_summary() + + finally: + if self.prisma_conn: + self.prisma_conn.close() + + def print_summary(self): + self.stdout.write("\n" + "=" * 60) + self.stdout.write(self.style.SUCCESS("MIGRATION SUMMARY")) + self.stdout.write("=" * 60) + + for model, count in self.stats["migrated"].items(): + self.stdout.write(f" {model}: {count} migrated") + + if self.stats["skipped"]: + self.stdout.write("\nSkipped:") + for model, count in self.stats["skipped"].items(): + self.stdout.write(f" {model}: {count} skipped") + + if self.stats["errors"]: + self.stdout.write(self.style.ERROR("\nErrors:")) + for model, error in self.stats["errors"].items(): + self.stdout.write(f" {model}: {error}") + + def execute_query(self, query, params=None): + """Execute a query on Prisma database and return results""" + with self.prisma_conn.cursor() as cursor: + cursor.execute(query, params) + return cursor.fetchall() + + def get_or_none(self, model, **kwargs): + """Get object or return None""" + try: + return model.objects.get(**kwargs) + except model.DoesNotExist: + return None + + # ========================================================================= + # MIGRATION FUNCTIONS + # ========================================================================= + + def migrate_organizations(self): + """Migrate Organization → Org""" + query = """ + SELECT id, name, "createdAt", "updatedAt", "isActive" + FROM "Organization" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Org.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + if not self.dry_run: + import uuid + + api_key = str(uuid.uuid4()) + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO organization (id, name, api_key, is_active, created_at, updated_at) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + is_active = EXCLUDED.is_active, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["name"], + api_key, + row["isActive"], + row["createdAt"], + row["updatedAt"], + ], + ) + + self.org_map[row["id"]] = row["id"] + migrated += 1 + + self.stats["migrated"]["Organization"] = migrated + self.stats["skipped"]["Organization"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} organizations")) + + def migrate_users(self): + """Migrate User → User (Django)""" + query = """ + SELECT id, email, name, "profilePhoto", "isActive", + "createdAt", "updatedAt" + FROM "User" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and User.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Split name into first/last (Django User doesn't have name field directly) + name = row["name"] or "" + name_parts = name.split(" ", 1) + + if not self.dry_run: + # Truncate profile_pic to 1000 chars if needed + profile_pic = row["profilePhoto"] + if profile_pic and len(profile_pic) > 1000: + profile_pic = profile_pic[:1000] + + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO users (id, email, profile_pic, is_active, is_staff, is_superuser, password, last_login) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + email = EXCLUDED.email, + profile_pic = EXCLUDED.profile_pic, + is_active = EXCLUDED.is_active + """, + [ + row["id"], + row["email"], + profile_pic, + row["isActive"], + False, # is_staff + False, # is_superuser + "", # password (needs to be reset) + None, # last_login + ], + ) + + migrated += 1 + + self.stats["migrated"]["User"] = migrated + self.stats["skipped"]["User"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} users")) + + def migrate_profiles(self): + """Migrate UserOrganization → Profile""" + query = """ + SELECT uo.id, uo."userId", uo."organizationId", uo.role, uo."joinedAt", + u.phone, u.email + FROM "UserOrganization" uo + JOIN "User" u ON u.id = uo."userId" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + profile_id = row["id"] + + if self.skip_existing and Profile.objects.filter(id=profile_id).exists(): + skipped += 1 + continue + + # Map role + role = "ADMIN" if row["role"] == "ADMIN" else "USER" + is_org_admin = row["role"] == "ADMIN" + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO profile ( + id, user_id, org_id, role, is_active, + is_organization_admin, has_sales_access, has_marketing_access, + created_at, updated_at + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + role = EXCLUDED.role, + is_organization_admin = EXCLUDED.is_organization_admin + """, + [ + profile_id, + row["userId"], + row["organizationId"], + role, + True, # is_active + is_org_admin, + True, # has_sales_access + True, # has_marketing_access + row["joinedAt"], + row["joinedAt"], + ], + ) + + # Store mapping for later use + if row["userId"] not in self.user_to_profile: + self.user_to_profile[row["userId"]] = {} + self.user_to_profile[row["userId"]][row["organizationId"]] = profile_id + + migrated += 1 + + self.stats["migrated"]["Profile"] = migrated + self.stats["skipped"]["Profile"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} profiles")) + + def migrate_teams(self): + """Teams don't exist in Prisma - skip""" + self.stats["migrated"]["Teams"] = 0 + self.stdout.write(" No teams in Prisma schema - skipping") + + def migrate_accounts(self): + """Migrate Account → Account""" + query = """ + SELECT id, name, industry, website, phone, + street, city, state, "postalCode", country, + "annualRevenue", description, "numberOfEmployees", + "createdAt", "updatedAt", "ownerId", "organizationId", "isActive" + FROM "Account" + WHERE "isDeleted" = false + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Account.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO accounts ( + id, name, industry, website, phone, + address_line, city, state, postcode, country, + annual_revenue, description, number_of_employees, + created_at, updated_at, org_id, is_active + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["name"], + row["industry"].upper() if row["industry"] else None, + row["website"], + row["phone"], + row["street"], + row["city"], + row["state"], + row["postalCode"], + ( + row["country"][:2] if row["country"] else None + ), # Convert to 2-letter code + row["annualRevenue"], + row["description"], + row["numberOfEmployees"], + row["createdAt"], + row["updatedAt"], + row["organizationId"], + row["isActive"], + ], + ) + + # Add owner to assigned_to M2M + owner_profile_id = self.user_to_profile.get(row["ownerId"], {}).get( + row["organizationId"] + ) + if owner_profile_id: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO accounts_assigned_to (account_id, profile_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], owner_profile_id], + ) + + migrated += 1 + + self.stats["migrated"]["Account"] = migrated + self.stats["skipped"]["Account"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} accounts")) + + def migrate_contacts(self): + """Migrate Contact → Contact""" + query = """ + SELECT id, "firstName", "lastName", email, phone, + title, department, street, city, state, "postalCode", country, + description, "createdAt", "updatedAt", "ownerId", "organizationId" + FROM "Contact" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Contact.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Skip contacts without organization + if not row["organizationId"]: + skipped += 1 + continue + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO contacts ( + id, first_name, last_name, email, phone, + title, department, address_line, city, state, postcode, country, + description, created_at, updated_at, org_id, is_active, do_not_call + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["firstName"], + row["lastName"], + row["email"], + row["phone"], + row["title"], + row["department"], + row["street"], + row["city"], + row["state"], + row["postalCode"], + row["country"][:2] if row["country"] else None, + row["description"], + row["createdAt"], + row["updatedAt"], + row["organizationId"], + True, # is_active + False, # do_not_call + ], + ) + + # Add owner to assigned_to M2M + if row["organizationId"]: + owner_profile_id = self.user_to_profile.get(row["ownerId"], {}).get( + row["organizationId"] + ) + if owner_profile_id: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO contacts_assigned_to (contact_id, profile_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], owner_profile_id], + ) + + migrated += 1 + + self.stats["migrated"]["Contact"] = migrated + self.stats["skipped"]["Contact"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} contacts")) + + def migrate_account_contacts(self): + """Migrate AccountContactRelationship → Account.contacts M2M""" + query = """ + SELECT "accountId", "contactId" + FROM "AccountContactRelationship" + """ + rows = self.execute_query(query) + migrated = 0 + + for row in rows: + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO accounts_contacts (account_id, contact_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["accountId"], row["contactId"]], + ) + migrated += 1 + + self.stats["migrated"]["AccountContacts"] = migrated + self.stdout.write( + self.style.SUCCESS(f" Migrated {migrated} account-contact relationships") + ) + + def migrate_leads(self): + """Migrate Lead → Lead""" + query = """ + SELECT id, "firstName", "lastName", email, phone, company, title, + status, "leadSource", industry, rating, description, + "createdAt", "updatedAt", "ownerId", "organizationId" + FROM "Lead" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Lead.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Map status and source + status = LEAD_STATUS_MAP.get(row["status"], "assigned") + source = ( + LEAD_SOURCE_MAP.get(row["leadSource"], "other") + if row["leadSource"] + else None + ) + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO lead ( + id, title, first_name, last_name, email, phone, + status, source, industry, rating, description, + created_at, updated_at, org_id, is_active, created_from_site + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name, + status = EXCLUDED.status, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["title"] or f"{row['firstName']} {row['lastName']}", + row["firstName"], + row["lastName"], + row["email"], + row["phone"], + status, + source, + row["industry"].upper() if row["industry"] else None, + row["rating"], + row["description"], + row["createdAt"], + row["updatedAt"], + row["organizationId"], + True, # is_active + False, # created_from_site + ], + ) + + # Add owner to assigned_to M2M + owner_profile_id = self.user_to_profile.get(row["ownerId"], {}).get( + row["organizationId"] + ) + if owner_profile_id: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO lead_assigned_to (lead_id, profile_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], owner_profile_id], + ) + + migrated += 1 + + self.stats["migrated"]["Lead"] = migrated + self.stats["skipped"]["Lead"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} leads")) + + def migrate_opportunities(self): + """Migrate Opportunity → Opportunity""" + query = """ + SELECT id, name, amount, stage, "closeDate", probability, + type, description, "leadSource", + "createdAt", "updatedAt", "accountId", "ownerId", "organizationId" + FROM "Opportunity" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Opportunity.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Map stage + stage = OPPORTUNITY_STAGE_MAP.get(row["stage"], "PROSPECTING") + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO opportunity ( + id, name, amount, stage, closed_on, probability, + opportunity_type, description, lead_source, + created_at, updated_at, account_id, org_id, is_active + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + stage = EXCLUDED.stage, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["name"], + row["amount"], + stage, + row["closeDate"], + int(row["probability"]) if row["probability"] else 0, + row["type"], + row["description"], + row["leadSource"], + row["createdAt"], + row["updatedAt"], + row["accountId"], + row["organizationId"], + True, # is_active + ], + ) + + # Add owner to assigned_to M2M + owner_profile_id = self.user_to_profile.get(row["ownerId"], {}).get( + row["organizationId"] + ) + if owner_profile_id: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO opportunity_assigned_to (opportunity_id, profile_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], owner_profile_id], + ) + + migrated += 1 + + # Migrate opportunity contacts M2M + self.migrate_opportunity_contacts() + + self.stats["migrated"]["Opportunity"] = migrated + self.stats["skipped"]["Opportunity"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} opportunities")) + + def migrate_opportunity_contacts(self): + """Migrate Opportunity-Contact M2M relationship""" + query = """ + SELECT "A" as opportunity_id, "B" as contact_id + FROM "_ContactToOpportunity" + """ + try: + rows = self.execute_query(query) + for row in rows: + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO opportunity_contacts (opportunity_id, contact_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["opportunity_id"], row["contact_id"]], + ) + except Exception as e: + logger.warning(f"Could not migrate opportunity contacts: {e}") + + def migrate_cases(self): + """Migrate Case → Case""" + query = """ + SELECT id, subject, status, description, priority, type, "closedAt", + "createdAt", "updatedAt", "accountId", "contactId", "ownerId", "organizationId" + FROM "Case" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Case.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Map status + status = CASE_STATUS_MAP.get(row["status"], "New") + + # Map priority + priority = ( + row["priority"] + if row["priority"] in ["Low", "Normal", "High", "Urgent"] + else "Normal" + ) + + # Map case type + case_type = "" + if row["type"]: + type_lower = row["type"].lower() + if "question" in type_lower: + case_type = "Question" + elif "problem" in type_lower: + case_type = "Problem" + else: + case_type = "Incident" + + if not self.dry_run: + # Truncate name to 64 chars if needed + case_name = row["subject"] + if case_name and len(case_name) > 64: + case_name = case_name[:64] + + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO "case" ( + id, name, status, description, priority, case_type, closed_on, + created_at, updated_at, account_id, org_id, is_active + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + status = EXCLUDED.status, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + case_name, + status, + row["description"], + priority, + case_type, + row["closedAt"].date() if row["closedAt"] else None, + row["createdAt"], + row["updatedAt"], + row["accountId"], + row["organizationId"], + True, # is_active + ], + ) + + # Add owner to assigned_to M2M + owner_profile_id = self.user_to_profile.get(row["ownerId"], {}).get( + row["organizationId"] + ) + if owner_profile_id: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO case_assigned_to (case_id, profile_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], owner_profile_id], + ) + + # Add contact to contacts M2M + if row["contactId"]: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO case_contacts (case_id, contact_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], row["contactId"]], + ) + + migrated += 1 + + self.stats["migrated"]["Case"] = migrated + self.stats["skipped"]["Case"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} cases")) + + def migrate_solutions(self): + """Migrate Solution → Solution""" + query = """ + SELECT id, title, description, status, "isPublished", + "createdAt", "updatedAt", "organizationId" + FROM "Solution" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Solution.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Map status + status = row["status"].lower() if row["status"] else "draft" + if status not in ["draft", "reviewed", "approved"]: + status = "draft" + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO solution ( + id, title, description, status, is_published, + created_at, updated_at, org_id + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + status = EXCLUDED.status, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["title"], + row["description"], + status, + row["isPublished"], + row["createdAt"], + row["updatedAt"], + row["organizationId"], + ], + ) + + migrated += 1 + + # Migrate solution-case M2M + self.migrate_solution_cases() + + self.stats["migrated"]["Solution"] = migrated + self.stats["skipped"]["Solution"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} solutions")) + + def migrate_solution_cases(self): + """Migrate Solution-Case M2M relationship""" + query = """ + SELECT "A" as case_id, "B" as solution_id + FROM "_CaseToSolution" + """ + try: + rows = self.execute_query(query) + for row in rows: + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO solution_cases (solution_id, case_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["solution_id"], row["case_id"]], + ) + except Exception as e: + logger.warning(f"Could not migrate solution cases: {e}") + + def migrate_tasks(self): + """Migrate Task → Task""" + query = """ + SELECT id, subject, status, priority, "dueDate", description, + "createdAt", "updatedAt", "accountId", "contactId", + "ownerId", "organizationId" + FROM "Task" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Task.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Map status and priority + status = TASK_STATUS_MAP.get(row["status"], "New") + priority = TASK_PRIORITY_MAP.get(row["priority"], "Medium") + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO task ( + id, title, status, priority, due_date, description, + created_at, updated_at, account_id, org_id + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + status = EXCLUDED.status, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["subject"], + status, + priority, + row["dueDate"].date() if row["dueDate"] else None, + row["description"], + row["createdAt"], + row["updatedAt"], + row["accountId"], + row["organizationId"], + ], + ) + + # Add owner to assigned_to M2M + owner_profile_id = self.user_to_profile.get(row["ownerId"], {}).get( + row["organizationId"] + ) + if owner_profile_id: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO task_assigned_to (task_id, profile_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], owner_profile_id], + ) + + # Add contact to contacts M2M + if row["contactId"]: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO task_contacts (task_id, contact_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], row["contactId"]], + ) + + migrated += 1 + + self.stats["migrated"]["Task"] = migrated + self.stats["skipped"]["Task"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} tasks")) + + def migrate_products(self): + """Migrate Product → Product""" + query = """ + SELECT id, name, code, description, "unitPrice", active, + "createdAt", "updatedAt", "organizationId" + FROM "Product" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Product.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO product ( + id, name, sku, description, price, is_active, + created_at, updated_at, org_id + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + price = EXCLUDED.price, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["name"], + row["code"], + row["description"], + row["unitPrice"], + row["active"], + row["createdAt"], + row["updatedAt"], + row["organizationId"], + ], + ) + + migrated += 1 + + self.stats["migrated"]["Product"] = migrated + self.stats["skipped"]["Product"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} products")) + + def migrate_comments(self): + """Migrate Comment → Comment (using GenericFK)""" + query = """ + SELECT id, body, "createdAt", "updatedAt", "authorId", "organizationId", + "caseId", "opportunityId", "leadId", "accountId", "contactId", "taskId" + FROM "Comment" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + # Get content types + content_types = { + "case": ContentType.objects.get_for_model(Case), + "opportunity": ContentType.objects.get_for_model(Opportunity), + "lead": ContentType.objects.get_for_model(Lead), + "account": ContentType.objects.get_for_model(Account), + "contact": ContentType.objects.get_for_model(Contact), + "task": ContentType.objects.get_for_model(Task), + } + + for row in rows: + if self.skip_existing and Comment.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Determine content type and object ID + content_type = None + object_id = None + + if row["caseId"]: + content_type = content_types["case"] + object_id = row["caseId"] + elif row["opportunityId"]: + content_type = content_types["opportunity"] + object_id = row["opportunityId"] + elif row["leadId"]: + content_type = content_types["lead"] + object_id = row["leadId"] + elif row["accountId"]: + content_type = content_types["account"] + object_id = row["accountId"] + elif row["contactId"]: + content_type = content_types["contact"] + object_id = row["contactId"] + elif row["taskId"]: + content_type = content_types["task"] + object_id = row["taskId"] + else: + skipped += 1 + continue + + # Get profile ID for author + author_profile_id = self.user_to_profile.get(row["authorId"], {}).get( + row["organizationId"] + ) + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO comment ( + id, content_type_id, object_id, comment, commented_on, + commented_by_id, org_id, created_at, updated_at + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + comment = EXCLUDED.comment, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + content_type.id, + object_id, + ( + row["body"][:255] if row["body"] else "" + ), # Truncate to 255 chars + row["createdAt"], + author_profile_id, + row["organizationId"], + row["createdAt"], + row["updatedAt"], + ], + ) + + migrated += 1 + + self.stats["migrated"]["Comment"] = migrated + self.stats["skipped"]["Comment"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} comments")) + + def migrate_boards(self): + """Migrate Board → Board""" + query = """ + SELECT id, name, description, "createdAt", "updatedAt", + "ownerId", "organizationId" + FROM "Board" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and Board.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Get owner profile + owner_profile_id = self.user_to_profile.get(row["ownerId"], {}).get( + row["organizationId"] + ) + + if not owner_profile_id: + skipped += 1 + continue + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO board ( + id, name, description, is_archived, + created_at, updated_at, owner_id, org_id + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["name"], + row["description"], + False, # is_archived + row["createdAt"], + row["updatedAt"], + owner_profile_id, + row["organizationId"], + ], + ) + + migrated += 1 + + self.stats["migrated"]["Board"] = migrated + self.stats["skipped"]["Board"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} boards")) + + def migrate_board_members(self): + """Migrate BoardMember → BoardMember""" + query = """ + SELECT bm.id, bm."boardId", bm."userId", bm.role, bm."createdAt", + b."organizationId" + FROM "BoardMember" bm + JOIN "Board" b ON b.id = bm."boardId" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and BoardMember.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Get profile ID for user + profile_id = self.user_to_profile.get(row["userId"], {}).get( + row["organizationId"] + ) + + if not profile_id: + skipped += 1 + continue + + # Map role + role = row["role"].lower() if row["role"] else "member" + if role not in ["owner", "admin", "member"]: + role = "member" + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO board_member ( + id, board_id, profile_id, role, created_at, updated_at + ) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + role = EXCLUDED.role + """, + [ + row["id"], + row["boardId"], + profile_id, + role, + row["createdAt"], + row["createdAt"], + ], + ) + + migrated += 1 + + self.stats["migrated"]["BoardMember"] = migrated + self.stats["skipped"]["BoardMember"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} board members")) + + def migrate_board_columns(self): + """Migrate BoardColumn → BoardColumn""" + query = """ + SELECT id, name, "order", "boardId", "createdAt", "updatedAt" + FROM "BoardColumn" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and BoardColumn.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO board_column ( + id, name, "order", color, board_id, created_at, updated_at + ) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + "order" = EXCLUDED."order", + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["name"], + row["order"], + "#6B7280", # default color + row["boardId"], + row["createdAt"], + row["updatedAt"], + ], + ) + + migrated += 1 + + self.stats["migrated"]["BoardColumn"] = migrated + self.stats["skipped"]["BoardColumn"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} board columns")) + + def migrate_board_tasks(self): + """Migrate BoardTask → BoardTask""" + query = """ + SELECT id, title, description, "order", "dueDate", completed, + "columnId", "assigneeId", "createdAt", "updatedAt" + FROM "BoardTask" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if self.skip_existing and BoardTask.objects.filter(id=row["id"]).exists(): + skipped += 1 + continue + + # Get board org to find profile + board_org_query = """ + SELECT b."organizationId" + FROM "BoardColumn" bc + JOIN "Board" b ON b.id = bc."boardId" + WHERE bc.id = %s + """ + org_rows = self.execute_query(board_org_query, [row["columnId"]]) + org_id = org_rows[0]["organizationId"] if org_rows else None + + if not self.dry_run: + completed_at = row["createdAt"] if row["completed"] else None + + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO board_task ( + id, title, description, "order", priority, + due_date, completed_at, column_id, created_at, updated_at + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + description = EXCLUDED.description, + updated_at = EXCLUDED.updated_at + """, + [ + row["id"], + row["title"], + row["description"], + row["order"], + "medium", # default priority + row["dueDate"], + completed_at, + row["columnId"], + row["createdAt"], + row["updatedAt"], + ], + ) + + # Add assignee to assigned_to M2M + if row["assigneeId"] and org_id: + profile_id = self.user_to_profile.get(row["assigneeId"], {}).get( + org_id + ) + if profile_id: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO board_task_assigned_to (boardtask_id, profile_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, + [row["id"], profile_id], + ) + + migrated += 1 + + self.stats["migrated"]["BoardTask"] = migrated + self.stats["skipped"]["BoardTask"] = skipped + self.stdout.write(self.style.SUCCESS(f" Migrated {migrated} board tasks")) + + def migrate_contact_submissions(self): + """Migrate ContactSubmission → ContactFormSubmission""" + query = """ + SELECT id, name, email, message, reason, + "ipAddress", "userAgent", referrer, "createdAt" + FROM "ContactSubmission" + """ + rows = self.execute_query(query) + migrated = 0 + skipped = 0 + + for row in rows: + if ( + self.skip_existing + and ContactFormSubmission.objects.filter(id=row["id"]).exists() + ): + skipped += 1 + continue + + # Map reason + reason = row["reason"].lower() if row["reason"] else "general" + valid_reasons = ["general", "sales", "support", "partnership", "other"] + if reason not in valid_reasons: + reason = "other" + + if not self.dry_run: + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO contact_form_submission ( + id, name, email, message, reason, + ip_address, user_agent, referrer, status, created_at, updated_at + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + email = EXCLUDED.email + """, + [ + row["id"], + row["name"], + row["email"], + row["message"], + reason, + row["ipAddress"], + row["userAgent"], + row["referrer"], + "new", # status + row["createdAt"], + row["createdAt"], + ], + ) + + migrated += 1 + + self.stats["migrated"]["ContactSubmission"] = migrated + self.stats["skipped"]["ContactSubmission"] = skipped + self.stdout.write( + self.style.SUCCESS(f" Migrated {migrated} contact submissions") + ) diff --git a/common/manager.py b/backend/common/manager.py similarity index 54% rename from common/manager.py rename to backend/common/manager.py index f29cc2f4..87cf6ca2 100644 --- a/common/manager.py +++ b/backend/common/manager.py @@ -1,9 +1,10 @@ from django.contrib.auth.models import BaseUserManager + class UserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): if not email: - raise ValueError('The Email field must be set') + raise ValueError("The Email field must be set") email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) @@ -11,14 +12,13 @@ class UserManager(BaseUserManager): return user def create_superuser(self, email, password=None, **extra_fields): - extra_fields.setdefault('is_superuser', True) - extra_fields.setdefault('is_staff', True) + extra_fields.setdefault("is_superuser", True) + extra_fields.setdefault("is_staff", True) - if extra_fields.get('is_staff') is not True: - raise ValueError('Superuser must have is_staff=True.') - + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser must have is_staff=True.") - if extra_fields.get('is_superuser') is not True: - raise ValueError('Superuser must have is_superuser=True.') + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser must have is_superuser=True.") return self.create_user(email, password, **extra_fields) diff --git a/common/middleware/__init__.py b/backend/common/middleware/__init__.py similarity index 100% rename from common/middleware/__init__.py rename to backend/common/middleware/__init__.py diff --git a/backend/common/middleware/get_company.py b/backend/common/middleware/get_company.py new file mode 100644 index 00000000..ae1600ae --- /dev/null +++ b/backend/common/middleware/get_company.py @@ -0,0 +1,152 @@ +import logging + +import jwt +from crum import get_current_user +from django.conf import settings +from django.contrib.auth import logout +from django.core.exceptions import PermissionDenied, ValidationError +from django.utils.functional import SimpleLazyObject +from rest_framework import status +from rest_framework.exceptions import AuthenticationFailed +from rest_framework.response import Response + +from common.models import Org, Profile, User + +logger = logging.getLogger(__name__) + + +def get_actual_value(request): + if request.user is None: + return None + return request.user + + +class GetProfileAndOrg(object): + """ + Middleware to extract and validate organization context from JWT tokens. + + SECURITY: This middleware validates org_id from the SIGNED JWT token, + not from client-provided headers. This prevents org spoofing attacks. + + The org context is cryptographically verified as part of the JWT signature. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + self.process_request(request) + return self.get_response(request) + + def process_request(self, request): + # Skip JWT validation for authentication endpoints that don't need org context + auth_skip_paths = [ + "/api/auth/login/", + "/api/auth/google/", + "/api/auth/register/", + "/api/auth/refresh-token/", + "/api/auth/me/", + "/api/auth/switch-org/", + ] + if request.path in auth_skip_paths: + return + + # Initialize request attributes + request.profile = None + request.org = None + + # Try JWT token first (primary authentication) + if request.headers.get("Authorization"): + self._process_jwt_auth(request) + return + + # Fall back to API key authentication + api_key = request.headers.get("Token") + if api_key: + self._process_api_key_auth(request, api_key) + return + + def _process_jwt_auth(self, request): + """ + Process JWT authentication and extract org context from token. + + SECURITY: Org ID is extracted from the signed JWT payload, not headers. + """ + from rest_framework_simplejwt.tokens import AccessToken + + try: + auth_header = request.headers.get("Authorization") + token_value = auth_header.split(" ")[1] + + # Validate and decode token + access_token = AccessToken(token_value) + user_id = access_token["user_id"] + + # Get org_id from JWT claim (SECURE - cryptographically signed) + org_id = access_token.get("org_id") + + if not org_id: + # Token doesn't have org context - this is allowed for some endpoints + # like /api/auth/me/ or initial login + logger.debug(f"JWT token for user {user_id} has no org_id claim") + return + + # Validate user membership in the org + try: + profile = Profile.objects.select_related("org").get( + user_id=user_id, org_id=org_id, is_active=True + ) + request.profile = profile + request.org = profile.org + logger.debug(f"Set org context from JWT: user={user_id}, org={org_id}") + + except Profile.DoesNotExist: + # User doesn't have access to this org anymore + # This can happen if membership was revoked after token was issued + logger.warning( + f"User {user_id} no longer has access to org {org_id}. " + "Token may be stale." + ) + raise PermissionDenied( + "You no longer have access to this organization. " + "Please login again." + ) + + except (IndexError, KeyError) as e: + logger.warning(f"Malformed Authorization header: {e}") + # Let DRF authentication handle this + return + + except Exception as e: + logger.warning(f"JWT validation failed: {e}") + # Let DRF authentication handle invalid tokens + return + + def _process_api_key_auth(self, request, api_key): + """ + Process API key authentication. + + Note: API keys are scoped to a specific org and use an admin profile. + TODO: Create dedicated API key model with granular permissions. + """ + try: + organization = Org.objects.get(api_key=api_key, is_active=True) + + # Get an admin profile for this org + profile = Profile.objects.filter( + org=organization, role="ADMIN", is_active=True + ).first() + + if not profile: + logger.error(f"No active admin profile found for org {organization.id}") + raise AuthenticationFailed("Invalid API Key configuration") + + request.profile = profile + request.org = organization + request.META["org"] = str(organization.id) + + logger.debug(f"Set org context from API key: org={organization.id}") + + except Org.DoesNotExist: + logger.warning(f"Invalid API key attempted: {api_key[:8]}...") + raise AuthenticationFailed("Invalid API Key") diff --git a/backend/common/middleware/rls_context.py b/backend/common/middleware/rls_context.py new file mode 100644 index 00000000..6d1b2098 --- /dev/null +++ b/backend/common/middleware/rls_context.py @@ -0,0 +1,187 @@ +""" +Database middleware for Row-Level Security (RLS) context. + +This middleware sets the PostgreSQL session variable `app.current_org` +which is used by RLS policies to filter data at the database level. + +Enable RLS policies after this middleware is in place: + + ALTER TABLE leads ENABLE ROW LEVEL SECURITY; + CREATE POLICY org_isolation ON leads + USING (org_id = current_setting('app.current_org', true)::uuid); + +Usage in settings.py: + MIDDLEWARE = [ + ... + 'common.middleware.get_company.GetProfileAndOrg', + 'common.middleware.rls_context.SetOrgContext', # After GetProfileAndOrg + ... + ] +""" + +import logging + +from django.db import connection + +logger = logging.getLogger(__name__) + + +class SetOrgContext: + """ + Middleware to set PostgreSQL session variable for Row-Level Security. + + This sets `app.current_org` to the user's organization ID, which is + used by RLS policies to automatically filter data at the database level. + + Security: This provides defense-in-depth. Even if application code + forgets to filter by org, the database will enforce isolation. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Set org context before processing request + self._set_org_context(request) + + response = self.get_response(request) + + # Reset context after request + self._reset_org_context() + + return response + + def _set_org_context(self, request): + """ + Set the PostgreSQL session variable for RLS. + + Args: + request: Django request object with profile attached + """ + if not hasattr(request, "org") or request.org is None: + return + + org_id = str(request.org.id) + + try: + with connection.cursor() as cursor: + # Set the session variable + cursor.execute( + "SELECT set_config('app.current_org', %s, true)", [org_id] + ) + logger.debug(f"Set RLS context: app.current_org = {org_id}") + + except Exception as e: + # RLS might not be configured - log but don't fail + logger.debug(f"Could not set RLS context: {e}") + + def _reset_org_context(self): + """ + Reset the PostgreSQL session variable after request. + """ + try: + with connection.cursor() as cursor: + cursor.execute("SELECT set_config('app.current_org', '', true)") + except Exception: + pass + + +class RequireOrgContext: + """ + Stricter middleware that fails if org context is not set. + + Use this instead of SetOrgContext when you want to ensure + all requests have proper org context (after RLS is fully enabled). + + Usage in settings.py: + MIDDLEWARE = [ + ... + 'common.middleware.get_company.GetProfileAndOrg', + 'common.middleware.rls_context.RequireOrgContext', + ... + ] + """ + + # Paths that don't require org context + EXEMPT_PATHS = [ + "/api/auth/login/", + "/api/auth/register/", + "/api/auth/refresh-token/", + "/api/auth/me/", + "/api/auth/switch-org/", + "/api/auth/google/", + "/admin/", + "/swagger-ui/", + "/api/schema/", + ] + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Check if path requires org context + if not self._is_exempt(request.path): + if not hasattr(request, "org") or request.org is None: + from rest_framework.exceptions import PermissionDenied + + raise PermissionDenied( + "Organization context is required. Please login again." + ) + + # Set org context + self._set_org_context(request) + + response = self.get_response(request) + + # Reset context + self._reset_org_context() + + return response + + def _is_exempt(self, path): + """Check if path is exempt from org context requirement.""" + return any(path.startswith(exempt) for exempt in self.EXEMPT_PATHS) + + def _set_org_context(self, request): + """Set PostgreSQL session variable.""" + if not hasattr(request, "org") or request.org is None: + return + + org_id = str(request.org.id) + + try: + with connection.cursor() as cursor: + cursor.execute( + "SELECT set_config('app.current_org', %s, true)", [org_id] + ) + except Exception as e: + logger.warning(f"Failed to set RLS context: {e}") + + def _reset_org_context(self): + """Reset PostgreSQL session variable.""" + try: + with connection.cursor() as cursor: + cursor.execute("SELECT set_config('app.current_org', '', true)") + except Exception: + pass + + +# SQL to enable RLS on all org-scoped tables +RLS_SETUP_SQL = """ +-- Enable RLS on main tables +-- Run this after all org-scoped tables are identified + +-- Example for leads table: +-- ALTER TABLE lead ENABLE ROW LEVEL SECURITY; +-- ALTER TABLE lead FORCE ROW LEVEL SECURITY; -- Apply to table owner too +-- CREATE POLICY org_isolation ON lead +-- USING (org_id = NULLIF(current_setting('app.current_org', true), '')::uuid); + +-- Tables that need RLS policies: +-- lead, accounts, contacts, opportunity, cases, tasks, invoices, +-- comment, attachments, document, teams, activity, tags, address, +-- api_settings, board, board_column, board_task, board_member + +-- Note: Use NULLIF to handle empty string when context is not set +-- This makes the policy return no rows when context is not set (fail-safe) +""" diff --git a/backend/common/migrations/0001_initial.py b/backend/common/migrations/0001_initial.py new file mode 100644 index 00000000..4a6405d5 --- /dev/null +++ b/backend/common/migrations/0001_initial.py @@ -0,0 +1,1616 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.conf import settings +from django.db import migrations, models + +import common.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("contenttypes", "0002_remove_content_type_name"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "email", + models.EmailField( + blank=True, + max_length=254, + unique=True, + verbose_name="email address", + ), + ), + ( + "profile_pic", + models.CharField(blank=True, max_length=1000, null=True), + ), + ( + "activation_key", + models.CharField(blank=True, max_length=150, null=True), + ), + ("key_expires", models.DateTimeField(blank=True, null=True)), + ("is_active", models.BooleanField(default=True)), + ( + "is_staff", + models.BooleanField(default=False, verbose_name="staff status"), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "User", + "verbose_name_plural": "Users", + "db_table": "users", + "ordering": ("-is_active",), + }, + ), + migrations.CreateModel( + name="Comment", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("object_id", models.UUIDField()), + ("comment", models.CharField(max_length=255)), + ("commented_on", models.DateTimeField(auto_now_add=True)), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="contenttypes.contenttype", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Comment", + "verbose_name_plural": "Comments", + "db_table": "comment", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="CommentFiles", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "comment_file", + models.FileField( + blank=True, + null=True, + upload_to="CommentFiles", + verbose_name="File", + ), + ), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="common.comment" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "CommentFile", + "verbose_name_plural": "CommentFiles", + "db_table": "commentFiles", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Org", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(blank=True, max_length=100, null=True)), + ( + "api_key", + models.TextField( + default=common.models.generate_unique_key, + editable=False, + unique=True, + ), + ), + ("is_active", models.BooleanField(default=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Organization", + "verbose_name_plural": "Organizations", + "db_table": "organization", + "ordering": ("-created_at",), + }, + ), + migrations.AddField( + model_name="comment", + name="org", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="common.org", + ), + ), + migrations.CreateModel( + name="Address", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "address_line", + models.CharField( + blank=True, default="", max_length=255, verbose_name="Address" + ), + ), + ( + "street", + models.CharField( + blank=True, default="", max_length=55, verbose_name="Street" + ), + ), + ( + "city", + models.CharField( + blank=True, default="", max_length=255, verbose_name="City" + ), + ), + ( + "state", + models.CharField( + blank=True, default="", max_length=255, verbose_name="State" + ), + ), + ( + "postcode", + models.CharField( + blank=True, + default="", + max_length=64, + verbose_name="Post/Zip-code", + ), + ), + ( + "country", + models.CharField( + blank=True, + choices=[ + ("GB", "United Kingdom"), + ("AF", "Afghanistan"), + ("AX", "Aland Islands"), + ("AL", "Albania"), + ("DZ", "Algeria"), + ("AS", "American Samoa"), + ("AD", "Andorra"), + ("AO", "Angola"), + ("AI", "Anguilla"), + ("AQ", "Antarctica"), + ("AG", "Antigua and Barbuda"), + ("AR", "Argentina"), + ("AM", "Armenia"), + ("AW", "Aruba"), + ("AU", "Australia"), + ("AT", "Austria"), + ("AZ", "Azerbaijan"), + ("BS", "Bahamas"), + ("BH", "Bahrain"), + ("BD", "Bangladesh"), + ("BB", "Barbados"), + ("BY", "Belarus"), + ("BE", "Belgium"), + ("BZ", "Belize"), + ("BJ", "Benin"), + ("BM", "Bermuda"), + ("BT", "Bhutan"), + ("BO", "Bolivia"), + ("BA", "Bosnia and Herzegovina"), + ("BW", "Botswana"), + ("BV", "Bouvet Island"), + ("BR", "Brazil"), + ("IO", "British Indian Ocean Territory"), + ("BN", "Brunei Darussalam"), + ("BG", "Bulgaria"), + ("BF", "Burkina Faso"), + ("BI", "Burundi"), + ("KH", "Cambodia"), + ("CM", "Cameroon"), + ("CA", "Canada"), + ("CV", "Cape Verde"), + ("KY", "Cayman Islands"), + ("CF", "Central African Republic"), + ("TD", "Chad"), + ("CL", "Chile"), + ("CN", "China"), + ("CX", "Christmas Island"), + ("CC", "Cocos (Keeling) Islands"), + ("CO", "Colombia"), + ("KM", "Comoros"), + ("CG", "Congo"), + ("CD", "Congo, The Democratic Republic of the"), + ("CK", "Cook Islands"), + ("CR", "Costa Rica"), + ("CI", "Cote d'Ivoire"), + ("HR", "Croatia"), + ("CU", "Cuba"), + ("CY", "Cyprus"), + ("CZ", "Czech Republic"), + ("DK", "Denmark"), + ("DJ", "Djibouti"), + ("DM", "Dominica"), + ("DO", "Dominican Republic"), + ("EC", "Ecuador"), + ("EG", "Egypt"), + ("SV", "El Salvador"), + ("GQ", "Equatorial Guinea"), + ("ER", "Eritrea"), + ("EE", "Estonia"), + ("ET", "Ethiopia"), + ("FK", "Falkland Islands (Malvinas)"), + ("FO", "Faroe Islands"), + ("FJ", "Fiji"), + ("FI", "Finland"), + ("FR", "France"), + ("GF", "French Guiana"), + ("PF", "French Polynesia"), + ("TF", "French Southern Territories"), + ("GA", "Gabon"), + ("GM", "Gambia"), + ("GE", "Georgia"), + ("DE", "Germany"), + ("GH", "Ghana"), + ("GI", "Gibraltar"), + ("GR", "Greece"), + ("GL", "Greenland"), + ("GD", "Grenada"), + ("GP", "Guadeloupe"), + ("GU", "Guam"), + ("GT", "Guatemala"), + ("GG", "Guernsey"), + ("GN", "Guinea"), + ("GW", "Guinea-Bissau"), + ("GY", "Guyana"), + ("HT", "Haiti"), + ("HM", "Heard Island and McDonald Islands"), + ("VA", "Holy See (Vatican City State)"), + ("HN", "Honduras"), + ("HK", "Hong Kong"), + ("HU", "Hungary"), + ("IS", "Iceland"), + ("IN", "India"), + ("ID", "Indonesia"), + ("IR", "Iran, Islamic Republic of"), + ("IQ", "Iraq"), + ("IE", "Ireland"), + ("IM", "Isle of Man"), + ("IL", "Israel"), + ("IT", "Italy"), + ("JM", "Jamaica"), + ("JP", "Japan"), + ("JE", "Jersey"), + ("JO", "Jordan"), + ("KZ", "Kazakhstan"), + ("KE", "Kenya"), + ("KI", "Kiribati"), + ("KP", "Korea, Democratic People's Republic of"), + ("KR", "Korea, Republic of"), + ("KW", "Kuwait"), + ("KG", "Kyrgyzstan"), + ("LA", "Lao People's Democratic Republic"), + ("LV", "Latvia"), + ("LB", "Lebanon"), + ("LS", "Lesotho"), + ("LR", "Liberia"), + ("LY", "Libyan Arab Jamahiriya"), + ("LI", "Liechtenstein"), + ("LT", "Lithuania"), + ("LU", "Luxembourg"), + ("MO", "Macao"), + ("MK", "Macedonia, The Former Yugoslav Republic of"), + ("MG", "Madagascar"), + ("MW", "Malawi"), + ("MY", "Malaysia"), + ("MV", "Maldives"), + ("ML", "Mali"), + ("MT", "Malta"), + ("MH", "Marshall Islands"), + ("MQ", "Martinique"), + ("MR", "Mauritania"), + ("MU", "Mauritius"), + ("YT", "Mayotte"), + ("MX", "Mexico"), + ("FM", "Micronesia, Federated States of"), + ("MD", "Moldova"), + ("MC", "Monaco"), + ("MN", "Mongolia"), + ("ME", "Montenegro"), + ("MS", "Montserrat"), + ("MA", "Morocco"), + ("MZ", "Mozambique"), + ("MM", "Myanmar"), + ("NA", "Namibia"), + ("NR", "Nauru"), + ("NP", "Nepal"), + ("NL", "Netherlands"), + ("AN", "Netherlands Antilles"), + ("NC", "New Caledonia"), + ("NZ", "New Zealand"), + ("NI", "Nicaragua"), + ("NE", "Niger"), + ("NG", "Nigeria"), + ("NU", "Niue"), + ("NF", "Norfolk Island"), + ("MP", "Northern Mariana Islands"), + ("NO", "Norway"), + ("OM", "Oman"), + ("PK", "Pakistan"), + ("PW", "Palau"), + ("PS", "Palestinian Territory, Occupied"), + ("PA", "Panama"), + ("PG", "Papua New Guinea"), + ("PY", "Paraguay"), + ("PE", "Peru"), + ("PH", "Philippines"), + ("PN", "Pitcairn"), + ("PL", "Poland"), + ("PT", "Portugal"), + ("PR", "Puerto Rico"), + ("QA", "Qatar"), + ("RE", "Reunion"), + ("RO", "Romania"), + ("RU", "Russian Federation"), + ("RW", "Rwanda"), + ("BL", "Saint Barthelemy"), + ("SH", "Saint Helena"), + ("KN", "Saint Kitts and Nevis"), + ("LC", "Saint Lucia"), + ("MF", "Saint Martin"), + ("PM", "Saint Pierre and Miquelon"), + ("VC", "Saint Vincent and the Grenadines"), + ("WS", "Samoa"), + ("SM", "San Marino"), + ("ST", "Sao Tome and Principe"), + ("SA", "Saudi Arabia"), + ("SN", "Senegal"), + ("RS", "Serbia"), + ("SC", "Seychelles"), + ("SL", "Sierra Leone"), + ("SG", "Singapore"), + ("SK", "Slovakia"), + ("SI", "Slovenia"), + ("SB", "Solomon Islands"), + ("SO", "Somalia"), + ("ZA", "South Africa"), + ("GS", "South Georgia and the South Sandwich Islands"), + ("ES", "Spain"), + ("LK", "Sri Lanka"), + ("SD", "Sudan"), + ("SR", "Suriname"), + ("SJ", "Svalbard and Jan Mayen"), + ("SZ", "Swaziland"), + ("SE", "Sweden"), + ("CH", "Switzerland"), + ("SY", "Syrian Arab Republic"), + ("TW", "Taiwan, Province of China"), + ("TJ", "Tajikistan"), + ("TZ", "Tanzania, United Republic of"), + ("TH", "Thailand"), + ("TL", "Timor-Leste"), + ("TG", "Togo"), + ("TK", "Tokelau"), + ("TO", "Tonga"), + ("TT", "Trinidad and Tobago"), + ("TN", "Tunisia"), + ("TR", "Turkey"), + ("TM", "Turkmenistan"), + ("TC", "Turks and Caicos Islands"), + ("TV", "Tuvalu"), + ("UG", "Uganda"), + ("UA", "Ukraine"), + ("AE", "United Arab Emirates"), + ("US", "United States"), + ("UM", "United States Minor Outlying Islands"), + ("UY", "Uruguay"), + ("UZ", "Uzbekistan"), + ("VU", "Vanuatu"), + ("VE", "Venezuela"), + ("VN", "Viet Nam"), + ("VG", "Virgin Islands, British"), + ("VI", "Virgin Islands, U.S."), + ("WF", "Wallis and Futuna"), + ("EH", "Western Sahara"), + ("YE", "Yemen"), + ("ZM", "Zambia"), + ("ZW", "Zimbabwe"), + ], + default="", + max_length=3, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="addresses", + to="common.org", + ), + ), + ], + options={ + "verbose_name": "Address", + "verbose_name_plural": "Addresses", + "db_table": "address", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Profile", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, max_length=128, null=True, region=None + ), + ), + ( + "alternate_phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, max_length=128, null=True, region=None + ), + ), + ( + "role", + models.CharField( + choices=[("ADMIN", "ADMIN"), ("USER", "USER")], + default="USER", + max_length=50, + ), + ), + ("has_sales_access", models.BooleanField(default=False)), + ("has_marketing_access", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ("is_organization_admin", models.BooleanField(default=False)), + ("date_of_joining", models.DateField(blank=True, null=True)), + ( + "address", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="address_users", + to="common.address", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="profiles", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="profiles", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Profile", + "verbose_name_plural": "Profiles", + "db_table": "profile", + "ordering": ("-created_at",), + "unique_together": {("phone", "org"), ("user", "org")}, + }, + ), + migrations.AddField( + model_name="comment", + name="commented_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="common.profile", + ), + ), + migrations.CreateModel( + name="Activity", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "action", + models.CharField( + choices=[ + ("CREATE", "Created"), + ("UPDATE", "Updated"), + ("DELETE", "Deleted"), + ("VIEW", "Viewed"), + ("COMMENT", "Commented"), + ("ASSIGN", "Assigned"), + ], + max_length=20, + ), + ), + ( + "entity_type", + models.CharField( + choices=[ + ("Account", "Account"), + ("Lead", "Lead"), + ("Contact", "Contact"), + ("Opportunity", "Opportunity"), + ("Case", "Case"), + ("Task", "Task"), + ("Invoice", "Invoice"), + ("Event", "Event"), + ("Document", "Document"), + ("Team", "Team"), + ], + max_length=50, + ), + ), + ("entity_id", models.UUIDField()), + ( + "entity_name", + models.CharField(blank=True, default="", max_length=255), + ), + ("description", models.TextField(blank=True, default="")), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="activities", + to="common.org", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="activities", + to="common.profile", + ), + ), + ], + options={ + "verbose_name": "Activity", + "verbose_name_plural": "Activities", + "db_table": "activity", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="SecurityAuditLog", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "event_type", + models.CharField( + choices=[ + ("LOGIN_SUCCESS", "Login Success"), + ("LOGIN_FAILURE", "Login Failure"), + ("LOGOUT", "Logout"), + ("ORG_SWITCH", "Organization Switch"), + ("TOKEN_REFRESH", "Token Refresh"), + ("TOKEN_REVOKED", "Token Revoked"), + ("PERMISSION_DENIED", "Permission Denied"), + ("CROSS_ORG_ATTEMPT", "Cross-Org Access Attempt"), + ("API_KEY_USED", "API Key Used"), + ("API_KEY_INVALID", "Invalid API Key"), + ("MEMBERSHIP_REVOKED", "Membership Revoked"), + ("SUSPICIOUS_ACTIVITY", "Suspicious Activity"), + ], + db_index=True, + max_length=50, + ), + ), + ("description", models.TextField(blank=True, default="")), + ("metadata", models.JSONField(blank=True, default=dict)), + ("ip_address", models.GenericIPAddressField(blank=True, null=True)), + ("user_agent", models.TextField(blank=True, default="")), + ( + "request_path", + models.CharField(blank=True, default="", max_length=500), + ), + ( + "request_method", + models.CharField(blank=True, default="", max_length=10), + ), + ("success", models.BooleanField(default=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="security_audit_logs", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="security_audit_logs", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Security Audit Log", + "verbose_name_plural": "Security Audit Logs", + "db_table": "security_audit_log", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="SessionToken", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "token_jti", + models.CharField(db_index=True, max_length=255, unique=True), + ), + ( + "refresh_token_jti", + models.CharField( + blank=True, + db_index=True, + max_length=255, + null=True, + unique=True, + ), + ), + ("ip_address", models.GenericIPAddressField(blank=True, null=True)), + ("user_agent", models.TextField(blank=True, null=True)), + ("expires_at", models.DateTimeField()), + ("is_active", models.BooleanField(default=True)), + ("revoked_at", models.DateTimeField(blank=True, null=True)), + ("last_used_at", models.DateTimeField(auto_now=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="session_tokens", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Session Token", + "verbose_name_plural": "Session Tokens", + "db_table": "session_token", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Tags", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=20)), + ("slug", models.CharField(blank=True, max_length=20)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tags", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Tag", + "verbose_name_plural": "Tags", + "db_table": "tags", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="APISettings", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.TextField()), + ("apikey", models.CharField(blank=True, max_length=16)), + ("website", models.URLField(max_length=255, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="api_settings", + to="common.org", + ), + ), + ( + "lead_assigned_to", + models.ManyToManyField( + related_name="lead_assignee_users", to="common.profile" + ), + ), + ("tags", models.ManyToManyField(blank=True, to="common.tags")), + ], + options={ + "verbose_name": "APISetting", + "verbose_name_plural": "APISettings", + "db_table": "apiSettings", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Teams", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.TextField()), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="teams", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "users", + models.ManyToManyField( + related_name="user_teams", to="common.profile" + ), + ), + ], + options={ + "verbose_name": "Team", + "verbose_name_plural": "Teams", + "db_table": "teams", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Document", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.TextField(blank=True, null=True)), + ( + "document_file", + models.FileField( + max_length=5000, upload_to=common.models.document_path + ), + ), + ( + "status", + models.CharField( + choices=[("active", "active"), ("inactive", "inactive")], + default="active", + max_length=64, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="documents", + to="common.org", + ), + ), + ( + "shared_to", + models.ManyToManyField( + related_name="document_shared_to", to="common.profile" + ), + ), + ( + "teams", + models.ManyToManyField( + related_name="document_teams", to="common.teams" + ), + ), + ], + options={ + "verbose_name": "Document", + "verbose_name_plural": "Documents", + "db_table": "document", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="ContactFormSubmission", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("email", models.EmailField(max_length=254)), + ("message", models.TextField()), + ( + "reason", + models.CharField( + choices=[ + ("general", "General Inquiry"), + ("sales", "Sales Question"), + ("support", "Technical Support"), + ("partnership", "Partnership Opportunity"), + ("other", "Other"), + ], + default="general", + max_length=100, + ), + ), + ("ip_address", models.GenericIPAddressField(blank=True, null=True)), + ("user_agent", models.TextField(blank=True, null=True)), + ("referrer", models.URLField(blank=True, max_length=500, null=True)), + ( + "status", + models.CharField( + choices=[ + ("new", "New"), + ("read", "Read"), + ("replied", "Replied"), + ("closed", "Closed"), + ], + default="new", + max_length=20, + ), + ), + ("replied_at", models.DateTimeField(blank=True, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "replied_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="contact_replies", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Contact Form Submission", + "verbose_name_plural": "Contact Form Submissions", + "db_table": "contact_form_submission", + "ordering": ["-created_at"], + "indexes": [ + models.Index(fields=["email"], name="contact_for_email_534ce8_idx"), + models.Index( + fields=["status"], name="contact_for_status_b48047_idx" + ), + models.Index( + fields=["created_at"], name="contact_for_created_f54fb1_idx" + ), + ], + }, + ), + migrations.CreateModel( + name="Attachments", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("object_id", models.UUIDField()), + ("file_name", models.CharField(max_length=60)), + ( + "attachment", + models.FileField(max_length=1001, upload_to="attachments/%Y/%m/"), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="attachments", + to="contenttypes.contenttype", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="attachments", + to="common.org", + ), + ), + ], + options={ + "verbose_name": "Attachment", + "verbose_name_plural": "Attachments", + "db_table": "attachments", + "ordering": ("-created_at",), + "indexes": [ + models.Index( + fields=["content_type", "object_id"], + name="attachments_content_7c0e26_idx", + ), + models.Index( + fields=["org", "-created_at"], + name="attachments_org_id_432f7a_idx", + ), + ], + }, + ), + migrations.AddIndex( + model_name="comment", + index=models.Index( + fields=["content_type", "object_id"], name="comment_content_3076b0_idx" + ), + ), + migrations.AddIndex( + model_name="comment", + index=models.Index( + fields=["org", "-created_at"], name="comment_org_id_ae4e49_idx" + ), + ), + migrations.AddIndex( + model_name="activity", + index=models.Index( + fields=["org", "-created_at"], name="activity_org_id_832f39_idx" + ), + ), + migrations.AddIndex( + model_name="activity", + index=models.Index( + fields=["entity_type", "entity_id"], name="activity_entity__e33c9a_idx" + ), + ), + migrations.AddIndex( + model_name="securityauditlog", + index=models.Index( + fields=["event_type", "-created_at"], + name="security_au_event_t_5c4296_idx", + ), + ), + migrations.AddIndex( + model_name="securityauditlog", + index=models.Index( + fields=["user", "-created_at"], name="security_au_user_id_ab6fd5_idx" + ), + ), + migrations.AddIndex( + model_name="securityauditlog", + index=models.Index( + fields=["org", "-created_at"], name="security_au_org_id_d5d1da_idx" + ), + ), + migrations.AddIndex( + model_name="securityauditlog", + index=models.Index( + fields=["ip_address"], name="security_au_ip_addr_a572f5_idx" + ), + ), + migrations.AddIndex( + model_name="securityauditlog", + index=models.Index( + fields=["success", "-created_at"], name="security_au_success_c444de_idx" + ), + ), + migrations.AddIndex( + model_name="sessiontoken", + index=models.Index( + fields=["user", "is_active"], name="session_tok_user_id_ce3d01_idx" + ), + ), + migrations.AddIndex( + model_name="sessiontoken", + index=models.Index( + fields=["token_jti"], name="session_tok_token_j_a0ab6c_idx" + ), + ), + migrations.AddIndex( + model_name="sessiontoken", + index=models.Index( + fields=["expires_at"], name="session_tok_expires_08e0bc_idx" + ), + ), + migrations.AlterUniqueTogether( + name="tags", + unique_together={("slug", "org")}, + ), + ] diff --git a/backend/common/migrations/0002_enable_rls.py b/backend/common/migrations/0002_enable_rls.py new file mode 100644 index 00000000..2d269978 --- /dev/null +++ b/backend/common/migrations/0002_enable_rls.py @@ -0,0 +1,119 @@ +# Row-Level Security (RLS) Migration +# Enables PostgreSQL RLS on all org-scoped tables for multi-tenant isolation +# +# RLS Configuration: See common/rls/__init__.py for centralized policy definitions + +from django.db import migrations + +from common.rls import ( + RLS_CONFIG, + get_check_table_exists_sql, + get_disable_policy_sql, + get_enable_policy_sql, +) + + +def check_column_exists(cursor, table, column): + """Check if a column exists in a table.""" + cursor.execute( + """ + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = %s + AND column_name = %s + ) + """, + [table, column], + ) + return cursor.fetchone()[0] + + +def enable_rls(apps, schema_editor): + """Enable RLS on all org-scoped tables.""" + if schema_editor.connection.vendor != "postgresql": + print("RLS is only supported on PostgreSQL. Skipping.") + return + + isolation_policy = RLS_CONFIG["policies"]["isolation"] + + with schema_editor.connection.cursor() as cursor: + for table in RLS_CONFIG["tables"]: + # Check if table exists + cursor.execute(get_check_table_exists_sql(), [table]) + if not cursor.fetchone()[0]: + print(f" Skipping {table} (table does not exist)") + continue + + # Check if org_id column exists + if not check_column_exists(cursor, table, "org_id"): + print(f" Skipping {table} (no org_id column)") + continue + + # Check if policy already exists + cursor.execute( + """ + SELECT COUNT(*) FROM pg_policies + WHERE tablename = %s AND policyname = %s + """, + [table, isolation_policy], + ) + + if cursor.fetchone()[0] == 0: + cursor.execute(get_enable_policy_sql(table)) + print(f" Enabled RLS on {table}") + else: + print(f" RLS already enabled on {table}") + + +def disable_rls(apps, schema_editor): + """Disable RLS on all org-scoped tables (rollback).""" + if schema_editor.connection.vendor != "postgresql": + return + + with schema_editor.connection.cursor() as cursor: + for table in RLS_CONFIG["tables"]: + cursor.execute(get_check_table_exists_sql(), [table]) + if not cursor.fetchone()[0]: + continue + + if not check_column_exists(cursor, table, "org_id"): + continue + + cursor.execute(get_disable_policy_sql(table)) + print(f" Disabled RLS on {table}") + + +class Migration(migrations.Migration): + """ + Enable Row-Level Security (RLS) on all organization-scoped tables. + + This provides database-level isolation ensuring that: + 1. Queries automatically filter by current org + 2. Even if application code misses a filter, data is protected + 3. Direct SQL access respects org boundaries + + Prerequisites: + - PostgreSQL 9.5+ + - Database user must NOT be a superuser (superusers bypass RLS) + - Application must set app.current_org before queries + + Configuration: + - Table list and policy SQL are defined in common/rls/__init__.py + - Use `python manage.py manage_rls --status` to check RLS status + """ + + dependencies = [ + ("common", "0001_initial"), + ("accounts", "0002_initial"), + ("cases", "0002_initial"), + ("contacts", "0001_initial"), + ("invoices", "0001_initial"), + ("leads", "0001_initial"), + ("opportunity", "0001_initial"), + ("tasks", "0001_initial"), + ] + + operations = [ + migrations.RunPython(enable_rls, disable_rls), + ] diff --git a/common/migrations/__init__.py b/backend/common/migrations/__init__.py similarity index 100% rename from common/migrations/__init__.py rename to backend/common/migrations/__init__.py diff --git a/backend/common/mixins.py b/backend/common/mixins.py new file mode 100644 index 00000000..cc15149e --- /dev/null +++ b/backend/common/mixins.py @@ -0,0 +1,422 @@ +# Python imports +import uuid + +# Django imports +from common.models import models + + +class TimeAuditModel(models.Model): + """To path when the record was created and last modified""" + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Created At", + ) + updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At") + + class Meta: + abstract = True + + +class UserAuditModel(models.Model): + """To path when the record was created and last modified""" + + created_by = models.ForeignKey( + "common.User", + on_delete=models.SET_NULL, + related_name="%(class)s_created_by", + verbose_name="Created By", + null=True, + ) + updated_by = models.ForeignKey( + "common.User", + on_delete=models.SET_NULL, + related_name="%(class)s_updated_by", + verbose_name="Last Modified By", + null=True, + ) + + class Meta: + abstract = True + + +class AuditModel(TimeAuditModel, UserAuditModel): + """To path when the record was created and last modified""" + + class Meta: + abstract = True + + +# ============================================================================= +# View Mixins for Organization-Scoped Access +# ============================================================================= + +from django.db.models import Q +from rest_framework.exceptions import NotFound, PermissionDenied + + +class OrgFilterMixin: + """ + Mixin for API views that automatically filters querysets by organization. + + Usage: + class LeadListView(OrgFilterMixin, APIView): + model = Lead + + def get(self, request): + queryset = self.get_org_queryset() + # queryset is already filtered by org + """ + + model = None # Override in subclass + + def get_org_queryset(self): + """ + Get queryset filtered by the user's organization. + + Returns: + QuerySet filtered by org, or empty queryset if no org context + """ + if not hasattr(self, "model") or self.model is None: + raise NotImplementedError("OrgFilterMixin requires 'model' attribute") + + if not hasattr(self.request, "profile") or self.request.profile is None: + return self.model.objects.none() + + return self.model.objects.filter(org=self.request.profile.org) + + def get_org_object(self, pk): + """ + Get a single object by pk, ensuring it belongs to user's org. + + Args: + pk: Primary key of the object + + Returns: + Model instance + + Raises: + NotFound: If object doesn't exist or belongs to different org + """ + try: + obj = self.model.objects.get(pk=pk) + except self.model.DoesNotExist: + raise NotFound(f"{self.model.__name__} not found") + + # Verify org ownership + if hasattr(obj, "org_id"): + if obj.org_id != self.request.profile.org_id: + raise NotFound(f"{self.model.__name__} not found") + + return obj + + def filter_by_role(self, queryset): + """ + Apply role-based filtering to queryset. + + Admins see all org data, users see only assigned/created items. + + Args: + queryset: QuerySet already filtered by org + + Returns: + QuerySet with role-based filtering applied + """ + if self.request.profile.role == "ADMIN": + return queryset + + # Non-admins see items they created or are assigned to + filters = Q(created_by=self.request.profile.user) + + if hasattr(self.model, "assigned_to"): + filters |= Q(assigned_to=self.request.profile) + + if hasattr(self.model, "teams"): + user_team_ids = self.request.profile.user_teams.values_list("id", flat=True) + filters |= Q(teams__id__in=user_team_ids) + + return queryset.filter(filters).distinct() + + +class OrgCreateMixin: + """ + Mixin for API views that automatically sets org on object creation. + + Usage: + class LeadCreateView(OrgCreateMixin, APIView): + model = Lead + serializer_class = LeadSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + instance = self.perform_org_create(serializer) + return Response(...) + """ + + def perform_org_create(self, serializer): + """ + Save serializer with org and created_by set automatically. + + Args: + serializer: Valid serializer instance + + Returns: + Created model instance + """ + return serializer.save( + org=self.request.profile.org, created_by=self.request.profile.user + ) + + def get_org_context(self): + """ + Get org-related context for serializer. + + Returns: + Dict with org and profile for serializer context + """ + return { + "org": self.request.profile.org, + "profile": self.request.profile, + "user": self.request.profile.user, + } + + +class OrgUpdateMixin: + """ + Mixin for API views that validates org ownership on update. + + Usage: + class LeadUpdateView(OrgUpdateMixin, OrgFilterMixin, APIView): + model = Lead + + def put(self, request, pk): + instance = self.get_org_object(pk) + self.validate_update_permission(instance) + # proceed with update + """ + + def validate_update_permission(self, instance): + """ + Validate user has permission to update this instance. + + Args: + instance: Model instance to update + + Raises: + PermissionDenied: If user cannot update this instance + """ + # Verify org ownership + if hasattr(instance, "org_id"): + if instance.org_id != self.request.profile.org_id: + raise PermissionDenied("Cannot update object from another organization") + + # Admins can update anything in their org + if self.request.profile.role == "ADMIN": + return + + # Check if user created the object + if hasattr(instance, "created_by_id"): + if instance.created_by_id == self.request.profile.user_id: + return + + # Check if user is assigned + if hasattr(instance, "assigned_to"): + if self.request.profile in instance.assigned_to.all(): + return + + raise PermissionDenied("You don't have permission to update this object") + + +class OrgDeleteMixin: + """ + Mixin for API views that validates org ownership on delete. + + Usage: + class LeadDeleteView(OrgDeleteMixin, OrgFilterMixin, APIView): + model = Lead + + def delete(self, request, pk): + instance = self.get_org_object(pk) + self.validate_delete_permission(instance) + instance.delete() + """ + + def validate_delete_permission(self, instance): + """ + Validate user has permission to delete this instance. + + Args: + instance: Model instance to delete + + Raises: + PermissionDenied: If user cannot delete this instance + """ + # Verify org ownership + if hasattr(instance, "org_id"): + if instance.org_id != self.request.profile.org_id: + raise PermissionDenied("Cannot delete object from another organization") + + # Only admins and creators can delete + if self.request.profile.role == "ADMIN": + return + + if hasattr(instance, "created_by_id"): + if instance.created_by_id == self.request.profile.user_id: + return + + raise PermissionDenied("You don't have permission to delete this object") + + +class OrgViewMixin(OrgFilterMixin, OrgCreateMixin, OrgUpdateMixin, OrgDeleteMixin): + """ + Combined mixin for standard CRUD operations with org isolation. + + Usage: + class LeadView(OrgViewMixin, APIView): + model = Lead + serializer_class = LeadSerializer + + def get(self, request, pk=None): + if pk: + instance = self.get_org_object(pk) + return Response(self.serializer_class(instance).data) + queryset = self.filter_by_role(self.get_org_queryset()) + return Response(self.serializer_class(queryset, many=True).data) + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + instance = self.perform_org_create(serializer) + return Response(self.serializer_class(instance).data) + + def put(self, request, pk): + instance = self.get_org_object(pk) + self.validate_update_permission(instance) + serializer = self.serializer_class(instance, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + + def delete(self, request, pk): + instance = self.get_org_object(pk) + self.validate_delete_permission(instance) + instance.delete() + return Response(status=204) + """ + + pass + + +class ActivityTrackingMixin: + """ + Mixin to automatically track activities on CRUD operations. + + Usage: + class LeadView(ActivityTrackingMixin, OrgViewMixin, APIView): + model = Lead + entity_type = 'Lead' + """ + + entity_type = None # Override in subclass + + def track_activity(self, action, instance, description=""): + """ + Create an activity record for the action. + + Args: + action: Action type (CREATE, UPDATE, DELETE, VIEW) + instance: Model instance + description: Optional description + """ + from common.models import Activity + + if not self.entity_type: + return + + entity_name = "" + if hasattr(instance, "name"): + entity_name = instance.name + elif hasattr(instance, "title"): + entity_name = instance.title + elif hasattr(instance, "first_name"): + entity_name = f"{instance.first_name} {getattr(instance, 'last_name', '')}" + + Activity.objects.create( + user=self.request.profile, + action=action, + entity_type=self.entity_type, + entity_id=instance.id, + entity_name=entity_name[:255], + description=description[:500] if description else "", + org=self.request.profile.org, + ) + + +class BulkOperationMixin: + """ + Mixin for bulk operations with org validation. + + Usage: + class LeadBulkView(BulkOperationMixin, OrgFilterMixin, APIView): + model = Lead + + def post(self, request): + ids = request.data.get('ids', []) + instances = self.get_bulk_objects(ids) + # perform bulk operation + """ + + def get_bulk_objects(self, ids): + """ + Get multiple objects by ids, ensuring all belong to user's org. + + Args: + ids: List of primary keys + + Returns: + QuerySet of objects + + Raises: + PermissionDenied: If any object belongs to different org + """ + queryset = self.model.objects.filter(pk__in=ids) + + # Verify all objects belong to user's org + for obj in queryset: + if hasattr(obj, "org_id"): + if obj.org_id != self.request.profile.org_id: + raise PermissionDenied( + "One or more objects do not belong to your organization" + ) + + return queryset + + def bulk_update(self, ids, update_data): + """ + Bulk update objects with org validation. + + Args: + ids: List of primary keys + update_data: Dict of fields to update + + Returns: + Number of updated objects + """ + queryset = self.get_bulk_objects(ids) + return queryset.update(**update_data) + + def bulk_delete(self, ids): + """ + Bulk delete objects with org validation. + + Args: + ids: List of primary keys + + Returns: + Number of deleted objects + """ + queryset = self.get_bulk_objects(ids) + count = queryset.count() + queryset.delete() + return count diff --git a/common/models.py b/backend/common/models.py similarity index 53% rename from common/models.py rename to backend/common/models.py index ad158fe7..fbef3bed 100644 --- a/common/models.py +++ b/backend/common/models.py @@ -3,15 +3,18 @@ import datetime import os import time import uuid + import arrow -from django.contrib.auth.models import AbstractUser -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin -from .manager import UserManager +from django.contrib.auth.models import AbstractBaseUser, AbstractUser, PermissionsMixin +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone +from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField +from common.base import BaseModel from common.templatetags.common_tags import ( is_document_file_audio, is_document_file_code, @@ -23,7 +26,8 @@ from common.templatetags.common_tags import ( is_document_file_zip, ) from common.utils import COUNTRIES, ROLES -from common.base import BaseModel + +from .manager import UserManager def img_url(self, filename): @@ -36,18 +40,15 @@ class User(AbstractBaseUser, PermissionsMixin): default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True ) email = models.EmailField(_("email address"), blank=True, unique=True) - profile_pic = models.CharField( - max_length=1000, null=True, blank=True - ) + profile_pic = models.CharField(max_length=1000, null=True, blank=True) activation_key = models.CharField(max_length=150, null=True, blank=True) key_expires = models.DateTimeField(null=True, blank=True) is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(_('staff status'),default=False) + is_staff = models.BooleanField(_("staff status"), default=False) USERNAME_FIELD = "email" REQUIRED_FIELDS = [] - objects = UserManager() class Meta: @@ -64,6 +65,7 @@ class User(AbstractBaseUser, PermissionsMixin): # self.key_expires = timezone.now() + datetime.timedelta(hours=2) # super().save(*args, **kwargs) + class Address(BaseModel): address_line = models.CharField( _("Address"), max_length=255, blank=True, default="" @@ -75,6 +77,11 @@ class Address(BaseModel): _("Post/Zip-code"), max_length=64, blank=True, default="" ) country = models.CharField(max_length=3, choices=COUNTRIES, blank=True, default="") + org = models.ForeignKey( + "Org", + on_delete=models.CASCADE, + related_name="addresses", + ) class Meta: verbose_name = "Address" @@ -116,14 +123,14 @@ class Address(BaseModel): address += self.get_country_display() return address + def generate_unique_key(): return str(uuid.uuid4()) + class Org(BaseModel): name = models.CharField(max_length=100, blank=True, null=True) - api_key = models.TextField( - default=generate_unique_key, unique=True, editable=False - ) + api_key = models.TextField(default=generate_unique_key, unique=True, editable=False) is_active = models.BooleanField(default=True) # address = models.TextField(blank=True, null=True) # user_limit = models.IntegerField(default=5) @@ -139,6 +146,32 @@ class Org(BaseModel): return str(self.name) +class Tags(BaseModel): + """Tags for categorizing CRM entities (Accounts, Leads, Opportunities)""" + + name = models.CharField(max_length=20) + slug = models.CharField(max_length=20, blank=True) + org = models.ForeignKey( + "Org", + on_delete=models.CASCADE, + related_name="tags", + ) + + class Meta: + verbose_name = "Tag" + verbose_name_plural = "Tags" + db_table = "tags" + ordering = ("-created_at",) + unique_together = ["slug", "org"] + + def __str__(self): + return f"{self.name}" + + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + # class User(AbstractBaseUser, PermissionsMixin): # email = models.EmailField(_("email address"), blank=True, unique=True) # profile_pic = models.FileField( @@ -185,18 +218,14 @@ class Org(BaseModel): # super().save(*args, **kwargs) - - class Profile(BaseModel): - user = models.ForeignKey(User, on_delete=models.CASCADE) - org = models.ForeignKey( - Org, null=True, on_delete=models.CASCADE, blank=True, related_name="user_org" - ) - phone = PhoneNumberField(null=True, unique=True) - alternate_phone = PhoneNumberField(null=True,blank=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="profiles") + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="profiles") + phone = PhoneNumberField(null=True, blank=True) + alternate_phone = PhoneNumberField(null=True, blank=True) address = models.ForeignKey( Address, - related_name="adress_users", + related_name="address_users", on_delete=models.CASCADE, blank=True, null=True, @@ -213,7 +242,7 @@ class Profile(BaseModel): verbose_name_plural = "Profiles" db_table = "profile" ordering = ("-created_at",) - unique_together = ["user", "org"] + unique_together = [["user", "org"], ["phone", "org"]] def __str__(self): return f"{self.user.email} <{self.org.name}>" @@ -224,85 +253,37 @@ class Profile(BaseModel): @property def user_details(self): - return { - 'email' : self.user.email, - 'id' : self.user.id, - 'is_active' : self.user.is_active, - 'profile_pic' : self.user.profile_pic + return { + "email": self.user.email, + "id": self.user.id, + "is_active": self.user.is_active, + "profile_pic": self.user.profile_pic, + "last_login": self.user.last_login, } class Comment(BaseModel): - case = models.ForeignKey( - "cases.Case", - blank=True, - null=True, - related_name="cases", - on_delete=models.CASCADE, + """ + Generic comment model using ContentType framework. + Can be attached to any model (Account, Lead, Contact, Opportunity, Case, Task, Invoice, Profile). + """ + + # Generic relation to any model + content_type = models.ForeignKey( + ContentType, on_delete=models.CASCADE, related_name="comments" ) + object_id = models.UUIDField() + content_object = GenericForeignKey("content_type", "object_id") + comment = models.CharField(max_length=255) commented_on = models.DateTimeField(auto_now_add=True) commented_by = models.ForeignKey( Profile, on_delete=models.CASCADE, blank=True, null=True ) - account = models.ForeignKey( - "accounts.Account", - blank=True, - null=True, - related_name="accounts_comments", - on_delete=models.CASCADE, - ) - lead = models.ForeignKey( - "leads.Lead", - blank=True, - null=True, - related_name="leads_comments", - on_delete=models.CASCADE, - ) - opportunity = models.ForeignKey( - "opportunity.Opportunity", - blank=True, - null=True, - related_name="opportunity_comments", - on_delete=models.CASCADE, - ) - contact = models.ForeignKey( - "contacts.Contact", - blank=True, - null=True, - related_name="contact_comments", - on_delete=models.CASCADE, - ) - profile = models.ForeignKey( - "Profile", - blank=True, - null=True, - related_name="user_comments", - on_delete=models.CASCADE, - ) - - task = models.ForeignKey( - "tasks.Task", - blank=True, - null=True, - related_name="tasks_comments", - on_delete=models.CASCADE, - ) - - invoice = models.ForeignKey( - "invoices.Invoice", - blank=True, - null=True, - related_name="invoice_comments", - on_delete=models.CASCADE, - ) - - event = models.ForeignKey( - "events.Event", - blank=True, - null=True, - related_name="events_comments", + org = models.ForeignKey( + "Org", on_delete=models.CASCADE, + related_name="comments", ) class Meta: @@ -310,6 +291,10 @@ class Comment(BaseModel): verbose_name_plural = "Comments" db_table = "comment" ordering = ("-created_at",) + indexes = [ + models.Index(fields=["content_type", "object_id"]), + models.Index(fields=["org", "-created_at"]), + ] def __str__(self): return f"{self.comment}" @@ -321,10 +306,30 @@ class Comment(BaseModel): def commented_on_arrow(self): return arrow.get(self.commented_on).humanize() + def clean(self): + """ + Validate that the comment's org matches the content object's org. + + SECURITY: This prevents cross-org data references where a comment + in org_a could reference an object in org_b. + """ + from django.core.exceptions import ValidationError + + if self.content_object and hasattr(self.content_object, "org"): + if self.content_object.org_id != self.org_id: + raise ValidationError( + { + "org": "Comment organization must match the referenced object's organization." + } + ) + + def save(self, *args, **kwargs): + self.full_clean() + super().save(*args, **kwargs) + class CommentFiles(BaseModel): comment = models.ForeignKey(Comment, on_delete=models.CASCADE) - updated_on = models.DateTimeField(auto_now_add=True) comment_file = models.FileField( "File", upload_to="CommentFiles", null=True, blank=True ) @@ -346,72 +351,24 @@ class CommentFiles(BaseModel): class Attachments(BaseModel): - created_by = models.ForeignKey( - User, - related_name="attachment_created_by", - on_delete=models.SET_NULL, - null=True, - blank=True, + """ + Generic attachment model using ContentType framework. + Can be attached to any model (Account, Lead, Contact, Opportunity, Case, Task, Invoice). + """ + + # Generic relation to any model + content_type = models.ForeignKey( + ContentType, on_delete=models.CASCADE, related_name="attachments" ) + object_id = models.UUIDField() + content_object = GenericForeignKey("content_type", "object_id") + file_name = models.CharField(max_length=60) attachment = models.FileField(max_length=1001, upload_to="attachments/%Y/%m/") - lead = models.ForeignKey( - "leads.Lead", - null=True, - blank=True, - related_name="lead_attachment", - on_delete=models.CASCADE, - ) - account = models.ForeignKey( - "accounts.Account", - null=True, - blank=True, - related_name="account_attachment", - on_delete=models.CASCADE, - ) - contact = models.ForeignKey( - "contacts.Contact", - on_delete=models.CASCADE, - related_name="contact_attachment", - blank=True, - null=True, - ) - opportunity = models.ForeignKey( - "opportunity.Opportunity", - blank=True, - null=True, - on_delete=models.CASCADE, - related_name="opportunity_attachment", - ) - case = models.ForeignKey( - "cases.Case", - blank=True, - null=True, - on_delete=models.CASCADE, - related_name="case_attachment", - ) - - task = models.ForeignKey( - "tasks.Task", - blank=True, - null=True, - related_name="tasks_attachment", - on_delete=models.CASCADE, - ) - - invoice = models.ForeignKey( - "invoices.Invoice", - blank=True, - null=True, - related_name="invoice_attachment", - on_delete=models.CASCADE, - ) - event = models.ForeignKey( - "events.Event", - blank=True, - null=True, - related_name="events_attachment", + org = models.ForeignKey( + "Org", on_delete=models.CASCADE, + related_name="attachments", ) class Meta: @@ -419,6 +376,10 @@ class Attachments(BaseModel): verbose_name_plural = "Attachments" db_table = "attachments" ordering = ("-created_at",) + indexes = [ + models.Index(fields=["content_type", "object_id"]), + models.Index(fields=["org", "-created_at"]), + ] def __str__(self): return f"{self.file_name}" @@ -455,6 +416,27 @@ class Attachments(BaseModel): def created_on_arrow(self): return arrow.get(self.created_at).humanize() + def clean(self): + """ + Validate that the attachment's org matches the content object's org. + + SECURITY: This prevents cross-org data references where an attachment + in org_a could reference an object in org_b. + """ + from django.core.exceptions import ValidationError + + if self.content_object and hasattr(self.content_object, "org"): + if self.content_object.org_id != self.org_id: + raise ValidationError( + { + "org": "Attachment organization must match the referenced object's organization." + } + ) + + def save(self, *args, **kwargs): + self.full_clean() + super().save(*args, **kwargs) + def document_path(self, filename): hash_ = int(time.time()) @@ -467,25 +449,15 @@ class Document(BaseModel): title = models.TextField(blank=True, null=True) document_file = models.FileField(upload_to=document_path, max_length=5000) - created_by = models.ForeignKey( - Profile, - related_name="document_uploaded", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - status = models.CharField( choices=DOCUMENT_STATUS_CHOICE, max_length=64, default="active" ) shared_to = models.ManyToManyField(Profile, related_name="document_shared_to") - teams = models.ManyToManyField("teams.Teams", related_name="document_teams") + teams = models.ManyToManyField("Teams", related_name="document_teams") org = models.ForeignKey( Org, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="document_org", + on_delete=models.CASCADE, + related_name="documents", ) class Meta: @@ -496,7 +468,7 @@ class Document(BaseModel): def __str__(self): return f"{self.title}" - + def file_type(self): name_ext_list = self.document_file.url.split(".") if len(name_ext_list) > 1: @@ -555,22 +527,12 @@ class APISettings(BaseModel): lead_assigned_to = models.ManyToManyField( Profile, related_name="lead_assignee_users" ) - tags = models.ManyToManyField("accounts.Tags", blank=True) - created_by = models.ForeignKey( - Profile, - related_name="settings_created_by", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) + tags = models.ManyToManyField(Tags, blank=True) org = models.ForeignKey( Org, - blank=True, - on_delete=models.SET_NULL, - null=True, - related_name="org_api_settings", + on_delete=models.CASCADE, + related_name="api_settings", ) - class Meta: verbose_name = "APISetting" @@ -580,9 +542,236 @@ class APISettings(BaseModel): def __str__(self): return f"{self.title}" - def save(self, *args, **kwargs): if not self.apikey or self.apikey is None or self.apikey == "": self.apikey = generate_key() super().save(*args, **kwargs) + + +# Phase 3: JWT Token Tracking + + +class SessionToken(BaseModel): + """Track active JWT sessions for security""" + + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="session_tokens" + ) + token_jti = models.CharField(max_length=255, unique=True, db_index=True) # JWT ID + refresh_token_jti = models.CharField( + max_length=255, unique=True, db_index=True, null=True, blank=True + ) + ip_address = models.GenericIPAddressField(null=True, blank=True) + user_agent = models.TextField(blank=True, null=True) + expires_at = models.DateTimeField() + is_active = models.BooleanField(default=True) + revoked_at = models.DateTimeField(null=True, blank=True) + last_used_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "Session Token" + verbose_name_plural = "Session Tokens" + db_table = "session_token" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["user", "is_active"]), + models.Index(fields=["token_jti"]), + models.Index(fields=["expires_at"]), + ] + + def __str__(self): + return f"{self.user.email} - {self.token_jti[:8]}..." + + def revoke(self): + """Revoke this session token""" + from django.utils import timezone + + self.is_active = False + self.revoked_at = timezone.now() + self.save() + + @property + def is_expired(self): + """Check if token is expired""" + from django.utils import timezone + + return timezone.now() > self.expires_at + + @classmethod + def cleanup_expired(cls): + """Remove expired tokens (call via cron/celery)""" + from django.utils import timezone + + return cls.objects.filter(expires_at__lt=timezone.now()).delete() + + +# Activity Tracking for Recent Activities Dashboard + + +class Activity(BaseModel): + """Track user activities across all CRM entities""" + + ACTION_CHOICES = ( + ("CREATE", "Created"), + ("UPDATE", "Updated"), + ("DELETE", "Deleted"), + ("VIEW", "Viewed"), + ("COMMENT", "Commented"), + ("ASSIGN", "Assigned"), + ) + + ENTITY_TYPE_CHOICES = ( + ("Account", "Account"), + ("Lead", "Lead"), + ("Contact", "Contact"), + ("Opportunity", "Opportunity"), + ("Case", "Case"), + ("Task", "Task"), + ("Invoice", "Invoice"), + ("Event", "Event"), + ("Document", "Document"), + ("Team", "Team"), + ) + + user = models.ForeignKey( + Profile, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="activities", + ) + action = models.CharField(max_length=20, choices=ACTION_CHOICES) + entity_type = models.CharField(max_length=50, choices=ENTITY_TYPE_CHOICES) + entity_id = models.UUIDField() + entity_name = models.CharField(max_length=255, blank=True, default="") + description = models.TextField(blank=True, default="") + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="activities") + + class Meta: + verbose_name = "Activity" + verbose_name_plural = "Activities" + db_table = "activity" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["org", "-created_at"]), + models.Index(fields=["entity_type", "entity_id"]), + ] + + def __str__(self): + return f"{self.user} {self.get_action_display()} {self.entity_type}: {self.entity_name}" + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() + + +# ============================================================================= +# Teams Model (merged from teams app) +# ============================================================================= + + +class Teams(BaseModel): + name = models.CharField(max_length=100) + description = models.TextField() + users = models.ManyToManyField(Profile, related_name="user_teams") + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="teams") + + class Meta: + verbose_name = "Team" + verbose_name_plural = "Teams" + db_table = "teams" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.name}" + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() + + def get_users(self): + return ",".join( + [str(_id) for _id in list(self.users.values_list("id", flat=True))] + ) + + +# ============================================================================= +# Contact Form Submission (for public website) +# ============================================================================= + + +class ContactFormSubmission(BaseModel): + """ + Contact form submission from public website. + Stores inquiries from potential customers via contact forms. + Not org-scoped as these are platform-level submissions. + """ + + name = models.CharField(max_length=255) + email = models.EmailField() + message = models.TextField() + reason = models.CharField( + max_length=100, + choices=[ + ("general", "General Inquiry"), + ("sales", "Sales Question"), + ("support", "Technical Support"), + ("partnership", "Partnership Opportunity"), + ("other", "Other"), + ], + default="general", + ) + + # Tracking fields + ip_address = models.GenericIPAddressField(null=True, blank=True) + user_agent = models.TextField(null=True, blank=True) + referrer = models.URLField(max_length=500, null=True, blank=True) + + # Status tracking + STATUS_CHOICES = [ + ("new", "New"), + ("read", "Read"), + ("replied", "Replied"), + ("closed", "Closed"), + ] + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="new") + replied_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="contact_replies", + ) + replied_at = models.DateTimeField(null=True, blank=True) + + class Meta: + verbose_name = "Contact Form Submission" + verbose_name_plural = "Contact Form Submissions" + db_table = "contact_form_submission" + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["email"]), + models.Index(fields=["status"]), + models.Index(fields=["created_at"]), + ] + + def __str__(self): + return f"{self.name} - {self.email} ({self.reason})" + + def mark_as_read(self): + """Mark submission as read""" + if self.status == "new": + self.status = "read" + self.save() + + def mark_as_replied(self, user): + """Mark submission as replied""" + self.status = "replied" + self.replied_by = user + self.replied_at = timezone.now() + self.save() + + +# Import SecurityAuditLog so Django discovers it for migrations +from common.audit_log import SecurityAuditLog diff --git a/backend/common/permissions.py b/backend/common/permissions.py new file mode 100644 index 00000000..e9bfa3be --- /dev/null +++ b/backend/common/permissions.py @@ -0,0 +1,236 @@ +""" +Custom Permission Classes for Multi-Tenancy Security + +These permission classes enforce organization context and access control +across all API endpoints. +""" + +from rest_framework import permissions +from rest_framework.exceptions import PermissionDenied + + +class HasOrgContext(permissions.BasePermission): + """ + Permission class that requires valid organization context. + + This should be used on all endpoints that require org-scoped data access. + It verifies that: + 1. User is authenticated + 2. request.profile is set (from middleware) + 3. request.org is set (from JWT or API key) + + Usage: + class MyView(APIView): + permission_classes = [IsAuthenticated, HasOrgContext] + """ + + message = "Organization context is required. Please login again." + + def has_permission(self, request, view): + # Must have profile set by middleware + if not hasattr(request, "profile") or request.profile is None: + return False + + # Must have org set + if not hasattr(request, "org") or request.org is None: + return False + + # Profile must be active + if not request.profile.is_active: + return False + + return True + + +class IsOrgAdmin(permissions.BasePermission): + """ + Permission class that requires user to be an organization admin. + + Usage: + class AdminOnlyView(APIView): + permission_classes = [IsAuthenticated, HasOrgContext, IsOrgAdmin] + """ + + message = "You must be an organization administrator to perform this action." + + def has_permission(self, request, view): + if not hasattr(request, "profile") or request.profile is None: + return False + + return request.profile.role == "ADMIN" or request.profile.is_organization_admin + + +class IsOrgMember(permissions.BasePermission): + """ + Permission class that verifies user belongs to the organization. + + This is a safety check that complements the middleware validation. + """ + + message = "You are not a member of this organization." + + def has_permission(self, request, view): + if not hasattr(request, "profile") or request.profile is None: + return False + + if not hasattr(request, "org") or request.org is None: + return False + + # Verify profile org matches request org + return request.profile.org_id == request.org.id + + +class HasSalesAccess(permissions.BasePermission): + """ + Permission class that requires sales module access. + + Usage: + class LeadsView(APIView): + permission_classes = [IsAuthenticated, HasOrgContext, HasSalesAccess] + """ + + message = "You do not have access to the sales module." + + def has_permission(self, request, view): + if not hasattr(request, "profile") or request.profile is None: + return False + + # Admins always have access + if request.profile.role == "ADMIN": + return True + + return request.profile.has_sales_access + + +class HasMarketingAccess(permissions.BasePermission): + """ + Permission class that requires marketing module access. + + Usage: + class CampaignsView(APIView): + permission_classes = [IsAuthenticated, HasOrgContext, HasMarketingAccess] + """ + + message = "You do not have access to the marketing module." + + def has_permission(self, request, view): + if not hasattr(request, "profile") or request.profile is None: + return False + + # Admins always have access + if request.profile.role == "ADMIN": + return True + + return request.profile.has_marketing_access + + +class CanAccessObject(permissions.BasePermission): + """ + Object-level permission that verifies user can access specific object. + + Checks that: + 1. Object belongs to user's organization + 2. User is assigned to object OR is admin OR created the object + + Usage: + class LeadDetailView(APIView): + permission_classes = [IsAuthenticated, HasOrgContext, CanAccessObject] + + def get_object(self): + return Lead.objects.get(pk=self.kwargs['pk']) + """ + + message = "You do not have permission to access this object." + + def has_object_permission(self, request, view, obj): + # Object must belong to same org + if hasattr(obj, "org_id"): + if obj.org_id != request.org.id: + return False + + # Admins can access all objects in their org + if request.profile.role == "ADMIN": + return True + + # Check if user created the object + if hasattr(obj, "created_by_id"): + if obj.created_by_id == request.profile.user_id: + return True + + # Check if user is assigned to the object + if hasattr(obj, "assigned_to"): + if request.profile in obj.assigned_to.all(): + return True + + # Check if user is in a team assigned to the object + if hasattr(obj, "teams"): + user_team_ids = request.profile.user_teams.values_list("id", flat=True) + if obj.teams.filter(id__in=user_team_ids).exists(): + return True + + return False + + +class IsSuperAdmin(permissions.BasePermission): + """ + Permission class for platform-level super admins. + + Super admins are identified by @micropyramid.com email domain. + They have access to admin panel and can manage all organizations. + """ + + message = "Super admin access required." + + def has_permission(self, request, view): + if not request.user or not request.user.is_authenticated: + return False + + # Check for super admin email domain + return request.user.email.endswith("@micropyramid.com") + + +def get_org_filtered_queryset(model, request): + """ + Helper function to get organization-filtered queryset. + + Usage: + queryset = get_org_filtered_queryset(Lead, request) + + Args: + model: Django model class with org field + request: DRF request object with profile attached + + Returns: + QuerySet filtered by organization + """ + if not hasattr(request, "profile") or request.profile is None: + return model.objects.none() + + return model.objects.filter(org=request.profile.org) + + +def validate_org_ownership(obj, request): + """ + Helper function to validate object belongs to user's org. + + Raises PermissionDenied if object doesn't belong to user's org. + + Usage: + lead = Lead.objects.get(pk=pk) + validate_org_ownership(lead, request) + + Args: + obj: Model instance with org field + request: DRF request object + + Raises: + PermissionDenied: If object doesn't belong to user's org + """ + if not hasattr(request, "profile") or request.profile is None: + raise PermissionDenied("Organization context required") + + if not hasattr(obj, "org_id"): + return # Object doesn't have org field + + if obj.org_id != request.profile.org_id: + raise PermissionDenied("Object does not belong to your organization") diff --git a/backend/common/rls/__init__.py b/backend/common/rls/__init__.py new file mode 100644 index 00000000..d37c64f8 --- /dev/null +++ b/backend/common/rls/__init__.py @@ -0,0 +1,214 @@ +""" +Row-Level Security (RLS) Configuration for PostgreSQL. + +This module provides centralized configuration and SQL generators for +database-level multi-tenancy using PostgreSQL Row-Level Security. + +RLS ensures that: +1. Queries automatically filter by current org +2. Even if application code misses a filter, data is protected +3. Direct SQL access respects org boundaries + +Usage: + from common.rls import RLS_CONFIG, get_enable_policy_sql + + # Enable RLS on a table + cursor.execute(get_enable_policy_sql('lead')) + + # Check config + tables = RLS_CONFIG['tables'] +""" + +# PostgreSQL session variable for org context +# Set via: SELECT set_config('app.current_org', '', true) +CONTEXT_VARIABLE = "app.current_org" + +# Policy names +ISOLATION_POLICY = "org_isolation" +INSERT_POLICY = "org_insert_check" + +# All org-scoped tables that need RLS policies +# Add new org-scoped tables here +# NOTE: Only include tables that have an org_id column directly +# Table names must match actual PostgreSQL table names (check via \dt) +ORG_SCOPED_TABLES = [ + # Core business entities + "lead", + "accounts", # Note: plural + "contacts", # Note: plural + "opportunity", + "case", # Note: singular + "task", + "invoice", + # Supporting entities + "comment", + "attachments", + "document", + "teams", + "activity", + "tags", + "address", + "solution", + # Boards (Kanban) + "board", + "board_column", + "board_task", + "board_member", + # Settings + "apiSettings", # Note: camelCase + # Email & Invoicing + "account_email", + "emailLogs", + "invoice_history", + # Security & Audit + "security_audit_log", +] + +# Centralized RLS configuration +RLS_CONFIG = { + "context_variable": CONTEXT_VARIABLE, + "tables": ORG_SCOPED_TABLES, + "policies": { + "isolation": ISOLATION_POLICY, + "insert": INSERT_POLICY, + }, +} + + +def get_check_table_exists_sql(): + """ + Returns SQL to check if a table exists. + + Usage: + cursor.execute(get_check_table_exists_sql(), [table_name]) + exists = cursor.fetchone()[0] + """ + return """ + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = %s + ) + """ + + +def get_check_rls_status_sql(): + """ + Returns SQL to check RLS status for a table. + + Usage: + cursor.execute(get_check_rls_status_sql(), [table_name]) + rls_enabled, rls_forced = cursor.fetchone() + """ + return """ + SELECT relrowsecurity, relforcerowsecurity + FROM pg_class + WHERE relname = %s AND relnamespace = 'public'::regnamespace + """ + + +def get_set_context_sql(): + """ + Returns SQL to set the org context. + + Usage: + cursor.execute(get_set_context_sql(), [org_id]) + """ + return f"SELECT set_config('{CONTEXT_VARIABLE}', %s, true)" + + +def get_enable_policy_sql(table): + """ + Returns SQL to enable RLS on a table with proper policies. + + This creates: + 1. Enables RLS on the table + 2. Forces RLS for table owner + 3. Creates isolation policy for SELECT/UPDATE/DELETE + 4. Creates insert check policy + + Args: + table: Table name + + Returns: + SQL string to execute + """ + return f""" + -- Enable RLS on table + ALTER TABLE "{table}" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "{table}" FORCE ROW LEVEL SECURITY; + + -- Drop existing policies if any + DROP POLICY IF EXISTS {ISOLATION_POLICY} ON "{table}"; + DROP POLICY IF EXISTS {INSERT_POLICY} ON "{table}"; + + -- Create isolation policy (SELECT, UPDATE, DELETE) + -- Uses NULLIF to return no rows when context is not set (fail-safe) + CREATE POLICY {ISOLATION_POLICY} ON "{table}" + FOR ALL + USING ( + org_id::text = NULLIF(current_setting('{CONTEXT_VARIABLE}', true), '') + ); + + -- Create insert check policy + CREATE POLICY {INSERT_POLICY} ON "{table}" + FOR INSERT + WITH CHECK ( + org_id::text = NULLIF(current_setting('{CONTEXT_VARIABLE}', true), '') + ); + """ + + +def get_disable_policy_sql(table): + """ + Returns SQL to disable RLS on a table. + + Args: + table: Table name + + Returns: + SQL string to execute + """ + return f""" + -- Drop policies + DROP POLICY IF EXISTS {ISOLATION_POLICY} ON "{table}"; + DROP POLICY IF EXISTS {INSERT_POLICY} ON "{table}"; + + -- Disable RLS + ALTER TABLE "{table}" DISABLE ROW LEVEL SECURITY; + """ + + +def get_update_policy_sql(table): + """ + Returns SQL to update RLS policies with secure defaults. + + This fixes the security issue where empty context allowed all data. + Uses NULLIF() to ensure no rows are returned when context is missing. + + Args: + table: Table name + + Returns: + SQL string to execute + """ + return f""" + -- Drop existing policies + DROP POLICY IF EXISTS {ISOLATION_POLICY} ON "{table}"; + DROP POLICY IF EXISTS {INSERT_POLICY} ON "{table}"; + + -- Create secure isolation policy + -- NULLIF returns NULL when context is empty, causing comparison to fail + CREATE POLICY {ISOLATION_POLICY} ON "{table}" + FOR ALL + USING ( + org_id::text = NULLIF(current_setting('{CONTEXT_VARIABLE}', true), '') + ); + + -- Create secure insert check policy + CREATE POLICY {INSERT_POLICY} ON "{table}" + FOR INSERT + WITH CHECK ( + org_id::text = NULLIF(current_setting('{CONTEXT_VARIABLE}', true), '') + ); + """ diff --git a/backend/common/serializer.py b/backend/common/serializer.py new file mode 100644 index 00000000..7f298995 --- /dev/null +++ b/backend/common/serializer.py @@ -0,0 +1,682 @@ +import re + +from django.contrib.auth import authenticate +from django.contrib.auth.hashers import check_password +from django.contrib.auth.tokens import default_token_generator +from django.utils.http import urlsafe_base64_decode +from rest_framework import serializers +from rest_framework_simplejwt.tokens import AccessToken, RefreshToken + +from common.models import ( + Activity, + Address, + APISettings, + Attachments, + Comment, + Document, + Org, + Profile, + Teams, + User, +) + + +class OrgAwareRefreshToken(RefreshToken): + """ + Custom RefreshToken that includes org_id in the token payload. + + This ensures the org context is cryptographically signed and cannot be + forged by the client. The middleware should validate org_id from this + token instead of trusting the org header. + """ + + @classmethod + def for_user_and_org(cls, user, org): + """ + Generate a refresh token for a user with org context. + + Args: + user: User instance + org: Org instance or org_id UUID + + Returns: + OrgAwareRefreshToken with org_id claim + """ + token = cls.for_user(user) + + # Add org_id to the token payload + if org: + org_id = str(org.id) if hasattr(org, "id") else str(org) + token["org_id"] = org_id + + return token + + +class OrganizationSerializer(serializers.ModelSerializer): + class Meta: + model = Org + fields = ("id", "name", "api_key") + + +class SocialLoginSerializer(serializers.Serializer): + token = serializers.CharField() + + +class CommentSerializer(serializers.ModelSerializer): + """Serializer for Comment model using ContentType""" + + content_type = serializers.SlugRelatedField(slug_field="model", read_only=True) + + class Meta: + model = Comment + fields = ( + "id", + "comment", + "commented_on", + "commented_by", + "content_type", + "object_id", + "org", + ) + + +class CommentCreateSerializer(serializers.ModelSerializer): + """Serializer for creating comments with ContentType""" + + content_type = serializers.CharField(write_only=True) + object_id = serializers.UUIDField(write_only=True) + + class Meta: + model = Comment + fields = ( + "comment", + "content_type", + "object_id", + ) + + def create(self, validated_data): + from django.contrib.contenttypes.models import ContentType + + content_type_str = validated_data.pop("content_type") + try: + content_type = ContentType.objects.get(model=content_type_str.lower()) + except ContentType.DoesNotExist: + raise serializers.ValidationError( + f"Invalid content type: {content_type_str}" + ) + + validated_data["content_type"] = content_type + return super().create(validated_data) + + +class LeadCommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = ( + "id", + "comment", + "commented_on", + "commented_by", + ) + + +class OrgProfileCreateSerializer(serializers.ModelSerializer): + """ + It is for creating organization + """ + + name = serializers.CharField(max_length=255) + + class Meta: + model = Org + fields = ["name"] + extra_kwargs = {"name": {"required": True}} + + def validate_name(self, name): + if bool(re.search(r"[~\!_.@#\$%\^&\*\ \(\)\+{}\":;'/\[\]]", name)): + raise serializers.ValidationError( + "organization name should not contain any special characters" + ) + if Org.objects.filter(name=name).exists(): + raise serializers.ValidationError( + "Organization already exists with this name" + ) + return name + + +class ShowOrganizationListSerializer(serializers.ModelSerializer): + """ + we are using it for show orjanization list + """ + + org = OrganizationSerializer() + + class Meta: + model = Profile + fields = ( + "role", + "alternate_phone", + "has_sales_access", + "has_marketing_access", + "is_organization_admin", + "org", + ) + + +class BillingAddressSerializer(serializers.ModelSerializer): + country = serializers.SerializerMethodField() + + def get_country(self, obj): + return obj.get_country_display() + + class Meta: + model = Address + fields = ("address_line", "street", "city", "state", "postcode", "country") + + def __init__(self, *args, **kwargs): + account_view = kwargs.pop("account", False) + + super().__init__(*args, **kwargs) + + if account_view: + self.fields["address_line"].required = True + self.fields["street"].required = True + self.fields["city"].required = True + self.fields["state"].required = True + self.fields["postcode"].required = True + self.fields["country"].required = True + + +class CreateUserSerializer(serializers.ModelSerializer): + + class Meta: + model = User + fields = ( + "email", + "profile_pic", + ) + + def __init__(self, *args, **kwargs): + self.org = kwargs.pop("org", None) + super().__init__(*args, **kwargs) + self.fields["email"].required = True + + def validate_email(self, email): + if self.instance: + if self.instance.email != email: + if not Profile.objects.filter(user__email=email, org=self.org).exists(): + return email + raise serializers.ValidationError("Email already exists") + return email + if not Profile.objects.filter(user__email=email.lower(), org=self.org).exists(): + return email + raise serializers.ValidationError("Given Email id already exists") + + +class CreateProfileSerializer(serializers.ModelSerializer): + class Meta: + model = Profile + fields = ( + "role", + "phone", + "alternate_phone", + "has_sales_access", + "has_marketing_access", + "is_organization_admin", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["alternate_phone"].required = False + self.fields["role"].required = True + self.fields["phone"].required = True + + +class UserSerializer(serializers.ModelSerializer): + + class Meta: + model = User + fields = ["id", "email", "profile_pic"] + + +class ProfileSerializer(serializers.ModelSerializer): + # address = BillingAddressSerializer() + + class Meta: + model = Profile + fields = ( + "id", + "user_details", + "role", + "address", + "has_marketing_access", + "has_sales_access", + "phone", + "date_of_joining", + "is_active", + "created_at", + ) + + +class AttachmentsSerializer(serializers.ModelSerializer): + """Serializer for Attachments model using ContentType""" + + file_path = serializers.SerializerMethodField() + content_type = serializers.SlugRelatedField(slug_field="model", read_only=True) + + def get_file_path(self, obj): + if obj.attachment: + return obj.attachment.url + return None + + class Meta: + model = Attachments + fields = [ + "id", + "created_by", + "file_name", + "created_at", + "file_path", + "content_type", + "object_id", + "org", + ] + + +class AttachmentsCreateSerializer(serializers.ModelSerializer): + """Serializer for creating attachments with ContentType""" + + content_type = serializers.CharField(write_only=True) + object_id = serializers.UUIDField(write_only=True) + + class Meta: + model = Attachments + fields = ( + "file_name", + "attachment", + "content_type", + "object_id", + ) + + def create(self, validated_data): + from django.contrib.contenttypes.models import ContentType + + content_type_str = validated_data.pop("content_type") + try: + content_type = ContentType.objects.get(model=content_type_str.lower()) + except ContentType.DoesNotExist: + raise serializers.ValidationError( + f"Invalid content type: {content_type_str}" + ) + + validated_data["content_type"] = content_type + return super().create(validated_data) + + +class DocumentSerializer(serializers.ModelSerializer): + shared_to = ProfileSerializer(read_only=True, many=True) + teams = serializers.SerializerMethodField() + created_by = UserSerializer() + org = OrganizationSerializer() + + def get_teams(self, obj): + return obj.teams.all().values() + + class Meta: + model = Document + fields = [ + "id", + "title", + "document_file", + "status", + "shared_to", + "teams", + "created_at", + "created_by", + "org", + ] + + +class DocumentCreateSerializer(serializers.ModelSerializer): + def __init__(self, *args, **kwargs): + request_obj = kwargs.pop("request_obj", None) + super().__init__(*args, **kwargs) + self.fields["title"].required = True + self.org = request_obj.profile.org + + def validate_title(self, title): + if self.instance: + if ( + Document.objects.filter(title__iexact=title, org=self.org) + .exclude(id=self.instance.id) + .exists() + ): + raise serializers.ValidationError( + "Document with this Title already exists" + ) + if Document.objects.filter(title__iexact=title, org=self.org).exists(): + raise serializers.ValidationError("Document with this Title already exists") + return title + + class Meta: + model = Document + fields = ["title", "document_file", "status", "org"] + + +def find_urls(string): + # website_regex = "^((http|https)://)?([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com or google.com + # website_regex = "^https?://([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com + # http(s)://google.com + website_regex = r"^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}$" + # http(s)://google.com:8000 + website_regex_port = r"^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}:[0-9]{2,4}$" + url = re.findall(website_regex, string) + url_port = re.findall(website_regex_port, string) + if url and url[0] != "": + return url + return url_port + + +class APISettingsSerializer(serializers.ModelSerializer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + class Meta: + model = APISettings + fields = ("title", "website") + + def validate_website(self, website): + if website and not ( + website.startswith("http://") or website.startswith("https://") + ): + raise serializers.ValidationError("Please provide valid schema") + if not len(find_urls(website)) > 0: + raise serializers.ValidationError( + "Please provide a valid URL with schema and without trailing slash - Example: http://google.com" + ) + return website + + +class APISettingsListSerializer(serializers.ModelSerializer): + created_by = UserSerializer() + lead_assigned_to = ProfileSerializer(read_only=True, many=True) + tags = serializers.SerializerMethodField() + org = OrganizationSerializer() + + def get_tags(self, obj): + return obj.tags.all().values() + + class Meta: + model = APISettings + fields = [ + "title", + "apikey", + "website", + "created_at", + "created_by", + "lead_assigned_to", + "tags", + "org", + ] + + +class APISettingsSwaggerSerializer(serializers.ModelSerializer): + class Meta: + model = APISettings + fields = [ + "title", + "website", + "lead_assigned_to", + "tags", + ] + + +class DocumentCreateSwaggerSerializer(serializers.ModelSerializer): + class Meta: + model = Document + fields = [ + "title", + "document_file", + "teams", + "shared_to", + ] + + +class DocumentEditSwaggerSerializer(serializers.ModelSerializer): + class Meta: + model = Document + fields = ["title", "document_file", "teams", "shared_to", "status"] + + +class UserCreateSwaggerSerializer(serializers.Serializer): + """ + It is swagger for creating or updating user + """ + + ROLE_CHOICES = ["ADMIN", "USER"] + + email = serializers.CharField(max_length=1000, required=True) + role = serializers.ChoiceField(choices=ROLE_CHOICES, required=True) + phone = serializers.CharField(max_length=12) + alternate_phone = serializers.CharField(max_length=12) + address_line = serializers.CharField(max_length=10000, required=True) + street = serializers.CharField(max_length=1000) + city = serializers.CharField(max_length=1000) + state = serializers.CharField(max_length=1000) + pincode = serializers.CharField(max_length=1000) + country = serializers.CharField(max_length=1000) + + +class UserUpdateStatusSwaggerSerializer(serializers.Serializer): + + STATUS_CHOICES = ["Active", "Inactive"] + + status = serializers.ChoiceField(choices=STATUS_CHOICES, required=True) + + +# JWT Authentication Serializers for SvelteKit Integration + + +class LoginSerializer(serializers.Serializer): + """Serializer for user login with email and password""" + + email = serializers.EmailField(required=True) + password = serializers.CharField(required=True, write_only=True) + + def validate(self, attrs): + email = attrs.get("email") + password = attrs.get("password") + + if email and password: + user = authenticate(username=email, password=password) + if not user: + raise serializers.ValidationError("Invalid email or password") + if not user.is_active: + raise serializers.ValidationError("User account is disabled") + else: + raise serializers.ValidationError('Must include "email" and "password"') + + attrs["user"] = user + return attrs + + +class RegisterSerializer(serializers.ModelSerializer): + """Serializer for user registration""" + + password = serializers.CharField(write_only=True, required=True, min_length=8) + confirm_password = serializers.CharField(write_only=True, required=True) + + class Meta: + model = User + fields = ["email", "password", "confirm_password"] + + def validate(self, attrs): + if attrs["password"] != attrs["confirm_password"]: + raise serializers.ValidationError( + {"password": "Password fields didn't match."} + ) + return attrs + + def create(self, validated_data): + validated_data.pop("confirm_password") + user = User.objects.create_user( + email=validated_data["email"], + password=validated_data["password"], + is_active=False, # User needs to activate account + ) + return user + + +class UserDetailSerializer(serializers.ModelSerializer): + """Detailed user serializer with profile and organizations""" + + organizations = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ["id", "email", "profile_pic", "is_active", "organizations"] + + def get_organizations(self, obj): + """Get all organizations the user belongs to""" + profiles = Profile.objects.filter(user=obj, is_active=True) + return [ + { + "id": str(profile.org.id), + "name": profile.org.name, + "role": profile.role, + "is_organization_admin": profile.is_organization_admin, + "has_sales_access": profile.has_sales_access, + "has_marketing_access": profile.has_marketing_access, + } + for profile in profiles + ] + + +class ProfileDetailSerializer(serializers.ModelSerializer): + """Detailed profile serializer for authenticated user""" + + user = UserSerializer(read_only=True) + org = OrganizationSerializer(read_only=True) + + class Meta: + model = Profile + fields = [ + "id", + "user", + "org", + "role", + "is_organization_admin", + "has_sales_access", + "has_marketing_access", + "phone", + "date_of_joining", + "is_active", + ] + + +# Activity Serializers for Dashboard Recent Activities + + +class ActivityUserSerializer(serializers.Serializer): + """Simplified user info for activity display""" + + id = serializers.UUIDField(source="user.id") + email = serializers.EmailField(source="user.email") + name = serializers.SerializerMethodField() + profile_pic = serializers.CharField(source="user.profile_pic", allow_null=True) + + def get_name(self, obj): + """Get display name from email""" + return obj.user.email.split("@")[0] + + +class ActivitySerializer(serializers.ModelSerializer): + """Serializer for recent activities""" + + user = ActivityUserSerializer(read_only=True) + action_display = serializers.CharField(source="get_action_display", read_only=True) + timestamp = serializers.DateTimeField(source="created_at", read_only=True) + humanized_time = serializers.CharField(source="created_on_arrow", read_only=True) + + class Meta: + model = Activity + fields = [ + "id", + "user", + "action", + "action_display", + "entity_type", + "entity_id", + "entity_name", + "description", + "timestamp", + "humanized_time", + ] + + +# ============================================================================= +# Teams Serializers (merged from teams app) +# ============================================================================= + + +class TeamsSerializer(serializers.ModelSerializer): + users = ProfileSerializer(read_only=True, many=True) + created_by = UserSerializer() + + class Meta: + model = Teams + fields = ( + "id", + "name", + "description", + "users", + "created_at", + "created_by", + "created_on_arrow", + ) + + +class TeamCreateSerializer(serializers.ModelSerializer): + + def __init__(self, *args, **kwargs): + request_obj = kwargs.pop("request_obj", None) + super().__init__(*args, **kwargs) + self.org = request_obj.profile.org + + self.fields["name"].required = True + self.fields["description"].required = False + + def validate_name(self, name): + if self.instance: + if ( + Teams.objects.filter(name__iexact=name, org=self.org) + .exclude(id=self.instance.id) + .exists() + ): + raise serializers.ValidationError("Team already exists with this name") + else: + if Teams.objects.filter(name__iexact=name).exists(): + raise serializers.ValidationError("Team already exists with this name") + return name + + class Meta: + model = Teams + fields = ( + "name", + "description", + "created_at", + "created_by", + "created_on_arrow", + "org", + ) + + +class TeamswaggerCreateSerializer(serializers.ModelSerializer): + + class Meta: + model = Teams + fields = ( + "name", + "description", + "users", + ) diff --git a/backend/common/signals.py b/backend/common/signals.py new file mode 100644 index 00000000..85204c81 --- /dev/null +++ b/backend/common/signals.py @@ -0,0 +1,146 @@ +""" +Django signals for automatic activity tracking across CRM entities. + +This module registers post_save and post_delete signals on all major CRM models +to automatically create Activity records when entities are created, updated, or deleted. +""" + +from crum import get_current_request +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver + +from common.models import Activity + + +def get_entity_name(instance): + """Get a display name for an entity instance""" + # Try common name attributes + if hasattr(instance, "name") and instance.name: + return str(instance.name) + if hasattr(instance, "title") and instance.title: + return str(instance.title) + if hasattr(instance, "first_name"): + name = f"{instance.first_name or ''} {instance.last_name or ''}".strip() + if name: + return name + if hasattr(instance, "email") and instance.email: + return str(instance.email) + if hasattr(instance, "subject") and instance.subject: + return str(instance.subject) + # Fallback to string representation + return str(instance)[:100] + + +def create_activity(instance, action, entity_type): + """Create an activity record for a model instance""" + request = get_current_request() + + # Skip if no request context (e.g., management commands) + if not request: + return + + # Get profile from request + profile = getattr(request, "profile", None) + if not profile: + return + + # Get org from instance or profile + org = getattr(instance, "org", None) + if not org: + org = getattr(profile, "org", None) + if not org: + return + + # Create activity record + Activity.objects.create( + user=profile, + action=action, + entity_type=entity_type, + entity_id=instance.id, + entity_name=get_entity_name(instance), + org=org, + ) + + +# Account signals +@receiver(post_save, sender="accounts.Account") +def account_post_save(sender, instance, created, **kwargs): + action = "CREATE" if created else "UPDATE" + create_activity(instance, action, "Account") + + +@receiver(post_delete, sender="accounts.Account") +def account_post_delete(sender, instance, **kwargs): + create_activity(instance, "DELETE", "Account") + + +# Lead signals +@receiver(post_save, sender="leads.Lead") +def lead_post_save(sender, instance, created, **kwargs): + action = "CREATE" if created else "UPDATE" + create_activity(instance, action, "Lead") + + +@receiver(post_delete, sender="leads.Lead") +def lead_post_delete(sender, instance, **kwargs): + create_activity(instance, "DELETE", "Lead") + + +# Contact signals +@receiver(post_save, sender="contacts.Contact") +def contact_post_save(sender, instance, created, **kwargs): + action = "CREATE" if created else "UPDATE" + create_activity(instance, action, "Contact") + + +@receiver(post_delete, sender="contacts.Contact") +def contact_post_delete(sender, instance, **kwargs): + create_activity(instance, "DELETE", "Contact") + + +# Opportunity signals +@receiver(post_save, sender="opportunity.Opportunity") +def opportunity_post_save(sender, instance, created, **kwargs): + action = "CREATE" if created else "UPDATE" + create_activity(instance, action, "Opportunity") + + +@receiver(post_delete, sender="opportunity.Opportunity") +def opportunity_post_delete(sender, instance, **kwargs): + create_activity(instance, "DELETE", "Opportunity") + + +# Case signals +@receiver(post_save, sender="cases.Case") +def case_post_save(sender, instance, created, **kwargs): + action = "CREATE" if created else "UPDATE" + create_activity(instance, action, "Case") + + +@receiver(post_delete, sender="cases.Case") +def case_post_delete(sender, instance, **kwargs): + create_activity(instance, "DELETE", "Case") + + +# Task signals +@receiver(post_save, sender="tasks.Task") +def task_post_save(sender, instance, created, **kwargs): + action = "CREATE" if created else "UPDATE" + create_activity(instance, action, "Task") + + +@receiver(post_delete, sender="tasks.Task") +def task_post_delete(sender, instance, **kwargs): + create_activity(instance, "DELETE", "Task") + + +# Invoice signals +@receiver(post_save, sender="invoices.Invoice") +def invoice_post_save(sender, instance, created, **kwargs): + action = "CREATE" if created else "UPDATE" + create_activity(instance, action, "Invoice") + + +@receiver(post_delete, sender="invoices.Invoice") +def invoice_post_delete(sender, instance, **kwargs): + create_activity(instance, "DELETE", "Invoice") diff --git a/common/status.py b/backend/common/status.py similarity index 99% rename from common/status.py rename to backend/common/status.py index d4522df3..6adebb07 100644 --- a/common/status.py +++ b/backend/common/status.py @@ -5,6 +5,7 @@ See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html And RFC 6585 - http://tools.ietf.org/html/rfc6585 And RFC 4918 - https://tools.ietf.org/html/rfc4918 """ + from __future__ import unicode_literals diff --git a/common/swagger-ui.html b/backend/common/swagger-ui.html similarity index 100% rename from common/swagger-ui.html rename to backend/common/swagger-ui.html diff --git a/common/swagger_params1.py b/backend/common/swagger_params.py similarity index 52% rename from common/swagger_params1.py rename to backend/common/swagger_params.py index 4c41c3a4..5a7642e8 100644 --- a/common/swagger_params1.py +++ b/backend/common/swagger_params.py @@ -11,9 +11,9 @@ organization_params = [ user_list_params = [ organization_params_in_header, - OpenApiParameter("email", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("email", OpenApiTypes.STR, OpenApiParameter.QUERY), OpenApiParameter( - "role", OpenApiTypes.STR, OpenApiParameter.QUERY,enum=["ADMIN", "USER"] + "role", OpenApiTypes.STR, OpenApiParameter.QUERY, enum=["ADMIN", "USER"] ), OpenApiParameter( "status", @@ -25,13 +25,20 @@ user_list_params = [ document_get_params = [ organization_params_in_header, - OpenApiParameter("title", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("title", OpenApiTypes.STR, OpenApiParameter.QUERY), OpenApiParameter( "status", OpenApiTypes.STR, OpenApiParameter.QUERY, enum=["Active", "In Active"], ), - OpenApiParameter("shared_to", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("shared_to", OpenApiTypes.STR, OpenApiParameter.QUERY), ] +# Teams params (merged from teams app) +teams_list_get_params = [ + organization_params_in_header, + OpenApiParameter("team_name", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("created_by", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("assigned_users", OpenApiTypes.STR, OpenApiParameter.QUERY), +] diff --git a/common/tasks.py b/backend/common/tasks.py similarity index 61% rename from common/tasks.py rename to backend/common/tasks.py index f6654c81..ee4598bc 100644 --- a/common/tasks.py +++ b/backend/common/tasks.py @@ -4,20 +4,37 @@ from celery import Celery from django.conf import settings from django.contrib.auth.tokens import default_token_generator from django.core.mail import EmailMessage +from django.db import connection from django.template.loader import render_to_string from django.utils import timezone from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode -from common.models import Comment, Profile, User +from common.models import Comment, Profile, Teams, User from common.token_generator import account_activation_token app = Celery("redis://") +def set_rls_context(org_id): + """ + Set RLS context for Celery tasks that query org-scoped tables. + + Celery workers don't go through Django middleware, so RLS context + must be set explicitly before querying org-scoped data. + + Args: + org_id: Organization UUID (string or UUID object) + """ + if org_id: + with connection.cursor() as cursor: + cursor.execute( + "SELECT set_config('app.current_org', %s, false)", [str(org_id)] + ) + + @app.task def send_email_to_new_user(user_id): - """Send Mail To Users When their account is created""" user_obj = User.objects.filter(id=user_id).first() @@ -62,8 +79,12 @@ def send_email_to_new_user(user_id): def send_email_user_mentions( comment_id, called_from, + org_id=None, ): """Send Mail To Mentioned Users In The Comment""" + # Set RLS context for org-scoped queries + set_rls_context(org_id) + comment = Comment.objects.filter(id=comment_id).first() if comment: comment_text = comment.comment @@ -102,8 +123,6 @@ def send_email_user_mentions( subject = "New comment on Task. " elif called_from == "invoices": subject = "New comment on Invoice. " - elif called_from == "events": - subject = "New comment on Event. " if subject: context["url"] = settings.DOMAIN_NAME else: @@ -269,3 +288,140 @@ def send_email_to_reset_password(user_email): ) msg.content_subtype = "html" msg.send() + + +# ============================================================================= +# Teams Celery Tasks (merged from teams app) +# ============================================================================= + + +@app.task +def remove_users(removed_users_list, team_id, org_id=None): + # Set RLS context for org-scoped queries + set_rls_context(org_id) + + removed_users_list = [i for i in removed_users_list if i.isdigit()] + users_list = Profile.objects.filter(id__in=removed_users_list) + if users_list.exists(): + team = Teams.objects.filter(id=team_id).first() + if team: + accounts = team.account_teams.all() + for account in accounts: + for user in users_list: + account.assigned_to.remove(user) + + # for contacts + contacts = team.contact_teams.all() + for contact in contacts: + for user in users_list: + contact.assigned_to.remove(user) + + # for leads + leads = team.lead_teams.all() + for lead in leads: + for user in users_list: + lead.assigned_to.remove(user) + + # for opportunities + opportunities = team.oppurtunity_teams.all() + for opportunity in opportunities: + for user in users_list: + opportunity.assigned_to.remove(user) + + # for cases + cases = team.cases_teams.all() + for case in cases: + for user in users_list: + case.assigned_to.remove(user) + + # for documents + docs = team.document_teams.all() + for doc in docs: + for user in users_list: + doc.shared_to.remove(user) + + # for tasks + tasks = team.tasks_teams.all() + for task in tasks: + for user in users_list: + task.assigned_to.remove(user) + + # for invoices + invoices = team.invoices_teams.all() + for invoice in invoices: + for user in users_list: + invoice.assigned_to.remove(user) + + +@app.task +def update_team_users(team_id, org_id=None): + """this function updates assigned_to field on all models when a team is updated""" + # Set RLS context for org-scoped queries + set_rls_context(org_id) + + team = Teams.objects.filter(id=team_id).first() + if team: + teams_members = team.users.all() + # for accounts + accounts = team.account_teams.all() + for account in accounts: + account_assigned_to_users = account.assigned_to.all() + for team_member in teams_members: + if team_member not in account_assigned_to_users: + account.assigned_to.add(team_member) + + # for contacts + contacts = team.contact_teams.all() + for contact in contacts: + contact_assigned_to_users = contact.assigned_to.all() + for team_member in teams_members: + if team_member not in contact_assigned_to_users: + contact.assigned_to.add(team_member) + + # for leads + leads = team.lead_teams.all() + for lead in leads: + lead_assigned_to_users = lead.assigned_to.all() + for team_member in teams_members: + if team_member not in lead_assigned_to_users: + lead.assigned_to.add(team_member) + + # for opportunities + opportunities = team.oppurtunity_teams.all() + for opportunity in opportunities: + opportunity_assigned_to_users = opportunity.assigned_to.all() + for team_member in teams_members: + if team_member not in opportunity_assigned_to_users: + opportunity.assigned_to.add(team_member) + + # for cases + cases = team.cases_teams.all() + for case in cases: + case_assigned_to_users = case.assigned_to.all() + for team_member in teams_members: + if team_member not in case_assigned_to_users: + case.assigned_to.add(team_member) + + # for documents + docs = team.document_teams.all() + for doc in docs: + doc_assigned_to_users = doc.shared_to.all() + for team_member in teams_members: + if team_member not in doc_assigned_to_users: + doc.shared_to.add(team_member) + + # for tasks + tasks = team.tasks_teams.all() + for task in tasks: + task_assigned_to_users = task.assigned_to.all() + for team_member in teams_members: + if team_member not in task_assigned_to_users: + task.assigned_to.add(team_member) + + # for invoices + invoices = team.invoices_teams.all() + for invoice in invoices: + invoice_assigned_to_users = invoice.assigned_to.all() + for team_member in teams_members: + if team_member not in invoice_assigned_to_users: + invoice.assigned_to.add(team_member) diff --git a/common/templates/comment_email.html b/backend/common/templates/comment_email.html similarity index 100% rename from common/templates/comment_email.html rename to backend/common/templates/comment_email.html diff --git a/common/templates/password_reset_email.html b/backend/common/templates/password_reset_email.html similarity index 100% rename from common/templates/password_reset_email.html rename to backend/common/templates/password_reset_email.html diff --git a/common/templates/user_delete_email.html b/backend/common/templates/user_delete_email.html similarity index 100% rename from common/templates/user_delete_email.html rename to backend/common/templates/user_delete_email.html diff --git a/common/templates/user_status.html b/backend/common/templates/user_status.html similarity index 100% rename from common/templates/user_status.html rename to backend/common/templates/user_status.html diff --git a/common/templates/user_status_activate.html b/backend/common/templates/user_status_activate.html similarity index 100% rename from common/templates/user_status_activate.html rename to backend/common/templates/user_status_activate.html diff --git a/common/templates/user_status_deactivate.html b/backend/common/templates/user_status_deactivate.html similarity index 100% rename from common/templates/user_status_deactivate.html rename to backend/common/templates/user_status_deactivate.html diff --git a/common/templates/user_status_in.html b/backend/common/templates/user_status_in.html similarity index 100% rename from common/templates/user_status_in.html rename to backend/common/templates/user_status_in.html diff --git a/common/templatetags/__init__.py b/backend/common/templatetags/__init__.py similarity index 100% rename from common/templatetags/__init__.py rename to backend/common/templatetags/__init__.py diff --git a/common/templatetags/common_tags.py b/backend/common/templatetags/common_tags.py similarity index 100% rename from common/templatetags/common_tags.py rename to backend/common/templatetags/common_tags.py diff --git a/contacts/__init__.py b/backend/common/tests/__init__.py similarity index 100% rename from contacts/__init__.py rename to backend/common/tests/__init__.py diff --git a/backend/common/tests/test_multitenancy.py b/backend/common/tests/test_multitenancy.py new file mode 100644 index 00000000..abe9780a --- /dev/null +++ b/backend/common/tests/test_multitenancy.py @@ -0,0 +1,616 @@ +""" +Multi-Tenancy Security Tests + +These tests verify that the multi-tenancy implementation properly isolates +data between organizations and prevents cross-org data access. + +Run with: pytest common/tests/test_multitenancy.py -v +""" + +from unittest.mock import MagicMock, patch + +import pytest +from django.core.exceptions import PermissionDenied, ValidationError +from django.test import RequestFactory, TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from accounts.models import Account +from common.middleware.get_company import GetProfileAndOrg +from common.models import Address, Attachments, Comment, Org, Profile, Tags, Teams, User +from common.serializer import OrgAwareRefreshToken +from leads.models import Lead + + +class MultiTenancyBaseTestCase(TestCase): + """Base test case with common setup for multi-tenancy tests""" + + def setUp(self): + # Create two organizations + self.org_a = Org.objects.create(name="Organization A") + self.org_b = Org.objects.create(name="Organization B") + + # Create users + self.user_a = User.objects.create_user( + email="user_a@test.com", password="testpass123" + ) + self.user_b = User.objects.create_user( + email="user_b@test.com", password="testpass123" + ) + + # Create profiles (user membership in orgs) + self.profile_a = Profile.objects.create( + user=self.user_a, org=self.org_a, role="ADMIN", is_active=True + ) + self.profile_b = Profile.objects.create( + user=self.user_b, org=self.org_b, role="ADMIN", is_active=True + ) + + # API client + self.client = APIClient() + self.factory = RequestFactory() + + +class TestOrgAwareRefreshToken(MultiTenancyBaseTestCase): + """Test JWT token generation with org context""" + + def test_token_includes_org_id(self): + """Token should include org_id in payload""" + token = OrgAwareRefreshToken.for_user_and_org(self.user_a, self.org_a) + + # Access token should have org_id + access_token = token.access_token + self.assertEqual(str(access_token["org_id"]), str(self.org_a.id)) + + def test_token_without_org(self): + """Token can be generated without org for initial login""" + token = OrgAwareRefreshToken.for_user_and_org(self.user_a, None) + + # Should not have org_id claim + access_token = token.access_token + self.assertNotIn("org_id", access_token) + + def test_different_orgs_different_tokens(self): + """Different orgs should produce different token claims""" + token_a = OrgAwareRefreshToken.for_user_and_org(self.user_a, self.org_a) + token_b = OrgAwareRefreshToken.for_user_and_org(self.user_a, self.org_b) + + self.assertNotEqual( + str(token_a.access_token["org_id"]), str(token_b.access_token["org_id"]) + ) + + +class TestMiddlewareOrgValidation(MultiTenancyBaseTestCase): + """Test middleware extracts org from JWT, not headers""" + + def test_org_extracted_from_jwt_not_header(self): + """Middleware should use org_id from JWT, ignoring org header""" + # Generate token for org_a + token = OrgAwareRefreshToken.for_user_and_org(self.user_a, self.org_a) + + # Make request with JWT for org_a but header claiming org_b + request = self.factory.get( + "/api/leads/", + HTTP_AUTHORIZATION=f"Bearer {token.access_token}", + HTTP_ORG=str(self.org_b.id), # Attempt to spoof org + ) + + middleware = GetProfileAndOrg(lambda r: r) + middleware(request) + + # Should use org from JWT (org_a), not header (org_b) + self.assertEqual(request.profile.org, self.org_a) + self.assertEqual(request.org, self.org_a) + + def test_revoked_membership_denied(self): + """User whose membership was revoked should be denied""" + # Generate token + token = OrgAwareRefreshToken.for_user_and_org(self.user_a, self.org_a) + + # Revoke membership (deactivate profile) + self.profile_a.is_active = False + self.profile_a.save() + + # Make request + request = self.factory.get( + "/api/leads/", HTTP_AUTHORIZATION=f"Bearer {token.access_token}" + ) + + middleware = GetProfileAndOrg(lambda r: r) + middleware(request) + + # Middleware should not set profile when membership is revoked + self.assertIsNone(getattr(request, "profile", None)) + + def test_api_key_scoped_to_org(self): + """API key authentication should be scoped to specific org""" + request = self.factory.get("/api/leads/", HTTP_TOKEN=self.org_a.api_key) + + middleware = GetProfileAndOrg(lambda r: r) + middleware(request) + + self.assertEqual(request.org, self.org_a) + + +class TestCrossOrgDataAccess(MultiTenancyBaseTestCase): + """Test that cross-org data access is prevented""" + + def setUp(self): + super().setUp() + + # Create data in each org + self.account_a = Account.objects.create(name="Account in Org A", org=self.org_a) + self.account_b = Account.objects.create(name="Account in Org B", org=self.org_b) + + self.lead_a = Lead.objects.create( + first_name="Lead", + last_name="In Org A", + email="lead_a@test.com", + org=self.org_a, + ) + self.lead_b = Lead.objects.create( + first_name="Lead", + last_name="In Org B", + email="lead_b@test.com", + org=self.org_b, + ) + + def test_queryset_filters_by_org(self): + """QuerySet should only return data for the specified org""" + # Filter for org_a + accounts_a = Account.objects.filter(org=self.org_a) + self.assertEqual(accounts_a.count(), 1) + self.assertEqual(accounts_a.first().name, "Account in Org A") + + # Filter for org_b + accounts_b = Account.objects.filter(org=self.org_b) + self.assertEqual(accounts_b.count(), 1) + self.assertEqual(accounts_b.first().name, "Account in Org B") + + def test_api_returns_only_org_data(self): + """API endpoints should only return data for user's org""" + # Login as user_a + token = OrgAwareRefreshToken.for_user_and_org(self.user_a, self.org_a) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token.access_token}") + + # Get accounts - should only see org_a data + response = self.client.get("/api/accounts/") + + if response.status_code == 200: + data = response.json() + # Verify all returned accounts belong to org_a + if "results" in data: + for account in data["results"]: + self.assertEqual(account.get("org"), str(self.org_a.id)) + + +class TestGenericFKOrgValidation(MultiTenancyBaseTestCase): + """Test that Comment and Attachments validate org matches content object""" + + def setUp(self): + super().setUp() + + self.lead_a = Lead.objects.create( + first_name="Lead", + last_name="In Org A", + email="lead_a@test.com", + org=self.org_a, + ) + + def test_comment_org_must_match_content_object(self): + """Comment org must match the referenced object's org""" + from django.contrib.contenttypes.models import ContentType + + lead_content_type = ContentType.objects.get_for_model(Lead) + + # Try to create comment with different org than lead + comment = Comment( + content_type=lead_content_type, + object_id=self.lead_a.id, + comment="Cross-org comment attempt", + org=self.org_b, # Different org than lead + commented_by=self.profile_b, + ) + + with self.assertRaises(ValidationError) as context: + comment.save() + + self.assertIn("org", context.exception.message_dict) + + def test_comment_same_org_succeeds(self): + """Comment with same org as content object should succeed""" + from django.contrib.contenttypes.models import ContentType + + lead_content_type = ContentType.objects.get_for_model(Lead) + + comment = Comment( + content_type=lead_content_type, + object_id=self.lead_a.id, + comment="Same-org comment", + org=self.org_a, # Same org as lead + commented_by=self.profile_a, + created_by=self.user_a, + updated_by=self.user_a, + ) + + # Should not raise + comment.save() + self.assertIsNotNone(comment.id) + + def test_attachment_org_must_match_content_object(self): + """Attachment org must match the referenced object's org""" + from django.contrib.contenttypes.models import ContentType + from django.core.files.uploadedfile import SimpleUploadedFile + + lead_content_type = ContentType.objects.get_for_model(Lead) + + attachment = Attachments( + content_type=lead_content_type, + object_id=self.lead_a.id, + file_name="test.txt", + attachment=SimpleUploadedFile("test.txt", b"content"), + org=self.org_b, # Different org than lead + ) + + with self.assertRaises(ValidationError) as context: + attachment.save() + + self.assertIn("org", context.exception.message_dict) + + +class TestOrgSwitching(MultiTenancyBaseTestCase): + """Test organization switching functionality""" + + def setUp(self): + super().setUp() + + # Give user_a access to both orgs + self.profile_a_in_b = Profile.objects.create( + user=self.user_a, org=self.org_b, role="USER", is_active=True + ) + + def test_switch_org_success(self): + """User with access to multiple orgs can switch""" + # Login to org_a first + token = OrgAwareRefreshToken.for_user_and_org(self.user_a, self.org_a) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token.access_token}") + + # Switch to org_b + response = self.client.post( + "/api/auth/switch-org/", {"org_id": str(self.org_b.id)} + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + + # Should have new tokens with org_b + self.assertIn("access_token", data) + self.assertEqual(data["current_org"]["id"], str(self.org_b.id)) + + def test_switch_to_unauthorized_org_fails(self): + """User cannot switch to org they don't have access to""" + # user_b only has access to org_b + token = OrgAwareRefreshToken.for_user_and_org(self.user_b, self.org_b) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token.access_token}") + + # Try to switch to org_a + response = self.client.post( + "/api/auth/switch-org/", {"org_id": str(self.org_a.id)} + ) + + self.assertEqual(response.status_code, 403) + + +class TestLoginWithOrgContext(MultiTenancyBaseTestCase): + """Test login endpoint includes org context""" + + def test_login_returns_current_org(self): + """Login should return current_org in response""" + response = self.client.post( + "/api/auth/login/", {"email": "user_a@test.com", "password": "testpass123"} + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + + self.assertIn("current_org", data) + self.assertEqual(data["current_org"]["id"], str(self.org_a.id)) + + def test_login_with_specific_org(self): + """Login can specify which org to use""" + # Give user_a access to org_b + Profile.objects.create( + user=self.user_a, org=self.org_b, role="USER", is_active=True + ) + + response = self.client.post( + "/api/auth/login/", + { + "email": "user_a@test.com", + "password": "testpass123", + "org_id": str(self.org_b.id), + }, + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + + self.assertEqual(data["current_org"]["id"], str(self.org_b.id)) + + def test_login_with_invalid_org_fails(self): + """Login with org user doesn't have access to should fail""" + response = self.client.post( + "/api/auth/login/", + { + "email": "user_a@test.com", + "password": "testpass123", + "org_id": str(self.org_b.id), # user_a doesn't have access + }, + ) + + self.assertEqual(response.status_code, 403) + + +class TestBaseOrgModel(TestCase): + """Test BaseOrgModel validation""" + + def test_org_required_on_save(self): + """BaseOrgModel descendants should require org on save""" + from common.base import BaseOrgModel + + # This is tested indirectly through models like Teams + org = Org.objects.create(name="Test Org") + + team = Teams(name="Test Team", description="Test Description", org=org) + team.save() # Should succeed + + self.assertIsNotNone(team.id) + + +class TestOrgScopedQuerySet(MultiTenancyBaseTestCase): + """Test OrgScopedQuerySet helper methods""" + + def setUp(self): + super().setUp() + + # Create teams in each org + self.team_a = Teams.objects.create( + name="Team A", description="In Org A", org=self.org_a + ) + self.team_b = Teams.objects.create( + name="Team B", description="In Org B", org=self.org_b + ) + + def test_for_org_filters_correctly(self): + """for_org() should filter by organization""" + teams = Teams.objects.filter(org=self.org_a) + self.assertEqual(teams.count(), 1) + self.assertEqual(teams.first().name, "Team A") + + def test_for_request_uses_profile_org(self): + """for_request() should use org from request.profile""" + # Create mock request + request = MagicMock() + request.profile = self.profile_a + + # This would be used with OrgScopedManager + # Teams.objects.for_request(request) + + +class TestNullOrgPrevention(MultiTenancyBaseTestCase): + """Test that models properly handle org field""" + + def test_comment_requires_org(self): + """Comment should require org field""" + from django.contrib.contenttypes.models import ContentType + + lead = Lead.objects.create( + first_name="Test", last_name="Lead", email="test@test.com", org=self.org_a + ) + + lead_content_type = ContentType.objects.get_for_model(Lead) + + # Try to create comment without org + comment = Comment( + content_type=lead_content_type, + object_id=lead.id, + comment="No org comment", + # org not set + ) + + with self.assertRaises(Exception): + comment.save() + + +class TestRLSIntegration(MultiTenancyBaseTestCase): + """ + Test Row-Level Security at the database level. + + These tests verify that PostgreSQL RLS policies properly isolate + data between organizations. They require: + - PostgreSQL database + - Non-superuser database connection (superusers bypass RLS) + - RLS migrations applied + """ + + @classmethod + def setUpClass(cls): + """Check if RLS testing is possible.""" + super().setUpClass() + from django.db import connection + + # Check if using PostgreSQL + cls.is_postgres = connection.vendor == "postgresql" + + if cls.is_postgres: + # Check if database user is a superuser (which bypasses RLS) + with connection.cursor() as cursor: + cursor.execute( + "SELECT usesuper FROM pg_user WHERE usename = current_user" + ) + result = cursor.fetchone() + cls.is_superuser = result[0] if result else True + + def setUp(self): + super().setUp() + + # Create test data in each org + self.lead_a = Lead.objects.create( + first_name="Lead", last_name="OrgA", email="lead_a@test.com", org=self.org_a + ) + self.lead_b = Lead.objects.create( + first_name="Lead", last_name="OrgB", email="lead_b@test.com", org=self.org_b + ) + + self.account_a = Account.objects.create( + name="Account A", email="account_a@test.com", org=self.org_a + ) + self.account_b = Account.objects.create( + name="Account B", email="account_b@test.com", org=self.org_b + ) + + def test_empty_context_returns_no_rows(self): + """With empty RLS context, queries should return zero rows (fail-safe).""" + if not self.is_postgres: + self.skipTest("RLS requires PostgreSQL") + + if self.is_superuser: + self.skipTest( + "RLS is bypassed for superusers - use non-superuser for testing" + ) + + from django.db import connection + + with connection.cursor() as cursor: + # Clear the org context (empty string means no context) + cursor.execute("SELECT set_config('app.current_org', '', true)") + + # Query should return no rows due to NULLIF fail-safe + cursor.execute("SELECT COUNT(*) FROM lead") + count = cursor.fetchone()[0] + + self.assertEqual(count, 0, "Empty context should return zero rows") + + def test_rls_isolates_between_orgs(self): + """RLS should isolate data between organizations.""" + if not self.is_postgres: + self.skipTest("RLS requires PostgreSQL") + + if self.is_superuser: + self.skipTest( + "RLS is bypassed for superusers - use non-superuser for testing" + ) + + from django.db import connection + + with connection.cursor() as cursor: + # Set context to org_a + cursor.execute( + "SELECT set_config('app.current_org', %s, true)", [str(self.org_a.id)] + ) + + # Should only see org_a's lead + cursor.execute("SELECT id, first_name FROM lead") + leads = cursor.fetchall() + + self.assertEqual(len(leads), 1, "Should only see one lead") + self.assertEqual(str(leads[0][0]), str(self.lead_a.id)) + + # Now switch to org_b + cursor.execute( + "SELECT set_config('app.current_org', %s, true)", [str(self.org_b.id)] + ) + + cursor.execute("SELECT id, first_name FROM lead") + leads = cursor.fetchall() + + self.assertEqual(len(leads), 1, "Should only see one lead") + self.assertEqual(str(leads[0][0]), str(self.lead_b.id)) + + def test_rls_prevents_insert_to_wrong_org(self): + """RLS should prevent inserting records with wrong org_id.""" + if not self.is_postgres: + self.skipTest("RLS requires PostgreSQL") + + if self.is_superuser: + self.skipTest( + "RLS is bypassed for superusers - use non-superuser for testing" + ) + + import uuid + + from django.db import connection + + with connection.cursor() as cursor: + # Set context to org_a + cursor.execute( + "SELECT set_config('app.current_org', %s, true)", [str(self.org_a.id)] + ) + + # Try to insert lead with org_b's ID (should fail due to WITH CHECK) + new_id = uuid.uuid4() + try: + cursor.execute( + """ + INSERT INTO lead (id, first_name, last_name, email, org_id, created_at, updated_at) + VALUES (%s, 'Test', 'Wrong Org', 'wrong@test.com', %s, NOW(), NOW()) + """, + [str(new_id), str(self.org_b.id)], + ) + self.fail("Should not be able to insert record with wrong org_id") + except Exception: + # Expected - RLS should prevent this insert + pass + + def test_celery_task_rls_context(self): + """Celery tasks should be able to set RLS context.""" + if not self.is_postgres: + self.skipTest("RLS requires PostgreSQL") + + if self.is_superuser: + self.skipTest( + "RLS is bypassed for superusers - use non-superuser for testing" + ) + + from django.db import connection + + from common.tasks import set_rls_context + + # Simulate Celery task setting RLS context + set_rls_context(str(self.org_a.id)) + + with connection.cursor() as cursor: + cursor.execute("SELECT current_setting('app.current_org', true)") + context = cursor.fetchone()[0] + + self.assertEqual(context, str(self.org_a.id)) + + def test_rls_status_check(self): + """Verify RLS is enabled on key tables.""" + if not self.is_postgres: + self.skipTest("RLS requires PostgreSQL") + + from django.db import connection + + from common.rls import RLS_CONFIG + + with connection.cursor() as cursor: + for table in ["lead", "accounts", "contacts"]: + cursor.execute( + """ + SELECT relrowsecurity, relforcerowsecurity + FROM pg_class + WHERE relname = %s AND relnamespace = 'public'::regnamespace + """, + [table], + ) + result = cursor.fetchone() + + if result: + rls_enabled, rls_forced = result + # Just check that RLS is enabled + if not rls_enabled: + self.fail(f"RLS not enabled on {table}") + + +# Run tests with: pytest common/tests/test_multitenancy.py -v diff --git a/common/tests_celery_tasks.py b/backend/common/tests_celery_tasks.py similarity index 90% rename from common/tests_celery_tasks.py rename to backend/common/tests_celery_tasks.py index c7dea2dd..8a09e6ea 100644 --- a/common/tests_celery_tasks.py +++ b/backend/common/tests_celery_tasks.py @@ -15,7 +15,6 @@ from common.tasks import ( ) from common.tests import ObjectsCreation from contacts.tests import ContactObjectsCreation -from events.tests import EventObjectTest from invoices.tests import InvoiceCreateTest from leads.tests import TestLeadModel from opportunity.tests import OpportunityModel @@ -105,6 +104,7 @@ class TestUserMentionsForAccountComments(AccountCreateTest, TestCase): ( self.comment.id, "accounts", + str(self.comment.org.id), ), ) self.assertEqual("SUCCESS", task.state) @@ -134,6 +134,7 @@ class TestUserMentionsForContactsComments(ContactObjectsCreation, TestCase): ( self.comment.id, "contacts", + str(self.comment.org.id), ), ) self.assertEqual("SUCCESS", task.state) @@ -163,6 +164,7 @@ class TestUserMentionsForLeadsComments(TestLeadModel, TestCase): ( self.comment.id, "leads", + str(self.comment.org.id), ), ) self.assertEqual("SUCCESS", task.state) @@ -192,6 +194,7 @@ class TestUserMentionsForOpportunityComments(OpportunityModel, TestCase): ( self.comment.id, "opportunity", + str(self.comment.org.id), ), ) self.assertEqual("SUCCESS", task.state) @@ -221,6 +224,7 @@ class TestUserMentionsForCasesComments(CaseCreation, TestCase): ( self.comment.id, "cases", + str(self.comment.org.id), ), ) self.assertEqual("SUCCESS", task.state) @@ -250,6 +254,7 @@ class TestUserMentionsForTasksComments(TaskCreateTest, TestCase): ( self.comment.id, "tasks", + str(self.comment.org.id), ), ) self.assertEqual("SUCCESS", task.state) @@ -279,35 +284,7 @@ class TestUserMentionsForInvoiceComments(InvoiceCreateTest, TestCase): ( self.comment.id, "invoices", - ), - ) - self.assertEqual("SUCCESS", task.state) - - -class TestUserMentionsForEventsComments(EventObjectTest, TestCase): - @override_settings( - CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, - CELERY_ALWAYS_EAGER=True, - BROKER_BACKEND="memory", - ) - def test_user_mentions_for_events_comments(self): - self.user_comment = User.objects.create( - first_name="johnComment", - username="johnDoeComment", - email="johnDoeComment@example.com", - role="ADMIN", - ) - self.user_comment.set_password("password") - self.user_comment.save() - - self.comment.comment = "content @{}".format(self.user_comment.username) - self.comment.event = self.event - self.comment.save() - - task = send_email_user_mentions.apply( - ( - self.comment.id, - "events", + str(self.comment.org.id), ), ) self.assertEqual("SUCCESS", task.state) diff --git a/common/token_generator.py b/backend/common/token_generator.py similarity index 100% rename from common/token_generator.py rename to backend/common/token_generator.py diff --git a/backend/common/urls.py b/backend/common/urls.py new file mode 100644 index 00000000..da427fb6 --- /dev/null +++ b/backend/common/urls.py @@ -0,0 +1,44 @@ +from django.urls import path +from rest_framework_simplejwt import views as jwt_views + +from common import views + +app_name = "api_common" + + +urlpatterns = [ + path("dashboard/", views.ApiHomeView.as_view()), + # JWT Authentication endpoints for SvelteKit integration + path("auth/login/", views.LoginView.as_view(), name="login"), + path("auth/register/", views.RegisterView.as_view(), name="register"), + path( + "auth/refresh-token/", + views.OrgAwareTokenRefreshView.as_view(), + name="token_refresh", + ), + path("auth/me/", views.MeView.as_view(), name="me"), + path("auth/profile/", views.ProfileDetailView.as_view(), name="profile_detail"), + path("auth/switch-org/", views.OrgSwitchView.as_view(), name="switch_org"), + # Google OAuth callback with PKCE (secure implementation) + path("auth/google/callback/", views.GoogleOAuthCallbackView.as_view()), + # Organization and profile management + path("org/", views.OrgProfileCreateView.as_view()), + path("org//", views.OrgUpdateView.as_view()), + path("profile/", views.ProfileView.as_view()), + # User management + path("users/get-teams-and-users/", views.GetTeamsAndUsersView.as_view()), + path("users/", views.UsersListView.as_view()), + path("user//", views.UserDetailView.as_view()), + path("user//status/", views.UserStatusView.as_view()), + # Documents + path("documents/", views.DocumentListView.as_view()), + path("documents//", views.DocumentDetailView.as_view()), + # API Settings + path("api-settings/", views.DomainList.as_view()), + path("api-settings//", views.DomainDetailView.as_view()), + # Activities (for dashboard recent activities) + path("activities/", views.ActivityListView.as_view(), name="activities"), + # Teams (merged from teams app) + path("teams/", views.TeamsListView.as_view()), + path("teams//", views.TeamsDetailView.as_view()), +] diff --git a/common/utils.py b/backend/common/utils.py similarity index 97% rename from common/utils.py rename to backend/common/utils.py index 908169c9..50201ce8 100644 --- a/common/utils.py +++ b/backend/common/utils.py @@ -108,15 +108,20 @@ PRIORITY_CHOICE = ( CASE_TYPE = (("Question", "Question"), ("Incident", "Incident"), ("Problem", "Problem")) STAGES = ( - ("QUALIFICATION", "QUALIFICATION"), - ("NEEDS ANALYSIS", "NEEDS ANALYSIS"), - ("VALUE PROPOSITION", "VALUE PROPOSITION"), - ("ID.DECISION MAKERS", "ID.DECISION MAKERS"), - ("PERCEPTION ANALYSIS", "PERCEPTION ANALYSIS"), - ("PROPOSAL/PRICE QUOTE", "PROPOSAL/PRICE QUOTE"), - ("NEGOTIATION/REVIEW", "NEGOTIATION/REVIEW"), - ("CLOSED WON", "CLOSED WON"), - ("CLOSED LOST", "CLOSED LOST"), + ("PROSPECTING", "Prospecting"), + ("QUALIFICATION", "Qualification"), + ("PROPOSAL", "Proposal"), + ("NEGOTIATION", "Negotiation"), + ("CLOSED_WON", "Closed Won"), + ("CLOSED_LOST", "Closed Lost"), +) + +OPPORTUNITY_TYPES = ( + ("NEW_BUSINESS", "New Business"), + ("EXISTING_BUSINESS", "Existing Business"), + ("RENEWAL", "Renewal"), + ("UPSELL", "Upsell"), + ("CROSS_SELL", "Cross-sell"), ) SOURCES = ( diff --git a/backend/common/views.py b/backend/common/views.py new file mode 100644 index 00000000..b4237946 --- /dev/null +++ b/backend/common/views.py @@ -0,0 +1,1755 @@ +import json +import secrets +from multiprocessing import context +from re import template + +import requests +from django.conf import settings +from django.contrib.auth import authenticate, login +from django.contrib.auth.base_user import BaseUserManager +from django.contrib.auth.hashers import make_password +from django.db import transaction +from django.db.models import Q +from django.http.response import JsonResponse +from django.shortcuts import get_object_or_404, render +from django.utils import timezone +from django.utils.encoding import force_str +from django.utils.http import urlsafe_base64_decode +from django.utils.translation import gettext as _ +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema + +# from common.external_auth import CustomDualAuthentication +from rest_framework import status +from rest_framework.authtoken.models import Token +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.utils import json +from rest_framework.views import APIView +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.tokens import RefreshToken + +from accounts.models import Account +from accounts.serializer import AccountSerializer +from cases.models import Case +from cases.serializer import CaseSerializer + +##from common.custom_auth import JSONWebTokenAuthentication +from common import serializer, swagger_params +from common.models import ( + Activity, + APISettings, + Document, + Org, + Profile, + Tags, + Teams, + User, +) +from common.serializer import * +from common.serializer import OrgAwareRefreshToken + +# from common.serializer import ( +# CreateUserSerializer, +# PasswordChangeSerializer, +# RegisterOrganizationSerializer, +# ) +from common.tasks import ( + remove_users, + resend_activation_link_to_user, + send_email_to_new_user, + send_email_to_reset_password, + send_email_user_delete, + update_team_users, +) +from common.token_generator import account_activation_token + +# from rest_framework_jwt.serializers import jwt_encode_handler +from common.utils import COUNTRIES, ROLES, jwt_payload_handler +from contacts.models import Contact +from contacts.serializer import ContactSerializer +from leads.models import Lead +from leads.serializer import LeadSerializer +from opportunity.models import Opportunity +from opportunity.serializer import OpportunitySerializer + + +class GetTeamsAndUsersView(APIView): + + permission_classes = (IsAuthenticated,) + + @extend_schema(tags=["users"], parameters=swagger_params.organization_params) + def get(self, request, *args, **kwargs): + data = {} + teams = Teams.objects.filter(org=request.profile.org).order_by("-id") + teams_data = TeamsSerializer(teams, many=True).data + profiles = Profile.objects.filter( + is_active=True, org=request.profile.org + ).order_by("user__email") + profiles_data = ProfileSerializer(profiles, many=True).data + data["teams"] = teams_data + data["profiles"] = profiles_data + return Response(data) + + +class UsersListView(APIView, LimitOffsetPagination): + + permission_classes = (IsAuthenticated,) + + @extend_schema( + parameters=swagger_params.organization_params, + request=UserCreateSwaggerSerializer, + ) + def post(self, request, format=None): + print(request.profile.role, request.user.is_superuser) + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + return Response( + {"error": True, "errors": "Permission Denied"}, + status=status.HTTP_403_FORBIDDEN, + ) + else: + params = request.data + if params: + user_serializer = CreateUserSerializer( + data=params, org=request.profile.org + ) + address_serializer = BillingAddressSerializer(data=params) + profile_serializer = CreateProfileSerializer(data=params) + data = {} + if not user_serializer.is_valid(): + data["user_errors"] = dict(user_serializer.errors) + if not profile_serializer.is_valid(): + data["profile_errors"] = profile_serializer.errors + if not address_serializer.is_valid(): + data["address_errors"] = (address_serializer.errors,) + if data: + return Response( + {"error": True, "errors": data}, + status=status.HTTP_400_BAD_REQUEST, + ) + if address_serializer.is_valid(): + address_obj = address_serializer.save() + user = user_serializer.save( + is_active=True, + ) + user.email = user.email + user.save() + # if params.get("password"): + # user.set_password(params.get("password")) + # user.save() + profile = Profile.objects.create( + user=user, + date_of_joining=timezone.now(), + role=params.get("role"), + address=address_obj, + org=request.profile.org, + ) + + # send_email_to_new_user.delay( + # profile.id, + # request.profile.org.id, + # ) + return Response( + {"error": False, "message": "User Created Successfully"}, + status=status.HTTP_201_CREATED, + ) + + @extend_schema(parameters=swagger_params.user_list_params) + def get(self, request, format=None): + # Check if profile exists and user has permission + if not self.request.profile: + return Response( + {"error": True, "errors": "Organization context required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + return Response( + {"error": True, "errors": "Permission Denied"}, + status=status.HTTP_403_FORBIDDEN, + ) + queryset = Profile.objects.filter(org=request.profile.org).order_by("-id") + params = request.query_params + if params: + if params.get("email"): + queryset = queryset.filter(user__email__icontains=params.get("email")) + if params.get("role"): + queryset = queryset.filter(role=params.get("role")) + if params.get("status"): + queryset = queryset.filter(is_active=params.get("status")) + + context = {} + queryset_active_users = queryset.filter(is_active=True) + results_active_users = self.paginate_queryset( + queryset_active_users.distinct(), self.request, view=self + ) + active_users = ProfileSerializer(results_active_users, many=True).data + if results_active_users: + offset = queryset_active_users.filter( + id__gte=results_active_users[-1].id + ).count() + if offset == queryset_active_users.count(): + offset = None + else: + offset = 0 + context["active_users"] = { + "active_users_count": self.count, + "active_users": active_users, + "offset": offset, + } + + queryset_inactive_users = queryset.filter(is_active=False) + results_inactive_users = self.paginate_queryset( + queryset_inactive_users.distinct(), self.request, view=self + ) + inactive_users = ProfileSerializer(results_inactive_users, many=True).data + if results_inactive_users: + offset = queryset_inactive_users.filter( + id__gte=results_inactive_users[-1].id + ).count() + if offset == queryset_inactive_users.count(): + offset = None + else: + offset = 0 + context["inactive_users"] = { + "inactive_users_count": self.count, + "inactive_users": inactive_users, + "offset": offset, + } + + context["admin_email"] = settings.ADMIN_EMAIL + context["roles"] = ROLES + context["status"] = [("True", "Active"), ("False", "In Active")] + return Response(context) + + +class UserDetailView(APIView): + permission_classes = (IsAuthenticated,) + + def get_object(self, pk): + profile = get_object_or_404(Profile, pk=pk) + return profile + + @extend_schema(tags=["users"], parameters=swagger_params.organization_params) + def get(self, request, pk, format=None): + profile_obj = self.get_object(pk) + if ( + self.request.profile.role != "ADMIN" + and not self.request.profile.is_admin + and self.request.profile.id != profile_obj.id + ): + return Response( + {"error": True, "errors": "Permission Denied"}, + status=status.HTTP_403_FORBIDDEN, + ) + if profile_obj.org != request.profile.org: + return Response( + {"error": True, "errors": "User company doesnot match with header...."}, + status=status.HTTP_403_FORBIDDEN, + ) + assigned_data = Profile.objects.filter( + org=request.profile.org, is_active=True + ).values("id", "user__email") + context = {} + context["profile_obj"] = ProfileSerializer(profile_obj).data + opportunity_list = Opportunity.objects.filter(assigned_to=profile_obj) + context["opportunity_list"] = OpportunitySerializer( + opportunity_list, many=True + ).data + contacts = Contact.objects.filter(assigned_to=profile_obj) + context["contacts"] = ContactSerializer(contacts, many=True).data + cases = Case.objects.filter(assigned_to=profile_obj) + context["cases"] = CaseSerializer(cases, many=True).data + context["assigned_data"] = assigned_data + comments = profile_obj.user_comments.all() + context["comments"] = CommentSerializer(comments, many=True).data + context["countries"] = COUNTRIES + return Response( + {"error": False, "data": context}, + status=status.HTTP_200_OK, + ) + + @extend_schema( + tags=["users"], + parameters=swagger_params.organization_params, + request=UserCreateSwaggerSerializer, + ) + def put(self, request, pk, format=None): + params = request.data + profile = self.get_object(pk) + address_obj = profile.address + if ( + self.request.profile.role != "ADMIN" + and not self.request.user.is_superuser + and self.request.profile.id != profile.id + ): + return Response( + {"error": True, "errors": "Permission Denied"}, + status=status.HTTP_403_FORBIDDEN, + ) + + if profile.org != request.profile.org: + return Response( + {"error": True, "errors": "User company doesnot match with header...."}, + status=status.HTTP_403_FORBIDDEN, + ) + serializer = CreateUserSerializer( + data=params, instance=profile.user, org=request.profile.org + ) + address_serializer = BillingAddressSerializer(data=params, instance=address_obj) + profile_serializer = CreateProfileSerializer(data=params, instance=profile) + data = {} + if not serializer.is_valid(): + data["contact_errors"] = serializer.errors + if not address_serializer.is_valid(): + data["address_errors"] = (address_serializer.errors,) + if not profile_serializer.is_valid(): + data["profile_errors"] = (profile_serializer.errors,) + if data: + data["error"] = True + return Response( + data, + status=status.HTTP_400_BAD_REQUEST, + ) + if address_serializer.is_valid(): + address_obj = address_serializer.save() + user = serializer.save() + user.email = user.email + user.save() + if profile_serializer.is_valid(): + profile = profile_serializer.save() + return Response( + {"error": False, "message": "User Updated Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + @extend_schema(tags=["users"], parameters=swagger_params.organization_params) + def delete(self, request, pk, format=None): + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + return Response( + {"error": True, "errors": "Permission Denied"}, + status=status.HTTP_403_FORBIDDEN, + ) + self.object = self.get_object(pk) + if self.object.id == request.profile.id: + return Response( + {"error": True, "errors": "Permission Denied"}, + status=status.HTTP_403_FORBIDDEN, + ) + deleted_by = self.request.profile.user.email + send_email_user_delete.delay( + self.object.user.email, + deleted_by=deleted_by, + ) + self.object.delete() + return Response({"status": "success"}, status=status.HTTP_200_OK) + + +# check_header not working +class ApiHomeView(APIView): + + permission_classes = (IsAuthenticated,) + + @extend_schema(parameters=swagger_params.organization_params) + def get(self, request, format=None): + accounts = Account.objects.filter(is_active=True, org=request.profile.org) + contacts = Contact.objects.filter(org=request.profile.org) + leads = Lead.objects.filter(org=request.profile.org).exclude( + Q(status="converted") | Q(status="closed") + ) + opportunities = Opportunity.objects.filter(org=request.profile.org) + + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + accounts = accounts.filter( + Q(assigned_to=self.request.profile) + | Q(created_by=self.request.profile.user) + ) + contacts = contacts.filter( + Q(assigned_to__id__in=self.request.profile) + | Q(created_by=self.request.profile.user) + ) + leads = leads.filter( + Q(assigned_to__id__in=self.request.profile) + | Q(created_by=self.request.profile.user) + ).exclude(status="closed") + opportunities = opportunities.filter( + Q(assigned_to__id__in=self.request.profile) + | Q(created_by=self.request.profile.user) + ) + context = {} + context["accounts_count"] = accounts.count() + context["contacts_count"] = contacts.count() + context["leads_count"] = leads.count() + context["opportunities_count"] = opportunities.count() + context["accounts"] = AccountSerializer(accounts, many=True).data + context["contacts"] = ContactSerializer(contacts, many=True).data + context["leads"] = LeadSerializer(leads, many=True).data + context["opportunities"] = OpportunitySerializer(opportunities, many=True).data + return Response(context, status=status.HTTP_200_OK) + + +class OrgProfileCreateView(APIView): + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + + model1 = Org + model2 = Profile + serializer_class = OrgProfileCreateSerializer + profile_serializer = CreateProfileSerializer + + @extend_schema( + description="Organization and profile Creation api", + request=OrgProfileCreateSerializer, + ) + def post(self, request, format=None): + data = request.data + data["api_key"] = secrets.token_hex(16) + serializer = self.serializer_class(data=data) + if serializer.is_valid(): + org_obj = serializer.save() + + # now creating the profile + profile_obj = self.model2.objects.create(user=request.user, org=org_obj) + # now the current user is the admin of the newly created organisation + profile_obj.is_organization_admin = True + profile_obj.role = "ADMIN" + profile_obj.save() + + return Response( + { + "error": False, + "message": "New Org is Created.", + "org": self.serializer_class(org_obj).data, + "status": status.HTTP_201_CREATED, + } + ) + else: + return Response( + { + "error": True, + "errors": serializer.errors, + "status": status.HTTP_400_BAD_REQUEST, + } + ) + + @extend_schema( + description="Just Pass the token, will return ORG list, associated with user" + ) + def get(self, request, format=None): + """ + here we are passing profile list of the user, where org details also included + """ + profile_list = Profile.objects.filter(user=request.user) + serializer = ShowOrganizationListSerializer(profile_list, many=True) + return Response( + { + "error": False, + "status": status.HTTP_200_OK, + "profile_org_list": serializer.data, + } + ) + + +class OrgUpdateView(APIView): + """ + Update organization details + Only organization admins can update + """ + + permission_classes = (IsAuthenticated,) + + @extend_schema( + description="Update organization details", + request=OrganizationSerializer, + responses={200: OrganizationSerializer}, + ) + def put(self, request, pk, format=None): + # Check if user has admin access to this organization + if not request.profile: + return Response( + {"error": True, "errors": "Organization context required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Verify the organization matches the current context + if str(request.profile.org.id) != str(pk): + return Response( + {"error": True, "errors": "Cannot update a different organization"}, + status=status.HTTP_403_FORBIDDEN, + ) + + # Check if user is admin + if ( + request.profile.role != "ADMIN" + and not request.profile.is_organization_admin + ): + return Response( + { + "error": True, + "errors": "Only organization admins can update organization details", + }, + status=status.HTTP_403_FORBIDDEN, + ) + + try: + org = Org.objects.get(id=pk) + except Org.DoesNotExist: + return Response( + {"error": True, "errors": "Organization not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + # Update fields + data = request.data + if "name" in data: + org.name = data["name"] + + org.save() + + return Response( + { + "error": False, + "message": "Organization updated successfully", + "org": OrganizationSerializer(org).data, + }, + status=status.HTTP_200_OK, + ) + + @extend_schema( + description="Get organization details", responses={200: OrganizationSerializer} + ) + def get(self, request, pk, format=None): + # Check if user has access to this organization + if not request.profile: + return Response( + {"error": True, "errors": "Organization context required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Verify the organization matches the current context + if str(request.profile.org.id) != str(pk): + return Response( + {"error": True, "errors": "Cannot access a different organization"}, + status=status.HTTP_403_FORBIDDEN, + ) + + try: + org = Org.objects.get(id=pk) + except Org.DoesNotExist: + return Response( + {"error": True, "errors": "Organization not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + return Response( + {"error": False, "org": OrganizationSerializer(org).data}, + status=status.HTTP_200_OK, + ) + + +class ProfileView(APIView): + permission_classes = (IsAuthenticated,) + + @extend_schema(parameters=swagger_params.organization_params) + def get(self, request, format=None): + # profile=Profile.objects.get(user=request.user) + context = {} + context["user_obj"] = ProfileSerializer(self.request.profile).data + return Response(context, status=status.HTTP_200_OK) + + @extend_schema(parameters=swagger_params.organization_params) + def patch(self, request, format=None): + profile = request.profile + data = request.data + + # Update phone on Profile if provided + if "phone" in data: + profile.phone = data.get("phone") + profile.save() + + # Note: name field is not available on User model + # If name updates are needed, the User model would need to be extended + + return Response( + { + "message": "Profile updated successfully", + "user_obj": ProfileSerializer(profile).data, + }, + status=status.HTTP_200_OK, + ) + + +class DocumentListView(APIView, LimitOffsetPagination): + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + model = Document + + def get_context_data(self, **kwargs): + params = self.request.query_params + queryset = self.model.objects.filter(org=self.request.profile.org).order_by( + "-id" + ) + if self.request.user.is_superuser or self.request.profile.role == "ADMIN": + queryset = queryset + else: + if self.request.profile.documents(): + doc_ids = self.request.profile.documents().values_list("id", flat=True) + shared_ids = queryset.filter( + Q(status="active") & Q(shared_to__id__in=[self.request.profile.id]) + ).values_list("id", flat=True) + queryset = queryset.filter(Q(id__in=doc_ids) | Q(id__in=shared_ids)) + else: + queryset = queryset.filter( + Q(status="active") & Q(shared_to__id__in=[self.request.profile.id]) + ) + + request_post = params + if request_post: + if request_post.get("title"): + queryset = queryset.filter(title__icontains=request_post.get("title")) + if request_post.get("status"): + queryset = queryset.filter(status=request_post.get("status")) + + if request_post.get("shared_to"): + queryset = queryset.filter( + shared_to__id__in=json.loads(request_post.get("shared_to")) + ) + + context = {} + profile_list = Profile.objects.filter( + is_active=True, org=self.request.profile.org + ) + if self.request.profile.role == "ADMIN" or self.request.profile.is_admin: + profiles = profile_list.order_by("user__email") + else: + profiles = profile_list.filter(role="ADMIN").order_by("user__email") + search = False + if ( + params.get("document_file") + or params.get("status") + or params.get("shared_to") + ): + search = True + context["search"] = search + + queryset_documents_active = queryset.filter(status="active") + results_documents_active = self.paginate_queryset( + queryset_documents_active.distinct(), self.request, view=self + ) + documents_active = DocumentSerializer(results_documents_active, many=True).data + if results_documents_active: + offset = queryset_documents_active.filter( + id__gte=results_documents_active[-1].id + ).count() + if offset == queryset_documents_active.count(): + offset = None + else: + offset = 0 + context["documents_active"] = { + "documents_active_count": self.count, + "documents_active": documents_active, + "offset": offset, + } + + queryset_documents_inactive = queryset.filter(status="inactive") + results_documents_inactive = self.paginate_queryset( + queryset_documents_inactive.distinct(), self.request, view=self + ) + documents_inactive = DocumentSerializer( + results_documents_inactive, many=True + ).data + if results_documents_inactive: + offset = queryset_documents_inactive.filter( + id__gte=results_documents_active[-1].id + ).count() + if offset == queryset_documents_inactive.count(): + offset = None + else: + offset = 0 + context["documents_inactive"] = { + "documents_inactive_count": self.count, + "documents_inactive": documents_inactive, + "offset": offset, + } + + context["users"] = ProfileSerializer(profiles, many=True).data + context["status_choices"] = Document.DOCUMENT_STATUS_CHOICE + return context + + @extend_schema(tags=["documents"], parameters=swagger_params.document_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + @extend_schema( + tags=["documents"], + parameters=swagger_params.organization_params, + request=DocumentCreateSwaggerSerializer, + ) + def post(self, request, *args, **kwargs): + params = request.data + serializer = DocumentCreateSerializer(data=params, request_obj=request) + if serializer.is_valid(): + doc = serializer.save( + created_by=request.profile.user, + org=request.profile.org, + document_file=request.FILES.get("document_file"), + ) + if params.get("shared_to"): + assinged_to_list = params.get("shared_to") + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org, is_active=True + ) + if profiles: + doc.shared_to.add(*profiles) + if params.get("teams"): + teams_list = params.get("teams") + teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) + if teams: + doc.teams.add(*teams) + + return Response( + {"error": False, "message": "Document Created Successfully"}, + status=status.HTTP_201_CREATED, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class DocumentDetailView(APIView): + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + + def get_object(self, pk): + return Document.objects.filter(id=pk).first() + + @extend_schema(tags=["documents"], parameters=swagger_params.organization_params) + def get(self, request, pk, format=None): + self.object = self.get_object(pk) + if not self.object: + return Response( + {"error": True, "errors": "Document does not exist"}, + status=status.HTTP_403_FORBIDDEN, + ) + if self.object.org != self.request.profile.org: + return Response( + {"error": True, "errors": "User company doesnot match with header...."}, + status=status.HTTP_403_FORBIDDEN, + ) + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + if not ( + (self.request.profile == self.object.created_by) + or (self.request.profile in self.object.shared_to.all()) + ): + return Response( + { + "error": True, + "errors": "You do not have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + profile_list = Profile.objects.filter(org=self.request.profile.org) + if request.profile.role == "ADMIN" or request.user.is_superuser: + profiles = profile_list.order_by("user__email") + else: + profiles = profile_list.filter(role="ADMIN").order_by("user__email") + context = {} + context.update( + { + "doc_obj": DocumentSerializer(self.object).data, + "file_type_code": self.object.file_type()[1], + "users": ProfileSerializer(profiles, many=True).data, + } + ) + return Response(context, status=status.HTTP_200_OK) + + @extend_schema(tags=["documents"], parameters=swagger_params.organization_params) + def delete(self, request, pk, format=None): + document = self.get_object(pk) + if not document: + return Response( + {"error": True, "errors": "Documdnt does not exist"}, + status=status.HTTP_403_FORBIDDEN, + ) + if document.org != self.request.profile.org: + return Response( + {"error": True, "errors": "User company doesnot match with header...."}, + status=status.HTTP_403_FORBIDDEN, + ) + + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + if ( + self.request.profile != document.created_by + ): # or (self.request.profile not in document.shared_to.all()): + return Response( + { + "error": True, + "errors": "You do not have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + document.delete() + return Response( + {"error": False, "message": "Document deleted Successfully"}, + status=status.HTTP_200_OK, + ) + + @extend_schema( + tags=["documents"], + parameters=swagger_params.organization_params, + request=DocumentEditSwaggerSerializer, + ) + def put(self, request, pk, format=None): + self.object = self.get_object(pk) + params = request.data + if not self.object: + return Response( + {"error": True, "errors": "Document does not exist"}, + status=status.HTTP_403_FORBIDDEN, + ) + if self.object.org != self.request.profile.org: + return Response( + {"error": True, "errors": "User company doesnot match with header...."}, + status=status.HTTP_403_FORBIDDEN, + ) + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + if not ( + (self.request.profile == self.object.created_by) + or (self.request.profile in self.object.shared_to.all()) + ): + return Response( + { + "error": True, + "errors": "You do not have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + serializer = DocumentCreateSerializer( + data=params, instance=self.object, request_obj=request + ) + if serializer.is_valid(): + doc = serializer.save( + document_file=request.FILES.get("document_file"), + status=params.get("status"), + org=request.profile.org, + ) + doc.shared_to.clear() + if params.get("shared_to"): + assinged_to_list = params.get("shared_to") + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org, is_active=True + ) + if profiles: + doc.shared_to.add(*profiles) + + doc.teams.clear() + if params.get("teams"): + teams_list = params.get("teams") + teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) + if teams: + doc.teams.add(*teams) + return Response( + {"error": False, "message": "Document Updated Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class UserStatusView(APIView): + permission_classes = (IsAuthenticated,) + + @extend_schema( + description="User Status View", + parameters=swagger_params.organization_params, + request=UserUpdateStatusSwaggerSerializer, + ) + def post(self, request, pk, format=None): + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + return Response( + { + "error": True, + "errors": "You do not have permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + params = request.data + profiles = Profile.objects.filter(org=request.profile.org) + profile = profiles.get(id=pk) + + if params.get("status"): + user_status = params.get("status") + if user_status == "Active": + profile.is_active = True + elif user_status == "Inactive": + profile.is_active = False + else: + return Response( + {"error": True, "errors": "Please enter Valid Status for user"}, + status=status.HTTP_400_BAD_REQUEST, + ) + profile.save() + + context = {} + active_profiles = profiles.filter(is_active=True) + inactive_profiles = profiles.filter(is_active=False) + context["active_profiles"] = ProfileSerializer(active_profiles, many=True).data + context["inactive_profiles"] = ProfileSerializer( + inactive_profiles, many=True + ).data + return Response(context) + + +class DomainList(APIView): + model = APISettings + permission_classes = (IsAuthenticated,) + + @extend_schema(tags=["Settings"], parameters=swagger_params.organization_params) + def get(self, request, *args, **kwargs): + api_settings = APISettings.objects.filter(org=request.profile.org) + users = Profile.objects.filter( + is_active=True, org=request.profile.org + ).order_by("user__email") + return Response( + { + "error": False, + "api_settings": APISettingsListSerializer(api_settings, many=True).data, + "users": ProfileSerializer(users, many=True).data, + }, + status=status.HTTP_200_OK, + ) + + @extend_schema( + tags=["Settings"], + parameters=swagger_params.organization_params, + request=APISettingsSwaggerSerializer, + ) + def post(self, request, *args, **kwargs): + params = request.data + assign_to_list = [] + if params.get("lead_assigned_to"): + assign_to_list = params.get("lead_assigned_to") + serializer = APISettingsSerializer(data=params) + if serializer.is_valid(): + settings_obj = serializer.save( + created_by=request.profile.user, org=request.profile.org + ) + if params.get("tags"): + tags = params.get("tags") + for tag in tags: + tag_obj = Tags.objects.filter(name=tag).first() + if not tag_obj: + tag_obj = Tags.objects.create(name=tag) + settings_obj.tags.add(tag_obj) + if assign_to_list: + settings_obj.lead_assigned_to.add(*assign_to_list) + return Response( + {"error": False, "message": "API key added sucessfully"}, + status=status.HTTP_201_CREATED, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class DomainDetailView(APIView): + model = APISettings + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + + @extend_schema(tags=["Settings"], parameters=swagger_params.organization_params) + def get(self, request, pk, format=None): + api_setting = self.get_object(pk) + return Response( + {"error": False, "domain": APISettingsListSerializer(api_setting).data}, + status=status.HTTP_200_OK, + ) + + @extend_schema( + tags=["Settings"], + parameters=swagger_params.organization_params, + request=APISettingsSwaggerSerializer, + ) + def put(self, request, pk, **kwargs): + api_setting = self.get_object(pk) + params = request.data + assign_to_list = [] + if params.get("lead_assigned_to"): + assign_to_list = params.get("lead_assigned_to") + serializer = APISettingsSerializer(data=params, instance=api_setting) + if serializer.is_valid(): + api_setting = serializer.save() + api_setting.tags.clear() + api_setting.lead_assigned_to.clear() + if params.get("tags"): + tags = params.get("tags") + for tag in tags: + tag_obj = Tags.objects.filter(name=tag).first() + if not tag_obj: + tag_obj = Tags.objects.create(name=tag) + api_setting.tags.add(tag_obj) + if assign_to_list: + api_setting.lead_assigned_to.add(*assign_to_list) + return Response( + {"error": False, "message": "API setting Updated sucessfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + @extend_schema(tags=["Settings"], parameters=swagger_params.organization_params) + def delete(self, request, pk, **kwargs): + api_setting = self.get_object(pk) + if api_setting: + api_setting.delete() + return Response( + {"error": False, "message": "API setting deleted sucessfully"}, + status=status.HTTP_200_OK, + ) + + +class GoogleOAuthCallbackView(APIView): + """ + Handle Google OAuth authorization code exchange with PKCE. + """ + + permission_classes = [] + authentication_classes = [] + + def post(self, request): + import base64 + + from django.conf import settings as django_settings + from django.utils import timezone + + code = request.data.get("code") + code_verifier = request.data.get("code_verifier") + redirect_uri = request.data.get("redirect_uri") + + if not all([code, code_verifier, redirect_uri]): + return Response( + {"error": "Missing required parameters"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Exchange code for tokens with Google + try: + token_response = requests.post( + "https://oauth2.googleapis.com/token", + data={ + "code": code, + "client_id": django_settings.GOOGLE_CLIENT_ID, + "client_secret": django_settings.GOOGLE_CLIENT_SECRET, + "redirect_uri": redirect_uri, + "grant_type": "authorization_code", + "code_verifier": code_verifier, + }, + timeout=30, + ) + except requests.RequestException: + return Response( + {"error": "Failed to communicate with Google"}, + status=status.HTTP_502_BAD_GATEWAY, + ) + + if token_response.status_code != 200: + error_data = token_response.json() if token_response.content else {} + return Response( + {"error": error_data.get("error_description", "Token exchange failed")}, + status=status.HTTP_400_BAD_REQUEST, + ) + + token_data = token_response.json() + id_token = token_data.get("id_token") + if not id_token: + return Response( + {"error": "No ID token received"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Decode ID token payload (no verification needed - we just got it from Google over HTTPS) + try: + payload_part = id_token.split(".")[1] + # Add padding if needed + payload_part += "=" * (4 - len(payload_part) % 4) + payload = json.loads(base64.urlsafe_b64decode(payload_part)) + email = payload.get("email") + picture = payload.get("picture", "") + except (IndexError, ValueError, json.JSONDecodeError): + return Response( + {"error": "Invalid ID token format"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if not email: + return Response( + {"error": "No email in token"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get or create user + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + user = User.objects.create( + email=email, + profile_pic=picture, + password=make_password(secrets.token_urlsafe(32)), + ) + + user.last_login = timezone.now() + user.save(update_fields=["last_login"]) + + # Generate JWT tokens + token = RefreshToken.for_user(user) + + return Response({ + "access_token": str(token.access_token), + "refresh_token": str(token), + "user": {"id": str(user.id), "email": user.email}, + }) + + +# JWT Authentication Views for SvelteKit Integration + + +class LoginView(APIView): + """ + Login with email and password, returns JWT tokens + """ + + permission_classes = [] + authentication_classes = [] + + @extend_schema( + description="Login with email and password", + request=serializer.LoginSerializer, + responses={ + 200: { + "type": "object", + "properties": { + "access_token": {"type": "string"}, + "refresh_token": {"type": "string"}, + "user": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "email": {"type": "string"}, + "organizations": {"type": "array"}, + }, + }, + }, + } + }, + ) + def post(self, request): + from django.utils import timezone + + from common.audit_log import audit_log + + serializer_obj = serializer.LoginSerializer(data=request.data) + if serializer_obj.is_valid(): + user = serializer_obj.validated_data["user"] + + # Update last_login timestamp + user.last_login = timezone.now() + user.save(update_fields=["last_login"]) + + # Get user's organizations + profiles = Profile.objects.filter(user=user, is_active=True) + + # Get the default org (first org or from request) + org_id = request.data.get("org_id") + default_org = None + + if org_id: + # Use specified org if user has access + try: + profile = profiles.get(org_id=org_id) + default_org = profile.org + except Profile.DoesNotExist: + audit_log.login_failure( + user.email, f"No access to org {org_id}", request + ) + return Response( + { + "error": "User does not have access to specified organization" + }, + status=status.HTTP_403_FORBIDDEN, + ) + elif profiles.exists(): + # Use first available org + default_org = profiles.first().org + + # Generate JWT tokens with org context + if default_org: + token = OrgAwareRefreshToken.for_user_and_org(user, default_org) + else: + # User has no orgs - generate token without org context + token = RefreshToken.for_user(user) + + # Audit log successful login + audit_log.login_success(user, default_org, request) + + # Get user details with organizations + user_serializer = serializer.UserDetailSerializer(user) + + response_data = { + "access_token": str(token.access_token), + "refresh_token": str(token), + "user": user_serializer.data, + } + + # Include current org info if available + if default_org: + response_data["current_org"] = { + "id": str(default_org.id), + "name": default_org.name, + } + + return Response(response_data, status=status.HTTP_200_OK) + + # Log failed login + email = request.data.get("email", "unknown") + audit_log.login_failure(email, str(serializer_obj.errors), request) + return Response(serializer_obj.errors, status=status.HTTP_400_BAD_REQUEST) + + +class RegisterView(APIView): + """ + Register a new user account + """ + + permission_classes = [] + authentication_classes = [] + + @extend_schema( + description="Register a new user account", + request=serializer.RegisterSerializer, + responses={ + 201: { + "type": "object", + "properties": { + "message": {"type": "string"}, + "user_id": {"type": "string"}, + "email": {"type": "string"}, + }, + } + }, + ) + def post(self, request): + serializer_obj = serializer.RegisterSerializer(data=request.data) + if serializer_obj.is_valid(): + user = serializer_obj.save() + + # TODO: Send activation email + # send_email_to_new_user.delay(user.email, user.id) + + return Response( + { + "message": "User registered successfully. Please check your email for activation link.", + "user_id": str(user.id), + "email": user.email, + }, + status=status.HTTP_201_CREATED, + ) + + return Response(serializer_obj.errors, status=status.HTTP_400_BAD_REQUEST) + + +class MeView(APIView): + """ + Get current authenticated user details + """ + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + @extend_schema( + description="Get current authenticated user with organizations", + responses={200: serializer.UserDetailSerializer}, + ) + def get(self, request): + user_serializer = serializer.UserDetailSerializer(request.user) + return Response(user_serializer.data, status=status.HTTP_200_OK) + + +class OrgAwareTokenRefreshView(APIView): + """ + Custom token refresh that validates org membership. + + When refreshing a token, this view: + 1. Validates the refresh token + 2. Checks that user still has access to the org in the token + 3. Issues new tokens with the same org context + + If membership was revoked, returns 403 and user must login again. + """ + + permission_classes = [] + authentication_classes = [] + + @extend_schema( + description="Refresh access token with org membership validation", + request={ + "type": "object", + "properties": { + "refresh": {"type": "string", "description": "Refresh token"} + }, + "required": ["refresh"], + }, + responses={ + 200: { + "type": "object", + "properties": { + "access": {"type": "string"}, + "refresh": {"type": "string"}, + }, + } + }, + ) + def post(self, request): + from rest_framework_simplejwt.exceptions import TokenError + from rest_framework_simplejwt.tokens import RefreshToken as BaseRefreshToken + + from common.audit_log import audit_log + + refresh_token = request.data.get("refresh") + + if not refresh_token: + return Response( + {"error": "Refresh token is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + # Decode and validate refresh token + token = BaseRefreshToken(refresh_token) + user_id = token["user_id"] + org_id = token.get("org_id") + + # Get user + user = User.objects.get(id=user_id) + + if not user.is_active: + return Response( + {"error": "User account is disabled"}, + status=status.HTTP_403_FORBIDDEN, + ) + + # If token has org context, validate membership + if org_id: + try: + profile = Profile.objects.get( + user=user, org_id=org_id, is_active=True + ) + org = profile.org + except Profile.DoesNotExist: + # Membership revoked - user must login again + audit_log.token_revoked( + user, None, f"Membership revoked for org {org_id}", request + ) + return Response( + { + "error": "Organization membership revoked. Please login again." + }, + status=status.HTTP_403_FORBIDDEN, + ) + + # Generate new tokens with same org context + new_token = OrgAwareRefreshToken.for_user_and_org(user, org) + audit_log.token_refresh(user, org, request) + else: + # No org context - standard refresh + new_token = BaseRefreshToken.for_user(user) + audit_log.token_refresh(user, None, request) + + return Response( + {"access": str(new_token.access_token), "refresh": str(new_token)}, + status=status.HTTP_200_OK, + ) + + except TokenError as e: + return Response({"error": str(e)}, status=status.HTTP_401_UNAUTHORIZED) + except User.DoesNotExist: + return Response( + {"error": "User not found"}, status=status.HTTP_401_UNAUTHORIZED + ) + + +class OrgSwitchView(APIView): + """ + Switch to a different organization and get new JWT tokens. + + This endpoint validates that the user has access to the target + organization and issues new tokens with the org_id claim. + """ + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + @extend_schema( + description="Switch to a different organization and get new JWT tokens", + request={ + "type": "object", + "properties": { + "org_id": { + "type": "string", + "format": "uuid", + "description": "Target organization ID", + } + }, + "required": ["org_id"], + }, + responses={ + 200: { + "type": "object", + "properties": { + "access_token": {"type": "string"}, + "refresh_token": {"type": "string"}, + "current_org": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"}, + }, + }, + }, + } + }, + ) + def post(self, request): + from common.audit_log import audit_log + + org_id = request.data.get("org_id") + + if not org_id: + return Response( + {"error": "org_id is required"}, status=status.HTTP_400_BAD_REQUEST + ) + + # Get current org for audit logging + from_org = getattr(request, "org", None) + + # Validate user has access to the target org + try: + profile = Profile.objects.get( + user=request.user, org_id=org_id, is_active=True + ) + except Profile.DoesNotExist: + audit_log.permission_denied( + request.user, from_org, "ORG_SWITCH", f"org:{org_id}", request + ) + return Response( + {"error": "User does not have access to this organization"}, + status=status.HTTP_403_FORBIDDEN, + ) + + # Generate new tokens with the target org + token = OrgAwareRefreshToken.for_user_and_org(request.user, profile.org) + + # Audit log the org switch + audit_log.org_switch(request.user, from_org, profile.org, request) + + return Response( + { + "access_token": str(token.access_token), + "refresh_token": str(token), + "current_org": {"id": str(profile.org.id), "name": profile.org.name}, + "profile": { + "id": str(profile.id), + "role": profile.role, + "is_organization_admin": profile.is_organization_admin, + }, + }, + status=status.HTTP_200_OK, + ) + + +class ProfileDetailView(APIView): + """ + Get profile details for a specific organization + Requires org header + """ + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + @extend_schema( + description="Get profile for current user in specified organization", + parameters=[ + { + "name": "org", + "in": "header", + "required": True, + "schema": {"type": "string", "format": "uuid"}, + "description": "Organization ID", + } + ], + responses={200: serializer.ProfileDetailSerializer}, + ) + def get(self, request): + # request.profile is set by middleware based on org header + if not hasattr(request, "profile") or request.profile is None: + return Response( + {"error": "Organization header required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + profile_serializer = serializer.ProfileDetailSerializer(request.profile) + return Response(profile_serializer.data, status=status.HTTP_200_OK) + + +# Activity Views for Dashboard Recent Activities + + +class ActivityListView(APIView): + """ + Get recent activities for the organization + Returns the last 10 activities by default + """ + + permission_classes = (IsAuthenticated,) + + @extend_schema( + tags=["activities"], + parameters=swagger_params.organization_params + + [ + OpenApiParameter( + name="limit", + type=int, + location=OpenApiParameter.QUERY, + description="Number of activities to return (default: 10, max: 50)", + ), + OpenApiParameter( + name="entity_type", + type=str, + location=OpenApiParameter.QUERY, + description="Filter by entity type (Account, Lead, Contact, etc.)", + ), + ], + responses={200: serializer.ActivitySerializer(many=True)}, + ) + def get(self, request, *args, **kwargs): + if not request.profile: + return Response( + {"error": True, "errors": "Organization context required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get query params + limit = min(int(request.query_params.get("limit", 10)), 50) + entity_type = request.query_params.get("entity_type", None) + + # Query activities for this organization + queryset = Activity.objects.filter(org=request.profile.org) + + # Filter by entity type if specified + if entity_type: + queryset = queryset.filter(entity_type=entity_type) + + # Get most recent activities + activities = queryset.select_related("user", "user__user")[:limit] + + # Serialize + activities_data = serializer.ActivitySerializer(activities, many=True).data + + return Response( + { + "error": False, + "count": len(activities_data), + "activities": activities_data, + }, + status=status.HTTP_200_OK, + ) + + +# ============================================================================= +# Teams Views (merged from teams app) +# ============================================================================= + + +class TeamsListView(APIView, LimitOffsetPagination): + model = Teams + permission_classes = (IsAuthenticated,) + + def get_context_data(self, **kwargs): + params = self.request.query_params + queryset = self.model.objects.filter(org=self.request.profile.org).order_by( + "-id" + ) + if params: + if params.get("team_name"): + queryset = queryset.filter(name__icontains=params.get("team_name")) + if params.get("created_by"): + queryset = queryset.filter(created_by=params.get("created_by")) + if params.get("assigned_users"): + queryset = queryset.filter(users__id__in=params.get("assigned_users")) + + context = {} + results_teams = self.paginate_queryset( + queryset.distinct(), self.request, view=self + ) + teams = TeamsSerializer(results_teams, many=True).data + if results_teams: + offset = queryset.filter(id__gte=results_teams[-1].id).count() + if offset == queryset.count(): + offset = None + else: + offset = 0 + context["per_page"] = 10 + page_number = (int(self.offset / 10) + 1,) + context["page_number"] = page_number + context.update({"teams_count": self.count, "offset": offset}) + context["teams"] = teams + return context + + @extend_schema(tags=["Teams"], parameters=swagger_params.teams_list_get_params) + def get(self, *args, **kwargs): + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + return Response( + { + "error": True, + "errors": "You don't have permission to perform this action.", + }, + status=status.HTTP_403_FORBIDDEN, + ) + context = self.get_context_data(**kwargs) + return Response(context) + + @extend_schema( + tags=["Teams"], + request=TeamswaggerCreateSerializer, + parameters=swagger_params.organization_params, + ) + def post(self, request, *args, **kwargs): + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + return Response( + { + "error": True, + "errors": "You don't have permission to perform this action.", + }, + status=status.HTTP_403_FORBIDDEN, + ) + params = self.request.data + serializer = TeamCreateSerializer(data=params, request_obj=request) + if serializer.is_valid(): + team_obj = serializer.save(org=request.profile.org) + + if params.get("assign_users"): + assinged_to_list = params.get("users") + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org + ) + if profiles: + team_obj.users.add(*profiles) + return Response( + {"error": False, "message": "Team Created Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class TeamsDetailView(APIView): + model = Teams + permission_classes = (IsAuthenticated,) + + def get_object(self, pk): + return self.model.objects.get(pk=pk, org=self.request.profile.org) + + @extend_schema(tags=["Teams"], parameters=swagger_params.organization_params) + def get(self, request, pk, **kwargs): + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + return Response( + { + "error": True, + "errors": "You don't have permission to perform this action.", + }, + status=status.HTTP_403_FORBIDDEN, + ) + self.team_obj = self.get_object(pk) + context = {} + context["team"] = TeamsSerializer(self.team_obj).data + return Response(context) + + @extend_schema( + tags=["Teams"], + request=TeamswaggerCreateSerializer, + parameters=swagger_params.organization_params, + ) + def put(self, request, pk, *args, **kwargs): + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + return Response( + { + "error": True, + "errors": "You don't have permission to perform this action.", + }, + status=status.HTTP_403_FORBIDDEN, + ) + params = request.data + self.team = self.get_object(pk) + actual_users = self.team.get_users() + removed_users = [] + serializer = TeamCreateSerializer( + data=params, instance=self.team, request_obj=request + ) + if serializer.is_valid(): + team_obj = serializer.save() + + team_obj.users.clear() + if params.get("assign_users"): + assinged_to_list = params.get("assign_users") + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org + ) + if profiles: + team_obj.users.add(*profiles) + update_team_users.delay(pk, str(request.profile.org.id)) + latest_users = team_obj.get_users() + for user in actual_users: + if user not in latest_users: + removed_users.append(user) + remove_users.delay(removed_users, pk, str(request.profile.org.id)) + return Response( + {"error": False, "message": "Team Updated Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + @extend_schema(tags=["Teams"], parameters=swagger_params.organization_params) + def delete(self, request, pk, **kwargs): + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + return Response( + { + "error": True, + "errors": "You don't have permission to perform this action.", + }, + status=status.HTTP_403_FORBIDDEN, + ) + self.team_obj = self.get_object(pk) + self.team_obj.delete() + return Response( + {"error": False, "message": "Team Deleted Successfully"}, + status=status.HTTP_200_OK, + ) diff --git a/contacts/migrations/__init__.py b/backend/contacts/__init__.py similarity index 100% rename from contacts/migrations/__init__.py rename to backend/contacts/__init__.py diff --git a/contacts/admin.py b/backend/contacts/admin.py similarity index 100% rename from contacts/admin.py rename to backend/contacts/admin.py diff --git a/contacts/apps.py b/backend/contacts/apps.py similarity index 100% rename from contacts/apps.py rename to backend/contacts/apps.py diff --git a/backend/contacts/migrations/0001_initial.py b/backend/contacts/migrations/0001_initial.py new file mode 100644 index 00000000..024c97b4 --- /dev/null +++ b/backend/contacts/migrations/0001_initial.py @@ -0,0 +1,438 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("common", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Contact", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "first_name", + models.CharField(max_length=255, verbose_name="First name"), + ), + ( + "last_name", + models.CharField(max_length=255, verbose_name="Last name"), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, null=True, verbose_name="Email" + ), + ), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, + max_length=128, + null=True, + region=None, + verbose_name="Phone", + ), + ), + ( + "organization", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Company" + ), + ), + ( + "title", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Job Title" + ), + ), + ( + "department", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Department" + ), + ), + ( + "do_not_call", + models.BooleanField(default=False, verbose_name="Do Not Call"), + ), + ( + "linked_in_url", + models.URLField(blank=True, null=True, verbose_name="LinkedIn URL"), + ), + ( + "address_line", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Address" + ), + ), + ( + "city", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="City" + ), + ), + ( + "state", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="State" + ), + ), + ( + "postcode", + models.CharField( + blank=True, max_length=64, null=True, verbose_name="Postal Code" + ), + ), + ( + "country", + models.CharField( + blank=True, + choices=[ + ("GB", "United Kingdom"), + ("AF", "Afghanistan"), + ("AX", "Aland Islands"), + ("AL", "Albania"), + ("DZ", "Algeria"), + ("AS", "American Samoa"), + ("AD", "Andorra"), + ("AO", "Angola"), + ("AI", "Anguilla"), + ("AQ", "Antarctica"), + ("AG", "Antigua and Barbuda"), + ("AR", "Argentina"), + ("AM", "Armenia"), + ("AW", "Aruba"), + ("AU", "Australia"), + ("AT", "Austria"), + ("AZ", "Azerbaijan"), + ("BS", "Bahamas"), + ("BH", "Bahrain"), + ("BD", "Bangladesh"), + ("BB", "Barbados"), + ("BY", "Belarus"), + ("BE", "Belgium"), + ("BZ", "Belize"), + ("BJ", "Benin"), + ("BM", "Bermuda"), + ("BT", "Bhutan"), + ("BO", "Bolivia"), + ("BA", "Bosnia and Herzegovina"), + ("BW", "Botswana"), + ("BV", "Bouvet Island"), + ("BR", "Brazil"), + ("IO", "British Indian Ocean Territory"), + ("BN", "Brunei Darussalam"), + ("BG", "Bulgaria"), + ("BF", "Burkina Faso"), + ("BI", "Burundi"), + ("KH", "Cambodia"), + ("CM", "Cameroon"), + ("CA", "Canada"), + ("CV", "Cape Verde"), + ("KY", "Cayman Islands"), + ("CF", "Central African Republic"), + ("TD", "Chad"), + ("CL", "Chile"), + ("CN", "China"), + ("CX", "Christmas Island"), + ("CC", "Cocos (Keeling) Islands"), + ("CO", "Colombia"), + ("KM", "Comoros"), + ("CG", "Congo"), + ("CD", "Congo, The Democratic Republic of the"), + ("CK", "Cook Islands"), + ("CR", "Costa Rica"), + ("CI", "Cote d'Ivoire"), + ("HR", "Croatia"), + ("CU", "Cuba"), + ("CY", "Cyprus"), + ("CZ", "Czech Republic"), + ("DK", "Denmark"), + ("DJ", "Djibouti"), + ("DM", "Dominica"), + ("DO", "Dominican Republic"), + ("EC", "Ecuador"), + ("EG", "Egypt"), + ("SV", "El Salvador"), + ("GQ", "Equatorial Guinea"), + ("ER", "Eritrea"), + ("EE", "Estonia"), + ("ET", "Ethiopia"), + ("FK", "Falkland Islands (Malvinas)"), + ("FO", "Faroe Islands"), + ("FJ", "Fiji"), + ("FI", "Finland"), + ("FR", "France"), + ("GF", "French Guiana"), + ("PF", "French Polynesia"), + ("TF", "French Southern Territories"), + ("GA", "Gabon"), + ("GM", "Gambia"), + ("GE", "Georgia"), + ("DE", "Germany"), + ("GH", "Ghana"), + ("GI", "Gibraltar"), + ("GR", "Greece"), + ("GL", "Greenland"), + ("GD", "Grenada"), + ("GP", "Guadeloupe"), + ("GU", "Guam"), + ("GT", "Guatemala"), + ("GG", "Guernsey"), + ("GN", "Guinea"), + ("GW", "Guinea-Bissau"), + ("GY", "Guyana"), + ("HT", "Haiti"), + ("HM", "Heard Island and McDonald Islands"), + ("VA", "Holy See (Vatican City State)"), + ("HN", "Honduras"), + ("HK", "Hong Kong"), + ("HU", "Hungary"), + ("IS", "Iceland"), + ("IN", "India"), + ("ID", "Indonesia"), + ("IR", "Iran, Islamic Republic of"), + ("IQ", "Iraq"), + ("IE", "Ireland"), + ("IM", "Isle of Man"), + ("IL", "Israel"), + ("IT", "Italy"), + ("JM", "Jamaica"), + ("JP", "Japan"), + ("JE", "Jersey"), + ("JO", "Jordan"), + ("KZ", "Kazakhstan"), + ("KE", "Kenya"), + ("KI", "Kiribati"), + ("KP", "Korea, Democratic People's Republic of"), + ("KR", "Korea, Republic of"), + ("KW", "Kuwait"), + ("KG", "Kyrgyzstan"), + ("LA", "Lao People's Democratic Republic"), + ("LV", "Latvia"), + ("LB", "Lebanon"), + ("LS", "Lesotho"), + ("LR", "Liberia"), + ("LY", "Libyan Arab Jamahiriya"), + ("LI", "Liechtenstein"), + ("LT", "Lithuania"), + ("LU", "Luxembourg"), + ("MO", "Macao"), + ("MK", "Macedonia, The Former Yugoslav Republic of"), + ("MG", "Madagascar"), + ("MW", "Malawi"), + ("MY", "Malaysia"), + ("MV", "Maldives"), + ("ML", "Mali"), + ("MT", "Malta"), + ("MH", "Marshall Islands"), + ("MQ", "Martinique"), + ("MR", "Mauritania"), + ("MU", "Mauritius"), + ("YT", "Mayotte"), + ("MX", "Mexico"), + ("FM", "Micronesia, Federated States of"), + ("MD", "Moldova"), + ("MC", "Monaco"), + ("MN", "Mongolia"), + ("ME", "Montenegro"), + ("MS", "Montserrat"), + ("MA", "Morocco"), + ("MZ", "Mozambique"), + ("MM", "Myanmar"), + ("NA", "Namibia"), + ("NR", "Nauru"), + ("NP", "Nepal"), + ("NL", "Netherlands"), + ("AN", "Netherlands Antilles"), + ("NC", "New Caledonia"), + ("NZ", "New Zealand"), + ("NI", "Nicaragua"), + ("NE", "Niger"), + ("NG", "Nigeria"), + ("NU", "Niue"), + ("NF", "Norfolk Island"), + ("MP", "Northern Mariana Islands"), + ("NO", "Norway"), + ("OM", "Oman"), + ("PK", "Pakistan"), + ("PW", "Palau"), + ("PS", "Palestinian Territory, Occupied"), + ("PA", "Panama"), + ("PG", "Papua New Guinea"), + ("PY", "Paraguay"), + ("PE", "Peru"), + ("PH", "Philippines"), + ("PN", "Pitcairn"), + ("PL", "Poland"), + ("PT", "Portugal"), + ("PR", "Puerto Rico"), + ("QA", "Qatar"), + ("RE", "Reunion"), + ("RO", "Romania"), + ("RU", "Russian Federation"), + ("RW", "Rwanda"), + ("BL", "Saint Barthelemy"), + ("SH", "Saint Helena"), + ("KN", "Saint Kitts and Nevis"), + ("LC", "Saint Lucia"), + ("MF", "Saint Martin"), + ("PM", "Saint Pierre and Miquelon"), + ("VC", "Saint Vincent and the Grenadines"), + ("WS", "Samoa"), + ("SM", "San Marino"), + ("ST", "Sao Tome and Principe"), + ("SA", "Saudi Arabia"), + ("SN", "Senegal"), + ("RS", "Serbia"), + ("SC", "Seychelles"), + ("SL", "Sierra Leone"), + ("SG", "Singapore"), + ("SK", "Slovakia"), + ("SI", "Slovenia"), + ("SB", "Solomon Islands"), + ("SO", "Somalia"), + ("ZA", "South Africa"), + ("GS", "South Georgia and the South Sandwich Islands"), + ("ES", "Spain"), + ("LK", "Sri Lanka"), + ("SD", "Sudan"), + ("SR", "Suriname"), + ("SJ", "Svalbard and Jan Mayen"), + ("SZ", "Swaziland"), + ("SE", "Sweden"), + ("CH", "Switzerland"), + ("SY", "Syrian Arab Republic"), + ("TW", "Taiwan, Province of China"), + ("TJ", "Tajikistan"), + ("TZ", "Tanzania, United Republic of"), + ("TH", "Thailand"), + ("TL", "Timor-Leste"), + ("TG", "Togo"), + ("TK", "Tokelau"), + ("TO", "Tonga"), + ("TT", "Trinidad and Tobago"), + ("TN", "Tunisia"), + ("TR", "Turkey"), + ("TM", "Turkmenistan"), + ("TC", "Turks and Caicos Islands"), + ("TV", "Tuvalu"), + ("UG", "Uganda"), + ("UA", "Ukraine"), + ("AE", "United Arab Emirates"), + ("US", "United States"), + ("UM", "United States Minor Outlying Islands"), + ("UY", "Uruguay"), + ("UZ", "Uzbekistan"), + ("VU", "Vanuatu"), + ("VE", "Venezuela"), + ("VN", "Viet Nam"), + ("VG", "Virgin Islands, British"), + ("VI", "Virgin Islands, U.S."), + ("WF", "Wallis and Futuna"), + ("EH", "Western Sahara"), + ("YE", "Yemen"), + ("ZM", "Zambia"), + ("ZW", "Zimbabwe"), + ], + max_length=3, + null=True, + verbose_name="Country", + ), + ), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Notes"), + ), + ("is_active", models.BooleanField(default=True)), + ( + "assigned_to", + models.ManyToManyField( + related_name="contact_assigned_users", to="common.profile" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contacts", + to="common.org", + ), + ), + ("tags", models.ManyToManyField(blank=True, to="common.tags")), + ( + "teams", + models.ManyToManyField( + related_name="contact_teams", to="common.teams" + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Contact", + "verbose_name_plural": "Contacts", + "db_table": "contacts", + "ordering": ("-created_at",), + "indexes": [ + models.Index( + fields=["organization"], name="contacts_organiz_8cebf0_idx" + ), + models.Index( + fields=["org", "-created_at"], name="contacts_org_id_f280a0_idx" + ), + ], + }, + ), + ] diff --git a/docs/__init__.py b/backend/contacts/migrations/__init__.py similarity index 100% rename from docs/__init__.py rename to backend/contacts/migrations/__init__.py diff --git a/backend/contacts/models.py b/backend/contacts/models.py new file mode 100644 index 00000000..1d9c859c --- /dev/null +++ b/backend/contacts/models.py @@ -0,0 +1,76 @@ +import arrow +from django.db import models +from django.utils.translation import gettext_lazy as _ +from phonenumber_field.modelfields import PhoneNumberField + +from common.base import AssignableMixin, BaseModel +from common.models import Org, Profile, Tags, Teams +from common.utils import COUNTRIES + + +class Contact(AssignableMixin, BaseModel): + """ + Contact model for CRM - Streamlined for modern sales workflow + Based on Twenty CRM and Salesforce patterns + """ + + # Core Contact Information + first_name = models.CharField(_("First name"), max_length=255) + last_name = models.CharField(_("Last name"), max_length=255) + email = models.EmailField(_("Email"), blank=True, null=True) + phone = PhoneNumberField(_("Phone"), null=True, blank=True) + + # Professional Information + organization = models.CharField(_("Company"), max_length=255, blank=True, null=True) + title = models.CharField(_("Job Title"), max_length=255, blank=True, null=True) + department = models.CharField( + _("Department"), max_length=255, blank=True, null=True + ) + + # Communication Preferences + do_not_call = models.BooleanField(_("Do Not Call"), default=False) + linked_in_url = models.URLField(_("LinkedIn URL"), blank=True, null=True) + + # Address (flat fields like Lead model) + address_line = models.CharField(_("Address"), max_length=255, blank=True, null=True) + city = models.CharField(_("City"), max_length=255, blank=True, null=True) + state = models.CharField(_("State"), max_length=255, blank=True, null=True) + postcode = models.CharField(_("Postal Code"), max_length=64, blank=True, null=True) + country = models.CharField( + _("Country"), max_length=3, choices=COUNTRIES, blank=True, null=True + ) + + # Assignment + assigned_to = models.ManyToManyField(Profile, related_name="contact_assigned_users") + teams = models.ManyToManyField(Teams, related_name="contact_teams") + + # Tags + tags = models.ManyToManyField(Tags, blank=True) + + # Notes + description = models.TextField(_("Notes"), blank=True, null=True) + + # System Fields + is_active = models.BooleanField(default=True) + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="contacts") + + class Meta: + verbose_name = "Contact" + verbose_name_plural = "Contacts" + db_table = "contacts" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["organization"]), + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return self.first_name + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() + + @property + def created_on(self): + return self.created_at diff --git a/contacts/serializer.py b/backend/contacts/serializer.py similarity index 60% rename from contacts/serializer.py rename to backend/contacts/serializer.py index d9a4b545..375ed362 100644 --- a/contacts/serializer.py +++ b/backend/contacts/serializer.py @@ -2,23 +2,22 @@ from rest_framework import serializers from common.serializer import ( AttachmentsSerializer, - BillingAddressSerializer, OrganizationSerializer, ProfileSerializer, + TeamsSerializer, ) from contacts.models import Contact -from teams.serializer import TeamsSerializer class ContactSerializer(serializers.ModelSerializer): + """Serializer for reading Contact data""" + teams = TeamsSerializer(read_only=True, many=True) assigned_to = ProfileSerializer(read_only=True, many=True) - address = BillingAddressSerializer(read_only=True) get_team_users = ProfileSerializer(read_only=True, many=True) get_team_and_assigned_users = ProfileSerializer(read_only=True, many=True) get_assigned_users_not_in_teams = ProfileSerializer(read_only=True, many=True) contact_attachment = AttachmentsSerializer(read_only=True, many=True) - date_of_birth = serializers.DateField() org = OrganizationSerializer() country = serializers.SerializerMethodField() @@ -29,87 +28,94 @@ class ContactSerializer(serializers.ModelSerializer): model = Contact fields = ( "id", - "salutation", + # Core Contact Information "first_name", "last_name", - "date_of_birth", + "email", + "phone", + # Professional Information "organization", "title", - "primary_email", - "secondary_email", - "mobile_number", - "secondary_number", "department", - "country", - "language", + # Communication Preferences "do_not_call", - "address", - "description", "linked_in_url", - "facebook_url", - "twitter_username", - "contact_attachment", + # Address + "address_line", + "city", + "state", + "postcode", + "country", + # Assignment "assigned_to", + "teams", + # Tags + "tags", + # Notes + "description", + # System "created_by", "created_at", "is_active", - "teams", + "org", "created_on_arrow", "get_team_users", "get_team_and_assigned_users", "get_assigned_users_not_in_teams", - "org", + "contact_attachment", ) class CreateContactSerializer(serializers.ModelSerializer): + """Serializer for creating/updating Contact data""" + def __init__(self, *args, **kwargs): request_obj = kwargs.pop("request_obj", None) super().__init__(*args, **kwargs) if request_obj: self.org = request_obj.profile.org - def validate_first_name(self, first_name): - if self.instance: - if ( - Contact.objects.filter(first_name__iexact=first_name, org=self.org) - .exclude(id=self.instance.id) - .exists() - ): - raise serializers.ValidationError( - "Contact already exists with this name" - ) - - else: - if Contact.objects.filter( - first_name__iexact=first_name, org=self.org - ).exists(): - raise serializers.ValidationError( - "Contact already exists with this name" - ) - return first_name + def validate_email(self, email): + if email: + if self.instance: + if ( + Contact.objects.filter(email__iexact=email, org=self.org) + .exclude(id=self.instance.id) + .exists() + ): + raise serializers.ValidationError( + "Contact already exists with this email" + ) + else: + if Contact.objects.filter(email__iexact=email, org=self.org).exists(): + raise serializers.ValidationError( + "Contact already exists with this email" + ) + return email class Meta: model = Contact fields = ( - "salutation", + # Core Contact Information "first_name", "last_name", + "email", + "phone", + # Professional Information "organization", "title", - "primary_email", - "secondary_email", - "mobile_number", - "secondary_number", "department", - "country", - "language", + # Communication Preferences "do_not_call", - "address", - "description", "linked_in_url", - "facebook_url", - "twitter_username", + # Address + "address_line", + "city", + "state", + "postcode", + "country", + # Notes + "description", ) @@ -117,5 +123,6 @@ class ContactDetailEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() contact_attachment = serializers.FileField() + class ContactCommentEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() diff --git a/backend/contacts/swagger_params.py b/backend/contacts/swagger_params.py new file mode 100644 index 00000000..9bb751c1 --- /dev/null +++ b/backend/contacts/swagger_params.py @@ -0,0 +1,47 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +organization_params_in_header = organization_params_in_header = OpenApiParameter( + "org", OpenApiTypes.STR, OpenApiParameter.HEADER +) + +organization_params = [ + organization_params_in_header, +] + +contact_list_get_params = [ + organization_params_in_header, + OpenApiParameter("name", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("city", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("assigned_to", OpenApiTypes.STR, OpenApiParameter.QUERY), +] + +contact_create_post_params = [ + organization_params_in_header, + OpenApiParameter("salutation", OpenApiParameter.QUERY, OpenApiTypes.STR), + OpenApiParameter("first_name", OpenApiParameter.QUERY, OpenApiTypes.STR), + OpenApiParameter("last_name", OpenApiParameter.QUERY, OpenApiTypes.STR), + OpenApiParameter("date_of_birth", OpenApiParameter.QUERY, OpenApiTypes.STR), + OpenApiParameter("organization", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("title", OpenApiParameter.QUERY, OpenApiTypes.STR), + OpenApiParameter("email", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("secondary_email", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("phone", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("secondary_number", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("department", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("language", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("do_not_call", OpenApiParameter.QUERY, OpenApiTypes.BOOL), + OpenApiParameter("address_line", OpenApiParameter.QUERY, OpenApiTypes.STR), + OpenApiParameter("street", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("city", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("state", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("pincode", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("country", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("description", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("linked_in_url", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("facebook_url", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("twitter_username", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("teams", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("assigned_to", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("contact_attachment", OpenApiParameter.QUERY, OpenApiTypes.BINARY), +] diff --git a/contacts/tasks.py b/backend/contacts/tasks.py similarity index 100% rename from contacts/tasks.py rename to backend/contacts/tasks.py diff --git a/contacts/tests_celery_tasks.py b/backend/contacts/tests_celery_tasks.py similarity index 100% rename from contacts/tests_celery_tasks.py rename to backend/contacts/tests_celery_tasks.py diff --git a/contacts/urls.py b/backend/contacts/urls.py similarity index 99% rename from contacts/urls.py rename to backend/contacts/urls.py index 6b9bb344..b9ec8b92 100644 --- a/contacts/urls.py +++ b/backend/contacts/urls.py @@ -10,4 +10,3 @@ urlpatterns = [ path("comment//", views.ContactCommentView.as_view()), path("attachment//", views.ContactAttachmentView.as_view()), ] - \ No newline at end of file diff --git a/contacts/views.py b/backend/contacts/views.py similarity index 68% rename from contacts/views.py rename to backend/contacts/views.py index 2adafdb1..e870e0ae 100644 --- a/contacts/views.py +++ b/backend/contacts/views.py @@ -1,5 +1,6 @@ import json +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.shortcuts import get_object_or_404 from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema @@ -9,31 +10,28 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from common.models import Attachments, Comment, Profile -from common.serializer import ( - AttachmentsSerializer, - BillingAddressSerializer, - CommentSerializer, -) +from common.models import Attachments, Comment, Profile, Teams +from common.serializer import AttachmentsSerializer, CommentSerializer from common.utils import COUNTRIES -#from common.external_auth import CustomDualAuthentication -from contacts import swagger_params1 +# from common.external_auth import CustomDualAuthentication +from contacts import swagger_params from contacts.models import Contact, Profile from contacts.serializer import * from contacts.tasks import send_email_to_assigned_user from tasks.serializer import TaskSerializer -from teams.models import Teams class ContactsListView(APIView, LimitOffsetPagination): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Contact def get_context_data(self, **kwargs): params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") + queryset = self.model.objects.filter(org=self.request.profile.org).order_by( + "-id" + ) if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: queryset = queryset.filter( Q(assigned_to__in=[self.request.profile]) @@ -46,9 +44,9 @@ class ContactsListView(APIView, LimitOffsetPagination): if params.get("city"): queryset = queryset.filter(address__city__icontains=params.get("city")) if params.get("phone"): - queryset = queryset.filter(mobile_number__icontains=params.get("phone")) + queryset = queryset.filter(phone__icontains=params.get("phone")) if params.get("email"): - queryset = queryset.filter(primary_email__icontains=params.get("email")) + queryset = queryset.filter(email__icontains=params.get("email")) if params.getlist("assigned_to"): queryset = queryset.filter( assigned_to__id__in=params.get("assigned_to") @@ -68,45 +66,42 @@ class ContactsListView(APIView, LimitOffsetPagination): context["per_page"] = 10 page_number = (int(self.offset / 10) + 1,) context["page_number"] = page_number - context.update({"contacts_count": self.count, "offset": offset}) + # Standard DRF pagination format for frontend compatibility + context["count"] = self.count + context["results"] = contacts + # Legacy format for backwards compatibility + context["contacts_count"] = self.count + context["offset"] = offset context["contact_obj_list"] = contacts context["countries"] = COUNTRIES - users = Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "id", "user__email" - ) + users = Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("id", "user__email") context["users"] = users return context - @extend_schema( - tags=["contacts"], parameters=swagger_params1.contact_list_get_params - ) + @extend_schema(tags=["contacts"], parameters=swagger_params.contact_list_get_params) def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return Response(context) @extend_schema( - tags=["contacts"], parameters=swagger_params1.organization_params,request=CreateContactSerializer + tags=["contacts"], + parameters=swagger_params.organization_params, + request=CreateContactSerializer, ) def post(self, request, *args, **kwargs): params = request.data contact_serializer = CreateContactSerializer(data=params, request_obj=request) - address_serializer = BillingAddressSerializer(data=params) - data = {} if not contact_serializer.is_valid(): - data["contact_errors"] = contact_serializer.errors - if not address_serializer.is_valid(): - data["address_errors"] = (address_serializer.errors,) - if data: return Response( - {"error": True, "errors": data}, + {"error": True, "errors": contact_serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) - # if contact_serializer.is_valid() and address_serializer.is_valid(): - address_obj = address_serializer.save() - contact_obj = contact_serializer.save(date_of_birth=params.get("date_of_birth")) - contact_obj.address = address_obj + # Contact model uses flat address fields, no separate Address object needed + contact_obj = contact_serializer.save() contact_obj.org = request.profile.org contact_obj.save() @@ -117,7 +112,9 @@ class ContactsListView(APIView, LimitOffsetPagination): if params.get("assigned_to"): assinged_to_list = params.get("assigned_to") - profiles = Profile.objects.filter(id__in=assinged_to_list, org=request.profile.org) + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org + ) contact_obj.assigned_to.add(*profiles) recipients = list(contact_obj.assigned_to.all().values_list("id", flat=True)) @@ -145,15 +142,16 @@ class ContactDetailView(APIView): model = Contact def get_object(self, pk): - return get_object_or_404(Contact, pk=pk) + return get_object_or_404(Contact, pk=pk, org=self.request.profile.org) @extend_schema( - tags=["contacts"], parameters=swagger_params1.contact_create_post_params,request=CreateContactSerializer + tags=["contacts"], + parameters=swagger_params.contact_create_post_params, + request=CreateContactSerializer, ) def put(self, request, pk, format=None): data = request.data contact_obj = self.get_object(pk=pk) - address_obj = contact_obj.address if contact_obj.org != request.profile.org: return Response( {"error": True, "errors": "User company doesnot match with header...."}, @@ -162,84 +160,65 @@ class ContactDetailView(APIView): contact_serializer = CreateContactSerializer( data=data, instance=contact_obj, request_obj=request ) - address_serializer = BillingAddressSerializer(data=data, instance=address_obj) - data = {} if not contact_serializer.is_valid(): - data["contact_errors"] = contact_serializer.errors - if not address_serializer.is_valid(): - data["address_errors"] = (address_serializer.errors,) - if data: - data["error"] = True return Response( - data, + {"error": True, "errors": contact_serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) - if contact_serializer.is_valid(): - if ( - self.request.profile.role != "ADMIN" - and not self.request.profile.is_admin + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + if not ( + (self.request.profile == contact_obj.created_by) + or (self.request.profile in contact_obj.assigned_to.all()) ): - if not ( - (self.request.profile == contact_obj.created_by) - or (self.request.profile in contact_obj.assigned_to.all()) - ): - return Response( - { - "error": True, - "errors": "You do not have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - address_obj = address_serializer.save() - contact_obj = contact_serializer.save( - date_of_birth=data.get("date_of_birth") - ) - contact_obj.address = address_obj - contact_obj.save() - contact_obj = contact_serializer.save() - contact_obj.teams.clear() - if data.get("teams"): - teams_list = json.loads(data.get("teams")) - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - contact_obj.teams.add(*teams) - - contact_obj.assigned_to.clear() - if data.get("assigned_to"): - assinged_to_list = json.loads(data.get("assigned_to")) - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org + return Response( + { + "error": True, + "errors": "You do not have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, ) - contact_obj.assigned_to.add(*profiles) - previous_assigned_to_users = list( - contact_obj.assigned_to.all().values_list("id", flat=True) - ) + contact_obj = contact_serializer.save() + contact_obj.teams.clear() + if data.get("teams"): + teams_list = json.loads(data.get("teams")) + teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) + contact_obj.teams.add(*teams) - assigned_to_list = list( - contact_obj.assigned_to.all().values_list("id", flat=True) - ) - recipients = list(set(assigned_to_list) - set(previous_assigned_to_users)) - send_email_to_assigned_user.delay( - recipients, - contact_obj.id, - ) - if request.FILES.get("contact_attachment"): - attachment = Attachments() - attachment.created_by = request.profile.user - attachment.file_name = request.FILES.get("contact_attachment").name - attachment.contact = contact_obj - attachment.attachment = request.FILES.get("contact_attachment") - attachment.save() - return Response( - {"error": False, "message": "Contact Updated Successfully"}, - status=status.HTTP_200_OK, + contact_obj.assigned_to.clear() + if data.get("assigned_to"): + assinged_to_list = json.loads(data.get("assigned_to")) + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org ) + contact_obj.assigned_to.add(*profiles) - @extend_schema( - tags=["contacts"], parameters=swagger_params1.organization_params - ) + previous_assigned_to_users = list( + contact_obj.assigned_to.all().values_list("id", flat=True) + ) + + assigned_to_list = list( + contact_obj.assigned_to.all().values_list("id", flat=True) + ) + recipients = list(set(assigned_to_list) - set(previous_assigned_to_users)) + send_email_to_assigned_user.delay( + recipients, + contact_obj.id, + ) + if request.FILES.get("contact_attachment"): + attachment = Attachments() + attachment.created_by = request.profile.user + attachment.file_name = request.FILES.get("contact_attachment").name + attachment.contact = contact_obj + attachment.attachment = request.FILES.get("contact_attachment") + attachment.save() + return Response( + {"error": False, "message": "Contact Updated Successfully"}, + status=status.HTTP_200_OK, + ) + + @extend_schema(tags=["contacts"], parameters=swagger_params.organization_params) def get(self, request, pk, format=None): context = {} contact_obj = self.get_object(pk) @@ -287,16 +266,30 @@ class ContactDetailView(APIView): if request.profile == contact_obj.created_by: user_assgn_list.append(self.request.profile.id) - context["address_obj"] = BillingAddressSerializer(contact_obj.address).data + # Address is now flat fields on Contact model + context["address_obj"] = { + "address_line": contact_obj.address_line, + "city": contact_obj.city, + "state": contact_obj.state, + "postcode": contact_obj.postcode, + "country": contact_obj.country, + } context["countries"] = COUNTRIES + contact_content_type = ContentType.objects.get_for_model(Contact) + comments = Comment.objects.filter( + content_type=contact_content_type, + object_id=contact_obj.id, + org=request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=contact_content_type, + object_id=contact_obj.id, + org=request.profile.org, + ).order_by("-id") context.update( { - "comments": CommentSerializer( - contact_obj.contact_comments.all(), many=True - ).data, - "attachments": AttachmentsSerializer( - contact_obj.contact_attachment.all(), many=True - ).data, + "comments": CommentSerializer(comments, many=True).data, + "attachments": AttachmentsSerializer(attachments, many=True).data, "assigned_data": assigned_data, "tasks": TaskSerializer( contact_obj.contacts_tasks.all(), many=True @@ -306,9 +299,7 @@ class ContactDetailView(APIView): ) return Response(context) - @extend_schema( - tags=["contacts"], parameters=swagger_params1.organization_params - ) + @extend_schema(tags=["contacts"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if self.object.org != request.profile.org: @@ -337,7 +328,9 @@ class ContactDetailView(APIView): ) @extend_schema( - tags=["contacts"], parameters=swagger_params1.organization_params,request=ContactDetailEditSwaggerSerializer + tags=["contacts"], + parameters=swagger_params.organization_params, + request=ContactDetailEditSwaggerSerializer, ) def post(self, request, pk, **kwargs): params = request.data @@ -372,11 +365,16 @@ class ContactDetailView(APIView): attachment.attachment = self.request.FILES.get("contact_attachment") attachment.save() - comments = Comment.objects.filter(contact__id=self.contact_obj.id).order_by( - "-id" - ) + contact_content_type = ContentType.objects.get_for_model(Contact) + comments = Comment.objects.filter( + content_type=contact_content_type, + object_id=self.contact_obj.id, + org=self.request.profile.org, + ).order_by("-id") attachments = Attachments.objects.filter( - contact__id=self.contact_obj.id + content_type=contact_content_type, + object_id=self.contact_obj.id, + org=self.request.profile.org, ).order_by("-id") context.update( { @@ -394,10 +392,12 @@ class ContactCommentView(APIView): permission_classes = (IsAuthenticated,) def get_object(self, pk): - return self.model.objects.get(pk=pk) + return self.model.objects.get(pk=pk, org=self.request.profile.org) @extend_schema( - tags=["contacts"], parameters=swagger_params1.organization_params,request=ContactCommentEditSwaggerSerializer + tags=["contacts"], + parameters=swagger_params.organization_params, + request=ContactCommentEditSwaggerSerializer, ) def put(self, request, pk, format=None): params = request.data @@ -426,9 +426,7 @@ class ContactCommentView(APIView): status=status.HTTP_403_FORBIDDEN, ) - @extend_schema( - tags=["contacts"], parameters=swagger_params1.organization_params - ) + @extend_schema(tags=["contacts"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if ( @@ -455,9 +453,7 @@ class ContactAttachmentView(APIView): # #authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) - @extend_schema( - tags=["contacts"], parameters=swagger_params1.organization_params - ) + @extend_schema(tags=["contacts"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.model.objects.get(pk=pk) if ( diff --git a/crm/__init__.py b/backend/crm/__init__.py similarity index 100% rename from crm/__init__.py rename to backend/crm/__init__.py diff --git a/crm/celery.py b/backend/crm/celery.py similarity index 100% rename from crm/celery.py rename to backend/crm/celery.py diff --git a/crm/server_settings.py b/backend/crm/server_settings.py similarity index 100% rename from crm/server_settings.py rename to backend/crm/server_settings.py diff --git a/crm/settings.py b/backend/crm/settings.py similarity index 90% rename from crm/settings.py rename to backend/crm/settings.py index aa2b8b23..7951533c 100644 --- a/crm/settings.py +++ b/backend/crm/settings.py @@ -37,21 +37,6 @@ DEBUG = True ALLOWED_HOSTS = ["*"] INSTALLED_APPS = [ - "wagtail.contrib.forms", - "wagtail.contrib.redirects", - "wagtail.embeds", - "wagtail.sites", - "wagtail.users", - "wagtail.snippets", - "wagtail.documents", - "wagtail.images", - "wagtail.search", - "wagtail.admin", - "wagtail", - "cms", - "wagtail.contrib.settings", - "modelcluster", - "taggit", "django.contrib.auth", "django.contrib.admin", "django.contrib.contenttypes", @@ -68,14 +53,11 @@ INSTALLED_APPS = [ "accounts", "cases", "contacts", - "emails", "leads", "opportunity", - "planner", "tasks", "invoices", - "events", - "teams", + # "teams", # Merged into common app ] MIDDLEWARE = [ @@ -90,7 +72,7 @@ MIDDLEWARE = [ "crum.CurrentRequestUserMiddleware", # "common.external_auth.CustomDualAuthentication" "common.middleware.get_company.GetProfileAndOrg", - "wagtail.contrib.redirects.middleware.RedirectMiddleware", + "common.middleware.rls_context.RequireOrgContext", # RLS: Enforce org context + set PostgreSQL session variable ] ROOT_URLCONF = "crm.urls" @@ -110,7 +92,6 @@ TEMPLATES = [ "django.contrib.messages.context_processors.messages", "common.context_processors.common.app_name", # "django_settings_export.settings_export", - "wagtail.contrib.settings.context_processors.settings", ], }, }, @@ -201,7 +182,11 @@ LOGGING = { "django.server": { "()": "django.utils.log.ServerFormatter", "format": "[%(server_time)s] %(message)s", - } + }, + "security": { + "format": "%(asctime)s | %(levelname)s | %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, }, "handlers": { "console": { @@ -228,6 +213,11 @@ LOGGING = { "class": "logging.FileHandler", "filename": "server.log", }, + "security_audit": { + "class": "logging.FileHandler", + "filename": "security_audit.log", + "formatter": "security", + }, }, "loggers": { "django": { @@ -243,22 +233,23 @@ LOGGING = { "level": "INFO", "propagate": False, }, + "security.audit": { + "handlers": ["security_audit", "console"], + "level": "INFO", + "propagate": False, + }, }, } APPLICATION_NAME = "bottlecrm" -WAGTAIL_SITE_NAME = "bottlecrm" - -WAGTAILADMIN_BASE_URL = "https://bottlecrm.com" - SETTINGS_EXPORT = ["APPLICATION_NAME"] REST_FRAMEWORK = { "EXCEPTION_HANDLER": "rest_framework.views.exception_handler", "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", - "common.external_auth.CustomDualAuthentication" + "common.external_auth.CustomDualAuthentication", # "rest_framework.authentication.SessionAuthentication", # "rest_framework.authentication.BasicAuthentication", ), @@ -275,7 +266,6 @@ SPECTACULAR_SETTINGS = { "SERVE_INCLUDE_SCHEMA": False, "COMPONENT_SPLIT_REQUEST": True, "PREPROCESSING_HOOKS": ["common.custom_openapi.preprocessing_filter_spec"], - } # JWT_SETTINGS = { @@ -333,3 +323,8 @@ JWT_ALGO = "HS256" DOMAIN_NAME = os.environ["DOMAIN_NAME"] SWAGGER_ROOT_URL = os.environ["SWAGGER_ROOT_URL"] + +# Google OAuth Configuration +GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "") +GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "") +GOOGLE_REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "") diff --git a/crm/urls.py b/backend/crm/urls.py similarity index 77% rename from crm/urls.py rename to backend/crm/urls.py index 2c215711..cc2d9c76 100644 --- a/crm/urls.py +++ b/backend/crm/urls.py @@ -1,7 +1,6 @@ -import os -from django.contrib import admin from django.conf import settings from django.conf.urls.static import static +from django.contrib import admin from django.contrib.auth import views from django.urls import include, path from django.urls import re_path as url @@ -11,12 +10,7 @@ from drf_spectacular.views import ( SpectacularRedocView, SpectacularSwaggerView, ) -# from drf_yasg import openapi -# from drf_yasg.views import get_schema_view from rest_framework import permissions -from wagtail import urls as wagtail_urls -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as wagtaildocs_urls app_name = "crm" @@ -30,10 +24,7 @@ urlpatterns = [ path( "logout/", views.LogoutView.as_view(), {"next_page": "/login/"}, name="logout" ), - path("admin/", include(wagtailadmin_urls)), - - path("django/admin/", admin.site.urls), - path("documents/", include(wagtaildocs_urls)), + path("admin/", admin.site.urls), path("schema/", SpectacularAPIView.as_view(), name="schema"), # Optional UI: path( @@ -46,7 +37,6 @@ urlpatterns = [ SpectacularRedocView.as_view(url_name="schema"), name="redoc", ), - path("", include(wagtail_urls)), ] diff --git a/crm/wsgi.py b/backend/crm/wsgi.py similarity index 100% rename from crm/wsgi.py rename to backend/crm/wsgi.py diff --git a/emails/__init__.py b/backend/invoices/__init__.py similarity index 100% rename from emails/__init__.py rename to backend/invoices/__init__.py diff --git a/backend/invoices/admin.py b/backend/invoices/admin.py new file mode 100644 index 00000000..724f683a --- /dev/null +++ b/backend/invoices/admin.py @@ -0,0 +1,73 @@ +from django.contrib import admin + +from invoices.models import Invoice, InvoiceHistory, InvoiceLineItem, Product + + +class InvoiceLineItemInline(admin.TabularInline): + model = InvoiceLineItem + extra = 1 + fields = ("product", "description", "quantity", "unit_price", "total", "order") + readonly_fields = ("total",) + + +@admin.register(Invoice) +class InvoiceAdmin(admin.ModelAdmin): + list_display = ( + "invoice_number", + "invoice_title", + "name", + "status", + "total_amount", + "due_date", + "created_at", + ) + list_filter = ("status", "created_at", "due_date") + search_fields = ("invoice_number", "invoice_title", "name", "email") + raw_id_fields = ("created_by", "org", "from_address", "to_address") + filter_horizontal = ("assigned_to", "accounts", "teams") + inlines = [InvoiceLineItemInline] + date_hierarchy = "created_at" + ordering = ("-created_at",) + + +@admin.register(InvoiceHistory) +class InvoiceHistoryAdmin(admin.ModelAdmin): + list_display = ("invoice", "invoice_number", "status", "updated_by", "created_at") + list_filter = ("status", "created_at") + search_fields = ("invoice_number", "invoice_title") + raw_id_fields = ("invoice", "updated_by") + ordering = ("-created_at",) + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ( + "name", + "sku", + "price", + "category", + "is_active", + "org", + "created_at", + ) + list_filter = ("is_active", "category", "org", "created_at") + search_fields = ("name", "sku", "description") + raw_id_fields = ("org",) + ordering = ("name",) + + +@admin.register(InvoiceLineItem) +class InvoiceLineItemAdmin(admin.ModelAdmin): + list_display = ( + "invoice", + "description", + "product", + "quantity", + "unit_price", + "total", + "order", + ) + list_filter = ("invoice__status", "created_at") + search_fields = ("description", "invoice__invoice_number", "product__name") + raw_id_fields = ("invoice", "product") + ordering = ("invoice", "order") diff --git a/invoices/api_urls.py b/backend/invoices/api_urls.py similarity index 100% rename from invoices/api_urls.py rename to backend/invoices/api_urls.py diff --git a/invoices/api_views.py b/backend/invoices/api_views.py similarity index 89% rename from invoices/api_views.py rename to backend/invoices/api_views.py index bd478be0..a5b77816 100644 --- a/invoices/api_views.py +++ b/backend/invoices/api_views.py @@ -2,9 +2,10 @@ import json import pytz from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.contrib.sites.shortcuts import get_current_site from django.db.models import Q -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.pagination import LimitOffsetPagination from rest_framework.permissions import IsAuthenticated @@ -13,22 +14,24 @@ from rest_framework.views import APIView from accounts.models import Account from accounts.serializer import AccountSerializer -from common.models import Attachments, Comment, User +from common.models import Attachments, Comment, Teams, User -#from common.external_auth import CustomDualAuthentication +# from common.external_auth import CustomDualAuthentication from common.serializer import ( AttachmentsSerializer, BillingAddressSerializer, CommentSerializer, + TeamsSerializer, UserSerializer, ) from common.utils import COUNTRIES, CURRENCY_CODES -from invoices import swagger_params1 +from invoices import swagger_params from invoices.models import Invoice from invoices.serializer import ( InvoiceCreateSerializer, InvoiceHistorySerializer, InvoiceSerailizer, + InvoiceSwaggerSerailizer, ) from invoices.tasks import ( create_invoice_history, @@ -36,8 +39,6 @@ from invoices.tasks import ( send_invoice_email, send_invoice_email_cancel, ) -from teams.models import Teams -from teams.serializer import TeamsSerializer INVOICE_STATUS = ( ("Draft", "Draft"), @@ -50,7 +51,7 @@ INVOICE_STATUS = ( class InvoiceListView(APIView, LimitOffsetPagination): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Invoice @@ -76,9 +77,7 @@ class InvoiceListView(APIView, LimitOffsetPagination): if params.get("created_by"): queryset = queryset.filter(created_by=params.get("created_by")) if params.get("assigned_users"): - queryset = queryset.filter( - assigned_to__in=params.get("assigned_users") - ) + queryset = queryset.filter(assigned_to__in=params.get("assigned_users")) if params.get("status"): queryset = queryset.filter(status=params.get("status")) if params.get("total_amount"): @@ -131,12 +130,19 @@ class InvoiceListView(APIView, LimitOffsetPagination): return context - @extend_schema(tags=["Invoices"], parameters=swagger_params1.invoice_list_get_params) + @extend_schema( + tags=["Invoices"], + parameters=swagger_params.invoice_list_get_params, + ) def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return Response(context) - @extend_schema(tags=["Invoices"], parameters=swagger_params1.organization_params,request=InvoiceSwaggerSerailizer) + @extend_schema( + tags=["Invoices"], + parameters=swagger_params.invoice_create_post_params, + request=InvoiceSwaggerSerailizer, + ) def post(self, request, *args, **kwargs): params = request.data data = {} @@ -240,15 +246,17 @@ class InvoiceListView(APIView, LimitOffsetPagination): class InvoiceDetailView(APIView): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Invoice def get_object(self, pk): - return self.model.objects.filter(id=pk).first() + return self.model.objects.filter(id=pk, org=self.request.profile.org).first() - @swagger_auto_schema( - tags=["Invoices"], manual_parameters=swagger_params1.invoice_create_post_params + @extend_schema( + tags=["Invoices"], + parameters=swagger_params.invoice_create_post_params, + request=InvoiceSwaggerSerailizer, ) def put(self, request, pk, format=None): params = request.data @@ -380,9 +388,7 @@ class InvoiceDetailView(APIView): status=status.HTTP_400_BAD_REQUEST, ) - @swagger_auto_schema( - tags=["Invoices"], manual_parameters=swagger_params1.invoice_delete_params - ) + @extend_schema(tags=["Invoices"], parameters=swagger_params.invoice_delete_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if self.object.company != request.company: @@ -407,9 +413,7 @@ class InvoiceDetailView(APIView): status=status.HTTP_200_OK, ) - @swagger_auto_schema( - tags=["Invoices"], manual_parameters=swagger_params1.invoice_delete_params - ) + @extend_schema(tags=["Invoices"], parameters=swagger_params.organization_params) def get(self, request, pk, format=None): self.invoice = self.get_object(pk=pk) if self.invoice.company != request.company: @@ -456,8 +460,17 @@ class InvoiceDetailView(APIView): else: users_mention = [] - attachments = Attachments.objects.filter(invoice=self.invoice).order_by("-id") - comments = Comment.objects.filter(invoice=self.invoice).order_by("-id") + invoice_content_type = ContentType.objects.get_for_model(Invoice) + attachments = Attachments.objects.filter( + content_type=invoice_content_type, + object_id=self.invoice.id, + org=self.request.profile.org, + ).order_by("-id") + comments = Comment.objects.filter( + content_type=invoice_content_type, + object_id=self.invoice.id, + org=self.request.profile.org, + ).order_by("-id") context.update( { "attachments": AttachmentsSerializer(attachments, many=True).data, @@ -484,8 +497,10 @@ class InvoiceDetailView(APIView): ) return Response(context) - @swagger_auto_schema( - tags=["Invoices"], manual_parameters=swagger_params1.invoice_detail_post_params + @extend_schema( + tags=["Invoices"], + parameters=swagger_params.invoice_detail_post_params, + request=CommentSerializer, ) def post(self, request, pk, **kwargs): params = request.data @@ -524,10 +539,17 @@ class InvoiceDetailView(APIView): attachment.attachment = self.request.FILES.get("invoice_attachment") attachment.save() - comments = Comment.objects.filter(invoice=self.invoice_obj).order_by("-id") - attachments = Attachments.objects.filter(invoice=self.invoice_obj).order_by( - "-id" - ) + invoice_content_type = ContentType.objects.get_for_model(Invoice) + comments = Comment.objects.filter( + content_type=invoice_content_type, + object_id=self.invoice_obj.id, + org=request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=invoice_content_type, + object_id=self.invoice_obj.id, + org=request.profile.org, + ).order_by("-id") context.update( { "invoice_obj": InvoiceSerailizer(self.invoice_obj).data, @@ -540,14 +562,16 @@ class InvoiceDetailView(APIView): class InvoiceCommentView(APIView): model = Comment - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) def get_object(self, pk): - return self.model.objects.get(pk=pk) + return self.model.objects.get(pk=pk, org=self.request.profile.org) - @swagger_auto_schema( - tags=["Invoices"], manual_parameters=swagger_params1.invoice_comment_edit_params + @extend_schema( + tags=["Invoices"], + parameters=swagger_params.invoice_comment_edit_params, + request=CommentSerializer, ) def put(self, request, pk, format=None): params = request.data @@ -577,9 +601,7 @@ class InvoiceCommentView(APIView): } ) - @swagger_auto_schema( - tags=["Invoices"], manual_parameters=swagger_params1.invoice_delete_params - ) + @extend_schema(tags=["Invoices"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if ( @@ -603,12 +625,10 @@ class InvoiceCommentView(APIView): class InvoiceAttachmentView(APIView): model = Attachments - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) - @swagger_auto_schema( - tags=["Invoices"], manual_parameters=swagger_params1.invoice_delete_params - ) + @extend_schema(tags=["Invoices"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.model.objects.get(pk=pk) if ( diff --git a/invoices/apps.py b/backend/invoices/apps.py similarity index 100% rename from invoices/apps.py rename to backend/invoices/apps.py diff --git a/invoices/forms.py b/backend/invoices/forms.py similarity index 96% rename from invoices/forms.py rename to backend/invoices/forms.py index 250d707b..1623e7a3 100644 --- a/invoices/forms.py +++ b/backend/invoices/forms.py @@ -2,9 +2,8 @@ from django import forms from django.db.models import Q from accounts.models import Account -from common.models import Address, Attachments, Comment, User +from common.models import Address, Attachments, Comment, Teams, User from invoices.models import Invoice -from teams.models import Teams class InvoiceForm(forms.ModelForm): @@ -117,6 +116,8 @@ class InvoiceAddressForm(forms.ModelForm): self.fields["city"].widget.attrs.update({"placeholder": "City"}) self.fields["state"].widget.attrs.update({"placeholder": "State"}) self.fields["postcode"].widget.attrs.update({"placeholder": "Postcode"}) - self.fields["country"].choices = [("", "--Country--"),] + list( + self.fields["country"].choices = [ + ("", "--Country--"), + ] + list( self.fields["country"].choices )[1:] diff --git a/backend/invoices/migrations/0001_initial.py b/backend/invoices/migrations/0001_initial.py new file mode 100644 index 00000000..60305d74 --- /dev/null +++ b/backend/invoices/migrations/0001_initial.py @@ -0,0 +1,840 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("accounts", "0001_initial"), + ("common", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Invoice", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "invoice_title", + models.CharField(max_length=50, verbose_name="Invoice Title"), + ), + ( + "invoice_number", + models.CharField(max_length=50, verbose_name="Invoice Number"), + ), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ("email", models.EmailField(max_length=254, verbose_name="Email")), + ("quantity", models.PositiveIntegerField(default=0)), + ( + "rate", + models.DecimalField(decimal_places=2, default=0, max_digits=12), + ), + ( + "total_amount", + models.DecimalField( + blank=True, decimal_places=2, max_digits=12, null=True + ), + ), + ( + "tax", + models.DecimalField( + blank=True, decimal_places=2, max_digits=12, null=True + ), + ), + ( + "currency", + models.CharField( + blank=True, + choices=[ + ("AED", "AED, Dirham"), + ("AFN", "AFN, Afghani"), + ("ALL", "ALL, Lek"), + ("AMD", "AMD, Dram"), + ("ANG", "ANG, Guilder"), + ("AOA", "AOA, Kwanza"), + ("ARS", "ARS, Peso"), + ("AUD", "AUD, Dollar"), + ("AWG", "AWG, Guilder"), + ("AZN", "AZN, Manat"), + ("BAM", "BAM, Marka"), + ("BBD", "BBD, Dollar"), + ("BDT", "BDT, Taka"), + ("BGN", "BGN, Lev"), + ("BHD", "BHD, Dinar"), + ("BIF", "BIF, Franc"), + ("BMD", "BMD, Dollar"), + ("BND", "BND, Dollar"), + ("BOB", "BOB, Boliviano"), + ("BRL", "BRL, Real"), + ("BSD", "BSD, Dollar"), + ("BTN", "BTN, Ngultrum"), + ("BWP", "BWP, Pula"), + ("BYR", "BYR, Ruble"), + ("BZD", "BZD, Dollar"), + ("CAD", "CAD, Dollar"), + ("CDF", "CDF, Franc"), + ("CHF", "CHF, Franc"), + ("CLP", "CLP, Peso"), + ("CNY", "CNY, Yuan Renminbi"), + ("COP", "COP, Peso"), + ("CRC", "CRC, Colon"), + ("CUP", "CUP, Peso"), + ("CVE", "CVE, Escudo"), + ("CZK", "CZK, Koruna"), + ("DJF", "DJF, Franc"), + ("DKK", "DKK, Krone"), + ("DOP", "DOP, Peso"), + ("DZD", "DZD, Dinar"), + ("EGP", "EGP, Pound"), + ("ERN", "ERN, Nakfa"), + ("ETB", "ETB, Birr"), + ("EUR", "EUR, Euro"), + ("FJD", "FJD, Dollar"), + ("FKP", "FKP, Pound"), + ("GBP", "GBP, Pound"), + ("GEL", "GEL, Lari"), + ("GHS", "GHS, Cedi"), + ("GIP", "GIP, Pound"), + ("GMD", "GMD, Dalasi"), + ("GNF", "GNF, Franc"), + ("GTQ", "GTQ, Quetzal"), + ("GYD", "GYD, Dollar"), + ("HKD", "HKD, Dollar"), + ("HNL", "HNL, Lempira"), + ("HRK", "HRK, Kuna"), + ("HTG", "HTG, Gourde"), + ("HUF", "HUF, Forint"), + ("IDR", "IDR, Rupiah"), + ("ILS", "ILS, Shekel"), + ("INR", "INR, Rupee"), + ("IQD", "IQD, Dinar"), + ("IRR", "IRR, Rial"), + ("ISK", "ISK, Krona"), + ("JMD", "JMD, Dollar"), + ("JOD", "JOD, Dinar"), + ("JPY", "JPY, Yen"), + ("KES", "KES, Shilling"), + ("KGS", "KGS, Som"), + ("KHR", "KHR, Riels"), + ("KMF", "KMF, Franc"), + ("KPW", "KPW, Won"), + ("KRW", "KRW, Won"), + ("KWD", "KWD, Dinar"), + ("KYD", "KYD, Dollar"), + ("KZT", "KZT, Tenge"), + ("LAK", "LAK, Kip"), + ("LBP", "LBP, Pound"), + ("LKR", "LKR, Rupee"), + ("LRD", "LRD, Dollar"), + ("LSL", "LSL, Loti"), + ("LTL", "LTL, Litas"), + ("LVL", "LVL, Lat"), + ("LYD", "LYD, Dinar"), + ("MAD", "MAD, Dirham"), + ("MDL", "MDL, Leu"), + ("MGA", "MGA, Ariary"), + ("MKD", "MKD, Denar"), + ("MMK", "MMK, Kyat"), + ("MNT", "MNT, Tugrik"), + ("MOP", "MOP, Pataca"), + ("MRO", "MRO, Ouguiya"), + ("MUR", "MUR, Rupee"), + ("MVR", "MVR, Rufiyaa"), + ("MWK", "MWK, Kwacha"), + ("MXN", "MXN, Peso"), + ("MYR", "MYR, Ringgit"), + ("MZN", "MZN, Metical"), + ("NAD", "NAD, Dollar"), + ("NGN", "NGN, Naira"), + ("NIO", "NIO, Cordoba"), + ("NOK", "NOK, Krone"), + ("NPR", "NPR, Rupee"), + ("NZD", "NZD, Dollar"), + ("OMR", "OMR, Rial"), + ("PAB", "PAB, Balboa"), + ("PEN", "PEN, Sol"), + ("PGK", "PGK, Kina"), + ("PHP", "PHP, Peso"), + ("PKR", "PKR, Rupee"), + ("PLN", "PLN, Zloty"), + ("PYG", "PYG, Guarani"), + ("QAR", "QAR, Rial"), + ("RON", "RON, Leu"), + ("RSD", "RSD, Dinar"), + ("RUB", "RUB, Ruble"), + ("RWF", "RWF, Franc"), + ("SAR", "SAR, Rial"), + ("SBD", "SBD, Dollar"), + ("SCR", "SCR, Rupee"), + ("SDG", "SDG, Pound"), + ("SEK", "SEK, Krona"), + ("SGD", "SGD, Dollar"), + ("SHP", "SHP, Pound"), + ("SLL", "SLL, Leone"), + ("SOS", "SOS, Shilling"), + ("SRD", "SRD, Dollar"), + ("SSP", "SSP, Pound"), + ("STD", "STD, Dobra"), + ("SYP", "SYP, Pound"), + ("SZL", "SZL, Lilangeni"), + ("THB", "THB, Baht"), + ("TJS", "TJS, Somoni"), + ("TMT", "TMT, Manat"), + ("TND", "TND, Dinar"), + ("TOP", "TOP, Paanga"), + ("TRY", "TRY, Lira"), + ("TTD", "TTD, Dollar"), + ("TWD", "TWD, Dollar"), + ("TZS", "TZS, Shilling"), + ("UAH", "UAH, Hryvnia"), + ("UGX", "UGX, Shilling"), + ("USD", "$, Dollar"), + ("UYU", "UYU, Peso"), + ("UZS", "UZS, Som"), + ("VEF", "VEF, Bolivar"), + ("VND", "VND, Dong"), + ("VUV", "VUV, Vatu"), + ("WST", "WST, Tala"), + ("XAF", "XAF, Franc"), + ("XCD", "XCD, Dollar"), + ("XOF", "XOF, Franc"), + ("XPF", "XPF, Franc"), + ("YER", "YER, Rial"), + ("ZAR", "ZAR, Rand"), + ("ZMK", "ZMK, Kwacha"), + ("ZWL", "ZWL, Dollar"), + ], + max_length=3, + null=True, + ), + ), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, max_length=128, null=True, region=None + ), + ), + ( + "amount_due", + models.DecimalField( + blank=True, decimal_places=2, max_digits=12, null=True + ), + ), + ( + "amount_paid", + models.DecimalField( + blank=True, decimal_places=2, max_digits=12, null=True + ), + ), + ("is_email_sent", models.BooleanField(default=False)), + ( + "status", + models.CharField( + choices=[ + ("Draft", "Draft"), + ("Sent", "Sent"), + ("Paid", "Paid"), + ("Pending", "Pending"), + ("Cancelled", "Cancel"), + ], + default="Draft", + max_length=15, + ), + ), + ( + "details", + models.TextField(blank=True, null=True, verbose_name="Details"), + ), + ("due_date", models.DateField(blank=True, null=True)), + ( + "accounts", + models.ManyToManyField( + related_name="accounts_invoices", to="accounts.account" + ), + ), + ( + "assigned_to", + models.ManyToManyField( + related_name="invoice_assigned_to", to="common.profile" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "from_address", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="invoice_from_address", + to="common.address", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invoices", + to="common.org", + ), + ), + ( + "teams", + models.ManyToManyField( + related_name="invoices_teams", to="common.teams" + ), + ), + ( + "to_address", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="invoice_to_address", + to="common.address", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Invoice", + "verbose_name_plural": "Invoices", + "db_table": "invoice", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Product", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ("sku", models.CharField(blank=True, max_length=100, null=True)), + ( + "price", + models.DecimalField(decimal_places=2, default=0, max_digits=12), + ), + ("category", models.CharField(blank=True, max_length=100, null=True)), + ("is_active", models.BooleanField(default=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="products", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Product", + "verbose_name_plural": "Products", + "db_table": "product", + "ordering": ("name",), + "unique_together": {("sku", "org")}, + }, + ), + migrations.CreateModel( + name="InvoiceHistory", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "invoice_title", + models.CharField(max_length=50, verbose_name="Invoice Title"), + ), + ( + "invoice_number", + models.CharField(max_length=50, verbose_name="Invoice Number"), + ), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ("email", models.EmailField(max_length=254, verbose_name="Email")), + ("quantity", models.PositiveIntegerField(default=0)), + ( + "rate", + models.DecimalField(decimal_places=2, default=0, max_digits=12), + ), + ( + "total_amount", + models.DecimalField( + blank=True, decimal_places=2, max_digits=12, null=True + ), + ), + ( + "currency", + models.CharField( + blank=True, + choices=[ + ("AED", "AED, Dirham"), + ("AFN", "AFN, Afghani"), + ("ALL", "ALL, Lek"), + ("AMD", "AMD, Dram"), + ("ANG", "ANG, Guilder"), + ("AOA", "AOA, Kwanza"), + ("ARS", "ARS, Peso"), + ("AUD", "AUD, Dollar"), + ("AWG", "AWG, Guilder"), + ("AZN", "AZN, Manat"), + ("BAM", "BAM, Marka"), + ("BBD", "BBD, Dollar"), + ("BDT", "BDT, Taka"), + ("BGN", "BGN, Lev"), + ("BHD", "BHD, Dinar"), + ("BIF", "BIF, Franc"), + ("BMD", "BMD, Dollar"), + ("BND", "BND, Dollar"), + ("BOB", "BOB, Boliviano"), + ("BRL", "BRL, Real"), + ("BSD", "BSD, Dollar"), + ("BTN", "BTN, Ngultrum"), + ("BWP", "BWP, Pula"), + ("BYR", "BYR, Ruble"), + ("BZD", "BZD, Dollar"), + ("CAD", "CAD, Dollar"), + ("CDF", "CDF, Franc"), + ("CHF", "CHF, Franc"), + ("CLP", "CLP, Peso"), + ("CNY", "CNY, Yuan Renminbi"), + ("COP", "COP, Peso"), + ("CRC", "CRC, Colon"), + ("CUP", "CUP, Peso"), + ("CVE", "CVE, Escudo"), + ("CZK", "CZK, Koruna"), + ("DJF", "DJF, Franc"), + ("DKK", "DKK, Krone"), + ("DOP", "DOP, Peso"), + ("DZD", "DZD, Dinar"), + ("EGP", "EGP, Pound"), + ("ERN", "ERN, Nakfa"), + ("ETB", "ETB, Birr"), + ("EUR", "EUR, Euro"), + ("FJD", "FJD, Dollar"), + ("FKP", "FKP, Pound"), + ("GBP", "GBP, Pound"), + ("GEL", "GEL, Lari"), + ("GHS", "GHS, Cedi"), + ("GIP", "GIP, Pound"), + ("GMD", "GMD, Dalasi"), + ("GNF", "GNF, Franc"), + ("GTQ", "GTQ, Quetzal"), + ("GYD", "GYD, Dollar"), + ("HKD", "HKD, Dollar"), + ("HNL", "HNL, Lempira"), + ("HRK", "HRK, Kuna"), + ("HTG", "HTG, Gourde"), + ("HUF", "HUF, Forint"), + ("IDR", "IDR, Rupiah"), + ("ILS", "ILS, Shekel"), + ("INR", "INR, Rupee"), + ("IQD", "IQD, Dinar"), + ("IRR", "IRR, Rial"), + ("ISK", "ISK, Krona"), + ("JMD", "JMD, Dollar"), + ("JOD", "JOD, Dinar"), + ("JPY", "JPY, Yen"), + ("KES", "KES, Shilling"), + ("KGS", "KGS, Som"), + ("KHR", "KHR, Riels"), + ("KMF", "KMF, Franc"), + ("KPW", "KPW, Won"), + ("KRW", "KRW, Won"), + ("KWD", "KWD, Dinar"), + ("KYD", "KYD, Dollar"), + ("KZT", "KZT, Tenge"), + ("LAK", "LAK, Kip"), + ("LBP", "LBP, Pound"), + ("LKR", "LKR, Rupee"), + ("LRD", "LRD, Dollar"), + ("LSL", "LSL, Loti"), + ("LTL", "LTL, Litas"), + ("LVL", "LVL, Lat"), + ("LYD", "LYD, Dinar"), + ("MAD", "MAD, Dirham"), + ("MDL", "MDL, Leu"), + ("MGA", "MGA, Ariary"), + ("MKD", "MKD, Denar"), + ("MMK", "MMK, Kyat"), + ("MNT", "MNT, Tugrik"), + ("MOP", "MOP, Pataca"), + ("MRO", "MRO, Ouguiya"), + ("MUR", "MUR, Rupee"), + ("MVR", "MVR, Rufiyaa"), + ("MWK", "MWK, Kwacha"), + ("MXN", "MXN, Peso"), + ("MYR", "MYR, Ringgit"), + ("MZN", "MZN, Metical"), + ("NAD", "NAD, Dollar"), + ("NGN", "NGN, Naira"), + ("NIO", "NIO, Cordoba"), + ("NOK", "NOK, Krone"), + ("NPR", "NPR, Rupee"), + ("NZD", "NZD, Dollar"), + ("OMR", "OMR, Rial"), + ("PAB", "PAB, Balboa"), + ("PEN", "PEN, Sol"), + ("PGK", "PGK, Kina"), + ("PHP", "PHP, Peso"), + ("PKR", "PKR, Rupee"), + ("PLN", "PLN, Zloty"), + ("PYG", "PYG, Guarani"), + ("QAR", "QAR, Rial"), + ("RON", "RON, Leu"), + ("RSD", "RSD, Dinar"), + ("RUB", "RUB, Ruble"), + ("RWF", "RWF, Franc"), + ("SAR", "SAR, Rial"), + ("SBD", "SBD, Dollar"), + ("SCR", "SCR, Rupee"), + ("SDG", "SDG, Pound"), + ("SEK", "SEK, Krona"), + ("SGD", "SGD, Dollar"), + ("SHP", "SHP, Pound"), + ("SLL", "SLL, Leone"), + ("SOS", "SOS, Shilling"), + ("SRD", "SRD, Dollar"), + ("SSP", "SSP, Pound"), + ("STD", "STD, Dobra"), + ("SYP", "SYP, Pound"), + ("SZL", "SZL, Lilangeni"), + ("THB", "THB, Baht"), + ("TJS", "TJS, Somoni"), + ("TMT", "TMT, Manat"), + ("TND", "TND, Dinar"), + ("TOP", "TOP, Paanga"), + ("TRY", "TRY, Lira"), + ("TTD", "TTD, Dollar"), + ("TWD", "TWD, Dollar"), + ("TZS", "TZS, Shilling"), + ("UAH", "UAH, Hryvnia"), + ("UGX", "UGX, Shilling"), + ("USD", "$, Dollar"), + ("UYU", "UYU, Peso"), + ("UZS", "UZS, Som"), + ("VEF", "VEF, Bolivar"), + ("VND", "VND, Dong"), + ("VUV", "VUV, Vatu"), + ("WST", "WST, Tala"), + ("XAF", "XAF, Franc"), + ("XCD", "XCD, Dollar"), + ("XOF", "XOF, Franc"), + ("XPF", "XPF, Franc"), + ("YER", "YER, Rial"), + ("ZAR", "ZAR, Rand"), + ("ZMK", "ZMK, Kwacha"), + ("ZWL", "ZWL, Dollar"), + ], + max_length=3, + null=True, + ), + ), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, max_length=128, null=True, region=None + ), + ), + ( + "amount_due", + models.DecimalField( + blank=True, decimal_places=2, max_digits=12, null=True + ), + ), + ( + "amount_paid", + models.DecimalField( + blank=True, decimal_places=2, max_digits=12, null=True + ), + ), + ("is_email_sent", models.BooleanField(default=False)), + ( + "status", + models.CharField( + choices=[ + ("Draft", "Draft"), + ("Sent", "Sent"), + ("Paid", "Paid"), + ("Pending", "Pending"), + ("Cancelled", "Cancel"), + ], + default="Draft", + max_length=15, + ), + ), + ( + "details", + models.TextField(blank=True, null=True, verbose_name="Details"), + ), + ("due_date", models.DateField(blank=True, null=True)), + ( + "assigned_to", + models.ManyToManyField( + related_name="invoice_history_assigned_to", to="common.profile" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "from_address", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="invoice_history_from_address", + to="common.address", + ), + ), + ( + "invoice", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invoice_history", + to="invoices.invoice", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invoice_histories", + to="common.org", + ), + ), + ( + "to_address", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="invoice_history_to_address", + to="common.address", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="invoice_history_updated_by", + to="common.profile", + ), + ), + ], + options={ + "verbose_name": "InvoiceHistory", + "verbose_name_plural": "InvoiceHistories", + "db_table": "invoice_history", + "ordering": ("-created_at",), + "indexes": [ + models.Index( + fields=["org", "-created_at"], + name="invoice_his_org_id_45400e_idx", + ) + ], + }, + ), + migrations.CreateModel( + name="InvoiceLineItem", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("description", models.CharField(max_length=500)), + ( + "quantity", + models.DecimalField(decimal_places=2, default=1, max_digits=10), + ), + ( + "unit_price", + models.DecimalField(decimal_places=2, default=0, max_digits=12), + ), + ( + "total", + models.DecimalField(decimal_places=2, default=0, max_digits=12), + ), + ("order", models.PositiveIntegerField(default=0)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "invoice", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="line_items", + to="invoices.invoice", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invoice_line_items", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "product", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="invoice_line_items", + to="invoices.product", + ), + ), + ], + options={ + "verbose_name": "Invoice Line Item", + "verbose_name_plural": "Invoice Line Items", + "db_table": "invoice_line_item", + "ordering": ("order",), + "indexes": [ + models.Index( + fields=["org", "order"], name="invoice_lin_org_id_474821_idx" + ) + ], + }, + ), + ] diff --git a/emails/migrations/__init__.py b/backend/invoices/migrations/__init__.py similarity index 100% rename from emails/migrations/__init__.py rename to backend/invoices/migrations/__init__.py diff --git a/invoices/models.py b/backend/invoices/models.py similarity index 68% rename from invoices/models.py rename to backend/invoices/models.py index 68fc8ff0..25d9649a 100644 --- a/invoices/models.py +++ b/backend/invoices/models.py @@ -6,10 +6,9 @@ from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from accounts.models import Account -from common.models import Address, Org, User from common.base import BaseModel +from common.models import Address, Org, Profile, Teams, User from common.utils import CURRENCY_CODES -from teams.models import Teams class Invoice(BaseModel): @@ -36,7 +35,7 @@ class Invoice(BaseModel): ) name = models.CharField(_("Name"), max_length=100) email = models.EmailField(_("Email")) - assigned_to = models.ManyToManyField(User, related_name="invoice_assigned_to") + assigned_to = models.ManyToManyField(Profile, related_name="invoice_assigned_to") # quantity is the number of hours worked quantity = models.PositiveIntegerField(default=0) # rate is the rate charged @@ -50,9 +49,6 @@ class Invoice(BaseModel): max_length=3, choices=CURRENCY_CODES, blank=True, null=True ) phone = PhoneNumberField(null=True, blank=True) - created_by = models.ForeignKey( - User, related_name="invoice_created_by", on_delete=models.SET_NULL, null=True - ) amount_due = models.DecimalField( blank=True, null=True, max_digits=12, decimal_places=2 @@ -67,8 +63,7 @@ class Invoice(BaseModel): accounts = models.ManyToManyField(Account, related_name="accounts_invoices") teams = models.ManyToManyField(Teams, related_name="invoices_teams") - org = models.ForeignKey(Org, on_delete=models.SET_NULL, null=True, blank=True) - tax = models.DecimalField(blank=True, null=True, max_digits=12, decimal_places=2) + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="invoices") class Meta: verbose_name = "Invoice" @@ -136,26 +131,27 @@ class Invoice(BaseModel): @property def get_team_users(self): team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return User.objects.filter(id__in=team_user_ids) + return Profile.objects.filter(id__in=team_user_ids) @property def get_team_and_assigned_users(self): team_user_ids = list(self.teams.values_list("users__id", flat=True)) assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) user_ids = team_user_ids + assigned_user_ids - return User.objects.filter(id__in=user_ids) + return Profile.objects.filter(id__in=user_ids) @property def get_assigned_users_not_in_teams(self): team_user_ids = list(self.teams.values_list("users__id", flat=True)) assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) user_ids = set(assigned_user_ids) - set(team_user_ids) - return User.objects.filter(id__in=list(user_ids)) + return Profile.objects.filter(id__in=list(user_ids)) class InvoiceHistory(BaseModel): """Model definition for InvoiceHistory. - This model is used to track/keep a record of the updates made to original invoice object.""" + This model is used to track/keep a record of the updates made to original invoice object. + """ INVOICE_STATUS = ( ("Draft", "Draft"), @@ -184,7 +180,7 @@ class InvoiceHistory(BaseModel): name = models.CharField(_("Name"), max_length=100) email = models.EmailField(_("Email")) assigned_to = models.ManyToManyField( - User, related_name="invoice_history_assigned_to" + Profile, related_name="invoice_history_assigned_to" ) # quantity is the number of hours worked quantity = models.PositiveIntegerField(default=0) @@ -198,12 +194,9 @@ class InvoiceHistory(BaseModel): max_length=3, choices=CURRENCY_CODES, blank=True, null=True ) phone = PhoneNumberField(null=True, blank=True) - # created_by = models.ForeignKey( - # User, related_name='invoice_history_created_by', - # on_delete=models.SET_NULL, null=True) updated_by = models.ForeignKey( - User, - related_name="invoice_history_created_by", + Profile, + related_name="invoice_history_updated_by", on_delete=models.SET_NULL, null=True, ) @@ -219,17 +212,30 @@ class InvoiceHistory(BaseModel): # details or description here stores the fields changed in the original invoice object details = models.TextField(_("Details"), null=True, blank=True) due_date = models.DateField(blank=True, null=True) + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="invoice_histories", + ) class Meta: verbose_name = "InvoiceHistory" verbose_name_plural = "InvoiceHistories" db_table = "invoice_history" ordering = ("-created_at",) + indexes = [ + models.Index(fields=["org", "-created_at"]), + ] def __str__(self): """Unicode representation of Invoice.""" return self.invoice_number + def save(self, *args, **kwargs): + if not self.org_id and self.invoice_id: + self.org_id = self.invoice.org_id + super().save(*args, **kwargs) + def formatted_total_amount(self): return self.currency + " " + str(self.total_amount) @@ -242,3 +248,84 @@ class InvoiceHistory(BaseModel): @property def created_on_arrow(self): return arrow.get(self.created_at).humanize() + + +# Phase 3: Enhanced Quotes with Line Items + + +class Product(BaseModel): + """Product Catalog for Line Items""" + + name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + sku = models.CharField(max_length=100, blank=True, null=True) + price = models.DecimalField(max_digits=12, decimal_places=2, default=0) + category = models.CharField(max_length=100, blank=True, null=True) + is_active = models.BooleanField(default=True) + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="products") + + class Meta: + verbose_name = "Product" + verbose_name_plural = "Products" + db_table = "product" + ordering = ("name",) + unique_together = [["sku", "org"]] + + def __str__(self): + return self.name + + +class InvoiceLineItem(BaseModel): + """Line Item for Invoices""" + + invoice = models.ForeignKey( + Invoice, on_delete=models.CASCADE, related_name="line_items" + ) + product = models.ForeignKey( + Product, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="invoice_line_items", + ) + description = models.CharField(max_length=500) + quantity = models.DecimalField(max_digits=10, decimal_places=2, default=1) + unit_price = models.DecimalField(max_digits=12, decimal_places=2, default=0) + total = models.DecimalField(max_digits=12, decimal_places=2, default=0) + order = models.PositiveIntegerField(default=0) + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="invoice_line_items", + ) + + class Meta: + verbose_name = "Invoice Line Item" + verbose_name_plural = "Invoice Line Items" + db_table = "invoice_line_item" + ordering = ("order",) + indexes = [ + models.Index(fields=["org", "order"]), + ] + + def __str__(self): + return f"{self.invoice.invoice_number} - {self.description}" + + def save(self, *args, **kwargs): + # Auto-calculate total + self.total = self.quantity * self.unit_price + if not self.org_id and self.invoice_id: + self.org_id = self.invoice.org_id + super().save(*args, **kwargs) + + @property + def formatted_unit_price(self): + if self.invoice and self.invoice.currency: + return f"{self.invoice.currency} {self.unit_price}" + return str(self.unit_price) + + @property + def formatted_total(self): + if self.invoice and self.invoice.currency: + return f"{self.invoice.currency} {self.total}" + return str(self.total) diff --git a/invoices/serializer.py b/backend/invoices/serializer.py similarity index 61% rename from invoices/serializer.py rename to backend/invoices/serializer.py index 856e23cb..e185f63a 100644 --- a/invoices/serializer.py +++ b/backend/invoices/serializer.py @@ -3,19 +3,20 @@ from rest_framework import serializers from common.serializer import ( BillingAddressSerializer, OrganizationSerializer, + ProfileSerializer, + TeamsSerializer, UserSerializer, ) from invoices.models import Invoice, InvoiceHistory -from teams.serializer import TeamsSerializer class InvoiceSerailizer(serializers.ModelSerializer): from_address = BillingAddressSerializer() to_address = BillingAddressSerializer() - created_by = UserSerializer() + created_by = ProfileSerializer() org = OrganizationSerializer() teams = TeamsSerializer(read_only=True, many=True) - assigned_to = UserSerializer(read_only=True, many=True) + assigned_to = ProfileSerializer(read_only=True, many=True) class Meta: model = Invoice @@ -48,7 +49,7 @@ class InvoiceSerailizer(serializers.ModelSerializer): class InvoiceHistorySerializer(serializers.ModelSerializer): - updated_by = UserSerializer() + updated_by = ProfileSerializer() class Meta: model = InvoiceHistory @@ -127,12 +128,14 @@ class InvoiceCreateSerializer(serializers.ModelSerializer): "org", ) + class InvoiceSwaggerSerailizer(serializers.ModelSerializer): from_address = BillingAddressSerializer() to_address = BillingAddressSerializer() - assigned_to = UserSerializer(read_only=True, many=True) + assigned_to = ProfileSerializer(read_only=True, many=True) quality_hours = serializers.CharField() + class Meta: model = Invoice fields = ( @@ -153,7 +156,88 @@ class InvoiceSwaggerSerailizer(serializers.ModelSerializer): "assigned_to", "currency", "accounts", - "quality_hours" + "quality_hours", ) +# Phase 3: Product and Line Item Serializers + +from invoices.models import InvoiceLineItem, Product + + +class ProductSerializer(serializers.ModelSerializer): + """Serializer for Product""" + + class Meta: + model = Product + fields = "__all__" + read_only_fields = ( + "id", + "created_at", + "updated_at", + "created_by", + "updated_by", + "org", + ) + + +class InvoiceLineItemSerializer(serializers.ModelSerializer): + """Serializer for Invoice Line Items""" + + product_name = serializers.CharField(source="product.name", read_only=True) + formatted_unit_price = serializers.CharField(read_only=True) + formatted_total = serializers.CharField(read_only=True) + + class Meta: + model = InvoiceLineItem + fields = "__all__" + read_only_fields = ( + "id", + "created_at", + "updated_at", + "created_by", + "updated_by", + "total", + ) + + +class InvoiceWithLineItemsSerializer(serializers.ModelSerializer): + """Enhanced Invoice Serializer with Line Items""" + + line_items = InvoiceLineItemSerializer(many=True, read_only=True) + created_by = ProfileSerializer(read_only=True) + org = OrganizationSerializer(read_only=True) + teams = TeamsSerializer(read_only=True, many=True) + assigned_to = ProfileSerializer(read_only=True, many=True) + line_items_total = serializers.DecimalField( + max_digits=12, decimal_places=2, read_only=True + ) + line_items_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Invoice + fields = ( + "id", + "invoice_title", + "invoice_number", + "status", + "due_date", + "name", + "email", + "phone", + "created_at", + "created_by", + "currency", + "tax", + "total_amount", + "amount_due", + "amount_paid", + "is_email_sent", + "details", + "teams", + "assigned_to", + "org", + "line_items", + "line_items_total", + "line_items_count", + ) diff --git a/backend/invoices/serializers.py b/backend/invoices/serializers.py new file mode 100644 index 00000000..3c5c9bf5 --- /dev/null +++ b/backend/invoices/serializers.py @@ -0,0 +1,79 @@ +from rest_framework import serializers + +from accounts.serializer import AccountSerializer +from common.serializer import ProfileSerializer, TeamsSerializer, UserSerializer +from invoices.models import Invoice, InvoiceHistory + + +class InvoiceSerializer(serializers.ModelSerializer): + """Serializer for Invoice model""" + + assigned_to = ProfileSerializer(many=True, read_only=True) + created_by = UserSerializer(read_only=True) + accounts = AccountSerializer(many=True, read_only=True) + teams = TeamsSerializer(many=True, read_only=True) + + # Write fields + assigned_to_ids = serializers.ListField( + child=serializers.UUIDField(), write_only=True, required=False + ) + account_ids = serializers.ListField( + child=serializers.UUIDField(), write_only=True, required=False + ) + team_ids = serializers.ListField( + child=serializers.UUIDField(), write_only=True, required=False + ) + + class Meta: + model = Invoice + fields = "__all__" + read_only_fields = ( + "id", + "created_at", + "updated_at", + "created_by", + "invoice_number", + ) + + +class InvoiceListSerializer(serializers.ModelSerializer): + """Simplified serializer for invoice lists""" + + assigned_to_count = serializers.SerializerMethodField() + accounts_count = serializers.SerializerMethodField() + + class Meta: + model = Invoice + fields = [ + "id", + "invoice_title", + "invoice_number", + "name", + "email", + "total_amount", + "amount_due", + "amount_paid", + "status", + "due_date", + "created_at", + "updated_at", + "assigned_to_count", + "accounts_count", + ] + + def get_assigned_to_count(self, obj): + return obj.assigned_to.count() + + def get_accounts_count(self, obj): + return obj.accounts.count() + + +class InvoiceHistorySerializer(serializers.ModelSerializer): + """Serializer for Invoice History""" + + updated_by = UserSerializer(read_only=True) + + class Meta: + model = InvoiceHistory + fields = "__all__" + read_only_fields = ("id", "created_at", "updated_at") diff --git a/backend/invoices/swagger_params.py b/backend/invoices/swagger_params.py new file mode 100644 index 00000000..53e3b2cd --- /dev/null +++ b/backend/invoices/swagger_params.py @@ -0,0 +1,65 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +company_params_in_header = OpenApiParameter( + "org", OpenApiTypes.STR, OpenApiParameter.HEADER, required=True +) + +organization_params = [company_params_in_header] + +invoice_list_get_params = organization_params + [ + OpenApiParameter( + "invoice_title_or_number", OpenApiTypes.STR, OpenApiParameter.QUERY + ), + OpenApiParameter("created_by", OpenApiTypes.INT, OpenApiParameter.QUERY), + OpenApiParameter("assigned_users", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("status", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("total_amount", OpenApiTypes.STR, OpenApiParameter.QUERY), +] + +invoice_detail_post_params = organization_params + [ + OpenApiParameter("invoice_attachment", OpenApiTypes.BINARY, OpenApiParameter.QUERY), + OpenApiParameter("comment", OpenApiTypes.STR, OpenApiParameter.QUERY), +] + +invoice_delete_params = organization_params + +invoice_create_post_params = organization_params + [ + OpenApiParameter( + "invoice_title", OpenApiTypes.STR, OpenApiParameter.QUERY, required=True + ), + OpenApiParameter("status", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("from_address_line", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("from_street", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("from_city", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("from_state", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("from_postcode", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("from_country", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("to_address_line", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("to_street", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("to_city", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("to_state", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("to_postcode", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("to_country", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("name", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("email", OpenApiTypes.STR, OpenApiParameter.QUERY, required=True), + OpenApiParameter("phone", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter( + "due_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, examples=["2021-02-28"] + ), + OpenApiParameter( + "currency", OpenApiTypes.STR, OpenApiParameter.QUERY, required=True + ), + OpenApiParameter("teams", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("assigned_to", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("accounts", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("quality_hours", OpenApiTypes.INT, OpenApiParameter.QUERY), + OpenApiParameter("rate", OpenApiTypes.INT, OpenApiParameter.QUERY), + OpenApiParameter("tax", OpenApiTypes.INT, OpenApiParameter.QUERY), + OpenApiParameter("total_amount", OpenApiTypes.INT, OpenApiParameter.QUERY), + OpenApiParameter("details", OpenApiTypes.STR, OpenApiParameter.QUERY), +] + +invoice_comment_edit_params = organization_params + [ + OpenApiParameter("comment", OpenApiTypes.STR, OpenApiParameter.QUERY), +] diff --git a/invoices/tasks.py b/backend/invoices/tasks.py similarity index 100% rename from invoices/tasks.py rename to backend/invoices/tasks.py diff --git a/invoices/templates/assigned_to_email_template.html b/backend/invoices/templates/assigned_to_email_template.html similarity index 100% rename from invoices/templates/assigned_to_email_template.html rename to backend/invoices/templates/assigned_to_email_template.html diff --git a/invoices/templates/invoice_cancelled.html b/backend/invoices/templates/invoice_cancelled.html similarity index 100% rename from invoices/templates/invoice_cancelled.html rename to backend/invoices/templates/invoice_cancelled.html diff --git a/invoices/templates/invoice_detail_email.html b/backend/invoices/templates/invoice_detail_email.html similarity index 100% rename from invoices/templates/invoice_detail_email.html rename to backend/invoices/templates/invoice_detail_email.html diff --git a/invoices/tests.py b/backend/invoices/tests.py similarity index 99% rename from invoices/tests.py rename to backend/invoices/tests.py index ee970f1a..90ede27a 100644 --- a/invoices/tests.py +++ b/backend/invoices/tests.py @@ -5,9 +5,8 @@ from django.test import TestCase from django.urls import reverse from accounts.models import Account -from common.models import Address, Attachments, Comment, Company, User +from common.models import Address, Attachments, Comment, Company, Teams, User from invoices.models import Invoice, InvoiceHistory -from teams.models import Teams class InvoiceCreateTest(object): diff --git a/invoices/tests_celery_tasks.py b/backend/invoices/tests_celery_tasks.py similarity index 100% rename from invoices/tests_celery_tasks.py rename to backend/invoices/tests_celery_tasks.py diff --git a/invoices/urls.py b/backend/invoices/urls.py similarity index 100% rename from invoices/urls.py rename to backend/invoices/urls.py diff --git a/backend/invoices/views.py b/backend/invoices/views.py new file mode 100644 index 00000000..979732b9 --- /dev/null +++ b/backend/invoices/views.py @@ -0,0 +1,2 @@ +# Invoice views removed - invoice functionality is now handled via API +# Email templates for invoices are still used by Celery tasks diff --git a/events/__init__.py b/backend/leads/__init__.py similarity index 100% rename from events/__init__.py rename to backend/leads/__init__.py diff --git a/leads/admin.py b/backend/leads/admin.py similarity index 100% rename from leads/admin.py rename to backend/leads/admin.py diff --git a/leads/apps.py b/backend/leads/apps.py similarity index 100% rename from leads/apps.py rename to backend/leads/apps.py diff --git a/leads/forms.py b/backend/leads/forms.py similarity index 100% rename from leads/forms.py rename to backend/leads/forms.py diff --git a/backend/leads/migrations/0001_initial.py b/backend/leads/migrations/0001_initial.py new file mode 100644 index 00000000..9361dc2b --- /dev/null +++ b/backend/leads/migrations/0001_initial.py @@ -0,0 +1,632 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("common", "0001_initial"), + ("contacts", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Company", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(blank=True, max_length=100, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="companies", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Company", + "verbose_name_plural": "Companies", + "db_table": "company", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="Lead", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.CharField(max_length=64, verbose_name="Title")), + ( + "first_name", + models.CharField( + max_length=255, null=True, verbose_name="First name" + ), + ), + ( + "last_name", + models.CharField( + max_length=255, null=True, verbose_name="Last name" + ), + ), + ("email", models.EmailField(blank=True, max_length=254, null=True)), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, max_length=128, null=True, region=None + ), + ), + ( + "contact_title", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Job Title" + ), + ), + ( + "website", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Website" + ), + ), + ( + "linkedin_url", + models.URLField( + blank=True, + max_length=500, + null=True, + verbose_name="LinkedIn URL", + ), + ), + ( + "status", + models.CharField( + blank=True, + choices=[ + ("assigned", "Assigned"), + ("in process", "In Process"), + ("converted", "Converted"), + ("recycled", "Recycled"), + ("closed", "Closed"), + ], + max_length=255, + null=True, + verbose_name="Status", + ), + ), + ( + "source", + models.CharField( + blank=True, + choices=[ + ("call", "Call"), + ("email", "Email"), + ("existing customer", "Existing Customer"), + ("partner", "Partner"), + ("public relations", "Public Relations"), + ("compaign", "Campaign"), + ("other", "Other"), + ], + max_length=255, + null=True, + verbose_name="Source", + ), + ), + ( + "industry", + models.CharField( + blank=True, + choices=[ + ("ADVERTISING", "ADVERTISING"), + ("AGRICULTURE", "AGRICULTURE"), + ("APPAREL & ACCESSORIES", "APPAREL & ACCESSORIES"), + ("AUTOMOTIVE", "AUTOMOTIVE"), + ("BANKING", "BANKING"), + ("BIOTECHNOLOGY", "BIOTECHNOLOGY"), + ( + "BUILDING MATERIALS & EQUIPMENT", + "BUILDING MATERIALS & EQUIPMENT", + ), + ("CHEMICAL", "CHEMICAL"), + ("COMPUTER", "COMPUTER"), + ("EDUCATION", "EDUCATION"), + ("ELECTRONICS", "ELECTRONICS"), + ("ENERGY", "ENERGY"), + ("ENTERTAINMENT & LEISURE", "ENTERTAINMENT & LEISURE"), + ("FINANCE", "FINANCE"), + ("FOOD & BEVERAGE", "FOOD & BEVERAGE"), + ("GROCERY", "GROCERY"), + ("HEALTHCARE", "HEALTHCARE"), + ("INSURANCE", "INSURANCE"), + ("LEGAL", "LEGAL"), + ("MANUFACTURING", "MANUFACTURING"), + ("PUBLISHING", "PUBLISHING"), + ("REAL ESTATE", "REAL ESTATE"), + ("SERVICE", "SERVICE"), + ("SOFTWARE", "SOFTWARE"), + ("SPORTS", "SPORTS"), + ("TECHNOLOGY", "TECHNOLOGY"), + ("TELECOMMUNICATIONS", "TELECOMMUNICATIONS"), + ("TELEVISION", "TELEVISION"), + ("TRANSPORTATION", "TRANSPORTATION"), + ("VENTURE CAPITAL", "VENTURE CAPITAL"), + ], + max_length=255, + null=True, + verbose_name="Industry", + ), + ), + ( + "rating", + models.CharField( + blank=True, + choices=[("HOT", "Hot"), ("WARM", "Warm"), ("COLD", "Cold")], + max_length=10, + null=True, + verbose_name="Rating", + ), + ), + ( + "opportunity_amount", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=12, + null=True, + verbose_name="Deal Value", + ), + ), + ( + "probability", + models.IntegerField( + blank=True, + default=0, + null=True, + verbose_name="Win Probability %", + ), + ), + ( + "close_date", + models.DateField( + default=None, null=True, verbose_name="Expected Close Date" + ), + ), + ( + "address_line", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="Address" + ), + ), + ( + "city", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="City" + ), + ), + ( + "state", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="State" + ), + ), + ( + "postcode", + models.CharField( + blank=True, max_length=64, null=True, verbose_name="Postal Code" + ), + ), + ( + "country", + models.CharField( + blank=True, + choices=[ + ("GB", "United Kingdom"), + ("AF", "Afghanistan"), + ("AX", "Aland Islands"), + ("AL", "Albania"), + ("DZ", "Algeria"), + ("AS", "American Samoa"), + ("AD", "Andorra"), + ("AO", "Angola"), + ("AI", "Anguilla"), + ("AQ", "Antarctica"), + ("AG", "Antigua and Barbuda"), + ("AR", "Argentina"), + ("AM", "Armenia"), + ("AW", "Aruba"), + ("AU", "Australia"), + ("AT", "Austria"), + ("AZ", "Azerbaijan"), + ("BS", "Bahamas"), + ("BH", "Bahrain"), + ("BD", "Bangladesh"), + ("BB", "Barbados"), + ("BY", "Belarus"), + ("BE", "Belgium"), + ("BZ", "Belize"), + ("BJ", "Benin"), + ("BM", "Bermuda"), + ("BT", "Bhutan"), + ("BO", "Bolivia"), + ("BA", "Bosnia and Herzegovina"), + ("BW", "Botswana"), + ("BV", "Bouvet Island"), + ("BR", "Brazil"), + ("IO", "British Indian Ocean Territory"), + ("BN", "Brunei Darussalam"), + ("BG", "Bulgaria"), + ("BF", "Burkina Faso"), + ("BI", "Burundi"), + ("KH", "Cambodia"), + ("CM", "Cameroon"), + ("CA", "Canada"), + ("CV", "Cape Verde"), + ("KY", "Cayman Islands"), + ("CF", "Central African Republic"), + ("TD", "Chad"), + ("CL", "Chile"), + ("CN", "China"), + ("CX", "Christmas Island"), + ("CC", "Cocos (Keeling) Islands"), + ("CO", "Colombia"), + ("KM", "Comoros"), + ("CG", "Congo"), + ("CD", "Congo, The Democratic Republic of the"), + ("CK", "Cook Islands"), + ("CR", "Costa Rica"), + ("CI", "Cote d'Ivoire"), + ("HR", "Croatia"), + ("CU", "Cuba"), + ("CY", "Cyprus"), + ("CZ", "Czech Republic"), + ("DK", "Denmark"), + ("DJ", "Djibouti"), + ("DM", "Dominica"), + ("DO", "Dominican Republic"), + ("EC", "Ecuador"), + ("EG", "Egypt"), + ("SV", "El Salvador"), + ("GQ", "Equatorial Guinea"), + ("ER", "Eritrea"), + ("EE", "Estonia"), + ("ET", "Ethiopia"), + ("FK", "Falkland Islands (Malvinas)"), + ("FO", "Faroe Islands"), + ("FJ", "Fiji"), + ("FI", "Finland"), + ("FR", "France"), + ("GF", "French Guiana"), + ("PF", "French Polynesia"), + ("TF", "French Southern Territories"), + ("GA", "Gabon"), + ("GM", "Gambia"), + ("GE", "Georgia"), + ("DE", "Germany"), + ("GH", "Ghana"), + ("GI", "Gibraltar"), + ("GR", "Greece"), + ("GL", "Greenland"), + ("GD", "Grenada"), + ("GP", "Guadeloupe"), + ("GU", "Guam"), + ("GT", "Guatemala"), + ("GG", "Guernsey"), + ("GN", "Guinea"), + ("GW", "Guinea-Bissau"), + ("GY", "Guyana"), + ("HT", "Haiti"), + ("HM", "Heard Island and McDonald Islands"), + ("VA", "Holy See (Vatican City State)"), + ("HN", "Honduras"), + ("HK", "Hong Kong"), + ("HU", "Hungary"), + ("IS", "Iceland"), + ("IN", "India"), + ("ID", "Indonesia"), + ("IR", "Iran, Islamic Republic of"), + ("IQ", "Iraq"), + ("IE", "Ireland"), + ("IM", "Isle of Man"), + ("IL", "Israel"), + ("IT", "Italy"), + ("JM", "Jamaica"), + ("JP", "Japan"), + ("JE", "Jersey"), + ("JO", "Jordan"), + ("KZ", "Kazakhstan"), + ("KE", "Kenya"), + ("KI", "Kiribati"), + ("KP", "Korea, Democratic People's Republic of"), + ("KR", "Korea, Republic of"), + ("KW", "Kuwait"), + ("KG", "Kyrgyzstan"), + ("LA", "Lao People's Democratic Republic"), + ("LV", "Latvia"), + ("LB", "Lebanon"), + ("LS", "Lesotho"), + ("LR", "Liberia"), + ("LY", "Libyan Arab Jamahiriya"), + ("LI", "Liechtenstein"), + ("LT", "Lithuania"), + ("LU", "Luxembourg"), + ("MO", "Macao"), + ("MK", "Macedonia, The Former Yugoslav Republic of"), + ("MG", "Madagascar"), + ("MW", "Malawi"), + ("MY", "Malaysia"), + ("MV", "Maldives"), + ("ML", "Mali"), + ("MT", "Malta"), + ("MH", "Marshall Islands"), + ("MQ", "Martinique"), + ("MR", "Mauritania"), + ("MU", "Mauritius"), + ("YT", "Mayotte"), + ("MX", "Mexico"), + ("FM", "Micronesia, Federated States of"), + ("MD", "Moldova"), + ("MC", "Monaco"), + ("MN", "Mongolia"), + ("ME", "Montenegro"), + ("MS", "Montserrat"), + ("MA", "Morocco"), + ("MZ", "Mozambique"), + ("MM", "Myanmar"), + ("NA", "Namibia"), + ("NR", "Nauru"), + ("NP", "Nepal"), + ("NL", "Netherlands"), + ("AN", "Netherlands Antilles"), + ("NC", "New Caledonia"), + ("NZ", "New Zealand"), + ("NI", "Nicaragua"), + ("NE", "Niger"), + ("NG", "Nigeria"), + ("NU", "Niue"), + ("NF", "Norfolk Island"), + ("MP", "Northern Mariana Islands"), + ("NO", "Norway"), + ("OM", "Oman"), + ("PK", "Pakistan"), + ("PW", "Palau"), + ("PS", "Palestinian Territory, Occupied"), + ("PA", "Panama"), + ("PG", "Papua New Guinea"), + ("PY", "Paraguay"), + ("PE", "Peru"), + ("PH", "Philippines"), + ("PN", "Pitcairn"), + ("PL", "Poland"), + ("PT", "Portugal"), + ("PR", "Puerto Rico"), + ("QA", "Qatar"), + ("RE", "Reunion"), + ("RO", "Romania"), + ("RU", "Russian Federation"), + ("RW", "Rwanda"), + ("BL", "Saint Barthelemy"), + ("SH", "Saint Helena"), + ("KN", "Saint Kitts and Nevis"), + ("LC", "Saint Lucia"), + ("MF", "Saint Martin"), + ("PM", "Saint Pierre and Miquelon"), + ("VC", "Saint Vincent and the Grenadines"), + ("WS", "Samoa"), + ("SM", "San Marino"), + ("ST", "Sao Tome and Principe"), + ("SA", "Saudi Arabia"), + ("SN", "Senegal"), + ("RS", "Serbia"), + ("SC", "Seychelles"), + ("SL", "Sierra Leone"), + ("SG", "Singapore"), + ("SK", "Slovakia"), + ("SI", "Slovenia"), + ("SB", "Solomon Islands"), + ("SO", "Somalia"), + ("ZA", "South Africa"), + ("GS", "South Georgia and the South Sandwich Islands"), + ("ES", "Spain"), + ("LK", "Sri Lanka"), + ("SD", "Sudan"), + ("SR", "Suriname"), + ("SJ", "Svalbard and Jan Mayen"), + ("SZ", "Swaziland"), + ("SE", "Sweden"), + ("CH", "Switzerland"), + ("SY", "Syrian Arab Republic"), + ("TW", "Taiwan, Province of China"), + ("TJ", "Tajikistan"), + ("TZ", "Tanzania, United Republic of"), + ("TH", "Thailand"), + ("TL", "Timor-Leste"), + ("TG", "Togo"), + ("TK", "Tokelau"), + ("TO", "Tonga"), + ("TT", "Trinidad and Tobago"), + ("TN", "Tunisia"), + ("TR", "Turkey"), + ("TM", "Turkmenistan"), + ("TC", "Turks and Caicos Islands"), + ("TV", "Tuvalu"), + ("UG", "Uganda"), + ("UA", "Ukraine"), + ("AE", "United Arab Emirates"), + ("US", "United States"), + ("UM", "United States Minor Outlying Islands"), + ("UY", "Uruguay"), + ("UZ", "Uzbekistan"), + ("VU", "Vanuatu"), + ("VE", "Venezuela"), + ("VN", "Viet Nam"), + ("VG", "Virgin Islands, British"), + ("VI", "Virgin Islands, U.S."), + ("WF", "Wallis and Futuna"), + ("EH", "Western Sahara"), + ("YE", "Yemen"), + ("ZM", "Zambia"), + ("ZW", "Zimbabwe"), + ], + max_length=3, + null=True, + verbose_name="Country", + ), + ), + ( + "last_contacted", + models.DateField( + blank=True, null=True, verbose_name="Last Contacted" + ), + ), + ( + "next_follow_up", + models.DateField( + blank=True, null=True, verbose_name="Next Follow-up" + ), + ), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Notes"), + ), + ("is_active", models.BooleanField(default=True)), + ("created_from_site", models.BooleanField(default=False)), + ( + "assigned_to", + models.ManyToManyField( + related_name="lead_assigned_users", to="common.profile" + ), + ), + ( + "company", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="lead_company", + to="leads.company", + ), + ), + ( + "contacts", + models.ManyToManyField( + related_name="lead_contacts", to="contacts.contact" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="leads", + to="common.org", + ), + ), + ("tags", models.ManyToManyField(blank=True, to="common.tags")), + ( + "teams", + models.ManyToManyField( + related_name="lead_teams", to="common.teams" + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Lead", + "verbose_name_plural": "Leads", + "db_table": "lead", + "ordering": ("-created_at",), + "indexes": [ + models.Index(fields=["status"], name="lead_status_e8cfda_idx"), + models.Index(fields=["source"], name="lead_source_b6a91c_idx"), + models.Index( + fields=["org", "-created_at"], name="lead_org_id_efd8ea_idx" + ), + ], + }, + ), + ] diff --git a/events/migrations/__init__.py b/backend/leads/migrations/__init__.py similarity index 100% rename from events/migrations/__init__.py rename to backend/leads/migrations/__init__.py diff --git a/leads/models.py b/backend/leads/models.py similarity index 56% rename from leads/models.py rename to backend/leads/models.py index 7294d5c4..725bf314 100644 --- a/leads/models.py +++ b/backend/leads/models.py @@ -4,9 +4,8 @@ from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy from phonenumber_field.modelfields import PhoneNumberField -from accounts.models import Tags -from common.models import Org, Profile -from common.base import BaseModel +from common.base import AssignableMixin, BaseModel +from common.models import Org, Profile, Tags, Teams from common.utils import ( COUNTRIES, INDCHOICES, @@ -15,12 +14,11 @@ from common.utils import ( return_complete_address, ) from contacts.models import Contact -from teams.models import Teams class Company(BaseModel): name = models.CharField(max_length=100, blank=True, null=True) - org = models.ForeignKey(Org, on_delete=models.SET_NULL, null=True, blank=True) + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="companies") class Meta: verbose_name = "Company" @@ -31,7 +29,14 @@ class Company(BaseModel): def __str__(self): return f"{self.name}" -class Lead(BaseModel): + +class Lead(AssignableMixin, BaseModel): + """ + Lead model for CRM - Streamlined for modern sales workflow + Based on Twenty CRM and Salesforce patterns + """ + + # Core Lead Information title = models.CharField( pgettext_lazy("Treatment Pronouns for the customer", "Title"), max_length=64 ) @@ -39,36 +44,63 @@ class Lead(BaseModel): last_name = models.CharField(_("Last name"), null=True, max_length=255) email = models.EmailField(null=True, blank=True) phone = PhoneNumberField(null=True, blank=True) + contact_title = models.CharField( + _("Job Title"), max_length=255, blank=True, null=True + ) + website = models.CharField(_("Website"), max_length=255, blank=True, null=True) + linkedin_url = models.URLField( + _("LinkedIn URL"), max_length=500, blank=True, null=True + ) + + # Sales Pipeline status = models.CharField( - _("Status of Lead"), max_length=255, blank=True, null=True, choices=LEAD_STATUS + _("Status"), max_length=255, blank=True, null=True, choices=LEAD_STATUS ) source = models.CharField( - _("Source of Lead"), max_length=255, blank=True, null=True, choices=LEAD_SOURCE + _("Source"), max_length=255, blank=True, null=True, choices=LEAD_SOURCE ) + industry = models.CharField( + _("Industry"), max_length=255, choices=INDCHOICES, blank=True, null=True + ) + rating = models.CharField( + _("Rating"), + max_length=10, + blank=True, + null=True, + choices=[("HOT", "Hot"), ("WARM", "Warm"), ("COLD", "Cold")], + ) + opportunity_amount = models.DecimalField( + _("Deal Value"), decimal_places=2, max_digits=12, blank=True, null=True + ) + probability = models.IntegerField( + _("Win Probability %"), default=0, blank=True, null=True + ) + close_date = models.DateField(_("Expected Close Date"), default=None, null=True) + + # Address address_line = models.CharField(_("Address"), max_length=255, blank=True, null=True) - street = models.CharField(_("Street"), max_length=55, blank=True, null=True) city = models.CharField(_("City"), max_length=255, blank=True, null=True) state = models.CharField(_("State"), max_length=255, blank=True, null=True) - postcode = models.CharField( - _("Post/Zip-code"), max_length=64, blank=True, null=True + postcode = models.CharField(_("Postal Code"), max_length=64, blank=True, null=True) + country = models.CharField( + _("Country"), max_length=3, choices=COUNTRIES, blank=True, null=True ) - country = models.CharField(max_length=3, choices=COUNTRIES, blank=True, null=True) - website = models.CharField(_("Website"), max_length=255, blank=True, null=True) - description = models.TextField(blank=True, null=True) + + # Assignment assigned_to = models.ManyToManyField(Profile, related_name="lead_assigned_users") - account_name = models.CharField(max_length=255, null=True, blank=True) - opportunity_amount = models.DecimalField( - _("Opportunity Amount"), decimal_places=2, max_digits=12, blank=True, null=True - ) - is_active = models.BooleanField(default=False) - enquiry_type = models.CharField(max_length=255, blank=True, null=True) + teams = models.ManyToManyField(Teams, related_name="lead_teams") + + # Activity Tracking + last_contacted = models.DateField(_("Last Contacted"), blank=True, null=True) + next_follow_up = models.DateField(_("Next Follow-up"), blank=True, null=True) + description = models.TextField(_("Notes"), blank=True, null=True) + + # System Fields + is_active = models.BooleanField(default=True) tags = models.ManyToManyField(Tags, blank=True) contacts = models.ManyToManyField(Contact, related_name="lead_contacts") created_from_site = models.BooleanField(default=False) - teams = models.ManyToManyField(Teams, related_name="lead_teams") - org = models.ForeignKey( - Org, on_delete=models.SET_NULL, null=True, blank=True, related_name="lead_org" - ) + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="leads") company = models.ForeignKey( Company, on_delete=models.SET_NULL, @@ -76,19 +108,17 @@ class Lead(BaseModel): blank=True, related_name="lead_company", ) - skype_ID = models.CharField(max_length=100, null=True, blank=True) - industry = models.CharField( - _("Industry Type"), max_length=255, choices=INDCHOICES, blank=True, null=True - ) - organization = models.CharField(_("Organization"), max_length=255, null=True) - probability = models.IntegerField(default=0, blank=True, null=True) - close_date = models.DateField(default=None, null=True) class Meta: verbose_name = "Lead" verbose_name_plural = "Leads" db_table = "lead" ordering = ("-created_at",) + indexes = [ + models.Index(fields=["status"]), + models.Index(fields=["source"]), + models.Index(fields=["org", "-created_at"]), + ] def __str__(self): return f"{self.title}" @@ -106,25 +136,6 @@ class Lead(BaseModel): def created_on_arrow(self): return arrow.get(self.created_at).humanize() - @property - def get_team_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return Profile.objects.filter(id__in=team_user_ids) - - @property - def get_team_and_assigned_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = team_user_ids + assigned_user_ids - return Profile.objects.filter(id__in=user_ids) - - @property - def get_assigned_users_not_in_teams(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = set(assigned_user_ids) - set(team_user_ids) - return Profile.objects.filter(id__in=list(user_ids)) - # def save(self, *args, **kwargs): # super(Lead, self).save(*args, **kwargs) # queryset = Lead.objects.all().exclude(status='converted').select_related('created_by' diff --git a/leads/serializer.py b/backend/leads/serializer.py similarity index 62% rename from leads/serializer.py rename to backend/leads/serializer.py index fd766ac5..d5988381 100644 --- a/leads/serializer.py +++ b/backend/leads/serializer.py @@ -1,16 +1,17 @@ from rest_framework import serializers -from accounts.models import Account, Tags +from accounts.models import Account +from common.models import Tags from common.serializer import ( AttachmentsSerializer, LeadCommentSerializer, OrganizationSerializer, ProfileSerializer, + TeamsSerializer, UserSerializer, ) from contacts.serializer import ContactSerializer from leads.models import Company, Lead -from teams.serializer import TeamsSerializer class TagsSerializer(serializers.ModelSerializer): @@ -24,6 +25,7 @@ class CompanySwaggerSerializer(serializers.ModelSerializer): model = Company fields = ("name",) + class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company @@ -45,54 +47,65 @@ class LeadSerializer(serializers.ModelSerializer): class Meta: model = Lead - # fields = ‘__all__’ fields = ( "id", + # Core Lead Information "title", "first_name", "last_name", - "phone", "email", + "phone", + "contact_title", + "website", + "linkedin_url", + # Sales Pipeline "status", "source", + "industry", + "rating", + "opportunity_amount", + "probability", + "close_date", + # Address "address_line", - "contacts", - "street", "city", "state", "postcode", "country", - "website", + # Assignment + "assigned_to", + "teams", + # Activity + "last_contacted", + "next_follow_up", "description", + # Related + "contacts", "lead_attachment", "lead_comments", - "assigned_to", - "account_name", - "opportunity_amount", + "tags", + # System "created_by", "created_at", "is_active", - "enquiry_type", - "tags", "created_from_site", - "teams", - "skype_ID", - "industry", "company", - "organization", - "probability", - "close_date", ) class LeadCreateSerializer(serializers.ModelSerializer): - probability = serializers.IntegerField(max_value=100) + probability = serializers.IntegerField( + max_value=100, required=False, allow_null=True + ) + opportunity_amount = serializers.DecimalField( + max_digits=12, decimal_places=2, required=False, allow_null=True + ) + close_date = serializers.DateField(required=False, allow_null=True) def __init__(self, *args, **kwargs): request_obj = kwargs.pop("request_obj", None) super().__init__(*args, **kwargs) if self.initial_data and self.initial_data.get("status") == "converted": - self.fields["account_name"].required = True self.fields["email"].required = True self.fields["first_name"].required = False self.fields["last_name"].required = False @@ -105,93 +118,103 @@ class LeadCreateSerializer(serializers.ModelSerializer): prev_choices = prev_choices + [("micropyramid", "Micropyramid")] self.fields["source"]._set_choices(prev_choices) - def validate_account_name(self, account_name): - if self.instance: - if ( - Account.objects.filter(name__iexact=account_name, org=self.org) - .exclude(id=self.instance.id) - .exists() - ): - raise serializers.ValidationError( - "Account already exists with this name" - ) - else: - if Account.objects.filter(name__iexact=account_name, org=self.org).exists(): - raise serializers.ValidationError( - "Account already exists with this name" - ) - return account_name - - def validate_title(self, title): - if self.instance: - if ( - Lead.objects.filter(title__iexact=title, org=self.org) - .exclude(id=self.instance.id) - .exists() - ): - raise serializers.ValidationError("Lead already exists with this title") - else: - if Lead.objects.filter(title__iexact=title, org=self.org).exists(): - raise serializers.ValidationError("Lead already exists with this title") - return title - class Meta: model = Lead fields = ( + # Core Lead Information + "title", "first_name", "last_name", - "account_name", - "title", - "phone", "email", + "phone", + "contact_title", + "website", + "linkedin_url", + # Sales Pipeline "status", "source", - "website", - "description", + "industry", + "rating", + "opportunity_amount", + "probability", + "close_date", + # Address "address_line", - # "contacts", - "street", "city", "state", "postcode", - "opportunity_amount", "country", + # Activity + "last_contacted", + "next_follow_up", + "description", + # System "org", - "skype_ID", - "industry", "company", - "organization", - "probability", - "close_date", - # "lead_attachment", ) + class LeadCreateSwaggerSerializer(serializers.ModelSerializer): class Meta: model = Lead - fields = ["title","first_name","last_name","account_name","phone","email","lead_attachment","opportunity_amount","website", - "description","teams","assigned_to","contacts","status","source","address_line","street","city","state","postcode", - "country","tags","company","probability","industry","skype_ID"] + fields = [ + # Core Lead Information + "title", + "first_name", + "last_name", + "email", + "phone", + "contact_title", + "website", + "linkedin_url", + # Sales Pipeline + "status", + "source", + "industry", + "rating", + "opportunity_amount", + "probability", + "close_date", + # Address + "address_line", + "city", + "state", + "postcode", + "country", + # Assignment & Related + "assigned_to", + "teams", + "contacts", + "tags", + # Activity + "last_contacted", + "next_follow_up", + "description", + # System + "company", + "lead_attachment", + ] class CreateLeadFromSiteSwaggerSerializer(serializers.Serializer): - apikey=serializers.CharField() - title=serializers.CharField() - first_name=serializers.CharField() - last_name=serializers.CharField() - phone=serializers.CharField() - email=serializers.CharField() - source=serializers.CharField() - description=serializers.CharField() + apikey = serializers.CharField() + title = serializers.CharField() + first_name = serializers.CharField() + last_name = serializers.CharField() + phone = serializers.CharField() + email = serializers.CharField() + source = serializers.CharField() + description = serializers.CharField() class LeadDetailEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() lead_attachment = serializers.FileField() + class LeadCommentEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() + class LeadUploadSwaggerSerializer(serializers.Serializer): leads_file = serializers.FileField() - diff --git a/leads/swagger_params1.py b/backend/leads/swagger_params.py similarity index 100% rename from leads/swagger_params1.py rename to backend/leads/swagger_params.py diff --git a/leads/tasks.py b/backend/leads/tasks.py similarity index 89% rename from leads/tasks.py rename to backend/leads/tasks.py index 30fcbd5a..5808a880 100644 --- a/leads/tasks.py +++ b/backend/leads/tasks.py @@ -7,6 +7,7 @@ from django.core.mail import EmailMessage, EmailMultiAlternatives from django.db.models import Q from django.template.loader import render_to_string +from accounts.models import Account from common.models import Org, Profile from leads.models import Lead @@ -55,7 +56,7 @@ def send_lead_assigned_emails(lead_id, new_assigned_to_list, site_address): users = Profile.objects.filter(id__in=new_assigned_to_list).distinct() subject = "Lead '%s' has been assigned to you" % lead_instance from_email = settings.DEFAULT_FROM_EMAIL - template_name = "lead_assigned.html" + template_name = "assigned_to/leads_assigned.html" url = site_address url += "/leads/" + str(lead_instance.id) + "/view/" @@ -104,7 +105,7 @@ def create_lead_from_file(validated_rows, invalid_rows, user_id, source, company """Parameters : validated_rows, invalid_rows, user_id. This function is used to create leads from a given file. """ - email_regex = "^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,4})$" + email_regex = r"^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,4})$" profile = Profile.objects.get(id=user_id) org = Org.objects.filter(id=company_id).first() for row in validated_rows: @@ -125,7 +126,14 @@ def create_lead_from_file(validated_rows, invalid_rows, user_id, source, company lead.country = row.get("country", "")[:3] lead.description = row.get("description", "") lead.status = row.get("status", "") - lead.account_name = row.get("account_name", "")[:255] + # Look up company by name if provided in CSV + account_name = row.get("account_name", "").strip()[:255] + if account_name: + company = Account.objects.filter( + name__iexact=account_name, org=org + ).first() + if company: + lead.company = company lead.created_from_site = False lead.created_by = profile lead.org = org diff --git a/leads/tests_celery_tasks.py b/backend/leads/tests_celery_tasks.py similarity index 100% rename from leads/tests_celery_tasks.py rename to backend/leads/tests_celery_tasks.py diff --git a/leads/urls.py b/backend/leads/urls.py similarity index 78% rename from leads/urls.py rename to backend/leads/urls.py index 55d27049..f418e373 100644 --- a/leads/urls.py +++ b/backend/leads/urls.py @@ -1,7 +1,8 @@ from django.urls import path from leads import views -from .import views + +from . import views app_name = "api_leads" @@ -16,6 +17,6 @@ urlpatterns = [ path("upload/", views.LeadUploadView.as_view()), path("comment//", views.LeadCommentView.as_view()), path("attachment//", views.LeadAttachmentView.as_view()), - path("companies",views.CompaniesView.as_view()), - path('company/', views.CompanyDetail.as_view()), + path("companies", views.CompaniesView.as_view()), + path("company/", views.CompanyDetail.as_view()), ] diff --git a/leads/views.py b/backend/leads/views.py similarity index 66% rename from leads/views.py rename to backend/leads/views.py index fcd7f03a..3c40dd86 100644 --- a/leads/views.py +++ b/backend/leads/views.py @@ -1,3 +1,4 @@ +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.shortcuts import get_object_or_404 from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema @@ -7,43 +8,42 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from accounts.models import Account, Tags -from common.models import APISettings, Attachments, Comment, Profile +from accounts.models import Account +from common.models import APISettings, Attachments, Comment, Profile, Tags, Teams, User -#from common.external_auth import CustomDualAuthentication +# from common.external_auth import CustomDualAuthentication from common.serializer import ( AttachmentsSerializer, CommentSerializer, LeadCommentSerializer, ProfileSerializer, + TeamsSerializer, ) -from .forms import LeadListForm -from .models import Company,Lead from common.utils import COUNTRIES, INDCHOICES, LEAD_SOURCE, LEAD_STATUS from contacts.models import Contact -from leads import swagger_params1 +from leads import swagger_params from leads.forms import LeadListForm from leads.models import Company, Lead from leads.serializer import ( CompanySerializer, CompanySwaggerSerializer, + CreateLeadFromSiteSwaggerSerializer, + LeadCommentEditSwaggerSerializer, LeadCreateSerializer, - LeadSerializer, - TagsSerializer, LeadCreateSwaggerSerializer, LeadDetailEditSwaggerSerializer, - LeadCommentEditSwaggerSerializer, - CreateLeadFromSiteSwaggerSerializer, - LeadUploadSwaggerSerializer + LeadSerializer, + LeadUploadSwaggerSerializer, + TagsSerializer, ) -from common.models import User from leads.tasks import ( create_lead_from_file, send_email_to_assigned_user, send_lead_assigned_emails, ) -from teams.models import Teams -from teams.serializer import TeamsSerializer + +from .forms import LeadListForm +from .models import Company, Lead class LeadListView(APIView, LimitOffsetPagination): @@ -56,7 +56,7 @@ class LeadListView(APIView, LimitOffsetPagination): self.model.objects.filter(org=self.request.profile.org) .exclude(status="converted") .select_related("created_by") - .prefetch_related( + .prefetch_related( "tags", "assigned_to", ) @@ -137,33 +137,39 @@ class LeadListView(APIView, LimitOffsetPagination): context["companies"] = CompanySerializer( Company.objects.filter(org=self.request.profile.org), many=True ).data - context["tags"] = TagsSerializer(Tags.objects.all(), many=True).data + context["tags"] = TagsSerializer( + Tags.objects.filter(org=self.request.profile.org), many=True + ).data - users = Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "id", "user__email" - ) + users = Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("id", "user__email") context["users"] = users context["countries"] = COUNTRIES context["industries"] = INDCHOICES return context - @extend_schema(tags=["Leads"], parameters=swagger_params1.lead_list_get_params) + @extend_schema(tags=["Leads"], parameters=swagger_params.lead_list_get_params) def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return Response(context) @extend_schema( - tags=["Leads"],description="Leads Create", parameters=swagger_params1.organization_params,request=LeadCreateSwaggerSerializer + tags=["Leads"], + description="Leads Create", + parameters=swagger_params.organization_params, + request=LeadCreateSwaggerSerializer, ) def post(self, request, *args, **kwargs): - print('test') + print("test") data = request.data serializer = LeadCreateSerializer(data=data, request_obj=request) if serializer.is_valid(): - lead_obj = serializer.save(created_by=request.profile.user - , org=request.profile.org) - if data.get("tags",None): + lead_obj = serializer.save( + created_by=request.profile.user, org=request.profile.org + ) + if data.get("tags", None): tags = data.get("tags") for t in tags: tag = Tags.objects.filter(slug=t.lower()) @@ -173,7 +179,7 @@ class LeadListView(APIView, LimitOffsetPagination): tag = Tags.objects.create(name=t) lead_obj.tags.add(tag) - if data.get("contacts",None): + if data.get("contacts", None): obj_contact = Contact.objects.filter( id__in=data.get("contacts"), org=request.profile.org ) @@ -193,12 +199,12 @@ class LeadListView(APIView, LimitOffsetPagination): attachment.attachment = request.FILES.get("lead_attachment") attachment.save() - if data.get("teams",None): + if data.get("teams", None): teams_list = data.get("teams") teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) lead_obj.teams.add(*teams) - if data.get("assigned_to",None): + if data.get("assigned_to", None): assinged_to_list = data.get("assigned_to") profiles = Profile.objects.filter( id__in=assinged_to_list, org=request.profile.org @@ -208,7 +214,11 @@ class LeadListView(APIView, LimitOffsetPagination): if data.get("status") == "converted": account_object = Account.objects.create( created_by=request.profile.user, - name=lead_obj.account_name, + name=( + lead_obj.company.name + if lead_obj.company + else f"{lead_obj.first_name} {lead_obj.last_name}" + ), email=lead_obj.email, phone=lead_obj.phone, description=data.get("description"), @@ -222,18 +232,32 @@ class LeadListView(APIView, LimitOffsetPagination): account_object.billing_state = lead_obj.state account_object.billing_postcode = lead_obj.postcode account_object.billing_country = lead_obj.country - comments = Comment.objects.filter(lead=self.lead_obj) + lead_content_type = ContentType.objects.get_for_model(Lead) + account_content_type = ContentType.objects.get_for_model(Account) + comments = Comment.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=request.profile.org, + ) if comments.exists(): for comment in comments: - comment.account_id = account_object.id - attachments = Attachments.objects.filter(lead=self.lead_obj) + comment.content_type = account_content_type + comment.object_id = account_object.id + comment.save() + attachments = Attachments.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=request.profile.org, + ) if attachments.exists(): for attachment in attachments: - attachment.account_id = account_object.id + attachment.content_type = account_content_type + attachment.object_id = account_object.id + attachment.save() for tag in lead_obj.tags.all(): account_object.tags.add(tag) - if data.get("assigned_to",None): + if data.get("assigned_to", None): assigned_to_list = data.getlist("assigned_to") recipients = assigned_to_list send_email_to_assigned_user.delay( @@ -259,11 +283,11 @@ class LeadListView(APIView, LimitOffsetPagination): class LeadDetailView(APIView): model = Lead - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) def get_object(self, pk): - return get_object_or_404(Lead, id=pk) + return get_object_or_404(Lead, id=pk, org=self.request.profile.org) def get_context_data(self, **kwargs): params = self.request.query_params @@ -283,8 +307,13 @@ class LeadDetailView(APIView): status=status.HTTP_403_FORBIDDEN, ) - comments = Comment.objects.filter(lead=self.lead_obj).order_by("-id") - attachments = Attachments.objects.filter(lead=self.lead_obj).order_by("-id") + lead_content_type = ContentType.objects.get_for_model(Lead) + comments = Comment.objects.filter( + content_type=lead_content_type, object_id=self.lead_obj.id + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=lead_content_type, object_id=self.lead_obj.id + ).order_by("-id") assigned_data = [] for each in self.lead_obj.assigned_to.all(): assigned_dict = {} @@ -294,24 +323,33 @@ class LeadDetailView(APIView): if self.request.user.is_superuser or self.request.profile.role == "ADMIN": users_mention = list( - Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "user__email" - ) + Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("user__email") ) elif self.request.profile.user != self.lead_obj.created_by: users_mention = [{"username": self.lead_obj.created_by.username}] else: - users_mention = list( - self.lead_obj.assigned_to.all().values("user__email") - ) + users_mention = list(self.lead_obj.assigned_to.all().values("user__email")) + lead_content_type = ContentType.objects.get_for_model(Lead) + comments = Comment.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=self.request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=self.request.profile.org, + ).order_by("-id") if self.request.profile.role == "ADMIN" or self.request.user.is_superuser: users = Profile.objects.filter( is_active=True, org=self.request.profile.org ).order_by("user__email") else: - users = Profile.objects.filter(role="ADMIN", org=self.request.profile.org).order_by( - "user__email" - ) + users = Profile.objects.filter( + role="ADMIN", org=self.request.profile.org + ).order_by("user__email") user_assgn_list = [ assigned_to.id for assigned_to in self.lead_obj.get_assigned_users_not_in_teams @@ -354,13 +392,21 @@ class LeadDetailView(APIView): return context - @extend_schema(tags=["Leads"],parameters=swagger_params1.organization_params,description="Lead Detail") + @extend_schema( + tags=["Leads"], + parameters=swagger_params.organization_params, + description="Lead Detail", + ) def get(self, request, pk, **kwargs): self.lead_obj = self.get_object(pk) context = self.get_context_data(**kwargs) return Response(context) - @extend_schema(tags=["Leads"], parameters=swagger_params1.organization_params,request=LeadDetailEditSwaggerSerializer) + @extend_schema( + tags=["Leads"], + parameters=swagger_params.organization_params, + request=LeadDetailEditSwaggerSerializer, + ) def post(self, request, pk, **kwargs): params = request.data @@ -393,17 +439,26 @@ class LeadDetailView(APIView): if self.request.FILES.get("lead_attachment"): attachment = Attachments() - attachment.created_by = User.objects.get(id=self.request.profile.user.id) + attachment.created_by = User.objects.get( + id=self.request.profile.user.id + ) attachment.file_name = self.request.FILES.get("lead_attachment").name attachment.lead = self.lead_obj attachment.attachment = self.request.FILES.get("lead_attachment") attachment.save() - comments = Comment.objects.filter(lead__id=self.lead_obj.id).order_by("-id") - attachments = Attachments.objects.filter(lead__id=self.lead_obj.id).order_by( - "-id" - ) + lead_content_type = ContentType.objects.get_for_model(Lead) + comments = Comment.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=self.request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=self.request.profile.org, + ).order_by("-id") context.update( { "lead_obj": LeadSerializer(self.lead_obj).data, @@ -413,7 +468,11 @@ class LeadDetailView(APIView): ) return Response(context) - @extend_schema(tags=["Leads"], parameters=swagger_params1.organization_params,request=LeadCreateSwaggerSerializer) + @extend_schema( + tags=["Leads"], + parameters=swagger_params.organization_params, + request=LeadCreateSwaggerSerializer, + ) def put(self, request, pk, **kwargs): params = request.data self.lead_obj = self.get_object(pk) @@ -489,7 +548,11 @@ class LeadDetailView(APIView): if params.get("status") == "converted": account_object = Account.objects.create( created_by=request.profile.user, - name=lead_obj.account_name, + name=( + lead_obj.company.name + if lead_obj.company + else f"{lead_obj.first_name} {lead_obj.last_name}" + ), email=lead_obj.email, phone=lead_obj.phone, description=params.get("description"), @@ -503,14 +566,28 @@ class LeadDetailView(APIView): account_object.billing_state = lead_obj.state account_object.billing_postcode = lead_obj.postcode account_object.billing_country = lead_obj.country - comments = Comment.objects.filter(lead=self.lead_obj) + lead_content_type = ContentType.objects.get_for_model(Lead) + account_content_type = ContentType.objects.get_for_model(Account) + comments = Comment.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=request.profile.org, + ) if comments.exists(): for comment in comments: - comment.account_id = account_object.id - attachments = Attachments.objects.filter(lead=self.lead_obj) + comment.content_type = account_content_type + comment.object_id = account_object.id + comment.save() + attachments = Attachments.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=request.profile.org, + ) if attachments.exists(): for attachment in attachments: - attachment.account_id = account_object.id + attachment.content_type = account_content_type + attachment.object_id = account_object.id + attachment.save() for tag in lead_obj.tags.all(): account_object.tags.add(tag) if params.get("assigned_to"): @@ -522,9 +599,7 @@ class LeadDetailView(APIView): lead_obj.id, ) - for comment in lead_obj.leads_comments.all(): - comment.account = account_object - comment.save() + # Comments and attachments already migrated above account_object.save() return Response( { @@ -542,14 +617,163 @@ class LeadDetailView(APIView): status=status.HTTP_400_BAD_REQUEST, ) - @extend_schema(tags=["Leads"],parameters=swagger_params1.organization_params, description="Lead Delete") + @extend_schema( + tags=["Leads"], + parameters=swagger_params.organization_params, + request=LeadDetailEditSwaggerSerializer, + description="Partial Lead Update", + ) + def patch(self, request, pk, **kwargs): + """Handle partial updates to a lead, including conversion.""" + params = request.data + self.lead_obj = self.get_object(pk) + + if self.lead_obj.org != request.profile.org: + return Response( + { + "error": True, + "errors": "User company does not match with header....", + }, + status=status.HTTP_403_FORBIDDEN, + ) + + if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: + if not ( + (self.request.profile.user == self.lead_obj.created_by) + or (self.request.profile in self.lead_obj.assigned_to.all()) + ): + return Response( + { + "error": True, + "errors": "You do not have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + + # Handle conversion if status is being set to converted + if params.get("status") == "converted" or params.get("is_converted"): + # Create Account from Lead + account_object = Account.objects.create( + created_by=request.profile.user, + name=( + self.lead_obj.company.name + if self.lead_obj.company + else f"{self.lead_obj.first_name} {self.lead_obj.last_name}" + ), + email=self.lead_obj.email, + phone=self.lead_obj.phone, + description=self.lead_obj.description, + website=self.lead_obj.website, + org=request.profile.org, + is_active=True, # Set as active when converted from lead + # Copy address fields + address_line=self.lead_obj.address_line, + city=self.lead_obj.city, + state=self.lead_obj.state, + postcode=self.lead_obj.postcode, + country=self.lead_obj.country, + ) + + # Copy tags + for tag in self.lead_obj.tags.all(): + account_object.tags.add(tag) + + # Move comments to account + lead_content_type = ContentType.objects.get_for_model(Lead) + account_content_type = ContentType.objects.get_for_model(Account) + comments = Comment.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=request.profile.org, + ) + for comment in comments: + comment.content_type = account_content_type + comment.object_id = account_object.id + comment.save() + + # Move attachments to account + attachments = Attachments.objects.filter( + content_type=lead_content_type, + object_id=self.lead_obj.id, + org=request.profile.org, + ) + for attachment in attachments: + attachment.content_type = account_content_type + attachment.object_id = account_object.id + attachment.save() + + # Copy assigned users + for profile in self.lead_obj.assigned_to.all(): + account_object.assigned_to.add(profile) + + account_object.save() + + # Update lead status + self.lead_obj.status = "converted" + self.lead_obj.save() + + # Create Contact from Lead + contact_obj = None + if self.lead_obj.email: + contact_obj = Contact.objects.create( + first_name=self.lead_obj.first_name or "", + last_name=self.lead_obj.last_name or "", + email=self.lead_obj.email, + phone=self.lead_obj.phone, + organization=( + self.lead_obj.company.name if self.lead_obj.company else "" + ), + title=self.lead_obj.contact_title, + description=self.lead_obj.description, + address_line=self.lead_obj.address_line, + city=self.lead_obj.city, + state=self.lead_obj.state, + postcode=self.lead_obj.postcode, + country=self.lead_obj.country, + created_by=request.profile.user, + org=request.profile.org, + ) + contact_obj.assigned_to.set(self.lead_obj.assigned_to.all()) + + return Response( + { + "error": False, + "message": "Lead Converted to Account Successfully", + "account_id": str(account_object.id), + "contact_id": str(contact_obj.id) if contact_obj else None, + }, + status=status.HTTP_200_OK, + ) + + # Handle regular partial updates + serializer = LeadCreateSerializer( + data=params, + instance=self.lead_obj, + request_obj=request, + partial=True, + ) + if serializer.is_valid(): + lead_obj = serializer.save() + return Response( + {"error": False, "message": "Lead updated Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + @extend_schema( + tags=["Leads"], + parameters=swagger_params.organization_params, + description="Lead Delete", + ) def delete(self, request, pk, **kwargs): self.object = self.get_object(pk) if ( request.profile.role == "ADMIN" or request.user.is_superuser - or request.profile.user - == self.object.created_by + or request.profile.user == self.object.created_by ) and self.object.org == request.profile.org: self.object.delete() return Response( @@ -564,10 +788,14 @@ class LeadDetailView(APIView): class LeadUploadView(APIView): model = Lead - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) - @extend_schema(tags=["Leads"], parameters=swagger_params1.organization_params,request=LeadUploadSwaggerSerializer) + @extend_schema( + tags=["Leads"], + parameters=swagger_params.organization_params, + request=LeadUploadSwaggerSerializer, + ) def post(self, request, *args, **kwargs): lead_form = LeadListForm(request.POST, request.FILES) if lead_form.is_valid(): @@ -590,13 +818,17 @@ class LeadUploadView(APIView): class LeadCommentView(APIView): model = Comment - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) def get_object(self, pk): - return self.model.objects.get(pk=pk) + return self.model.objects.get(pk=pk, org=self.request.profile.org) - @extend_schema(tags=["Leads"], parameters=swagger_params1.organization_params,request=LeadCommentEditSwaggerSerializer) + @extend_schema( + tags=["Leads"], + parameters=swagger_params.organization_params, + request=LeadCommentEditSwaggerSerializer, + ) def put(self, request, pk, format=None): params = request.data obj = self.get_object(pk) @@ -624,7 +856,7 @@ class LeadCommentView(APIView): status=status.HTTP_403_FORBIDDEN, ) - @extend_schema(tags=["Leads"], parameters=swagger_params1.organization_params) + @extend_schema(tags=["Leads"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.get_object(pk) if ( @@ -649,10 +881,10 @@ class LeadCommentView(APIView): class LeadAttachmentView(APIView): model = Attachments - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) - @extend_schema(tags=["Leads"], parameters=swagger_params1.organization_params) + @extend_schema(tags=["Leads"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): self.object = self.model.objects.get(pk=pk) if ( @@ -677,7 +909,8 @@ class LeadAttachmentView(APIView): class CreateLeadFromSite(APIView): @extend_schema( tags=["Leads"], - parameters=swagger_params1.organization_params,request=CreateLeadFromSiteSwaggerSerializer + parameters=swagger_params.organization_params, + request=CreateLeadFromSiteSwaggerSerializer, ) def post(self, request, *args, **kwargs): params = request.data @@ -745,29 +978,31 @@ class CompaniesView(APIView): permission_classes = (IsAuthenticated,) - @extend_schema(tags=["Company"],parameters=swagger_params1.organization_params) + @extend_schema(tags=["Company"], parameters=swagger_params.organization_params) def get(self, request, *args, **kwargs): try: - companies=Company.objects.filter(org=request.profile.org) - serializer=CompanySerializer(companies,many=True) + companies = Company.objects.filter(org=request.profile.org) + serializer = CompanySerializer(companies, many=True) return Response( - {"error": False, "data": serializer.data}, - status=status.HTTP_200_OK, - ) + {"error": False, "data": serializer.data}, + status=status.HTTP_200_OK, + ) except: return Response( {"error": True, "message": "Organization is missing"}, status=status.HTTP_400_BAD_REQUEST, ) - @extend_schema( - tags=["Company"],description="Company Create",parameters=swagger_params1.organization_params,request=CompanySwaggerSerializer + tags=["Company"], + description="Company Create", + parameters=swagger_params.organization_params, + request=CompanySwaggerSerializer, ) def post(self, request, *args, **kwargs): - request.data['org'] = request.profile.org.id + request.data["org"] = request.profile.org.id print(request.data) - company=CompanySerializer(data=request.data) + company = CompanySerializer(data=request.data) if Company.objects.filter(**request.data).exists(): return Response( {"error": True, "message": "This data already exists"}, @@ -785,47 +1020,55 @@ class CompaniesView(APIView): status=status.HTTP_400_BAD_REQUEST, ) + class CompanyDetail(APIView): - + permission_classes = (IsAuthenticated,) - def get_object(self, pk): try: - return Company.objects.get( - pk=pk - ) + return Company.objects.get(pk=pk) except Company.DoesNotExist: raise Http404 - @extend_schema(tags=["Company"],parameters=swagger_params1.organization_params) + @extend_schema(tags=["Company"], parameters=swagger_params.organization_params) def get(self, request, pk, format=None): company = self.get_object(pk) serializer = CompanySerializer(company) return Response( - {"error": False, "data": serializer.data}, - status=status.HTTP_200_OK, - ) - @extend_schema(tags=["Company"],description="Company Update",parameters=swagger_params1.organization_params,request=CompanySerializer) + {"error": False, "data": serializer.data}, + status=status.HTTP_200_OK, + ) + + @extend_schema( + tags=["Company"], + description="Company Update", + parameters=swagger_params.organization_params, + request=CompanySerializer, + ) def put(self, request, pk, format=None): company = self.get_object(pk) serializer = CompanySerializer(company, data=request.data) if serializer.is_valid(): serializer.save() return Response( - {"error": False, "data": serializer.data,'message': 'Updated Successfully'}, + { + "error": False, + "data": serializer.data, + "message": "Updated Successfully", + }, status=status.HTTP_200_OK, ) return Response( - {"error": True,'message': serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - @extend_schema(tags=["Company"],parameters=swagger_params1.organization_params) + {"error": True, "message": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + @extend_schema(tags=["Company"], parameters=swagger_params.organization_params) def delete(self, request, pk, format=None): company = self.get_object(pk) company.delete() return Response( - {"error": False, 'message': 'Deleted successfully'}, - status=status.HTTP_200_OK, - ) - \ No newline at end of file + {"error": False, "message": "Deleted successfully"}, + status=status.HTTP_200_OK, + ) diff --git a/manage.py b/backend/manage.py similarity index 100% rename from manage.py rename to backend/manage.py diff --git a/invoices/__init__.py b/backend/opportunity/__init__.py similarity index 100% rename from invoices/__init__.py rename to backend/opportunity/__init__.py diff --git a/opportunity/admin.py b/backend/opportunity/admin.py similarity index 100% rename from opportunity/admin.py rename to backend/opportunity/admin.py diff --git a/opportunity/apps.py b/backend/opportunity/apps.py similarity index 100% rename from opportunity/apps.py rename to backend/opportunity/apps.py diff --git a/backend/opportunity/migrations/0001_initial.py b/backend/opportunity/migrations/0001_initial.py new file mode 100644 index 00000000..771cc1b8 --- /dev/null +++ b/backend/opportunity/migrations/0001_initial.py @@ -0,0 +1,379 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("accounts", "0001_initial"), + ("common", "0001_initial"), + ("contacts", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Opportunity", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField(max_length=255, verbose_name="Opportunity Name"), + ), + ( + "stage", + models.CharField( + choices=[ + ("PROSPECTING", "Prospecting"), + ("QUALIFICATION", "Qualification"), + ("PROPOSAL", "Proposal"), + ("NEGOTIATION", "Negotiation"), + ("CLOSED_WON", "Closed Won"), + ("CLOSED_LOST", "Closed Lost"), + ], + default="PROSPECTING", + max_length=64, + verbose_name="Stage", + ), + ), + ( + "opportunity_type", + models.CharField( + blank=True, + choices=[ + ("NEW_BUSINESS", "New Business"), + ("EXISTING_BUSINESS", "Existing Business"), + ("RENEWAL", "Renewal"), + ("UPSELL", "Upsell"), + ("CROSS_SELL", "Cross-sell"), + ], + max_length=64, + null=True, + verbose_name="Type", + ), + ), + ( + "currency", + models.CharField( + blank=True, + choices=[ + ("AED", "AED, Dirham"), + ("AFN", "AFN, Afghani"), + ("ALL", "ALL, Lek"), + ("AMD", "AMD, Dram"), + ("ANG", "ANG, Guilder"), + ("AOA", "AOA, Kwanza"), + ("ARS", "ARS, Peso"), + ("AUD", "AUD, Dollar"), + ("AWG", "AWG, Guilder"), + ("AZN", "AZN, Manat"), + ("BAM", "BAM, Marka"), + ("BBD", "BBD, Dollar"), + ("BDT", "BDT, Taka"), + ("BGN", "BGN, Lev"), + ("BHD", "BHD, Dinar"), + ("BIF", "BIF, Franc"), + ("BMD", "BMD, Dollar"), + ("BND", "BND, Dollar"), + ("BOB", "BOB, Boliviano"), + ("BRL", "BRL, Real"), + ("BSD", "BSD, Dollar"), + ("BTN", "BTN, Ngultrum"), + ("BWP", "BWP, Pula"), + ("BYR", "BYR, Ruble"), + ("BZD", "BZD, Dollar"), + ("CAD", "CAD, Dollar"), + ("CDF", "CDF, Franc"), + ("CHF", "CHF, Franc"), + ("CLP", "CLP, Peso"), + ("CNY", "CNY, Yuan Renminbi"), + ("COP", "COP, Peso"), + ("CRC", "CRC, Colon"), + ("CUP", "CUP, Peso"), + ("CVE", "CVE, Escudo"), + ("CZK", "CZK, Koruna"), + ("DJF", "DJF, Franc"), + ("DKK", "DKK, Krone"), + ("DOP", "DOP, Peso"), + ("DZD", "DZD, Dinar"), + ("EGP", "EGP, Pound"), + ("ERN", "ERN, Nakfa"), + ("ETB", "ETB, Birr"), + ("EUR", "EUR, Euro"), + ("FJD", "FJD, Dollar"), + ("FKP", "FKP, Pound"), + ("GBP", "GBP, Pound"), + ("GEL", "GEL, Lari"), + ("GHS", "GHS, Cedi"), + ("GIP", "GIP, Pound"), + ("GMD", "GMD, Dalasi"), + ("GNF", "GNF, Franc"), + ("GTQ", "GTQ, Quetzal"), + ("GYD", "GYD, Dollar"), + ("HKD", "HKD, Dollar"), + ("HNL", "HNL, Lempira"), + ("HRK", "HRK, Kuna"), + ("HTG", "HTG, Gourde"), + ("HUF", "HUF, Forint"), + ("IDR", "IDR, Rupiah"), + ("ILS", "ILS, Shekel"), + ("INR", "INR, Rupee"), + ("IQD", "IQD, Dinar"), + ("IRR", "IRR, Rial"), + ("ISK", "ISK, Krona"), + ("JMD", "JMD, Dollar"), + ("JOD", "JOD, Dinar"), + ("JPY", "JPY, Yen"), + ("KES", "KES, Shilling"), + ("KGS", "KGS, Som"), + ("KHR", "KHR, Riels"), + ("KMF", "KMF, Franc"), + ("KPW", "KPW, Won"), + ("KRW", "KRW, Won"), + ("KWD", "KWD, Dinar"), + ("KYD", "KYD, Dollar"), + ("KZT", "KZT, Tenge"), + ("LAK", "LAK, Kip"), + ("LBP", "LBP, Pound"), + ("LKR", "LKR, Rupee"), + ("LRD", "LRD, Dollar"), + ("LSL", "LSL, Loti"), + ("LTL", "LTL, Litas"), + ("LVL", "LVL, Lat"), + ("LYD", "LYD, Dinar"), + ("MAD", "MAD, Dirham"), + ("MDL", "MDL, Leu"), + ("MGA", "MGA, Ariary"), + ("MKD", "MKD, Denar"), + ("MMK", "MMK, Kyat"), + ("MNT", "MNT, Tugrik"), + ("MOP", "MOP, Pataca"), + ("MRO", "MRO, Ouguiya"), + ("MUR", "MUR, Rupee"), + ("MVR", "MVR, Rufiyaa"), + ("MWK", "MWK, Kwacha"), + ("MXN", "MXN, Peso"), + ("MYR", "MYR, Ringgit"), + ("MZN", "MZN, Metical"), + ("NAD", "NAD, Dollar"), + ("NGN", "NGN, Naira"), + ("NIO", "NIO, Cordoba"), + ("NOK", "NOK, Krone"), + ("NPR", "NPR, Rupee"), + ("NZD", "NZD, Dollar"), + ("OMR", "OMR, Rial"), + ("PAB", "PAB, Balboa"), + ("PEN", "PEN, Sol"), + ("PGK", "PGK, Kina"), + ("PHP", "PHP, Peso"), + ("PKR", "PKR, Rupee"), + ("PLN", "PLN, Zloty"), + ("PYG", "PYG, Guarani"), + ("QAR", "QAR, Rial"), + ("RON", "RON, Leu"), + ("RSD", "RSD, Dinar"), + ("RUB", "RUB, Ruble"), + ("RWF", "RWF, Franc"), + ("SAR", "SAR, Rial"), + ("SBD", "SBD, Dollar"), + ("SCR", "SCR, Rupee"), + ("SDG", "SDG, Pound"), + ("SEK", "SEK, Krona"), + ("SGD", "SGD, Dollar"), + ("SHP", "SHP, Pound"), + ("SLL", "SLL, Leone"), + ("SOS", "SOS, Shilling"), + ("SRD", "SRD, Dollar"), + ("SSP", "SSP, Pound"), + ("STD", "STD, Dobra"), + ("SYP", "SYP, Pound"), + ("SZL", "SZL, Lilangeni"), + ("THB", "THB, Baht"), + ("TJS", "TJS, Somoni"), + ("TMT", "TMT, Manat"), + ("TND", "TND, Dinar"), + ("TOP", "TOP, Paanga"), + ("TRY", "TRY, Lira"), + ("TTD", "TTD, Dollar"), + ("TWD", "TWD, Dollar"), + ("TZS", "TZS, Shilling"), + ("UAH", "UAH, Hryvnia"), + ("UGX", "UGX, Shilling"), + ("USD", "$, Dollar"), + ("UYU", "UYU, Peso"), + ("UZS", "UZS, Som"), + ("VEF", "VEF, Bolivar"), + ("VND", "VND, Dong"), + ("VUV", "VUV, Vatu"), + ("WST", "WST, Tala"), + ("XAF", "XAF, Franc"), + ("XCD", "XCD, Dollar"), + ("XOF", "XOF, Franc"), + ("XPF", "XPF, Franc"), + ("YER", "YER, Rial"), + ("ZAR", "ZAR, Rand"), + ("ZMK", "ZMK, Kwacha"), + ("ZWL", "ZWL, Dollar"), + ], + max_length=3, + null=True, + verbose_name="Currency", + ), + ), + ( + "amount", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=12, + null=True, + verbose_name="Amount", + ), + ), + ( + "probability", + models.IntegerField( + blank=True, default=0, null=True, verbose_name="Probability (%)" + ), + ), + ( + "closed_on", + models.DateField( + blank=True, null=True, verbose_name="Expected Close Date" + ), + ), + ( + "lead_source", + models.CharField( + blank=True, + choices=[ + ("NONE", "NONE"), + ("CALL", "CALL"), + ("EMAIL", " EMAIL"), + ("EXISTING CUSTOMER", "EXISTING CUSTOMER"), + ("PARTNER", "PARTNER"), + ("PUBLIC RELATIONS", "PUBLIC RELATIONS"), + ("CAMPAIGN", "CAMPAIGN"), + ("WEBSITE", "WEBSITE"), + ("OTHER", "OTHER"), + ], + max_length=255, + null=True, + verbose_name="Lead Source", + ), + ), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Notes"), + ), + ("is_active", models.BooleanField(default=True)), + ( + "account", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="opportunities", + to="accounts.account", + ), + ), + ( + "assigned_to", + models.ManyToManyField( + related_name="opportunity_assigned_to", to="common.profile" + ), + ), + ( + "closed_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="opportunity_closed_by", + to="common.profile", + ), + ), + ( + "contacts", + models.ManyToManyField( + related_name="opportunity_contacts", to="contacts.contact" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="opportunities", + to="common.org", + ), + ), + ("tags", models.ManyToManyField(blank=True, to="common.tags")), + ( + "teams", + models.ManyToManyField( + related_name="opportunity_teams", to="common.teams" + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Opportunity", + "verbose_name_plural": "Opportunities", + "db_table": "opportunity", + "ordering": ("-created_at",), + "indexes": [ + models.Index(fields=["stage"], name="opportunity_stage_39fb59_idx"), + models.Index( + fields=["org", "-created_at"], + name="opportunity_org_id_945a7b_idx", + ), + ], + }, + ), + ] diff --git a/invoices/migrations/__init__.py b/backend/opportunity/migrations/__init__.py similarity index 100% rename from invoices/migrations/__init__.py rename to backend/opportunity/migrations/__init__.py diff --git a/backend/opportunity/models.py b/backend/opportunity/models.py new file mode 100644 index 00000000..9fda29ba --- /dev/null +++ b/backend/opportunity/models.py @@ -0,0 +1,97 @@ +import arrow +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import pgettext_lazy + +from accounts.models import Account +from common.base import AssignableMixin, BaseModel +from common.models import Org, Profile, Tags, Teams +from common.utils import CURRENCY_CODES, OPPORTUNITY_TYPES, SOURCES, STAGES +from contacts.models import Contact + + +class Opportunity(AssignableMixin, BaseModel): + """ + Opportunity model for CRM - Sales pipeline management + Based on Twenty CRM and Salesforce patterns + """ + + # Core Opportunity Information + name = models.CharField(_("Opportunity Name"), max_length=255) + account = models.ForeignKey( + Account, + related_name="opportunities", + on_delete=models.CASCADE, + blank=True, + null=True, + ) + stage = models.CharField( + _("Stage"), max_length=64, choices=STAGES, default="PROSPECTING" + ) + opportunity_type = models.CharField( + _("Type"), max_length=64, choices=OPPORTUNITY_TYPES, blank=True, null=True + ) + + # Financial Information + currency = models.CharField( + _("Currency"), max_length=3, choices=CURRENCY_CODES, blank=True, null=True + ) + amount = models.DecimalField( + _("Amount"), decimal_places=2, max_digits=12, blank=True, null=True + ) + probability = models.IntegerField( + _("Probability (%)"), default=0, blank=True, null=True + ) + closed_on = models.DateField(_("Expected Close Date"), blank=True, null=True) + + # Source & Context + lead_source = models.CharField( + _("Lead Source"), max_length=255, choices=SOURCES, blank=True, null=True + ) + + # Relationships + contacts = models.ManyToManyField(Contact, related_name="opportunity_contacts") + + # Assignment + assigned_to = models.ManyToManyField( + Profile, related_name="opportunity_assigned_to" + ) + teams = models.ManyToManyField(Teams, related_name="opportunity_teams") + closed_by = models.ForeignKey( + Profile, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="opportunity_closed_by", + ) + + # Tags + tags = models.ManyToManyField(Tags, blank=True) + + # Notes + description = models.TextField(_("Notes"), blank=True, null=True) + + # System Fields + is_active = models.BooleanField(default=True) + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="opportunities", + ) + + class Meta: + verbose_name = "Opportunity" + verbose_name_plural = "Opportunities" + db_table = "opportunity" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["stage"]), + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return f"{self.name}" + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() diff --git a/opportunity/serializer.py b/backend/opportunity/serializer.py similarity index 67% rename from opportunity/serializer.py rename to backend/opportunity/serializer.py index 436b3f31..a465d2c7 100644 --- a/opportunity/serializer.py +++ b/backend/opportunity/serializer.py @@ -1,11 +1,16 @@ from rest_framework import serializers -from accounts.models import Tags from accounts.serializer import AccountSerializer -from common.serializer import AttachmentsSerializer, ProfileSerializer,UserSerializer +from common.models import Tags +from common.serializer import ( + AttachmentsSerializer, + OrganizationSerializer, + ProfileSerializer, + TeamsSerializer, + UserSerializer, +) from contacts.serializer import ContactSerializer from opportunity.models import Opportunity -from teams.serializer import TeamsSerializer class TagsSerializer(serializers.ModelSerializer): @@ -15,53 +20,74 @@ class TagsSerializer(serializers.ModelSerializer): class OpportunitySerializer(serializers.ModelSerializer): + """Serializer for reading Opportunity data""" + account = AccountSerializer() closed_by = ProfileSerializer() created_by = UserSerializer() + org = OrganizationSerializer() tags = TagsSerializer(read_only=True, many=True) assigned_to = ProfileSerializer(read_only=True, many=True) contacts = ContactSerializer(read_only=True, many=True) teams = TeamsSerializer(read_only=True, many=True) opportunity_attachment = AttachmentsSerializer(read_only=True, many=True) + get_team_users = ProfileSerializer(read_only=True, many=True) + get_team_and_assigned_users = ProfileSerializer(read_only=True, many=True) + get_assigned_users_not_in_teams = ProfileSerializer(read_only=True, many=True) class Meta: model = Opportunity - # fields = ‘__all__’ fields = ( "id", + # Core Opportunity Information "name", + "account", "stage", + "opportunity_type", + # Financial Information "currency", "amount", - "lead_source", "probability", - "contacts", - "closed_by", "closed_on", - "description", + # Source & Context + "lead_source", + # Relationships + "contacts", + # Assignment "assigned_to", + "teams", + "closed_by", + # Tags + "tags", + # Notes + "description", + # Related + "opportunity_attachment", + # System "created_by", "created_at", "is_active", - "tags", - "opportunity_attachment", - "teams", + "org", "created_on_arrow", - "account", - # "get_team_users", - # "get_team_and_assigned_users", - # "get_assigned_users_not_in_teams", + "get_team_users", + "get_team_and_assigned_users", + "get_assigned_users_not_in_teams", ) class OpportunityCreateSerializer(serializers.ModelSerializer): - probability = serializers.IntegerField(max_value=100) - closed_on = serializers.DateField + """Serializer for creating/updating Opportunity data""" + + probability = serializers.IntegerField( + max_value=100, required=False, allow_null=True + ) + closed_on = serializers.DateField(required=False, allow_null=True) def __init__(self, *args, **kwargs): request_obj = kwargs.pop("request_obj", None) super().__init__(*args, **kwargs) - self.org = request_obj.profile.org + if request_obj: + self.org = request_obj.profile.org def validate_name(self, name): if self.instance: @@ -73,7 +99,6 @@ class OpportunityCreateSerializer(serializers.ModelSerializer): raise serializers.ValidationError( "Opportunity already exists with this name" ) - else: if Opportunity.objects.filter(name__iexact=name, org=self.org).exists(): raise serializers.ValidationError( @@ -84,50 +109,52 @@ class OpportunityCreateSerializer(serializers.ModelSerializer): class Meta: model = Opportunity fields = ( + # Core Opportunity Information "name", "account", "stage", + "opportunity_type", + # Financial Information "currency", "amount", - "lead_source", "probability", "closed_on", + # Source & Context + "lead_source", + # Notes "description", - "created_by", - "created_at", - "is_active", - "created_on_arrow", - "org" - # "get_team_users", - # "get_team_and_assigned_users", - # "get_assigned_users_not_in_teams", ) + class OpportunityCreateSwaggerSerializer(serializers.ModelSerializer): - due_date = serializers.DateField() + closed_on = serializers.DateField() opportunity_attachment = serializers.FileField() + class Meta: model = Opportunity fields = ( "name", "account", + "stage", + "opportunity_type", "amount", "currency", - "stage", - "teams", - "lead_source", "probability", + "closed_on", + "lead_source", "description", "assigned_to", "contacts", - "due_date", + "teams", "tags", - "opportunity_attachment" + "opportunity_attachment", ) + class OpportunityDetailEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() opportunity_attachment = serializers.FileField() + class OpportunityCommentEditSwaggerSerializer(serializers.Serializer): comment = serializers.CharField() diff --git a/opportunity/swagger_params1.py b/backend/opportunity/swagger_params.py similarity index 55% rename from opportunity/swagger_params1.py rename to backend/opportunity/swagger_params.py index 929658cb..c50b67a7 100644 --- a/opportunity/swagger_params1.py +++ b/backend/opportunity/swagger_params.py @@ -12,11 +12,11 @@ organization_params = [ opportunity_list_get_params = [ organization_params_in_header, - OpenApiParameter("name", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("account", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("stage", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("lead_source", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("tags", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("name", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("account", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("stage", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("lead_source", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("tags", OpenApiTypes.STR, OpenApiParameter.QUERY), ] opportunity_detail_get_params = [ @@ -26,10 +26,10 @@ opportunity_detail_get_params = [ OpenApiParameter.QUERY, OpenApiTypes.BINARY, ), - OpenApiParameter("comment", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("comment", OpenApiTypes.STR, OpenApiParameter.QUERY), ] opportunity_comment_edit_params = [ organization_params_in_header, - OpenApiParameter("comment", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("comment", OpenApiTypes.STR, OpenApiParameter.QUERY), ] diff --git a/opportunity/tasks.py b/backend/opportunity/tasks.py similarity index 100% rename from opportunity/tasks.py rename to backend/opportunity/tasks.py diff --git a/opportunity/tests_celery_tasks.py b/backend/opportunity/tests_celery_tasks.py similarity index 100% rename from opportunity/tests_celery_tasks.py rename to backend/opportunity/tests_celery_tasks.py diff --git a/opportunity/urls.py b/backend/opportunity/urls.py similarity index 100% rename from opportunity/urls.py rename to backend/opportunity/urls.py diff --git a/opportunity/views.py b/backend/opportunity/views.py similarity index 83% rename from opportunity/views.py rename to backend/opportunity/views.py index 63ee66eb..b0e98b2e 100644 --- a/opportunity/views.py +++ b/backend/opportunity/views.py @@ -1,5 +1,6 @@ import json +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema from rest_framework import status @@ -8,11 +9,11 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from accounts.models import Account, Tags +from accounts.models import Account from accounts.serializer import AccountSerializer, TagsSerailizer -from common.models import Attachments, Comment, Profile +from common.models import Attachments, Comment, Profile, Tags, Teams -#from common.external_auth import CustomDualAuthentication +# from common.external_auth import CustomDualAuthentication from common.serializer import ( AttachmentsSerializer, CommentSerializer, @@ -21,33 +22,37 @@ from common.serializer import ( from common.utils import CURRENCY_CODES, SOURCES, STAGES from contacts.models import Contact from contacts.serializer import ContactSerializer -from opportunity import swagger_params1 +from opportunity import swagger_params from opportunity.models import Opportunity from opportunity.serializer import * from opportunity.tasks import send_email_to_assigned_user -from teams.models import Teams class OpportunityListView(APIView, LimitOffsetPagination): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Opportunity def get_context_data(self, **kwargs): params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") + queryset = self.model.objects.filter(org=self.request.profile.org).order_by( + "-id" + ) accounts = Account.objects.filter(org=self.request.profile.org) contacts = Contact.objects.filter(org=self.request.profile.org) if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: queryset = queryset.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) ).distinct() accounts = accounts.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) ).distinct() contacts = contacts.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) ).distinct() if params: @@ -62,9 +67,7 @@ class OpportunityListView(APIView, LimitOffsetPagination): lead_source__contains=params.get("lead_source") ) if params.get("tags"): - queryset = queryset.filter( - tags__in=params.get("tags") - ).distinct() + queryset = queryset.filter(tags__in=params.get("tags")).distinct() context = {} results_opportunities = self.paginate_queryset( @@ -89,7 +92,9 @@ class OpportunityListView(APIView, LimitOffsetPagination): context["opportunities"] = opportunities context["accounts_list"] = AccountSerializer(accounts, many=True).data context["contacts_list"] = ContactSerializer(contacts, many=True).data - context["tags"] = TagsSerailizer(Tags.objects.filter(), many=True).data + context["tags"] = TagsSerailizer( + Tags.objects.filter(org=self.request.profile.org), many=True + ).data context["stage"] = STAGES context["lead_source"] = SOURCES context["currency"] = CURRENCY_CODES @@ -98,7 +103,7 @@ class OpportunityListView(APIView, LimitOffsetPagination): @extend_schema( tags=["Opportunities"], - parameters=swagger_params1.opportunity_list_get_params, + parameters=swagger_params.opportunity_list_get_params, ) def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) @@ -106,7 +111,8 @@ class OpportunityListView(APIView, LimitOffsetPagination): @extend_schema( tags=["Opportunities"], - parameters=swagger_params1.organization_params,request=OpportunityCreateSwaggerSerializer + parameters=swagger_params.organization_params, + request=OpportunityCreateSwaggerSerializer, ) def post(self, request, *args, **kwargs): params = request.data @@ -120,7 +126,9 @@ class OpportunityListView(APIView, LimitOffsetPagination): if params.get("contacts"): contacts_list = params.get("contacts") - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) opportunity_obj.contacts.add(*contacts) if params.get("tags"): @@ -180,16 +188,17 @@ class OpportunityListView(APIView, LimitOffsetPagination): class OpportunityDetailView(APIView): - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) model = Opportunity def get_object(self, pk): - return self.model.objects.filter(id=pk).first() + return self.model.objects.filter(id=pk, org=self.request.profile.org).first() @extend_schema( tags=["Opportunities"], - parameters=swagger_params1.organization_params,request=OpportunityCreateSwaggerSerializer + parameters=swagger_params.organization_params, + request=OpportunityCreateSwaggerSerializer, ) def put(self, request, pk, format=None): params = request.data @@ -227,7 +236,9 @@ class OpportunityDetailView(APIView): opportunity_object.contacts.clear() if params.get("contacts"): contacts_list = params.get("contacts") - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) opportunity_object.contacts.add(*contacts) opportunity_object.tags.clear() @@ -288,7 +299,7 @@ class OpportunityDetailView(APIView): ) @extend_schema( - tags=["Opportunities"], parameters=swagger_params1.organization_params + tags=["Opportunities"], parameters=swagger_params.organization_params ) def delete(self, request, pk, format=None): self.object = self.get_object(pk) @@ -313,7 +324,7 @@ class OpportunityDetailView(APIView): ) @extend_schema( - tags=["Opportunities"], parameters=swagger_params1.organization_params + tags=["Opportunities"], parameters=swagger_params.organization_params ) def get(self, request, pk, format=None): self.opportunity = self.get_object(pk=pk) @@ -348,28 +359,33 @@ class OpportunityDetailView(APIView): if self.request.user.is_superuser or self.request.profile.role == "ADMIN": users_mention = list( - Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "user__email" - ) + Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("user__email") ) elif self.request.profile != self.opportunity.created_by: if self.opportunity.created_by: - users_mention = [ - {"username": self.opportunity.created_by.user.email} - ] + users_mention = [{"username": self.opportunity.created_by.user.email}] else: users_mention = [] else: users_mention = [] + opportunity_content_type = ContentType.objects.get_for_model(Opportunity) + comments = Comment.objects.filter( + content_type=opportunity_content_type, + object_id=self.opportunity.id, + org=self.request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=opportunity_content_type, + object_id=self.opportunity.id, + org=self.request.profile.org, + ).order_by("-id") context.update( { - "comments": CommentSerializer( - self.opportunity.opportunity_comments.all(), many=True - ).data, - "attachments": AttachmentsSerializer( - self.opportunity.opportunity_attachment.all(), many=True - ).data, + "comments": CommentSerializer(comments, many=True).data, + "attachments": AttachmentsSerializer(attachments, many=True).data, "contacts": ContactSerializer( self.opportunity.contacts.all(), many=True ).data, @@ -390,12 +406,13 @@ class OpportunityDetailView(APIView): @extend_schema( tags=["Opportunities"], - parameters=swagger_params1.organization_params,request=OpportunityDetailEditSwaggerSerializer + parameters=swagger_params.organization_params, + request=OpportunityDetailEditSwaggerSerializer, ) def post(self, request, pk, **kwargs): params = request.data context = {} - self.opportunity_obj = Opportunity.objects.get(pk=pk) + self.opportunity_obj = Opportunity.objects.get(pk=pk, org=request.profile.org) if self.opportunity_obj.org != request.profile.org: return Response( {"error": True, "errors": "User company doesnot match with header...."}, @@ -431,11 +448,16 @@ class OpportunityDetailView(APIView): attachment.attachment = self.request.FILES.get("opportunity_attachment") attachment.save() - comments = Comment.objects.filter(opportunity=self.opportunity_obj).order_by( - "-id" - ) + opportunity_content_type = ContentType.objects.get_for_model(Opportunity) + comments = Comment.objects.filter( + content_type=opportunity_content_type, + object_id=self.opportunity_obj.id, + org=request.profile.org, + ).order_by("-id") attachments = Attachments.objects.filter( - opportunity=self.opportunity_obj + content_type=opportunity_content_type, + object_id=self.opportunity_obj.id, + org=request.profile.org, ).order_by("-id") context.update( { @@ -449,15 +471,16 @@ class OpportunityDetailView(APIView): class OpportunityCommentView(APIView): model = Comment - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) def get_object(self, pk): - return self.model.objects.get(pk=pk) + return self.model.objects.get(pk=pk, org=self.request.profile.org) @extend_schema( tags=["Opportunities"], - parameters=swagger_params1.organization_params,request=OpportunityCommentEditSwaggerSerializer + parameters=swagger_params.organization_params, + request=OpportunityCommentEditSwaggerSerializer, ) def put(self, request, pk, format=None): params = request.data @@ -488,7 +511,7 @@ class OpportunityCommentView(APIView): ) @extend_schema( - tags=["Opportunities"], parameters=swagger_params1.organization_params + tags=["Opportunities"], parameters=swagger_params.organization_params ) def delete(self, request, pk, format=None): self.object = self.get_object(pk) @@ -513,11 +536,11 @@ class OpportunityCommentView(APIView): class OpportunityAttachmentView(APIView): model = Attachments - #authentication_classes = (CustomDualAuthentication,) + # authentication_classes = (CustomDualAuthentication,) permission_classes = (IsAuthenticated,) @extend_schema( - tags=["Opportunities"], parameters=swagger_params1.organization_params + tags=["Opportunities"], parameters=swagger_params.organization_params ) def delete(self, request, pk, format=None): self.object = self.model.objects.get(pk=pk) diff --git a/pytest.ini b/backend/pytest.ini similarity index 100% rename from pytest.ini rename to backend/pytest.ini diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..298c21a3 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,18 @@ +django==5.2.8 +celery==5.5.3 +python-dotenv==1.0.0 +django-cors-headers==4.6.0 +djangorestframework==3.16.0 +djangorestframework-simplejwt==5.5.0 +drf-spectacular==0.28.0 +django-ses==4.3.1 +psycopg2-binary==2.9.11 +whitenoise==6.8.2 +sentry-sdk==2.19.2 +django_extensions==3.2.3 +django_storages==1.14.4 + +django-crum==0.7.9 +Redis==5.2.1 +django-phonenumber-field==8.0.0 +arrow==1.3.0 diff --git a/setup.py b/backend/setup.py similarity index 100% rename from setup.py rename to backend/setup.py diff --git a/leads/__init__.py b/backend/static/.gitkeep similarity index 100% rename from leads/__init__.py rename to backend/static/.gitkeep diff --git a/leads/migrations/__init__.py b/backend/tasks/__init__.py similarity index 100% rename from leads/migrations/__init__.py rename to backend/tasks/__init__.py diff --git a/backend/tasks/admin.py b/backend/tasks/admin.py new file mode 100644 index 00000000..98a0839c --- /dev/null +++ b/backend/tasks/admin.py @@ -0,0 +1,52 @@ +from django.contrib import admin + +from tasks.models import Board, BoardColumn, BoardMember, BoardTask, Task + +# Register Task model +admin.site.register(Task) + + +# ============================================================================= +# Kanban Board Admin (merged from boards app) +# ============================================================================= + + +@admin.register(Board) +class BoardAdmin(admin.ModelAdmin): + list_display = ("name", "owner", "org", "is_archived", "created_at") + list_filter = ("is_archived", "org", "created_at") + search_fields = ("name", "owner__user__email") + raw_id_fields = ("owner", "org") + + +@admin.register(BoardMember) +class BoardMemberAdmin(admin.ModelAdmin): + list_display = ("board", "profile", "role", "created_at") + list_filter = ("role", "created_at") + search_fields = ("board__name", "profile__user__email") + raw_id_fields = ("board", "profile") + + +@admin.register(BoardColumn) +class BoardColumnAdmin(admin.ModelAdmin): + list_display = ("board", "name", "order", "color", "limit") + list_filter = ("board__org", "created_at") + search_fields = ("name", "board__name") + raw_id_fields = ("board",) + ordering = ("board", "order") + + +@admin.register(BoardTask) +class BoardTaskAdmin(admin.ModelAdmin): + list_display = ( + "title", + "column", + "priority", + "due_date", + "is_completed", + "created_at", + ) + list_filter = ("priority", "completed_at", "created_at") + search_fields = ("title", "description") + raw_id_fields = ("column", "account", "contact", "opportunity") + filter_horizontal = ("assigned_to",) diff --git a/tasks/apps.py b/backend/tasks/apps.py similarity index 100% rename from tasks/apps.py rename to backend/tasks/apps.py diff --git a/tasks/celery_tasks.py b/backend/tasks/celery_tasks.py similarity index 100% rename from tasks/celery_tasks.py rename to backend/tasks/celery_tasks.py diff --git a/backend/tasks/migrations/0001_initial.py b/backend/tasks/migrations/0001_initial.py new file mode 100644 index 00000000..81da08da --- /dev/null +++ b/backend/tasks/migrations/0001_initial.py @@ -0,0 +1,548 @@ +# Generated by Django 5.2.8 on 2025-11-27 12:17 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("accounts", "0001_initial"), + ("common", "0001_initial"), + ("contacts", "0001_initial"), + ("opportunity", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Board", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ("is_archived", models.BooleanField(default=False)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="boards", + to="common.org", + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="owned_boards", + to="common.profile", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Board", + "verbose_name_plural": "Boards", + "db_table": "board", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="BoardColumn", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=100)), + ("order", models.PositiveIntegerField(default=0)), + ("color", models.CharField(default="#6B7280", max_length=7)), + ("limit", models.PositiveIntegerField(blank=True, null=True)), + ( + "board", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="columns", + to="tasks.board", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="board_columns", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Board Column", + "verbose_name_plural": "Board Columns", + "db_table": "board_column", + "ordering": ("order",), + }, + ), + migrations.CreateModel( + name="BoardMember", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "role", + models.CharField( + choices=[ + ("owner", "Owner"), + ("admin", "Admin"), + ("member", "Member"), + ], + default="member", + max_length=20, + ), + ), + ( + "board", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="memberships", + to="tasks.board", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="board_members", + to="common.org", + ), + ), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="board_memberships", + to="common.profile", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Board Member", + "verbose_name_plural": "Board Members", + "db_table": "board_member", + }, + ), + migrations.AddField( + model_name="board", + name="members", + field=models.ManyToManyField( + related_name="boards", through="tasks.BoardMember", to="common.profile" + ), + ), + migrations.CreateModel( + name="BoardTask", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ("order", models.PositiveIntegerField(default=0)), + ( + "priority", + models.CharField( + choices=[ + ("low", "Low"), + ("medium", "Medium"), + ("high", "High"), + ("urgent", "Urgent"), + ], + default="medium", + max_length=10, + ), + ), + ("due_date", models.DateTimeField(blank=True, null=True)), + ("completed_at", models.DateTimeField(blank=True, null=True)), + ( + "account", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="board_tasks", + to="accounts.account", + ), + ), + ( + "assigned_to", + models.ManyToManyField( + blank=True, + related_name="assigned_board_tasks", + to="common.profile", + ), + ), + ( + "column", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tasks", + to="tasks.boardcolumn", + ), + ), + ( + "contact", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="board_tasks", + to="contacts.contact", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "opportunity", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="board_tasks", + to="opportunity.opportunity", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="board_tasks", + to="common.org", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Board Task", + "verbose_name_plural": "Board Tasks", + "db_table": "board_task", + "ordering": ("order",), + }, + ), + migrations.CreateModel( + name="Task", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.CharField(max_length=200, verbose_name="title")), + ( + "status", + models.CharField( + choices=[ + ("New", "New"), + ("In Progress", "In Progress"), + ("Completed", "Completed"), + ], + max_length=50, + verbose_name="status", + ), + ), + ( + "priority", + models.CharField( + choices=[ + ("Low", "Low"), + ("Medium", "Medium"), + ("High", "High"), + ], + max_length=50, + verbose_name="priority", + ), + ), + ("due_date", models.DateField(blank=True, null=True)), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Notes"), + ), + ( + "account", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="accounts_tasks", + to="accounts.account", + ), + ), + ( + "assigned_to", + models.ManyToManyField( + related_name="users_tasks", to="common.profile" + ), + ), + ( + "contacts", + models.ManyToManyField( + related_name="contacts_tasks", to="contacts.contact" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tasks", + to="common.org", + ), + ), + ( + "teams", + models.ManyToManyField( + related_name="tasks_teams", to="common.teams" + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "verbose_name": "Task", + "verbose_name_plural": "Tasks", + "db_table": "task", + "ordering": ("-due_date",), + }, + ), + migrations.AddIndex( + model_name="boardcolumn", + index=models.Index( + fields=["org", "order"], name="board_colum_org_id_c9ff44_idx" + ), + ), + migrations.AlterUniqueTogether( + name="boardcolumn", + unique_together={("board", "name")}, + ), + migrations.AddIndex( + model_name="boardmember", + index=models.Index( + fields=["org", "-created_at"], name="board_membe_org_id_16f9e3_idx" + ), + ), + migrations.AlterUniqueTogether( + name="boardmember", + unique_together={("board", "profile")}, + ), + migrations.AddIndex( + model_name="board", + index=models.Index( + fields=["org", "-created_at"], name="board_org_id_07a8d8_idx" + ), + ), + migrations.AddIndex( + model_name="boardtask", + index=models.Index( + fields=["org", "order"], name="board_task_org_id_6055e2_idx" + ), + ), + migrations.AddIndex( + model_name="task", + index=models.Index(fields=["status"], name="task_status_e9a322_idx"), + ), + migrations.AddIndex( + model_name="task", + index=models.Index(fields=["due_date"], name="task_due_dat_6ddd76_idx"), + ), + migrations.AddIndex( + model_name="task", + index=models.Index( + fields=["org", "-created_at"], name="task_org_id_74b184_idx" + ), + ), + ] diff --git a/opportunity/__init__.py b/backend/tasks/migrations/__init__.py similarity index 100% rename from opportunity/__init__.py rename to backend/tasks/migrations/__init__.py diff --git a/backend/tasks/models.py b/backend/tasks/models.py new file mode 100644 index 00000000..69214b5c --- /dev/null +++ b/backend/tasks/models.py @@ -0,0 +1,268 @@ +import arrow +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from accounts.models import Account +from common.base import AssignableMixin, BaseModel +from common.models import Org, Profile, Teams +from contacts.models import Contact + +# ============================================================================= +# Kanban Board Models (merged from boards app) +# ============================================================================= + + +class Board(BaseModel): + """Kanban Board""" + + name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + owner = models.ForeignKey( + Profile, on_delete=models.CASCADE, related_name="owned_boards" + ) + members = models.ManyToManyField( + Profile, through="BoardMember", related_name="boards" + ) + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="boards") + is_archived = models.BooleanField(default=False) + + class Meta: + verbose_name = "Board" + verbose_name_plural = "Boards" + db_table = "board" + ordering = ("-created_at",) + indexes = [ + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return self.name + + +class BoardMember(BaseModel): + """Board membership with roles""" + + ROLE_CHOICES = ( + ("owner", "Owner"), + ("admin", "Admin"), + ("member", "Member"), + ) + + board = models.ForeignKey( + Board, on_delete=models.CASCADE, related_name="memberships" + ) + profile = models.ForeignKey( + Profile, on_delete=models.CASCADE, related_name="board_memberships" + ) + role = models.CharField(max_length=20, choices=ROLE_CHOICES, default="member") + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="board_members", + ) + + class Meta: + verbose_name = "Board Member" + verbose_name_plural = "Board Members" + db_table = "board_member" + unique_together = ("board", "profile") + indexes = [ + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return f"{self.profile} - {self.board} ({self.role})" + + def save(self, *args, **kwargs): + if not self.org_id and self.board_id: + self.org_id = self.board.org_id + super().save(*args, **kwargs) + + +class BoardColumn(BaseModel): + """Column in a board (e.g., To Do, In Progress, Done)""" + + board = models.ForeignKey(Board, on_delete=models.CASCADE, related_name="columns") + name = models.CharField(max_length=100) + order = models.PositiveIntegerField(default=0) + color = models.CharField(max_length=7, default="#6B7280") # Hex color + limit = models.PositiveIntegerField(null=True, blank=True) # WIP limit + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="board_columns", + ) + + class Meta: + verbose_name = "Board Column" + verbose_name_plural = "Board Columns" + db_table = "board_column" + ordering = ("order",) + unique_together = ("board", "name") + indexes = [ + models.Index(fields=["org", "order"]), + ] + + def __str__(self): + return f"{self.board.name} - {self.name}" + + def save(self, *args, **kwargs): + if not self.org_id and self.board_id: + self.org_id = self.board.org_id + super().save(*args, **kwargs) + + +class BoardTask(BaseModel): + """Task/Card in a board column""" + + PRIORITY_CHOICES = ( + ("low", "Low"), + ("medium", "Medium"), + ("high", "High"), + ("urgent", "Urgent"), + ) + + column = models.ForeignKey( + BoardColumn, on_delete=models.CASCADE, related_name="tasks" + ) + title = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + order = models.PositiveIntegerField(default=0) + priority = models.CharField( + max_length=10, choices=PRIORITY_CHOICES, default="medium" + ) + assigned_to = models.ManyToManyField( + Profile, related_name="assigned_board_tasks", blank=True + ) + due_date = models.DateTimeField(null=True, blank=True) + completed_at = models.DateTimeField(null=True, blank=True) + + # Link to CRM entities (optional) + account = models.ForeignKey( + Account, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="board_tasks", + ) + contact = models.ForeignKey( + Contact, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="board_tasks", + ) + opportunity = models.ForeignKey( + "opportunity.Opportunity", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="board_tasks", + ) + org = models.ForeignKey( + Org, + on_delete=models.CASCADE, + related_name="board_tasks", + ) + + class Meta: + verbose_name = "Board Task" + verbose_name_plural = "Board Tasks" + db_table = "board_task" + ordering = ("order",) + indexes = [ + models.Index(fields=["org", "order"]), + ] + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + if not self.org_id and self.column_id: + self.org_id = self.column.board.org_id + super().save(*args, **kwargs) + + def mark_complete(self): + """Mark task as completed""" + from django.utils import timezone + + self.completed_at = timezone.now() + self.save() + + def mark_incomplete(self): + """Mark task as incomplete""" + self.completed_at = None + self.save() + + @property + def is_completed(self): + return self.completed_at is not None + + @property + def is_overdue(self): + from django.utils import timezone + + if self.due_date and not self.is_completed: + return timezone.now() > self.due_date + return False + + +# ============================================================================= +# Original Task Model +# ============================================================================= + + +class Task(AssignableMixin, BaseModel): + + STATUS_CHOICES = ( + ("New", "New"), + ("In Progress", "In Progress"), + ("Completed", "Completed"), + ) + + PRIORITY_CHOICES = (("Low", "Low"), ("Medium", "Medium"), ("High", "High")) + + title = models.CharField(_("title"), max_length=200) + status = models.CharField(_("status"), max_length=50, choices=STATUS_CHOICES) + priority = models.CharField(_("priority"), max_length=50, choices=PRIORITY_CHOICES) + due_date = models.DateField(blank=True, null=True) + description = models.TextField(_("Notes"), blank=True, null=True) + account = models.ForeignKey( + Account, + related_name="accounts_tasks", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + contacts = models.ManyToManyField(Contact, related_name="contacts_tasks") + + assigned_to = models.ManyToManyField(Profile, related_name="users_tasks") + + # created_by = models.ForeignKey( + # Profile, + # related_name="task_created", + # blank=True, + # null=True, + # on_delete=models.SET_NULL, + # ) + teams = models.ManyToManyField(Teams, related_name="tasks_teams") + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="tasks") + + class Meta: + verbose_name = "Task" + verbose_name_plural = "Tasks" + db_table = "task" + ordering = ("-due_date",) + indexes = [ + models.Index(fields=["status"]), + models.Index(fields=["due_date"]), + models.Index(fields=["org", "-created_at"]), + ] + + def __str__(self): + return f"{self.title}" + + @property + def created_on_arrow(self): + return arrow.get(self.created_at).humanize() diff --git a/backend/tasks/serializer.py b/backend/tasks/serializer.py new file mode 100644 index 00000000..534fbc8a --- /dev/null +++ b/backend/tasks/serializer.py @@ -0,0 +1,224 @@ +from rest_framework import serializers + +from common.serializer import ( + AttachmentsSerializer, + CommentSerializer, + ProfileSerializer, + TeamsSerializer, + UserSerializer, +) +from contacts.serializer import ContactSerializer +from tasks.models import Board, BoardColumn, BoardMember, BoardTask, Task + +# ============================================================================= +# Kanban Board Serializers (merged from boards app) +# ============================================================================= + + +class BoardMemberSerializer(serializers.ModelSerializer): + """Serializer for board members""" + + profile = ProfileSerializer(read_only=True) + profile_id = serializers.UUIDField(write_only=True, required=False) + + class Meta: + model = BoardMember + fields = "__all__" + read_only_fields = ("id", "created_at", "updated_at", "board") + + +class BoardTaskSerializer(serializers.ModelSerializer): + """Serializer for board tasks""" + + assigned_to = ProfileSerializer(many=True, read_only=True) + assigned_to_ids = serializers.ListField( + child=serializers.UUIDField(), write_only=True, required=False + ) + is_completed = serializers.BooleanField(read_only=True) + is_overdue = serializers.BooleanField(read_only=True) + + class Meta: + model = BoardTask + fields = "__all__" + read_only_fields = ("id", "created_at", "updated_at", "completed_at") + + +class BoardColumnSerializer(serializers.ModelSerializer): + """Serializer for board columns""" + + tasks = BoardTaskSerializer(many=True, read_only=True) + task_count = serializers.SerializerMethodField() + + class Meta: + model = BoardColumn + fields = "__all__" + read_only_fields = ("id", "created_at", "updated_at", "board") + + def get_task_count(self, obj): + return obj.tasks.count() + + +class BoardColumnListSerializer(serializers.ModelSerializer): + """Simplified column serializer for lists""" + + task_count = serializers.SerializerMethodField() + + class Meta: + model = BoardColumn + fields = ["id", "name", "order", "color", "limit", "task_count"] + + def get_task_count(self, obj): + return obj.tasks.count() + + +class BoardSerializer(serializers.ModelSerializer): + """Serializer for boards""" + + owner = ProfileSerializer(read_only=True) + columns = BoardColumnListSerializer(many=True, read_only=True) + members = BoardMemberSerializer(source="memberships", many=True, read_only=True) + member_count = serializers.SerializerMethodField() + task_count = serializers.SerializerMethodField() + + class Meta: + model = Board + fields = "__all__" + read_only_fields = ( + "id", + "created_at", + "updated_at", + "created_by", + "updated_by", + "owner", + "org", + ) + + def get_member_count(self, obj): + return obj.members.count() + + def get_task_count(self, obj): + return BoardTask.objects.filter(column__board=obj).count() + + +class BoardListSerializer(serializers.ModelSerializer): + """Simplified board serializer for lists""" + + owner = ProfileSerializer(read_only=True) + member_count = serializers.SerializerMethodField() + column_count = serializers.SerializerMethodField() + task_count = serializers.SerializerMethodField() + + class Meta: + model = Board + fields = [ + "id", + "name", + "description", + "owner", + "is_archived", + "member_count", + "column_count", + "task_count", + "created_at", + "updated_at", + ] + + def get_member_count(self, obj): + return obj.members.count() + + def get_column_count(self, obj): + return obj.columns.count() + + def get_task_count(self, obj): + return BoardTask.objects.filter(column__board=obj).count() + + +# ============================================================================= +# Original Task Serializers +# ============================================================================= + + +class TaskSerializer(serializers.ModelSerializer): + created_by = UserSerializer() + assigned_to = ProfileSerializer(read_only=True, many=True) + contacts = ContactSerializer(read_only=True, many=True) + teams = TeamsSerializer(read_only=True, many=True) + task_attachment = AttachmentsSerializer(read_only=True, many=True) + task_comments = CommentSerializer(read_only=True, many=True) + + class Meta: + model = Task + fields = ( + "id", + "title", + "status", + "priority", + "due_date", + "account", + "created_by", + "created_at", + "contacts", + "teams", + "assigned_to", + "task_attachment", + "task_comments", + ) + + +class TaskCreateSerializer(serializers.ModelSerializer): + def __init__(self, *args, **kwargs): + request_obj = kwargs.pop("request_obj", None) + super().__init__(*args, **kwargs) + self.org = request_obj.profile.org + + self.fields["title"].required = True + + def validate_title(self, title): + if self.instance: + if ( + Task.objects.filter(title__iexact=title, org=self.org) + .exclude(id=self.instance.id) + .exists() + ): + raise serializers.ValidationError("Task already exists with this title") + else: + if Task.objects.filter(title__iexact=title, org=self.org).exists(): + raise serializers.ValidationError("Task already exists with this title") + return title + + class Meta: + model = Task + fields = ( + "id", + "title", + "status", + "priority", + "due_date", + "account", + "created_by", + "created_at", + ) + + +class TaskDetailEditSwaggerSerializer(serializers.Serializer): + comment = serializers.CharField() + task_attachment = serializers.FileField() + + +class TaskCommentEditSwaggerSerializer(serializers.Serializer): + comment = serializers.CharField() + + +class TaskCreateSwaggerSerializer(serializers.ModelSerializer): + class Meta: + model = Task + fields = ( + "title", + "status", + "priority", + "due_date", + "account", + "contacts", + "teams", + "assigned_to", + ) diff --git a/tasks/swagger_params1.py b/backend/tasks/swagger_params.py similarity index 63% rename from tasks/swagger_params1.py rename to backend/tasks/swagger_params.py index 494aef2e..c4d591ea 100644 --- a/tasks/swagger_params1.py +++ b/backend/tasks/swagger_params.py @@ -11,7 +11,7 @@ organization_params = [ task_list_get_params = [ organization_params_in_header, - OpenApiParameter("title", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("status", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("priority", OpenApiTypes.STR,OpenApiParameter.QUERY), + OpenApiParameter("title", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("status", OpenApiTypes.STR, OpenApiParameter.QUERY), + OpenApiParameter("priority", OpenApiTypes.STR, OpenApiParameter.QUERY), ] diff --git a/tasks/templates/tasks_email_template.html b/backend/tasks/templates/tasks_email_template.html similarity index 100% rename from tasks/templates/tasks_email_template.html rename to backend/tasks/templates/tasks_email_template.html diff --git a/tasks/tests_celery_tasks.py b/backend/tasks/tests_celery_tasks.py similarity index 100% rename from tasks/tests_celery_tasks.py rename to backend/tasks/tests_celery_tasks.py diff --git a/backend/tasks/urls.py b/backend/tasks/urls.py new file mode 100644 index 00000000..c4d33653 --- /dev/null +++ b/backend/tasks/urls.py @@ -0,0 +1,33 @@ +from django.urls import path + +from tasks import views + +app_name = "api_tasks" + +urlpatterns = [ + # Task endpoints + path("", views.TaskListView.as_view()), + path("/", views.TaskDetailView.as_view()), + path("comment//", views.TaskCommentView.as_view()), + path("attachment//", views.TaskAttachmentView.as_view()), +] + +# Board URLs (kept separate for namespace compatibility with frontend) +board_urlpatterns = [ + # Boards + path("", views.BoardListCreateView.as_view(), name="board_list_create"), + path("/", views.BoardDetailView.as_view(), name="board_detail"), + # Columns + path( + "/columns/", + views.BoardColumnListCreateView.as_view(), + name="column_list_create", + ), + # Tasks + path( + "columns//tasks/", + views.BoardTaskListCreateView.as_view(), + name="task_list_create", + ), + path("tasks//", views.BoardTaskDetailView.as_view(), name="task_detail"), +] diff --git a/tasks/utils.py b/backend/tasks/utils.py similarity index 100% rename from tasks/utils.py rename to backend/tasks/utils.py diff --git a/backend/tasks/views.py b/backend/tasks/views.py new file mode 100644 index 00000000..7565ac2b --- /dev/null +++ b/backend/tasks/views.py @@ -0,0 +1,891 @@ +import json + +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema +from rest_framework import status +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from accounts.models import Account +from accounts.serializer import AccountSerializer +from common.models import Attachments, Comment, Profile, Teams + +# from common.external_auth import CustomDualAuthentication +from common.serializer import ( + AttachmentsSerializer, + CommentSerializer, + ProfileSerializer, + TeamsSerializer, +) +from contacts.models import Contact +from contacts.serializer import ContactSerializer +from tasks import swagger_params +from tasks.models import Board, BoardColumn, BoardMember, BoardTask, Task +from tasks.serializer import ( + BoardColumnSerializer, + BoardListSerializer, + BoardMemberSerializer, + BoardSerializer, + BoardTaskSerializer, + TaskCommentEditSwaggerSerializer, + TaskCreateSerializer, + TaskCreateSwaggerSerializer, + TaskDetailEditSwaggerSerializer, + TaskSerializer, +) +from tasks.utils import PRIORITY_CHOICES, STATUS_CHOICES + + +class TaskListView(APIView, LimitOffsetPagination): + model = Task + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + + def get_context_data(self, **kwargs): + params = self.request.query_params + queryset = self.model.objects.filter(org=self.request.profile.org).order_by( + "-id" + ) + accounts = Account.objects.filter(org=self.request.profile.org) + contacts = Contact.objects.filter(org=self.request.profile.org) + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + queryset = queryset.filter( + Q(assigned_to__in=[self.request.profile]) + | Q(created_by=self.request.profile.user) + ) + accounts = accounts.filter( + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) + ).distinct() + contacts = contacts.filter( + Q(created_by=self.request.profile.user) + | Q(assigned_to=self.request.profile) + ).distinct() + + if params: + if params.get("title"): + queryset = queryset.filter(title__icontains=params.get("title")) + if params.get("status"): + queryset = queryset.filter(status=params.get("status")) + if params.get("priority"): + queryset = queryset.filter(priority=params.get("priority")) + context = {} + results_tasks = self.paginate_queryset( + queryset.distinct(), self.request, view=self + ) + tasks = TaskSerializer(results_tasks, many=True).data + if results_tasks: + offset = queryset.filter(id__gte=results_tasks[-1].id).count() + if offset == queryset.count(): + offset = None + else: + offset = 0 + context.update( + { + "tasks_count": self.count, + "offset": offset, + } + ) + context["tasks"] = tasks + context["status"] = STATUS_CHOICES + context["priority"] = PRIORITY_CHOICES + context["accounts_list"] = AccountSerializer(accounts, many=True).data + context["contacts_list"] = ContactSerializer(contacts, many=True).data + return context + + @extend_schema(tags=["Tasks"], parameters=swagger_params.task_list_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + @extend_schema( + tags=["Tasks"], + parameters=swagger_params.organization_params, + request=TaskCreateSwaggerSerializer, + ) + def post(self, request, *args, **kwargs): + params = request.data + serializer = TaskCreateSerializer(data=params, request_obj=request) + if serializer.is_valid(): + task_obj = serializer.save( + created_by=request.profile.user, + due_date=params.get("due_date"), + org=request.profile.org, + ) + if params.get("contacts"): + contacts_list = params.get("contacts") + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) + task_obj.contacts.add(*contacts) + + if params.get("teams"): + teams_list = params.get("teams") + teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) + task_obj.teams.add(*teams) + + if params.get("assigned_to"): + assinged_to_list = params.get("assigned_to") + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org, is_active=True + ) + task_obj.assigned_to.add(*profiles) + + return Response( + {"error": False, "message": "Task Created Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class TaskDetailView(APIView): + model = Task + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + + def get_object(self, pk): + return Task.objects.get(pk=pk, org=self.request.profile.org) + + def get_context_data(self, **kwargs): + context = {} + user_assgn_list = [ + assigned_to.id for assigned_to in self.task_obj.assigned_to.all() + ] + if self.request.profile == self.task_obj.created_by: + user_assgn_list.append(self.request.profile.id) + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + if self.request.profile.id not in user_assgn_list: + return Response( + { + "error": True, + "errors": "You don't have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + + task_content_type = ContentType.objects.get_for_model(Task) + comments = Comment.objects.filter( + content_type=task_content_type, + object_id=self.task_obj.id, + org=self.request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=task_content_type, + object_id=self.task_obj.id, + org=self.request.profile.org, + ).order_by("-id") + + assigned_data = self.task_obj.assigned_to.values("id", "user__email") + + if self.request.profile.is_admin or self.request.profile.role == "ADMIN": + users_mention = list( + Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).values("user__email") + ) + elif self.request.profile != self.task_obj.created_by: + users_mention = [{"username": self.task_obj.created_by.user.email}] + else: + users_mention = list(self.task_obj.assigned_to.all().values("user__email")) + if self.request.profile.role == "ADMIN" or self.request.profile.is_admin: + users = Profile.objects.filter( + is_active=True, org=self.request.profile.org + ).order_by("user__email") + else: + users = Profile.objects.filter( + role="ADMIN", org=self.request.profile.org + ).order_by("user__email") + + if self.request.profile == self.task_obj.created_by: + user_assgn_list.append(self.request.profile.id) + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + if self.request.profile.id not in user_assgn_list: + return Response( + { + "error": True, + "errors": "You don't have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + team_ids = [user.id for user in self.task_obj.get_team_users] + all_user_ids = users.values_list("id", flat=True) + users_excluding_team_id = set(all_user_ids) - set(team_ids) + users_excluding_team = Profile.objects.filter(id__in=users_excluding_team_id) + context.update( + { + "task_obj": TaskSerializer(self.task_obj).data, + "attachments": AttachmentsSerializer(attachments, many=True).data, + "comments": CommentSerializer(comments, many=True).data, + "users_mention": users_mention, + "assigned_data": assigned_data, + } + ) + context["users"] = ProfileSerializer(users, many=True).data + context["users_excluding_team"] = ProfileSerializer( + users_excluding_team, many=True + ).data + context["teams"] = TeamsSerializer(Teams.objects.all(), many=True).data + return context + + @extend_schema(tags=["Tasks"], parameters=swagger_params.organization_params) + def get(self, request, pk, **kwargs): + self.task_obj = self.get_object(pk) + context = self.get_context_data(**kwargs) + return Response(context) + + @extend_schema( + tags=["Tasks"], + parameters=swagger_params.organization_params, + request=TaskDetailEditSwaggerSerializer, + ) + def post(self, request, pk, **kwargs): + params = request.data + context = {} + self.task_obj = Task.objects.get(pk=pk) + if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: + if not ( + (self.request.profile == self.task_obj.created_by) + or (self.request.profile in self.task_obj.assigned_to.all()) + ): + return Response( + { + "error": True, + "errors": "You don't have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + comment_serializer = CommentSerializer(data=params) + if comment_serializer.is_valid(): + if params.get("comment"): + comment_serializer.save( + task_id=self.task_obj.id, + commented_by_id=self.request.profile.id, + ) + + if self.request.FILES.get("task_attachment"): + attachment = Attachments() + attachment.created_by = self.request.profile.user + attachment.file_name = self.request.FILES.get("task_attachment").name + attachment.task = self.task_obj + attachment.attachment = self.request.FILES.get("task_attachment") + attachment.save() + + task_content_type = ContentType.objects.get_for_model(Task) + comments = Comment.objects.filter( + content_type=task_content_type, + object_id=self.task_obj.id, + org=self.request.profile.org, + ).order_by("-id") + attachments = Attachments.objects.filter( + content_type=task_content_type, + object_id=self.task_obj.id, + org=self.request.profile.org, + ).order_by("-id") + context.update( + { + "task_obj": TaskSerializer(self.task_obj).data, + "attachments": AttachmentsSerializer(attachments, many=True).data, + "comments": CommentSerializer(comments, many=True).data, + } + ) + return Response(context) + + @extend_schema( + tags=["Tasks"], + parameters=swagger_params.organization_params, + request=TaskCreateSwaggerSerializer, + ) + def put(self, request, pk, **kwargs): + params = request.data + self.task_obj = self.get_object(pk) + serializer = TaskCreateSerializer( + data=params, + instance=self.task_obj, + request_obj=request, + ) + if serializer.is_valid(): + task_obj = serializer.save() + previous_assigned_to_users = list( + task_obj.assigned_to.all().values_list("id", flat=True) + ) + task_obj.contacts.clear() + if params.get("contacts"): + contacts_list = params.get("contacts") + contacts = Contact.objects.filter( + id__in=contacts_list, org=request.profile.org + ) + task_obj.contacts.add(*contacts) + + task_obj.teams.clear() + if params.get("teams"): + teams_list = params.get("teams") + teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) + task_obj.teams.add(*teams) + + task_obj.assigned_to.clear() + if params.get("assigned_to"): + assinged_to_list = params.get("assigned_to") + profiles = Profile.objects.filter( + id__in=assinged_to_list, org=request.profile.org, is_active=True + ) + task_obj.assigned_to.add(*profiles) + + return Response( + {"error": False, "message": "Task updated Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + @extend_schema(tags=["Tasks"], parameters=swagger_params.organization_params) + def delete(self, request, pk, **kwargs): + self.object = self.get_object(pk) + if ( + request.profile.role == "ADMIN" + or request.profile.is_admin + or request.profile == self.object.created_by + ): + self.object.delete() + return Response( + {"error": False, "message": "Task deleted Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": "you don't have permission to delete this task"}, + status=status.HTTP_403_FORBIDDEN, + ) + + +class TaskCommentView(APIView): + model = Comment + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + + def get_object(self, pk): + return self.model.objects.get(pk=pk, org=self.request.profile.org) + + @extend_schema( + tags=["Tasks"], + parameters=swagger_params.organization_params, + request=TaskCommentEditSwaggerSerializer, + ) + def put(self, request, pk, format=None): + params = request.data + obj = self.get_object(pk) + if ( + request.profile.role == "ADMIN" + or request.profile.is_admin + or request.profile == obj.commented_by + ): + serializer = CommentSerializer(obj, data=params) + if params.get("comment"): + if serializer.is_valid(): + serializer.save() + return Response( + {"error": False, "message": "Comment Submitted"}, + status=status.HTTP_200_OK, + ) + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response( + { + "error": True, + "errors": "You don't have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + + @extend_schema(tags=["Tasks"], parameters=swagger_params.organization_params) + def delete(self, request, pk, format=None): + self.object = self.get_object(pk) + if ( + request.profile.role == "ADMIN" + or request.profile.is_admin + or request.profile == self.object.commented_by + ): + self.object.delete() + return Response( + {"error": False, "message": "Comment Deleted Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + { + "error": True, + "errors": "You don't have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + + +class TaskAttachmentView(APIView): + model = Attachments + # authentication_classes = (CustomDualAuthentication,) + permission_classes = (IsAuthenticated,) + + @extend_schema(tags=["Tasks"], parameters=swagger_params.organization_params) + def delete(self, request, pk, format=None): + self.object = self.model.objects.get(pk=pk) + if ( + request.profile.role == "ADMIN" + or request.profile.is_admin + or request.profile == self.object.created_by + ): + self.object.delete() + return Response( + {"error": False, "message": "Attachment Deleted Successfully"}, + status=status.HTTP_200_OK, + ) + return Response( + { + "error": True, + "errors": "You don't have Permission to perform this action", + }, + status=status.HTTP_403_FORBIDDEN, + ) + + +# ============================================================================= +# Kanban Board Views (merged from boards app) +# ============================================================================= + + +class BoardListCreateView(APIView, LimitOffsetPagination): + """List all boards or create a new board""" + + permission_classes = (IsAuthenticated,) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ), + OpenApiParameter( + name="search", + description="Search in board name", + required=False, + type=str, + ), + OpenApiParameter( + name="archived", + description="Filter archived boards", + required=False, + type=bool, + ), + ], + responses={200: BoardListSerializer(many=True)}, + ) + def get(self, request): + """List all boards for the user's organization""" + org = request.profile.org + user_profile = request.profile + + # Get boards where user is owner or member + queryset = ( + Board.objects.filter( + Q(org=org) & (Q(owner=user_profile) | Q(members=user_profile)) + ) + .distinct() + .order_by("-created_at") + ) + + # Filtering + archived = request.query_params.get("archived") + if archived is not None: + is_archived = archived.lower() in ["true", "1", "yes"] + queryset = queryset.filter(is_archived=is_archived) + + search = request.query_params.get("search") + if search: + queryset = queryset.filter(name__icontains=search) + + # Pagination + results = self.paginate_queryset(queryset, request, view=self) + serializer = BoardListSerializer(results, many=True) + + return Response( + { + "count": self.count, + "next": self.get_next_link(), + "previous": self.get_previous_link(), + "results": serializer.data, + } + ) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + request=BoardSerializer, + responses={201: BoardSerializer}, + ) + def post(self, request): + """Create a new board with default columns""" + org = request.profile.org + user_profile = request.profile + data = request.data.copy() + + serializer = BoardSerializer(data=data) + if not serializer.is_valid(): + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Create board + board = serializer.save(org=org, owner=user_profile, created_by=request.user) + + # Add owner as member with owner role + BoardMember.objects.create(board=board, profile=user_profile, role="owner") + + # Create default columns if requested + if request.data.get("create_default_columns", True): + default_columns = [ + {"name": "To Do", "order": 1, "color": "#EF4444"}, + {"name": "In Progress", "order": 2, "color": "#F59E0B"}, + {"name": "Done", "order": 3, "color": "#10B981"}, + ] + for col_data in default_columns: + BoardColumn.objects.create(board=board, **col_data) + + return Response(BoardSerializer(board).data, status=status.HTTP_201_CREATED) + + +class BoardDetailView(APIView): + """Retrieve, update or delete a board""" + + permission_classes = (IsAuthenticated,) + + def get_object(self, pk, org, user_profile): + """Get board if user has access""" + board = get_object_or_404(Board, pk=pk, org=org) + # Check if user is owner or member + if ( + board.owner != user_profile + and not board.members.filter(id=user_profile.id).exists() + ): + return None + return board + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + responses={200: BoardSerializer}, + ) + def get(self, request, pk): + """Get board details with columns and tasks""" + board = self.get_object(pk, request.profile.org, request.profile) + if not board: + return Response( + {"error": "Board not found or access denied"}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = BoardSerializer(board) + return Response(serializer.data) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + request=BoardSerializer, + responses={200: BoardSerializer}, + ) + def put(self, request, pk): + """Update board""" + board = self.get_object(pk, request.profile.org, request.profile) + if not board: + return Response( + {"error": "Board not found or access denied"}, + status=status.HTTP_404_NOT_FOUND, + ) + + # Only owner or admin can update board + membership = BoardMember.objects.filter( + board=board, profile=request.profile + ).first() + if not membership or membership.role not in ["owner", "admin"]: + return Response( + {"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN + ) + + serializer = BoardSerializer(board, data=request.data, partial=True) + if not serializer.is_valid(): + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + board = serializer.save(updated_by=request.user) + return Response(BoardSerializer(board).data) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + responses={204: None}, + ) + def delete(self, request, pk): + """Delete board (owner only)""" + board = self.get_object(pk, request.profile.org, request.profile) + if not board: + return Response( + {"error": "Board not found or access denied"}, + status=status.HTTP_404_NOT_FOUND, + ) + + # Only owner can delete + if board.owner != request.profile: + return Response( + {"error": "Only board owner can delete the board"}, + status=status.HTTP_403_FORBIDDEN, + ) + + board.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class BoardColumnListCreateView(APIView): + """List or create columns for a board""" + + permission_classes = (IsAuthenticated,) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + responses={200: BoardColumnSerializer(many=True)}, + ) + def get(self, request, board_pk): + """List all columns for a board""" + org = request.profile.org + board = get_object_or_404(Board, pk=board_pk, org=org) + + # Check access + if ( + board.owner != request.profile + and not board.members.filter(id=request.profile.id).exists() + ): + return Response( + {"error": "Board not found or access denied"}, + status=status.HTTP_404_NOT_FOUND, + ) + + columns = board.columns.all() + serializer = BoardColumnSerializer(columns, many=True) + return Response(serializer.data) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + request=BoardColumnSerializer, + responses={201: BoardColumnSerializer}, + ) + def post(self, request, board_pk): + """Create a new column""" + org = request.profile.org + board = get_object_or_404(Board, pk=board_pk, org=org) + + # Check permission + membership = BoardMember.objects.filter( + board=board, profile=request.profile + ).first() + if not membership or membership.role not in ["owner", "admin"]: + return Response( + {"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN + ) + + data = request.data.copy() + serializer = BoardColumnSerializer(data=data) + if not serializer.is_valid(): + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + column = serializer.save(board=board, created_by=request.user) + return Response( + BoardColumnSerializer(column).data, status=status.HTTP_201_CREATED + ) + + +class BoardTaskListCreateView(APIView): + """List or create tasks for a column""" + + permission_classes = (IsAuthenticated,) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + responses={200: BoardTaskSerializer(many=True)}, + ) + def get(self, request, column_pk): + """List all tasks for a column""" + org = request.profile.org + column = get_object_or_404(BoardColumn, pk=column_pk, board__org=org) + + # Check access + board = column.board + if ( + board.owner != request.profile + and not board.members.filter(id=request.profile.id).exists() + ): + return Response( + {"error": "Access denied"}, status=status.HTTP_404_NOT_FOUND + ) + + tasks = column.tasks.all() + serializer = BoardTaskSerializer(tasks, many=True) + return Response(serializer.data) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + request=BoardTaskSerializer, + responses={201: BoardTaskSerializer}, + ) + def post(self, request, column_pk): + """Create a new task""" + org = request.profile.org + column = get_object_or_404(BoardColumn, pk=column_pk, board__org=org) + + # Check access + board = column.board + membership = BoardMember.objects.filter( + board=board, profile=request.profile + ).first() + if not membership: + return Response( + {"error": "Access denied"}, status=status.HTTP_403_FORBIDDEN + ) + + data = request.data.copy() + serializer = BoardTaskSerializer(data=data) + if not serializer.is_valid(): + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + task = serializer.save(column=column, created_by=request.user) + + # Handle assigned_to + if "assigned_to_ids" in data: + profiles = Profile.objects.filter(id__in=data["assigned_to_ids"], org=org) + task.assigned_to.set(profiles) + + return Response(BoardTaskSerializer(task).data, status=status.HTTP_201_CREATED) + + +class BoardTaskDetailView(APIView): + """Retrieve, update or delete a task""" + + permission_classes = (IsAuthenticated,) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + request=BoardTaskSerializer, + responses={200: BoardTaskSerializer}, + ) + def put(self, request, pk): + """Update task (including moving to different column)""" + org = request.profile.org + task = get_object_or_404(BoardTask, pk=pk, column__board__org=org) + + # Check access + board = task.column.board + membership = BoardMember.objects.filter( + board=board, profile=request.profile + ).first() + if not membership: + return Response( + {"error": "Access denied"}, status=status.HTTP_403_FORBIDDEN + ) + + data = request.data.copy() + serializer = BoardTaskSerializer(task, data=data, partial=True) + if not serializer.is_valid(): + return Response( + {"error": True, "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + task = serializer.save(updated_by=request.user) + + # Handle assigned_to + if "assigned_to_ids" in data: + profiles = Profile.objects.filter(id__in=data["assigned_to_ids"], org=org) + task.assigned_to.set(profiles) + + return Response(BoardTaskSerializer(task).data) + + @extend_schema( + tags=["Boards"], + parameters=[ + OpenApiParameter( + name="org", description="Organization ID", required=True, type=str + ) + ], + responses={204: None}, + ) + def delete(self, request, pk): + """Delete task""" + org = request.profile.org + task = get_object_or_404(BoardTask, pk=pk, column__board__org=org) + + # Check permission + board = task.column.board + membership = BoardMember.objects.filter( + board=board, profile=request.profile + ).first() + if not membership or membership.role not in ["owner", "admin", "member"]: + return Response( + {"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN + ) + + task.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/templates/assigned_to/account_assigned.html b/backend/templates/assigned_to/account_assigned.html similarity index 100% rename from templates/assigned_to/account_assigned.html rename to backend/templates/assigned_to/account_assigned.html diff --git a/templates/assigned_to/cases_assigned.html b/backend/templates/assigned_to/cases_assigned.html similarity index 100% rename from templates/assigned_to/cases_assigned.html rename to backend/templates/assigned_to/cases_assigned.html diff --git a/templates/assigned_to/contact_assigned.html b/backend/templates/assigned_to/contact_assigned.html similarity index 100% rename from templates/assigned_to/contact_assigned.html rename to backend/templates/assigned_to/contact_assigned.html diff --git a/templates/assigned_to/leads_assigned.html b/backend/templates/assigned_to/leads_assigned.html similarity index 100% rename from templates/assigned_to/leads_assigned.html rename to backend/templates/assigned_to/leads_assigned.html diff --git a/templates/assigned_to/opportunity_assigned.html b/backend/templates/assigned_to/opportunity_assigned.html similarity index 100% rename from templates/assigned_to/opportunity_assigned.html rename to backend/templates/assigned_to/opportunity_assigned.html diff --git a/templates/common/user_activation_status.html b/backend/templates/common/user_activation_status.html similarity index 100% rename from templates/common/user_activation_status.html rename to backend/templates/common/user_activation_status.html diff --git a/templates/healthz.html b/backend/templates/healthz.html similarity index 100% rename from templates/healthz.html rename to backend/templates/healthz.html diff --git a/templates/registration/password_reset_email.html b/backend/templates/registration/password_reset_email.html similarity index 100% rename from templates/registration/password_reset_email.html rename to backend/templates/registration/password_reset_email.html diff --git a/templates/root_email_template.html b/backend/templates/root_email_template.html similarity index 100% rename from templates/root_email_template.html rename to backend/templates/root_email_template.html diff --git a/templates/root_email_template_new.html b/backend/templates/root_email_template_new.html similarity index 100% rename from templates/root_email_template_new.html rename to backend/templates/root_email_template_new.html diff --git a/cases/migrations/0001_initial.py b/cases/migrations/0001_initial.py deleted file mode 100644 index accb8d22..00000000 --- a/cases/migrations/0001_initial.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Case', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=64, verbose_name='Name')), - ('status', models.CharField(choices=[('New', 'New'), ('Assigned', 'Assigned'), ('Pending', 'Pending'), ('Closed', 'Closed'), ('Rejected', 'Rejected'), ('Duplicate', 'Duplicate')], max_length=64)), - ('priority', models.CharField(choices=[('Low', 'Low'), ('Normal', 'Normal'), ('High', 'High'), ('Urgent', 'Urgent')], max_length=64)), - ('case_type', models.CharField(blank=True, choices=[('Question', 'Question'), ('Incident', 'Incident'), ('Problem', 'Problem')], default='', max_length=255, null=True)), - ('closed_on', models.DateField()), - ('description', models.TextField(blank=True, null=True)), - ('is_active', models.BooleanField(default=False)), - ('account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_cases', to='accounts.account')), - ], - options={ - 'verbose_name': 'Case', - 'verbose_name_plural': 'Cases', - 'db_table': 'case', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/cases/migrations/0002_initial.py b/cases/migrations/0002_initial.py deleted file mode 100644 index 36256f5a..00000000 --- a/cases/migrations/0002_initial.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('teams', '0001_initial'), - ('contacts', '0001_initial'), - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('cases', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='case', - name='assigned_to', - field=models.ManyToManyField(related_name='case_assigned_users', to='common.profile'), - ), - migrations.AddField( - model_name='case', - name='contacts', - field=models.ManyToManyField(to='contacts.contact'), - ), - migrations.AddField( - model_name='case', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='case_created_by', to='common.profile'), - ), - migrations.AddField( - model_name='case', - name='org', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='case_org', to='common.org'), - ), - migrations.AddField( - model_name='case', - name='teams', - field=models.ManyToManyField(related_name='cases_teams', to='teams.teams'), - ), - migrations.AddField( - model_name='case', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - ] diff --git a/cases/migrations/0003_alter_case_created_by.py b/cases/migrations/0003_alter_case_created_by.py deleted file mode 100644 index 350a4bfd..00000000 --- a/cases/migrations/0003_alter_case_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-31 12:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('cases', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='case', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/cases/models.py b/cases/models.py deleted file mode 100644 index eaf8dc72..00000000 --- a/cases/models.py +++ /dev/null @@ -1,122 +0,0 @@ -import arrow -from django.contrib.contenttypes.models import ContentType -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import pgettext_lazy - -from accounts.models import Account -from common.models import Org, Profile -from common.base import BaseModel -from common.utils import CASE_TYPE, PRIORITY_CHOICE, STATUS_CHOICE -from contacts.models import Contact -from planner.models import PlannerEvent as Event -from teams.models import Teams - - -class Case(BaseModel): - name = models.CharField(pgettext_lazy("Name of the case", "Name"), max_length=64) - status = models.CharField(choices=STATUS_CHOICE, max_length=64) - priority = models.CharField(choices=PRIORITY_CHOICE, max_length=64) - case_type = models.CharField( - choices=CASE_TYPE, max_length=255, blank=True, null=True, default="" - ) - account = models.ForeignKey( - Account, - on_delete=models.CASCADE, - blank=True, - null=True, - related_name="accounts_cases", - ) - contacts = models.ManyToManyField(Contact) - # closed_on = models.DateTimeField() - closed_on = models.DateField() - description = models.TextField(blank=True, null=True) - assigned_to = models.ManyToManyField(Profile, related_name="case_assigned_users") - # created_by = models.ForeignKey( - # Profile, related_name="case_created_by", on_delete=models.SET_NULL, null=True - # ) - is_active = models.BooleanField(default=False) - teams = models.ManyToManyField(Teams, related_name="cases_teams") - org = models.ForeignKey( - Org, on_delete=models.SET_NULL, null=True, blank=True, related_name="case_org" - ) - - class Meta: - verbose_name = "Case" - verbose_name_plural = "Cases" - db_table = "case" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.name}" - - @property - def get_team_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return Profile.objects.filter(id__in=team_user_ids) - - @property - def get_team_and_assigned_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = team_user_ids + assigned_user_ids - return Profile.objects.filter(id__in=user_ids) - - @property - def get_assigned_users_not_in_teams(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = set(assigned_user_ids) - set(team_user_ids) - return Profile.objects.filter(id__in=list(user_ids)) - - def get_meetings(self): - content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter( - content_type=content_type, - object_id=self.id, - event_type="Meeting", - status="Planned", - ) - - def get_completed_meetings(self): - content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter( - content_type=content_type, object_id=self.id, event_type="Meeting" - ).exclude(status="Planned") - - def get_tasks(self): - content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter( - content_type=content_type, - object_id=self.id, - event_type="Task", - status="Planned", - ) - - def get_completed_tasks(self): - content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter( - content_type=content_type, object_id=self.id, event_type="Task" - ).exclude(status="Planned") - - def get_calls(self): - content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter( - content_type=content_type, - object_id=self.id, - event_type="Call", - status="Planned", - ) - - def get_completed_calls(self): - content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter( - content_type=content_type, object_id=self.id, event_type="Call" - ).exclude(status="Planned") - - def get_assigned_user(self): - return Profile.objects.get(id=self.assigned_to_id) - - @property - def created_on_arrow(self): - return arrow.get(self.created_at).humanize() diff --git a/cases/urls.py b/cases/urls.py deleted file mode 100644 index 2467a6eb..00000000 --- a/cases/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path - -from cases import views - -app_name = "api_cases" - -urlpatterns = [ - path("", views.CaseListView.as_view()), - path("/", views.CaseDetailView.as_view()), - path("comment//", views.CaseCommentView.as_view()), - path("attachment//", views.CaseAttachmentView.as_view()), -] diff --git a/cms/admin.py b/cms/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/cms/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/cms/apps.py b/cms/apps.py deleted file mode 100644 index 80bf9e5c..00000000 --- a/cms/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class CmsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "cms" diff --git a/cms/blocks.py b/cms/blocks.py deleted file mode 100644 index d4051818..00000000 --- a/cms/blocks.py +++ /dev/null @@ -1,221 +0,0 @@ -from django.db import models -from modelcluster.models import ClusterableModel -from wagtail.blocks import CharBlock, ListBlock, RichTextBlock, TextBlock -from wagtail import blocks -from wagtail.blocks import StructBlock -from wagtail.models import Orderable -from wagtail.fields import RichTextField -from wagtail.images.blocks import ImageChooserBlock -from wagtail.snippets.blocks import SnippetChooserBlock -from wagtail.snippets.models import register_snippet - - -class RightImageLeftText(blocks.StructBlock): - heading = blocks.CharBlock(required=False) - paragraph = blocks.RichTextBlock(required=False) - image = ImageChooserBlock() - links = blocks.ListBlock( - blocks.StructBlock( - [ - ("cta_url", blocks.URLBlock(required=False)), - ("cta_page", blocks.PageChooserBlock(required=False)), - ( - "cta_text", - blocks.CharBlock(required=False, default="Submit", max_length="50"), - ), - ("cta_image", ImageChooserBlock(required=False)), - ] - ) - ) - - class Meta: - template = "cms/streams/right_image_left_content.html" - icon = "image" - label = "Right Image Left Content" - - -class AboutApp(blocks.StructBlock): - heading = blocks.CharBlock(required=False) - paragraph = blocks.RichTextBlock(required=False) - image = ImageChooserBlock() - sub_heading = blocks.CharBlock(required=False) - sub_paragraph = blocks.RichTextBlock(required=False) - cta_url = blocks.URLBlock(required=False) - cta_page = blocks.PageChooserBlock(required=False) - cta_text = blocks.CharBlock(required=False, default="Submit", max_length="50") - links = blocks.ListBlock( - blocks.StructBlock( - [ - ("text", blocks.CharBlock(required=False)), - ] - ) - ) - - class Meta: - template = "cms/streams/aboutapp.html" - icon = "image" - label = "About App" - - -class Count(blocks.StructBlock): - links = blocks.ListBlock( - blocks.StructBlock( - [ - ("count", blocks.CharBlock(required=False, default=85)), - ("count_value", blocks.CharBlock(required=False)), - ("text", blocks.CharBlock(required=False)), - ] - ) - ) - - class Meta: - template = "cms/streams/count.html" - icon = "image" - label = "Count" - - -class AppFeatures(blocks.StructBlock): - heading = blocks.CharBlock(required=False) - description = blocks.RichTextBlock(required=False) - cards = blocks.ListBlock( - blocks.StructBlock( - [ - ("icon", blocks.CharBlock(required=False)), - ("card_heading", blocks.CharBlock(required=False)), - ("card_description", blocks.CharBlock(required=False)), - ] - ) - ) - - class Meta: - template = "cms/streams/app_features.html" - icon = "image" - label = "App Features" - - -class Pricing(blocks.StructBlock): - heading = blocks.CharBlock(required=False) - description = blocks.RichTextBlock(required=False) - cards = blocks.ListBlock( - blocks.StructBlock( - [ - ("card_heading", blocks.CharBlock(required=False)), - ("card_price", blocks.CharBlock(required=False)), - ("card_description", blocks.RichTextBlock(required=False)), - ("cta_url", blocks.URLBlock(required=False)), - ("cta_page", blocks.PageChooserBlock(required=False)), - ( - "cta_text", - blocks.CharBlock( - required=False, default="Choose Plan", max_length="50" - ), - ), - ] - ) - ) - - class Meta: - template = "cms/streams/pricing.html" - icon = "image" - label = "Pricing" - - -class Carousel(blocks.StructBlock): - sliders = blocks.ListBlock( - blocks.StructBlock( - [ - ("description", blocks.RichTextBlock(required=False)), - ("image", ImageChooserBlock()), - ("author", blocks.CharBlock(required=False)), - ("role", blocks.CharBlock(required=False)), - ] - ) - ) - - class Meta: - template = "cms/streams/carousel.html" - icon = "image" - label = "Carousel" - - -class LeftImageRightText(blocks.StructBlock): - heading = blocks.CharBlock(required=False) - paragraph = blocks.RichTextBlock(required=False) - image = ImageChooserBlock() - links = blocks.ListBlock( - blocks.StructBlock( - [ - ("cta_url", blocks.URLBlock(required=False)), - ("cta_page", blocks.PageChooserBlock(required=False)), - ( - "cta_text", - blocks.CharBlock(required=False, default="Submit", max_length="50"), - ), - ("cta_image", ImageChooserBlock(required=False)), - ] - ) - ) - - class Meta: - template = "cms/streams/left_image_right_content.html" - icon = "image" - label = "Left Image Right Content" - - -class HomeBlog(blocks.StructBlock): - heading = blocks.CharBlock(required=False) - description = blocks.RichTextBlock(required=False) - cards = blocks.ListBlock( - blocks.StructBlock( - [ - ("image", ImageChooserBlock()), - ("blog_tag", blocks.CharBlock(required=False)), - ("blog_heading", blocks.CharBlock(required=False)), - ("cta_url", blocks.URLBlock(required=False)), - ("cta_page", blocks.PageChooserBlock(required=False)), - ( - "cta_text", - blocks.CharBlock( - required=False, default="Read More", max_length="50" - ), - ), - ] - ) - ) - - class Meta: - template = "cms/streams/home_blog.html" - icon = "image" - label = "Home Blog" - - -class BlogContentTitleBlock(CharBlock): - title = CharBlock(required=True, help_text="add your title") - - class Meta: - template = "cms/streams/blog/content_title.html" - label = "Content Title" - - -class BlogCodeBlock(TextBlock): - code = CharBlock(required=True, help_text="add your title") - - class Meta: - template = "cms/streams/blog/code_block.html" - label = "Code Block" - - -class BlogRichTextBlock(RichTextBlock): - text = CharBlock(required=True, help_text="add your title") - - class Meta: - template = "cms/streams/blog/rich_text.html" - label = "Rich Text" - - -class BlogImageBlock(ImageChooserBlock): - image = ImageChooserBlock(required=True, help_text="add your title") - - class Meta: - template = "cms/streams/blog/image.html" - label = "Image" diff --git a/cms/migrations/0001_initial.py b/cms/migrations/0001_initial.py deleted file mode 100644 index 4d054cb5..00000000 --- a/cms/migrations/0001_initial.py +++ /dev/null @@ -1,116 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -import cms.blocks -from django.db import migrations, models -import django.db.models.deletion -import django_extensions.db.fields -import modelcluster.fields -import wagtail.blocks -import wagtail.contrib.routable_page.models -import wagtail.fields -import wagtail.images.blocks - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('wagtailcore', '0083_workflowcontenttype'), - ('wagtailimages', '0025_alter_image_file_alter_rendition_file'), - ] - - operations = [ - migrations.CreateModel( - name='BlogDetailPage', - fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ('excerpt', wagtail.fields.RichTextField()), - ('content', wagtail.fields.StreamField([('content_title', cms.blocks.BlogContentTitleBlock()), ('code', cms.blocks.BlogCodeBlock()), ('rich_text', cms.blocks.BlogRichTextBlock()), ('image', cms.blocks.BlogImageBlock())], null=True, use_json_field=True)), - ], - options={ - 'abstract': False, - }, - bases=('wagtailcore.page',), - ), - migrations.CreateModel( - name='BlogListPage', - fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ('intro', wagtail.fields.RichTextField(blank=True)), - ], - options={ - 'abstract': False, - }, - bases=(wagtail.contrib.routable_page.models.RoutablePageMixin, 'wagtailcore.page'), - ), - migrations.CreateModel( - name='HomePage', - fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ('content', wagtail.fields.StreamField([('right_image_left_content', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(required=False)), ('paragraph', wagtail.blocks.RichTextBlock(required=False)), ('image', wagtail.images.blocks.ImageChooserBlock()), ('links', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('cta_url', wagtail.blocks.URLBlock(required=False)), ('cta_page', wagtail.blocks.PageChooserBlock(required=False)), ('cta_text', wagtail.blocks.CharBlock(default='Submit', max_length='50', required=False)), ('cta_image', wagtail.images.blocks.ImageChooserBlock(required=False))])))])), ('aboutapp', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(required=False)), ('paragraph', wagtail.blocks.RichTextBlock(required=False)), ('image', wagtail.images.blocks.ImageChooserBlock()), ('sub_heading', wagtail.blocks.CharBlock(required=False)), ('sub_paragraph', wagtail.blocks.RichTextBlock(required=False)), ('cta_url', wagtail.blocks.URLBlock(required=False)), ('cta_page', wagtail.blocks.PageChooserBlock(required=False)), ('cta_text', wagtail.blocks.CharBlock(default='Submit', max_length='50', required=False)), ('links', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('text', wagtail.blocks.CharBlock(required=False))])))])), ('count', wagtail.blocks.StructBlock([('links', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('count', wagtail.blocks.CharBlock(default=85, required=False)), ('count_value', wagtail.blocks.CharBlock(required=False)), ('text', wagtail.blocks.CharBlock(required=False))])))])), ('appfeatures', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(required=False)), ('description', wagtail.blocks.RichTextBlock(required=False)), ('cards', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('icon', wagtail.blocks.CharBlock(required=False)), ('card_heading', wagtail.blocks.CharBlock(required=False)), ('card_description', wagtail.blocks.CharBlock(required=False))])))])), ('pricing', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(required=False)), ('description', wagtail.blocks.RichTextBlock(required=False)), ('cards', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('card_heading', wagtail.blocks.CharBlock(required=False)), ('card_price', wagtail.blocks.CharBlock(required=False)), ('card_description', wagtail.blocks.RichTextBlock(required=False)), ('cta_url', wagtail.blocks.URLBlock(required=False)), ('cta_page', wagtail.blocks.PageChooserBlock(required=False)), ('cta_text', wagtail.blocks.CharBlock(default='Choose Plan', max_length='50', required=False))])))])), ('carousel', wagtail.blocks.StructBlock([('sliders', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('description', wagtail.blocks.RichTextBlock(required=False)), ('image', wagtail.images.blocks.ImageChooserBlock()), ('author', wagtail.blocks.CharBlock(required=False)), ('role', wagtail.blocks.CharBlock(required=False))])))])), ('leftimagerighttext', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(required=False)), ('paragraph', wagtail.blocks.RichTextBlock(required=False)), ('image', wagtail.images.blocks.ImageChooserBlock()), ('links', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('cta_url', wagtail.blocks.URLBlock(required=False)), ('cta_page', wagtail.blocks.PageChooserBlock(required=False)), ('cta_text', wagtail.blocks.CharBlock(default='Submit', max_length='50', required=False)), ('cta_image', wagtail.images.blocks.ImageChooserBlock(required=False))])))])), ('homeblog', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(required=False)), ('description', wagtail.blocks.RichTextBlock(required=False)), ('cards', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('blog_tag', wagtail.blocks.CharBlock(required=False)), ('blog_heading', wagtail.blocks.CharBlock(required=False)), ('cta_url', wagtail.blocks.URLBlock(required=False)), ('cta_page', wagtail.blocks.PageChooserBlock(required=False)), ('cta_text', wagtail.blocks.CharBlock(default='Read More', max_length='50', required=False))])))]))], null=True, use_json_field=True)), - ], - options={ - 'abstract': False, - }, - bases=('wagtailcore.page',), - ), - migrations.CreateModel( - name='Menu', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='link_title')), - ('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, populate_from='title')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='MenuItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), - ('link_title', models.CharField(blank=True, max_length=50, null=True, verbose_name='link_title')), - ('link_url', models.CharField(blank=True, max_length=500)), - ('open_in_new_tab', models.BooleanField(blank=True, default=False)), - ('link_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.page')), - ('page', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='menu_items', to='cms.menu')), - ], - options={ - 'ordering': ['sort_order'], - 'abstract': False, - }, - ), - migrations.CreateModel( - name='SubMenuItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), - ('link_title', models.CharField(blank=True, max_length=50, null=True, verbose_name='link_title')), - ('link_url', models.CharField(blank=True, max_length=500)), - ('open_in_new_tab', models.BooleanField(blank=True, default=False)), - ('link_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.page')), - ('page', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_menu_items', to='cms.menuitem')), - ], - options={ - 'ordering': ['sort_order'], - 'abstract': False, - }, - ), - migrations.CreateModel( - name='SiteSettings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('github', models.URLField(blank=True, help_text='Github URL', null=True)), - ('twitter', models.URLField(blank=True, help_text='Twitter URL', null=True)), - ('linkedin', models.URLField(blank=True, help_text='linkedin URL', null=True)), - ('sitemap', models.URLField(blank=True, help_text='Sitemap rss.xml', null=True)), - ('logo', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')), - ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/cms/models.py b/cms/models.py deleted file mode 100644 index 334c097f..00000000 --- a/cms/models.py +++ /dev/null @@ -1,297 +0,0 @@ -from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.db import models -from django.shortcuts import render -from django.utils.translation import gettext_lazy as _ -from django_extensions.db.fields import AutoSlugField -from modelcluster.fields import ParentalKey -from modelcluster.models import ClusterableModel -from wagtail.admin.panels import ( - FieldPanel, - InlinePanel, - MultiFieldPanel, - PageChooserPanel, -) -from wagtail.contrib.routable_page.models import RoutablePageMixin, route -from wagtail.contrib.settings.models import BaseSiteSetting, register_setting -from wagtail import blocks -from wagtail.fields import StreamField -from wagtail.models import Orderable, Page -from wagtail.fields import RichTextField -# from wagtail.images. import ImageChooserPanel -from wagtail.snippets.models import register_snippet - -from cms import blocks - - -class HomePage(Page): - - content = StreamField( - [ - ("right_image_left_content", blocks.RightImageLeftText()), - ("aboutapp", blocks.AboutApp()), - ("count", blocks.Count()), - ("appfeatures", blocks.AppFeatures()), - ("pricing", blocks.Pricing()), - ("carousel", blocks.Carousel()), - ("leftimagerighttext", blocks.LeftImageRightText()), - ("homeblog", blocks.HomeBlog()), - ], - null=True, - blank=False, - use_json_field=True, - ) - - content_panels = Page.content_panels + [FieldPanel("content")] - - -# class BlogCategory(models.Model): -# name = models.CharField(max_length=255) -# slug = models.SlugField(max_length=255) - -# def __str__(self): -# return self.name - -# panels = [FieldPanel("name"), FieldPanel("slug")] - -# class Meta: -# ordering = ["name"] - -# register_snippet(BlogCategory) - - -class BlogListPage(RoutablePageMixin, Page): - - template = "cms/blog/blog_list_page.html" - max_count = 1 - intro = RichTextField(blank=True) - - @route(r"^(?P\d+)") - def get_context(self, request, *args, **kwargs): - # Update context to include only published posts, ordered by reverse-chron - # request.session.clear() - context = super().get_context(request, *args, **kwargs) - - all_posts = ( - BlogDetailPage.objects.live().public().order_by("-first_published_at") - ) - - paginator = Paginator(all_posts, 1) - - page = int(kwargs.get("page_num", 1)) - - try: - posts = paginator.page(page) - except PageNotAnInteger: - posts = paginator.page(1) - except EmptyPage: - posts = paginator.page(paginator.num_pages) - # context["category_count"] = BlogCategory.objects.annotate( - # num_posts=Count("blogdetailpage") - # ) - context["posts"] = posts - # context["categories"] = BlogCategory.objects.all() - if kwargs.get("page_num"): - return render(request, "cms/blog/blog_list_page.html", context) - else: - return context - - # @route(r"^category/(?P[-\w]*)/$", name="category_view") - # def category_view(self, request, cat_slug): - # context = self.get_context(request) - # category = BlogCategory.objects.get(slug=cat_slug) - # category_posts = ( - # BlogDetailPage.objects.live().public().filter(category=category) - # ) - - # paginator = Paginator(category_posts, 10) - - # # page = int(kwargs.get('page_num', 1)) - # page = 1 - - # try: - # category_posts = paginator.page(page) - # except PageNotAnInteger: - # category_posts = paginator.page(1) - # except EmptyPage: - # category_posts = paginator.page(paginator.num_pages) - - # context["category_posts"] = category_posts - # context["current_category"] = category - # return render(request, "blog/blog_category.html", context) - - # content_panels = Page.content_panels + [ - # FieldPanel("intro", classname="full"), - # ] - - -class BlogDetailPage(Page): - template = "cms/blog/blog_detail_page.html" - excerpt = RichTextField() - # show_legend = models.BooleanField(default=False) - # category = models.ForeignKey(BlogCategory, null=True, on_delete=models.SET_NULL) - - content = StreamField( - [ - ("content_title", blocks.BlogContentTitleBlock()), - ("code", blocks.BlogCodeBlock()), - ("rich_text", blocks.BlogRichTextBlock()), - ("image", blocks.BlogImageBlock()), - ], - null=True, - blank=False, - use_json_field=True, - ) - - content_panels = Page.content_panels + [ - # FieldPanel("category"), - # FieldPanel("show_legend"), - FieldPanel("excerpt"), - FieldPanel("content"), - ] - - -# ============================ Navigation Menu =============================================== - - -@register_snippet -class Menu(ClusterableModel): - """The main menu clusterable model.""" - - title = models.CharField(_("link_title"), max_length=100) - slug = AutoSlugField(populate_from="title", editable=True) - - panels = [ - MultiFieldPanel( - [ - FieldPanel("title"), - FieldPanel("slug"), - ], - heading="Menu", - ), - InlinePanel("menu_items", label="Menu Item"), - ] - - def __str__(self): - return self.title - - -class MenuItem(Orderable, ClusterableModel): - """The First level menu of the Navigationbar.""" - - link_title = models.CharField(_("link_title"), blank=True, null=True, max_length=50) - link_url = models.CharField(max_length=500, blank=True) - link_page = models.ForeignKey( - "wagtailcore.Page", - null=True, - blank=True, - related_name="+", - on_delete=models.CASCADE, - ) - open_in_new_tab = models.BooleanField(default=False, blank=True) - - page = ParentalKey("Menu", related_name="menu_items") - - panels = [ - MultiFieldPanel( - [ - FieldPanel("link_title"), - FieldPanel("link_url"), - PageChooserPanel("link_page"), - FieldPanel("open_in_new_tab"), - ], - heading="MenuItem", - ), - InlinePanel("sub_menu_items", label="Sub Menu Item"), - ] - - @property - def link(self): - if self.link_page: - return self.link_page.url - elif self.link_url: - return self.link_url - return "#" - - @property - def title(self): - if self.link_page and not self.link_title: - return self.link_page.title - elif self.link_title: - return self.link_title - return "Missing Title" - - -class SubMenuItem(Orderable): - """The Second level menu of the Navigationbar.""" - - link_title = models.CharField(_("link_title"), blank=True, null=True, max_length=50) - link_url = models.CharField(max_length=500, blank=True) - link_page = models.ForeignKey( - "wagtailcore.Page", - null=True, - blank=True, - related_name="+", - on_delete=models.CASCADE, - ) - - open_in_new_tab = models.BooleanField(default=False, blank=True) - - page = ParentalKey("MenuItem", related_name="sub_menu_items") - - panels = [ - MultiFieldPanel( - [ - FieldPanel("link_title"), - FieldPanel("link_url"), - PageChooserPanel("link_page"), - FieldPanel("open_in_new_tab"), - ], - heading="SubMenuItem", - ), - ] - - @property - def link(self): - if self.link_page: - return self.link_page.url - elif self.link_url: - return self.link_url - return "#" - - @property - def title(self): - if self.link_page and not self.link_title: - return self.link_page.title - elif self.link_title: - return self.link_title - return "Missing Title" - - -@register_setting -class SiteSettings(BaseSiteSetting): - """Site settings for our custom website.""" - - logo = models.ForeignKey( - "wagtailimages.Image", - null=True, - blank=True, - default=None, - on_delete=models.SET_NULL, - related_name="+", - ) - github = models.URLField(blank=True, null=True, help_text="Github URL") - twitter = models.URLField(blank=True, null=True, help_text="Twitter URL") - linkedin = models.URLField(blank=True, null=True, help_text="linkedin URL") - sitemap = models.URLField(blank=True, null=True, help_text="Sitemap rss.xml") - panels = [ - MultiFieldPanel( - [ - FieldPanel("github"), - FieldPanel("twitter"), - FieldPanel("linkedin"), - FieldPanel("sitemap"), - FieldPanel("logo"), - ], - heading="Site Settings", - ) - ] diff --git a/cms/templatetags/menus_tags.py b/cms/templatetags/menus_tags.py deleted file mode 100644 index b1fccaa2..00000000 --- a/cms/templatetags/menus_tags.py +++ /dev/null @@ -1,10 +0,0 @@ -from django import template - -from ..models import Menu - -register = template.Library() - - -@register.simple_tag() -def get_menu(slug): - return Menu.objects.get(slug=slug) diff --git a/cms/tests.py b/cms/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/cms/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/cms/views.py b/cms/views.py deleted file mode 100644 index 91ea44a2..00000000 --- a/cms/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/common/admin.py b/common/admin.py deleted file mode 100644 index f4fd4389..00000000 --- a/common/admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from common.models import Address, Comment, CommentFiles, User - -# Register your models here. - -admin.site.register(User) -admin.site.register(Address) -admin.site.register(Comment) -admin.site.register(CommentFiles) diff --git a/common/apps.py b/common/apps.py deleted file mode 100644 index 3ce38941..00000000 --- a/common/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class CommonConfig(AppConfig): - name = "common" diff --git a/common/base.py b/common/base.py deleted file mode 100644 index 6930f8f0..00000000 --- a/common/base.py +++ /dev/null @@ -1,38 +0,0 @@ -import uuid - -# Django imports -from django.db import models - -# Third party imports -from crum import get_current_user - -# Module imports -from common.mixins import AuditModel - - -class BaseModel(AuditModel): - id = models.UUIDField( - default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True - ) - - class Meta: - abstract = True - - def save(self, *args, **kwargs): - user = get_current_user() - if user is None or user.is_anonymous: - self.created_by = None - self.updated_by = None - super(BaseModel, self).save(*args, **kwargs) - else: - # Check if the model is being created or updated - if self._state.adding: - # If created only set created_by value: set updated_by to None - self.created_by = user - self.updated_by = None - # If updated only set updated_by value don't touch created_by - self.updated_by = user - super(BaseModel, self).save(*args, **kwargs) - - def __str__(self): - return str(self.id) \ No newline at end of file diff --git a/common/middleware/get_company.py b/common/middleware/get_company.py deleted file mode 100644 index a8694d6d..00000000 --- a/common/middleware/get_company.py +++ /dev/null @@ -1,75 +0,0 @@ -import jwt -from django.conf import settings -from django.contrib.auth import logout -from django.core.exceptions import ValidationError,PermissionDenied -from rest_framework import status -from rest_framework.response import Response -from crum import get_current_user -from django.utils.functional import SimpleLazyObject - -from common.models import Org, Profile, User - - -# def set_profile_request(request, org, token): -# # we are decoding the token -# decoded = jwt.decode(token, (settings.SECRET_KEY), algorithms=[settings.JWT_ALGO]) - -# request.user = User.objects.get(id=decoded["user_id"]) - -# if request.user: -# request.profile = Profile.objects.get( -# user=request.user, org=org, is_active=True -# ) -# request.profile.role = "ADMIN" -# request.profile.save() -# if request.profile is None: -# logout(request) -# return Response( -# {"error": False}, -# status=status.HTTP_200_OK, - # ) - -def get_actual_value(request): - if request.user is None: - return None - - return request.user #here should have value, so any code using request.user will work - -class GetProfileAndOrg(object): - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - self.process_request(request) - return self.get_response(request) - - def process_request(self, request): - try : - request.profile = None - user_id = None - # here I am getting the the jwt token passing in header - if request.headers.get("Authorization"): - token1 = request.headers.get("Authorization") - token = token1.split(" ")[1] # getting the token value - decoded = jwt.decode(token, (settings.SECRET_KEY), algorithms=[settings.JWT_ALGO]) - user_id = decoded['user_id'] - api_key = request.headers.get('Token') # Get API key from request query params - if api_key: - try: - organization = Org.objects.get(api_key=api_key) - api_key_user = organization - request.META['org'] = api_key_user.id - profile = Profile.objects.filter(org=api_key_user, role="ADMIN").first() - user_id = profile.user.id - except Org.DoesNotExist: - raise AuthenticationFailed('Invalid API Key') - if user_id is not None: - if request.headers.get("org"): - profile = Profile.objects.get( - user_id=user_id, org=request.headers.get("org"), is_active=True - ) - if profile: - request.profile = profile - except : - print('test1') - raise PermissionDenied() diff --git a/common/migrations/0001_initial.py b/common/migrations/0001_initial.py deleted file mode 100644 index 15f3db3e..00000000 --- a/common/migrations/0001_initial.py +++ /dev/null @@ -1,192 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -import common.models -from django.conf import settings -import django.contrib.auth.models -from django.db import migrations, models -import django.db.models.deletion -import phonenumber_field.modelfields -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')), - ('profile_pic', models.FileField(blank=True, max_length=1000, null=True, upload_to=common.models.img_url)), - ('activation_key', models.CharField(blank=True, max_length=150, null=True)), - ('key_expires', models.DateTimeField(blank=True, null=True)), - ('is_active', models.BooleanField(default=True)), - ], - options={ - 'verbose_name': 'User', - 'verbose_name_plural': 'Users', - 'db_table': 'users', - 'ordering': ('-is_active',), - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='Address', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('address_line', models.CharField(blank=True, default='', max_length=255, verbose_name='Address')), - ('street', models.CharField(blank=True, default='', max_length=55, verbose_name='Street')), - ('city', models.CharField(blank=True, default='', max_length=255, verbose_name='City')), - ('state', models.CharField(blank=True, default='', max_length=255, verbose_name='State')), - ('postcode', models.CharField(blank=True, default='', max_length=64, verbose_name='Post/Zip-code')), - ('country', models.CharField(blank=True, choices=[('GB', 'United Kingdom'), ('AF', 'Afghanistan'), ('AX', 'Aland Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('CV', 'Cape Verde'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo, The Democratic Republic of the'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "Cote d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands (Malvinas)'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See (Vatican City State)'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran, Islamic Republic of'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', "Lao People's Democratic Republic"), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libyan Arab Jamahiriya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia, The Former Yugoslav Republic of'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia, Federated States of'), ('MD', 'Moldova'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('AN', 'Netherlands Antilles'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestinian Territory, Occupied'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('BL', 'Saint Barthelemy'), ('SH', 'Saint Helena'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syrian Arab Republic'), ('TW', 'Taiwan, Province of China'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania, United Republic of'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('US', 'United States'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Viet Nam'), ('VG', 'Virgin Islands, British'), ('VI', 'Virgin Islands, U.S.'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], default='', max_length=3)), - ], - options={ - 'verbose_name': 'Address', - 'verbose_name_plural': 'Addresses', - 'db_table': 'address', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='APISettings', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('title', models.TextField()), - ('apikey', models.CharField(blank=True, max_length=16)), - ('website', models.URLField(max_length=255, null=True)), - ], - options={ - 'verbose_name': 'APISetting', - 'verbose_name_plural': 'APISettings', - 'db_table': 'apiSettings', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='Attachments', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('file_name', models.CharField(max_length=60)), - ('attachment', models.FileField(max_length=1001, upload_to='attachments/%Y/%m/')), - ], - options={ - 'verbose_name': 'Attachment', - 'verbose_name_plural': 'Attachments', - 'db_table': 'attachments', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('comment', models.CharField(max_length=255)), - ('commented_on', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Comment', - 'verbose_name_plural': 'Comments', - 'db_table': 'comment', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='CommentFiles', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('updated_on', models.DateTimeField(auto_now_add=True)), - ('comment_file', models.FileField(blank=True, null=True, upload_to='CommentFiles', verbose_name='File')), - ], - options={ - 'verbose_name': 'CommentFile', - 'verbose_name_plural': 'CommentFiles', - 'db_table': 'commentFiles', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='Org', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(blank=True, max_length=100, null=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Organization', - 'verbose_name_plural': 'Organizations', - 'db_table': 'organization', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='Profile', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True)), - ('alternate_phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None)), - ('role', models.CharField(choices=[('ADMIN', 'ADMIN'), ('USER', 'USER')], default='USER', max_length=50)), - ('has_sales_access', models.BooleanField(default=False)), - ('has_marketing_access', models.BooleanField(default=False)), - ('is_active', models.BooleanField(default=True)), - ('is_organization_admin', models.BooleanField(default=False)), - ('date_of_joining', models.DateField(blank=True, null=True)), - ('address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='adress_users', to='common.address')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_org', to='common.org')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Profile', - 'verbose_name_plural': 'Profiles', - 'db_table': 'profile', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='Document', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('title', models.TextField(blank=True, null=True)), - ('document_file', models.FileField(max_length=5000, upload_to=common.models.document_path)), - ('status', models.CharField(choices=[('active', 'active'), ('inactive', 'inactive')], default='active', max_length=64)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='document_uploaded', to='common.profile')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='document_org', to='common.org')), - ('shared_to', models.ManyToManyField(related_name='document_shared_to', to='common.profile')), - ], - options={ - 'verbose_name': 'Document', - 'verbose_name_plural': 'Documents', - 'db_table': 'document', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/common/migrations/0002_initial.py b/common/migrations/0002_initial.py deleted file mode 100644 index aa21817c..00000000 --- a/common/migrations/0002_initial.py +++ /dev/null @@ -1,211 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('leads', '0001_initial'), - ('invoices', '0001_initial'), - ('teams', '0001_initial'), - ('auth', '0012_alter_user_first_name_max_length'), - ('accounts', '0002_initial'), - ('opportunity', '0001_initial'), - ('contacts', '0001_initial'), - ('common', '0001_initial'), - ('tasks', '0001_initial'), - ('cases', '0002_initial'), - ('events', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='document', - name='teams', - field=models.ManyToManyField(related_name='document_teams', to='teams.teams'), - ), - migrations.AddField( - model_name='document', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='commentfiles', - name='comment', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='common.comment'), - ), - migrations.AddField( - model_name='commentfiles', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - migrations.AddField( - model_name='commentfiles', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='comment', - name='account', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_comments', to='accounts.account'), - ), - migrations.AddField( - model_name='comment', - name='case', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cases', to='cases.case'), - ), - migrations.AddField( - model_name='comment', - name='commented_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='common.profile'), - ), - migrations.AddField( - model_name='comment', - name='contact', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contact_comments', to='contacts.contact'), - ), - migrations.AddField( - model_name='comment', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - migrations.AddField( - model_name='comment', - name='event', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events_comments', to='events.event'), - ), - migrations.AddField( - model_name='comment', - name='invoice', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoice_comments', to='invoices.invoice'), - ), - migrations.AddField( - model_name='comment', - name='lead', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leads_comments', to='leads.lead'), - ), - migrations.AddField( - model_name='comment', - name='opportunity', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity_comments', to='opportunity.opportunity'), - ), - migrations.AddField( - model_name='comment', - name='profile', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_comments', to='common.profile'), - ), - migrations.AddField( - model_name='comment', - name='task', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks_comments', to='tasks.task'), - ), - migrations.AddField( - model_name='comment', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='attachments', - name='account', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='account_attachment', to='accounts.account'), - ), - migrations.AddField( - model_name='attachments', - name='case', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='case_attachment', to='cases.case'), - ), - migrations.AddField( - model_name='attachments', - name='contact', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contact_attachment', to='contacts.contact'), - ), - migrations.AddField( - model_name='attachments', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='attachment_created_by', to='common.profile'), - ), - migrations.AddField( - model_name='attachments', - name='event', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events_attachment', to='events.event'), - ), - migrations.AddField( - model_name='attachments', - name='invoice', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoice_attachment', to='invoices.invoice'), - ), - migrations.AddField( - model_name='attachments', - name='lead', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lead_attachment', to='leads.lead'), - ), - migrations.AddField( - model_name='attachments', - name='opportunity', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity_attachment', to='opportunity.opportunity'), - ), - migrations.AddField( - model_name='attachments', - name='task', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks_attachment', to='tasks.task'), - ), - migrations.AddField( - model_name='attachments', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='apisettings', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='settings_created_by', to='common.profile'), - ), - migrations.AddField( - model_name='apisettings', - name='lead_assigned_to', - field=models.ManyToManyField(related_name='lead_assignee_users', to='common.profile'), - ), - migrations.AddField( - model_name='apisettings', - name='org', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='org_api_settings', to='common.org'), - ), - migrations.AddField( - model_name='apisettings', - name='tags', - field=models.ManyToManyField(blank=True, to='accounts.tags'), - ), - migrations.AddField( - model_name='apisettings', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='address', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - migrations.AddField( - model_name='address', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), - migrations.AddField( - model_name='user', - name='groups', - field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'), - ), - migrations.AddField( - model_name='user', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), - ), - migrations.AlterUniqueTogether( - name='profile', - unique_together={('user', 'org')}, - ), - ] diff --git a/common/migrations/0003_alter_user_profile_pic.py b/common/migrations/0003_alter_user_profile_pic.py deleted file mode 100644 index c6fa937f..00000000 --- a/common/migrations/0003_alter_user_profile_pic.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-12 09:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='profile_pic', - field=models.CharField(blank=True, max_length=1000, null=True), - ), - ] diff --git a/common/migrations/0004_alter_profile_alternate_phone.py b/common/migrations/0004_alter_profile_alternate_phone.py deleted file mode 100644 index 7a83303a..00000000 --- a/common/migrations/0004_alter_profile_alternate_phone.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-21 11:23 - -from django.db import migrations -import phonenumber_field.modelfields - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0003_alter_user_profile_pic'), - ] - - operations = [ - migrations.AlterField( - model_name='profile', - name='alternate_phone', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), - ), - ] diff --git a/common/migrations/0005_org_api_key.py b/common/migrations/0005_org_api_key.py deleted file mode 100644 index 2fb1db59..00000000 --- a/common/migrations/0005_org_api_key.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-02 11:19 - -import common.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0004_alter_profile_alternate_phone'), - ] - - operations = [ - migrations.AddField( - model_name='org', - name='api_key', - field=models.TextField(default=common.models.generate_unique_key, editable=False), - ), - ] diff --git a/common/migrations/0006_alter_org_api_key.py b/common/migrations/0006_alter_org_api_key.py deleted file mode 100644 index fb672699..00000000 --- a/common/migrations/0006_alter_org_api_key.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-02 11:20 - -import common.models -from django.db import migrations, models -import uuid - -def generate_unique_key(): - return str(uuid.uuid4()) - -def set_unique_api_keys(apps, schema_editor): - Org = apps.get_model('common', 'Org') - for org in Org.objects.all(): - org.api_key = generate_unique_key() - org.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0005_org_api_key'), - ] - - operations = [ - migrations.RunPython(set_unique_api_keys), - migrations.AlterField( - model_name='org', - name='api_key', - field=models.TextField(default=common.models.generate_unique_key, editable=False, unique=True), - ), - ] diff --git a/common/migrations/0007_org_is_active.py b/common/migrations/0007_org_is_active.py deleted file mode 100644 index 0f274f21..00000000 --- a/common/migrations/0007_org_is_active.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-02 11:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0006_alter_org_api_key'), - ] - - operations = [ - migrations.AddField( - model_name='org', - name='is_active', - field=models.BooleanField(default=True), - ), - ] diff --git a/common/migrations/0008_alter_user_managers.py b/common/migrations/0008_alter_user_managers.py deleted file mode 100644 index a5470099..00000000 --- a/common/migrations/0008_alter_user_managers.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2024-02-14 06:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0007_org_is_active'), - ] - - operations = [ - migrations.AlterModelManagers( - name='user', - managers=[ - ], - ), - ] diff --git a/common/migrations/0009_user_is_staff.py b/common/migrations/0009_user_is_staff.py deleted file mode 100644 index 99541d50..00000000 --- a/common/migrations/0009_user_is_staff.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2024-02-14 07:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0008_alter_user_managers'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='is_staff', - field=models.BooleanField(default=False, verbose_name='staff status'), - ), - ] diff --git a/common/mixins.py b/common/mixins.py deleted file mode 100644 index d99d997f..00000000 --- a/common/mixins.py +++ /dev/null @@ -1,50 +0,0 @@ -# Python imports -import uuid - -# Django imports -from common.models import models - - -class TimeAuditModel(models.Model): - - """To path when the record was created and last modified""" - - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="Created At", - ) - updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At") - - class Meta: - abstract = True - - -class UserAuditModel(models.Model): - - """To path when the record was created and last modified""" - - created_by = models.ForeignKey( - "common.User", - on_delete=models.SET_NULL, - related_name="%(class)s_created_by", - verbose_name="Created By", - null=True, - ) - updated_by = models.ForeignKey( - "common.User", - on_delete=models.SET_NULL, - related_name="%(class)s_updated_by", - verbose_name="Last Modified By", - null=True, - ) - - class Meta: - abstract = True - - -class AuditModel(TimeAuditModel, UserAuditModel): - - """To path when the record was created and last modified""" - - class Meta: - abstract = True \ No newline at end of file diff --git a/common/serializer.py b/common/serializer.py deleted file mode 100644 index 409de6bf..00000000 --- a/common/serializer.py +++ /dev/null @@ -1,379 +0,0 @@ -import re - -from django.contrib.auth import authenticate -from django.contrib.auth.hashers import check_password -from django.contrib.auth.tokens import default_token_generator -from django.utils.http import urlsafe_base64_decode -from rest_framework import serializers -from rest_framework_simplejwt.tokens import AccessToken, RefreshToken - -from common.models import ( - Address, - APISettings, - Attachments, - Comment, - Document, - Org, - Profile, - User, -) - - -class OrganizationSerializer(serializers.ModelSerializer): - class Meta: - model = Org - fields = ("id", "name","api_key") - - -class SocialLoginSerializer(serializers.Serializer): - token = serializers.CharField() - - -class CommentSerializer(serializers.ModelSerializer): - class Meta: - model = Comment - fields = ( - "id", - "comment", - "commented_on", - "commented_by", - "account", - "lead", - "opportunity", - "contact", - "case", - "task", - "invoice", - "event", - "profile", - ) - - -class LeadCommentSerializer(serializers.ModelSerializer): - class Meta: - model = Comment - fields = ( - "id", - "comment", - "commented_on", - "commented_by", - "lead", - ) - - - -class OrgProfileCreateSerializer(serializers.ModelSerializer): - """ - It is for creating organization - """ - - name = serializers.CharField(max_length=255) - - class Meta: - model = Org - fields = ["name"] - extra_kwargs = { - "name": {"required": True} - } - - def validate_name(self, name): - if bool(re.search(r"[~\!_.@#\$%\^&\*\ \(\)\+{}\":;'/\[\]]", name)): - raise serializers.ValidationError( - "organization name should not contain any special characters" - ) - if Org.objects.filter(name=name).exists(): - raise serializers.ValidationError( - "Organization already exists with this name" - ) - return name - - -class ShowOrganizationListSerializer(serializers.ModelSerializer): - """ - we are using it for show orjanization list - """ - - org = OrganizationSerializer() - - class Meta: - model = Profile - fields = ( - "role", - "alternate_phone", - "has_sales_access", - "has_marketing_access", - "is_organization_admin", - "org", - ) - - -class BillingAddressSerializer(serializers.ModelSerializer): - country = serializers.SerializerMethodField() - - def get_country(self, obj): - return obj.get_country_display() - - class Meta: - model = Address - fields = ("address_line", "street", "city", "state", "postcode", "country") - - def __init__(self, *args, **kwargs): - account_view = kwargs.pop("account", False) - - super().__init__(*args, **kwargs) - - if account_view: - self.fields["address_line"].required = True - self.fields["street"].required = True - self.fields["city"].required = True - self.fields["state"].required = True - self.fields["postcode"].required = True - self.fields["country"].required = True - - -class CreateUserSerializer(serializers.ModelSerializer): - - class Meta: - model = User - fields = ( - "email", - "profile_pic", - ) - - def __init__(self, *args, **kwargs): - self.org = kwargs.pop("org", None) - super().__init__(*args, **kwargs) - self.fields["email"].required = True - - def validate_email(self, email): - if self.instance: - if self.instance.email != email: - if not Profile.objects.filter(user__email=email, org=self.org).exists(): - return email - raise serializers.ValidationError("Email already exists") - return email - if not Profile.objects.filter(user__email=email.lower(), org=self.org).exists(): - return email - raise serializers.ValidationError("Given Email id already exists") - - -class CreateProfileSerializer(serializers.ModelSerializer): - class Meta: - model = Profile - fields = ( - "role", - "phone", - "alternate_phone", - "has_sales_access", - "has_marketing_access", - "is_organization_admin", - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["alternate_phone"].required = False - self.fields["role"].required = True - self.fields["phone"].required = True - - -class UserSerializer(serializers.ModelSerializer): - - class Meta: - model = User - fields = ["id","email","profile_pic"] - - -class ProfileSerializer(serializers.ModelSerializer): - # address = BillingAddressSerializer() - - class Meta: - model = Profile - fields = ( - "id", - "user_details", - "role", - "address", - "has_marketing_access", - "has_sales_access", - "phone", - "date_of_joining", - "is_active", - ) - - -class AttachmentsSerializer(serializers.ModelSerializer): - file_path = serializers.SerializerMethodField() - - def get_file_path(self, obj): - if obj.attachment: - return obj.attachment.url - None - - class Meta: - model = Attachments - fields = ["id", "created_by", "file_name", "created_at", "file_path"] - - -class DocumentSerializer(serializers.ModelSerializer): - shared_to = ProfileSerializer(read_only=True, many=True) - teams = serializers.SerializerMethodField() - created_by = UserSerializer() - org = OrganizationSerializer() - - def get_teams(self, obj): - return obj.teams.all().values() - - class Meta: - model = Document - fields = [ - "id", - "title", - "document_file", - "status", - "shared_to", - "teams", - "created_at", - "created_by", - "org", - ] - - -class DocumentCreateSerializer(serializers.ModelSerializer): - def __init__(self, *args, **kwargs): - request_obj = kwargs.pop("request_obj", None) - super().__init__(*args, **kwargs) - self.fields["title"].required = True - self.org = request_obj.profile.org - - def validate_title(self, title): - if self.instance: - if ( - Document.objects.filter(title__iexact=title, org=self.org) - .exclude(id=self.instance.id) - .exists() - ): - raise serializers.ValidationError( - "Document with this Title already exists" - ) - if Document.objects.filter(title__iexact=title, org=self.org).exists(): - raise serializers.ValidationError("Document with this Title already exists") - return title - - class Meta: - model = Document - fields = ["title", "document_file", "status", "org"] - - -def find_urls(string): - # website_regex = "^((http|https)://)?([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com or google.com - # website_regex = "^https?://([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com - # http(s)://google.com - website_regex = "^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}$" - # http(s)://google.com:8000 - website_regex_port = "^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}:[0-9]{2,4}$" - url = re.findall(website_regex, string) - url_port = re.findall(website_regex_port, string) - if url and url[0] != "": - return url - return url_port - - -class APISettingsSerializer(serializers.ModelSerializer): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - class Meta: - model = APISettings - fields = ("title", "website") - - def validate_website(self, website): - if website and not ( - website.startswith("http://") or website.startswith("https://") - ): - raise serializers.ValidationError("Please provide valid schema") - if not len(find_urls(website)) > 0: - raise serializers.ValidationError( - "Please provide a valid URL with schema and without trailing slash - Example: http://google.com" - ) - return website - - -class APISettingsListSerializer(serializers.ModelSerializer): - created_by = UserSerializer() - lead_assigned_to = ProfileSerializer(read_only=True, many=True) - tags = serializers.SerializerMethodField() - org = OrganizationSerializer() - - def get_tags(self, obj): - return obj.tags.all().values() - - class Meta: - model = APISettings - fields = [ - "title", - "apikey", - "website", - "created_at", - "created_by", - "lead_assigned_to", - "tags", - "org", - ] - -class APISettingsSwaggerSerializer(serializers.ModelSerializer): - class Meta: - model = APISettings - fields = [ - "title", - "website", - "lead_assigned_to", - "tags", - ] - - -class DocumentCreateSwaggerSerializer(serializers.ModelSerializer): - class Meta: - model = Document - fields = [ - "title", - "document_file", - "teams", - "shared_to", - ] - -class DocumentEditSwaggerSerializer(serializers.ModelSerializer): - class Meta: - model = Document - fields = [ - "title", - "document_file", - "teams", - "shared_to", - "status" - ] - - -class UserCreateSwaggerSerializer(serializers.Serializer): - """ - It is swagger for creating or updating user - """ - ROLE_CHOICES = ["ADMIN", "USER"] - - email = serializers.CharField(max_length=1000,required=True) - role = serializers.ChoiceField(choices = ROLE_CHOICES,required=True) - phone = serializers.CharField(max_length=12) - alternate_phone = serializers.CharField(max_length=12) - address_line = serializers.CharField(max_length=10000,required=True) - street = serializers.CharField(max_length=1000) - city = serializers.CharField(max_length=1000) - state = serializers.CharField(max_length=1000) - pincode = serializers.CharField(max_length=1000) - country = serializers.CharField(max_length=1000) - -class UserUpdateStatusSwaggerSerializer(serializers.Serializer): - - STATUS_CHOICES = ["Active", "Inactive"] - - status = serializers.ChoiceField(choices = STATUS_CHOICES,required=True) - - diff --git a/common/urls.py b/common/urls.py deleted file mode 100644 index 7dcd5607..00000000 --- a/common/urls.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.urls import path -from rest_framework_simplejwt import views as jwt_views - -from common import views - -app_name = "api_common" - - -urlpatterns = [ - path("dashboard/", views.ApiHomeView.as_view()), - path( - "auth/refresh-token/", - jwt_views.TokenRefreshView.as_view(), - name="token_refresh", - ), - # GoogleLoginView - path("auth/google/", views.GoogleLoginView.as_view()), - path("org/", views.OrgProfileCreateView.as_view()), - path("profile/", views.ProfileView.as_view()), - path("users/get-teams-and-users/", views.GetTeamsAndUsersView.as_view()), - path("users/", views.UsersListView.as_view()), - path("user//", views.UserDetailView.as_view()), - path("documents/", views.DocumentListView.as_view()), - path("documents//", views.DocumentDetailView.as_view()), - path("api-settings/", views.DomainList.as_view()), - path("api-settings//", views.DomainDetailView.as_view()), - path("user//status/", views.UserStatusView.as_view()), -] diff --git a/common/views.py b/common/views.py deleted file mode 100644 index 94785d15..00000000 --- a/common/views.py +++ /dev/null @@ -1,911 +0,0 @@ -import json -import secrets -from multiprocessing import context -from re import template - -import requests -from django.contrib.auth.base_user import BaseUserManager -from django.contrib.auth.hashers import make_password -from rest_framework_simplejwt.tokens import RefreshToken -from rest_framework.utils import json -from django.conf import settings -from django.contrib.auth import authenticate, login -from django.contrib.auth.hashers import make_password -from django.db import transaction -from django.db.models import Q -from django.http.response import JsonResponse -from django.shortcuts import get_object_or_404, render -from django.utils import timezone -from django.utils.encoding import force_str -from django.utils.http import urlsafe_base64_decode -from django.utils.translation import gettext as _ -from django.views import View -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema -#from common.external_auth import CustomDualAuthentication -from rest_framework import status -from rest_framework.authtoken.models import Token -from rest_framework.pagination import LimitOffsetPagination -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView - -from accounts.models import Account, Contact, Tags -from accounts.serializer import AccountSerializer -from cases.models import Case -from cases.serializer import CaseSerializer - -##from common.custom_auth import JSONWebTokenAuthentication -from common import serializer, swagger_params1 -from common.models import APISettings, Document, Org, Profile, User -from common.serializer import * -# from common.serializer import ( -# CreateUserSerializer, -# PasswordChangeSerializer, -# RegisterOrganizationSerializer, -# ) -from common.tasks import ( - resend_activation_link_to_user, - send_email_to_new_user, - send_email_to_reset_password, - send_email_user_delete, -) -from common.token_generator import account_activation_token - -# from rest_framework_jwt.serializers import jwt_encode_handler -from common.utils import COUNTRIES, ROLES, jwt_payload_handler -from contacts.serializer import ContactSerializer -from leads.models import Lead -from leads.serializer import LeadSerializer -from opportunity.models import Opportunity -from opportunity.serializer import OpportunitySerializer -from teams.models import Teams -from teams.serializer import TeamsSerializer - - -class GetTeamsAndUsersView(APIView): - - permission_classes = (IsAuthenticated,) - - @extend_schema(tags=["users"], parameters=swagger_params1.organization_params) - def get(self, request, *args, **kwargs): - data = {} - teams = Teams.objects.filter(org=request.profile.org).order_by("-id") - teams_data = TeamsSerializer(teams, many=True).data - profiles = Profile.objects.filter(is_active=True, org=request.profile.org).order_by( - "user__email" - ) - profiles_data = ProfileSerializer(profiles, many=True).data - data["teams"] = teams_data - data["profiles"] = profiles_data - return Response(data) - - -class UsersListView(APIView, LimitOffsetPagination): - - permission_classes = (IsAuthenticated,) - @extend_schema(parameters=swagger_params1.organization_params,request=UserCreateSwaggerSerializer) - def post(self, request, format=None): - print(request.profile.role, request.user.is_superuser) - if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: - return Response( - {"error": True, "errors": "Permission Denied"}, - status=status.HTTP_403_FORBIDDEN, - ) - else: - params = request.data - if params: - user_serializer = CreateUserSerializer(data=params, org=request.profile.org) - address_serializer = BillingAddressSerializer(data=params) - profile_serializer = CreateProfileSerializer(data=params) - data = {} - if not user_serializer.is_valid(): - data["user_errors"] = dict(user_serializer.errors) - if not profile_serializer.is_valid(): - data["profile_errors"] = profile_serializer.errors - if not address_serializer.is_valid(): - data["address_errors"] = (address_serializer.errors,) - if data: - return Response( - {"error": True, "errors": data}, - status=status.HTTP_400_BAD_REQUEST, - ) - if address_serializer.is_valid(): - address_obj = address_serializer.save() - user = user_serializer.save( - is_active=True, - ) - user.email = user.email - user.save() - # if params.get("password"): - # user.set_password(params.get("password")) - # user.save() - profile = Profile.objects.create( - user=user, - date_of_joining=timezone.now(), - role=params.get("role"), - address=address_obj, - org=request.profile.org, - ) - - # send_email_to_new_user.delay( - # profile.id, - # request.profile.org.id, - # ) - return Response( - {"error": False, "message": "User Created Successfully"}, - status=status.HTTP_201_CREATED, - ) - - - @extend_schema(parameters=swagger_params1.user_list_params) - def get(self, request, format=None): - if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: - return Response( - {"error": True, "errors": "Permission Denied"}, - status=status.HTTP_403_FORBIDDEN, - ) - queryset = Profile.objects.filter(org=request.profile.org).order_by("-id") - params = request.query_params - if params: - if params.get("email"): - queryset = queryset.filter(user__email__icontains=params.get("email")) - if params.get("role"): - queryset = queryset.filter(role=params.get("role")) - if params.get("status"): - queryset = queryset.filter(is_active=params.get("status")) - - context = {} - queryset_active_users = queryset.filter(is_active=True) - results_active_users = self.paginate_queryset( - queryset_active_users.distinct(), self.request, view=self - ) - active_users = ProfileSerializer(results_active_users, many=True).data - if results_active_users: - offset = queryset_active_users.filter( - id__gte=results_active_users[-1].id - ).count() - if offset == queryset_active_users.count(): - offset = None - else: - offset = 0 - context["active_users"] = { - "active_users_count": self.count, - "active_users": active_users, - "offset": offset, - } - - queryset_inactive_users = queryset.filter(is_active=False) - results_inactive_users = self.paginate_queryset( - queryset_inactive_users.distinct(), self.request, view=self - ) - inactive_users = ProfileSerializer(results_inactive_users, many=True).data - if results_inactive_users: - offset = queryset_inactive_users.filter( - id__gte=results_inactive_users[-1].id - ).count() - if offset == queryset_inactive_users.count(): - offset = None - else: - offset = 0 - context["inactive_users"] = { - "inactive_users_count": self.count, - "inactive_users": inactive_users, - "offset": offset, - } - - context["admin_email"] = settings.ADMIN_EMAIL - context["roles"] = ROLES - context["status"] = [("True", "Active"), ("False", "In Active")] - return Response(context) - - -class UserDetailView(APIView): - permission_classes = (IsAuthenticated,) - - def get_object(self, pk): - profile = get_object_or_404(Profile, pk=pk) - return profile - - @extend_schema(tags=["users"], parameters=swagger_params1.organization_params) - def get(self, request, pk, format=None): - profile_obj = self.get_object(pk) - if ( - self.request.profile.role != "ADMIN" - and not self.request.profile.is_admin - and self.request.profile.id != profile_obj.id - ): - return Response( - {"error": True, "errors": "Permission Denied"}, - status=status.HTTP_403_FORBIDDEN, - ) - if profile_obj.org != request.profile.org: - return Response( - {"error": True, "errors": "User company doesnot match with header...."}, - status=status.HTTP_403_FORBIDDEN, - ) - assigned_data = Profile.objects.filter(org=request.profile.org, is_active=True).values( - "id", "user__email" - ) - context = {} - context["profile_obj"] = ProfileSerializer(profile_obj).data - opportunity_list = Opportunity.objects.filter(assigned_to=profile_obj) - context["opportunity_list"] = OpportunitySerializer( - opportunity_list, many=True - ).data - contacts = Contact.objects.filter(assigned_to=profile_obj) - context["contacts"] = ContactSerializer(contacts, many=True).data - cases = Case.objects.filter(assigned_to=profile_obj) - context["cases"] = CaseSerializer(cases, many=True).data - context["assigned_data"] = assigned_data - comments = profile_obj.user_comments.all() - context["comments"] = CommentSerializer(comments, many=True).data - context["countries"] = COUNTRIES - return Response( - {"error": False, "data": context}, - status=status.HTTP_200_OK, - ) - - @extend_schema(tags=["users"],parameters=swagger_params1.organization_params, request=UserCreateSwaggerSerializer) - def put(self, request, pk, format=None): - params = request.data - profile = self.get_object(pk) - address_obj = profile.address - if ( - self.request.profile.role != "ADMIN" - and not self.request.user.is_superuser - and self.request.profile.id != profile.id - ): - return Response( - {"error": True, "errors": "Permission Denied"}, - status=status.HTTP_403_FORBIDDEN, - ) - - if profile.org != request.profile.org: - return Response( - {"error": True, "errors": "User company doesnot match with header...."}, - status=status.HTTP_403_FORBIDDEN, - ) - serializer = CreateUserSerializer( - data=params, instance=profile.user, org=request.profile.org - ) - address_serializer = BillingAddressSerializer(data=params, instance=address_obj) - profile_serializer = CreateProfileSerializer(data=params, instance=profile) - data = {} - if not serializer.is_valid(): - data["contact_errors"] = serializer.errors - if not address_serializer.is_valid(): - data["address_errors"] = (address_serializer.errors,) - if not profile_serializer.is_valid(): - data["profile_errors"] = (profile_serializer.errors,) - if data: - data["error"] = True - return Response( - data, - status=status.HTTP_400_BAD_REQUEST, - ) - if address_serializer.is_valid(): - address_obj = address_serializer.save() - user = serializer.save() - user.email = user.email - user.save() - if profile_serializer.is_valid(): - profile = profile_serializer.save() - return Response( - {"error": False, "message": "User Updated Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - @extend_schema( - tags=["users"],parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, format=None): - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - return Response( - {"error": True, "errors": "Permission Denied"}, - status=status.HTTP_403_FORBIDDEN, - ) - self.object = self.get_object(pk) - if self.object.id == request.profile.id: - return Response( - {"error": True, "errors": "Permission Denied"}, - status=status.HTTP_403_FORBIDDEN, - ) - deleted_by = self.request.profile.user.email - send_email_user_delete.delay( - self.object.user.email, - deleted_by=deleted_by, - ) - self.object.delete() - return Response({"status": "success"}, status=status.HTTP_200_OK) - - -# check_header not working -class ApiHomeView(APIView): - - permission_classes = (IsAuthenticated,) - - @extend_schema(parameters=swagger_params1.organization_params) - def get(self, request, format=None): - accounts = Account.objects.filter(status="open", org=request.profile.org) - contacts = Contact.objects.filter(org=request.profile.org) - leads = Lead.objects.filter(org=request.profile.org).exclude( - Q(status="converted") | Q(status="closed") - ) - opportunities = Opportunity.objects.filter(org=request.profile.org) - - if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: - accounts = accounts.filter( - Q(assigned_to=self.request.profile) | Q(created_by=self.request.profile.user) - ) - contacts = contacts.filter( - Q(assigned_to__id__in=self.request.profile) - | Q(created_by=self.request.profile.user) - ) - leads = leads.filter( - Q(assigned_to__id__in=self.request.profile) - | Q(created_by=self.request.profile.user) - ).exclude(status="closed") - opportunities = opportunities.filter( - Q(assigned_to__id__in=self.request.profile) - | Q(created_by=self.request.profile.user) - ) - context = {} - context["accounts_count"] = accounts.count() - context["contacts_count"] = contacts.count() - context["leads_count"] = leads.count() - context["opportunities_count"] = opportunities.count() - context["accounts"] = AccountSerializer(accounts, many=True).data - context["contacts"] = ContactSerializer(contacts, many=True).data - context["leads"] = LeadSerializer(leads, many=True).data - context["opportunities"] = OpportunitySerializer(opportunities, many=True).data - return Response(context, status=status.HTTP_200_OK) - - -class OrgProfileCreateView(APIView): - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - model1 = Org - model2 = Profile - serializer_class = OrgProfileCreateSerializer - profile_serializer = CreateProfileSerializer - - @extend_schema( - description="Organization and profile Creation api", - request=OrgProfileCreateSerializer - ) - def post(self, request, format=None): - data = request.data - data['api_key'] = secrets.token_hex(16) - serializer = self.serializer_class(data=data) - if serializer.is_valid(): - org_obj = serializer.save() - - # now creating the profile - profile_obj = self.model2.objects.create(user=request.user, org=org_obj) - # now the current user is the admin of the newly created organisation - profile_obj.is_organization_admin = True - profile_obj.role = 'ADMIN' - profile_obj.save() - - return Response( - { - "error": False, - "message": "New Org is Created.", - "org": self.serializer_class(org_obj).data, - "status": status.HTTP_201_CREATED, - } - ) - else: - return Response( - { - "error": True, - "errors": serializer.errors, - "status": status.HTTP_400_BAD_REQUEST, - } - ) - - @extend_schema( - description="Just Pass the token, will return ORG list, associated with user" - ) - def get(self, request, format=None): - """ - here we are passing profile list of the user, where org details also included - """ - profile_list = Profile.objects.filter(user=request.user) - serializer = ShowOrganizationListSerializer(profile_list, many=True) - return Response( - { - "error": False, - "status": status.HTTP_200_OK, - "profile_org_list": serializer.data, - } - ) - - -class ProfileView(APIView): - permission_classes = (IsAuthenticated,) - - @extend_schema(parameters=swagger_params1.organization_params) - def get(self, request, format=None): - # profile=Profile.objects.get(user=request.user) - context = {} - context["user_obj"] = ProfileSerializer(self.request.profile).data - return Response(context, status=status.HTTP_200_OK) - -class DocumentListView(APIView, LimitOffsetPagination): - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - model = Document - - def get_context_data(self, **kwargs): - params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") - if self.request.user.is_superuser or self.request.profile.role == "ADMIN": - queryset = queryset - else: - if self.request.profile.documents(): - doc_ids = self.request.profile.documents().values_list("id", flat=True) - shared_ids = queryset.filter( - Q(status="active") & Q(shared_to__id__in=[self.request.profile.id]) - ).values_list("id", flat=True) - queryset = queryset.filter(Q(id__in=doc_ids) | Q(id__in=shared_ids)) - else: - queryset = queryset.filter( - Q(status="active") & Q(shared_to__id__in=[self.request.profile.id]) - ) - - request_post = params - if request_post: - if request_post.get("title"): - queryset = queryset.filter(title__icontains=request_post.get("title")) - if request_post.get("status"): - queryset = queryset.filter(status=request_post.get("status")) - - if request_post.get("shared_to"): - queryset = queryset.filter( - shared_to__id__in=json.loads(request_post.get("shared_to")) - ) - - context = {} - profile_list = Profile.objects.filter(is_active=True, org=self.request.profile.org) - if self.request.profile.role == "ADMIN" or self.request.profile.is_admin: - profiles = profile_list.order_by("user__email") - else: - profiles = profile_list.filter(role="ADMIN").order_by("user__email") - search = False - if ( - params.get("document_file") - or params.get("status") - or params.get("shared_to") - ): - search = True - context["search"] = search - - queryset_documents_active = queryset.filter(status="active") - results_documents_active = self.paginate_queryset( - queryset_documents_active.distinct(), self.request, view=self - ) - documents_active = DocumentSerializer(results_documents_active, many=True).data - if results_documents_active: - offset = queryset_documents_active.filter( - id__gte=results_documents_active[-1].id - ).count() - if offset == queryset_documents_active.count(): - offset = None - else: - offset = 0 - context["documents_active"] = { - "documents_active_count": self.count, - "documents_active": documents_active, - "offset": offset, - } - - queryset_documents_inactive = queryset.filter(status="inactive") - results_documents_inactive = self.paginate_queryset( - queryset_documents_inactive.distinct(), self.request, view=self - ) - documents_inactive = DocumentSerializer( - results_documents_inactive, many=True - ).data - if results_documents_inactive: - offset = queryset_documents_inactive.filter( - id__gte=results_documents_active[-1].id - ).count() - if offset == queryset_documents_inactive.count(): - offset = None - else: - offset = 0 - context["documents_inactive"] = { - "documents_inactive_count": self.count, - "documents_inactive": documents_inactive, - "offset": offset, - } - - context["users"] = ProfileSerializer(profiles, many=True).data - context["status_choices"] = Document.DOCUMENT_STATUS_CHOICE - return context - - @extend_schema( - tags=["documents"], parameters=swagger_params1.document_get_params - ) - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) - return Response(context) - - @extend_schema( - tags=["documents"], parameters=swagger_params1.organization_params,request=DocumentCreateSwaggerSerializer - ) - def post(self, request, *args, **kwargs): - params = request.data - serializer = DocumentCreateSerializer(data=params, request_obj=request) - if serializer.is_valid(): - doc = serializer.save( - created_by=request.profile.user, - org=request.profile.org, - document_file=request.FILES.get("document_file"), - ) - if params.get("shared_to"): - assinged_to_list = params.get("shared_to") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org, is_active=True - ) - if profiles: - doc.shared_to.add(*profiles) - if params.get("teams"): - teams_list = params.get("teams") - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - if teams: - doc.teams.add(*teams) - - return Response( - {"error": False, "message": "Document Created Successfully"}, - status=status.HTTP_201_CREATED, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class DocumentDetailView(APIView): - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_object(self, pk): - return Document.objects.filter(id=pk).first() - - @extend_schema( - tags=["documents"], parameters=swagger_params1.organization_params - ) - def get(self, request, pk, format=None): - self.object = self.get_object(pk) - if not self.object: - return Response( - {"error": True, "errors": "Document does not exist"}, - status=status.HTTP_403_FORBIDDEN, - ) - if self.object.org != self.request.profile.org: - return Response( - {"error": True, "errors": "User company doesnot match with header...."}, - status=status.HTTP_403_FORBIDDEN, - ) - if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: - if not ( - (self.request.profile == self.object.created_by) - or (self.request.profile in self.object.shared_to.all()) - ): - return Response( - { - "error": True, - "errors": "You do not have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - profile_list = Profile.objects.filter(org=self.request.profile.org) - if request.profile.role == "ADMIN" or request.user.is_superuser: - profiles = profile_list.order_by("user__email") - else: - profiles = profile_list.filter(role="ADMIN").order_by("user__email") - context = {} - context.update( - { - "doc_obj": DocumentSerializer(self.object).data, - "file_type_code": self.object.file_type()[1], - "users": ProfileSerializer(profiles, many=True).data, - } - ) - return Response(context, status=status.HTTP_200_OK) - - @extend_schema( - tags=["documents"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, format=None): - document = self.get_object(pk) - if not document: - return Response( - {"error": True, "errors": "Documdnt does not exist"}, - status=status.HTTP_403_FORBIDDEN, - ) - if document.org != self.request.profile.org: - return Response( - {"error": True, "errors": "User company doesnot match with header...."}, - status=status.HTTP_403_FORBIDDEN, - ) - - if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: - if ( - self.request.profile != document.created_by - ): # or (self.request.profile not in document.shared_to.all()): - return Response( - { - "error": True, - "errors": "You do not have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - document.delete() - return Response( - {"error": False, "message": "Document deleted Successfully"}, - status=status.HTTP_200_OK, - ) - - - @extend_schema( - tags=["documents"], parameters=swagger_params1.organization_params,request=DocumentEditSwaggerSerializer - ) - def put(self, request, pk, format=None): - self.object = self.get_object(pk) - params = request.data - if not self.object: - return Response( - {"error": True, "errors": "Document does not exist"}, - status=status.HTTP_403_FORBIDDEN, - ) - if self.object.org != self.request.profile.org: - return Response( - {"error": True, "errors": "User company doesnot match with header...."}, - status=status.HTTP_403_FORBIDDEN, - ) - if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: - if not ( - (self.request.profile == self.object.created_by) - or (self.request.profile in self.object.shared_to.all()) - ): - return Response( - { - "error": True, - "errors": "You do not have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - serializer = DocumentCreateSerializer( - data=params, instance=self.object, request_obj=request - ) - if serializer.is_valid(): - doc = serializer.save( - document_file=request.FILES.get("document_file"), - status=params.get("status"), - org=request.profile.org, - ) - doc.shared_to.clear() - if params.get("shared_to"): - assinged_to_list = params.get("shared_to") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org, is_active=True - ) - if profiles: - doc.shared_to.add(*profiles) - - doc.teams.clear() - if params.get("teams"): - teams_list = params.get("teams") - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - if teams: - doc.teams.add(*teams) - return Response( - {"error": False, "message": "Document Updated Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class UserStatusView(APIView): - permission_classes = (IsAuthenticated,) - - @extend_schema( - description="User Status View",parameters=swagger_params1.organization_params, request=UserUpdateStatusSwaggerSerializer - ) - def post(self, request, pk, format=None): - if self.request.profile.role != "ADMIN" and not self.request.user.is_superuser: - return Response( - { - "error": True, - "errors": "You do not have permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - params = request.data - profiles = Profile.objects.filter(org=request.profile.org) - profile = profiles.get(id=pk) - - if params.get("status"): - user_status = params.get("status") - if user_status == "Active": - profile.is_active = True - elif user_status == "Inactive": - profile.is_active = False - else: - return Response( - {"error": True, "errors": "Please enter Valid Status for user"}, - status=status.HTTP_400_BAD_REQUEST, - ) - profile.save() - - context = {} - active_profiles = profiles.filter(is_active=True) - inactive_profiles = profiles.filter(is_active=False) - context["active_profiles"] = ProfileSerializer(active_profiles, many=True).data - context["inactive_profiles"] = ProfileSerializer( - inactive_profiles, many=True - ).data - return Response(context) - - -class DomainList(APIView): - model = APISettings - permission_classes = (IsAuthenticated,) - - @extend_schema( - tags=["Settings"],parameters=swagger_params1.organization_params - ) - def get(self, request, *args, **kwargs): - api_settings = APISettings.objects.filter(org=request.profile.org) - users = Profile.objects.filter(is_active=True, org=request.profile.org).order_by( - "user__email" - ) - return Response( - { - "error": False, - "api_settings": APISettingsListSerializer(api_settings, many=True).data, - "users": ProfileSerializer(users, many=True).data, - }, - status=status.HTTP_200_OK, - ) - - @extend_schema( - tags=["Settings"],parameters=swagger_params1.organization_params, request=APISettingsSwaggerSerializer - ) - def post(self, request, *args, **kwargs): - params = request.data - assign_to_list = [] - if params.get("lead_assigned_to"): - assign_to_list = params.get("lead_assigned_to") - serializer = APISettingsSerializer(data=params) - if serializer.is_valid(): - settings_obj = serializer.save(created_by=request.profile.user, org=request.profile.org) - if params.get("tags"): - tags = params.get("tags") - for tag in tags: - tag_obj = Tags.objects.filter(name=tag).first() - if not tag_obj: - tag_obj = Tags.objects.create(name=tag) - settings_obj.tags.add(tag_obj) - if assign_to_list: - settings_obj.lead_assigned_to.add(*assign_to_list) - return Response( - {"error": False, "message": "API key added sucessfully"}, - status=status.HTTP_201_CREATED, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class DomainDetailView(APIView): - model = APISettings - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - @extend_schema( - tags=["Settings"],parameters=swagger_params1.organization_params - ) - def get(self, request, pk, format=None): - api_setting = self.get_object(pk) - return Response( - {"error": False, "domain": APISettingsListSerializer(api_setting).data}, - status=status.HTTP_200_OK, - ) - - @extend_schema( - tags=["Settings"],parameters=swagger_params1.organization_params, request=APISettingsSwaggerSerializer - ) - def put(self, request, pk, **kwargs): - api_setting = self.get_object(pk) - params = request.data - assign_to_list = [] - if params.get("lead_assigned_to"): - assign_to_list = params.get("lead_assigned_to") - serializer = APISettingsSerializer(data=params, instance=api_setting) - if serializer.is_valid(): - api_setting = serializer.save() - api_setting.tags.clear() - api_setting.lead_assigned_to.clear() - if params.get("tags"): - tags = params.get("tags") - for tag in tags: - tag_obj = Tags.objects.filter(name=tag).first() - if not tag_obj: - tag_obj = Tags.objects.create(name=tag) - api_setting.tags.add(tag_obj) - if assign_to_list: - api_setting.lead_assigned_to.add(*assign_to_list) - return Response( - {"error": False, "message": "API setting Updated sucessfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - @extend_schema( - tags=["Settings"],parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, **kwargs): - api_setting = self.get_object(pk) - if api_setting: - api_setting.delete() - return Response( - {"error": False, "message": "API setting deleted sucessfully"}, - status=status.HTTP_200_OK, - ) - -class GoogleLoginView(APIView): - """ - Check for authentication with google - post: - Returns token of logged In user - """ - - - @extend_schema( - description="Login through Google", request=SocialLoginSerializer, - ) - def post(self, request): - payload = {'access_token': request.data.get("token")} # validate the token - r = requests.get('https://www.googleapis.com/oauth2/v2/userinfo', params=payload) - data = json.loads(r.text) - print(data) - if 'error' in data: - content = {'message': 'wrong google token / this google token is already expired.'} - return Response(content) - # create user if not exist - try: - user = User.objects.get(email=data['email']) - except User.DoesNotExist: - user = User() - user.email = data['email'] - user.profile_pic = data['picture'] - # provider random default password - user.password = make_password(BaseUserManager().make_random_password()) - user.email = data['email'] - user.save() - token = RefreshToken.for_user(user) # generate token without username & password - response = {} - response['username'] = user.email - response['access_token'] = str(token.access_token) - response['refresh_token'] = str(token) - response['user_id'] = user.id - return Response(response) diff --git a/contacts/migrations/0001_initial.py b/contacts/migrations/0001_initial.py deleted file mode 100644 index 87991dd5..00000000 --- a/contacts/migrations/0001_initial.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import phonenumber_field.modelfields -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('teams', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Contact', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('salutation', models.CharField(blank=True, default='', max_length=255, verbose_name='Salutation')), - ('first_name', models.CharField(max_length=255, verbose_name='First name')), - ('last_name', models.CharField(max_length=255, verbose_name='Last name')), - ('date_of_birth', models.DateField(blank=True, null=True)), - ('organization', models.CharField(max_length=255, null=True, verbose_name='Organization')), - ('title', models.CharField(blank=True, default='', max_length=255, verbose_name='Title')), - ('primary_email', models.EmailField(max_length=254, unique=True)), - ('secondary_email', models.EmailField(blank=True, default='', max_length=254)), - ('mobile_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True)), - ('secondary_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None)), - ('department', models.CharField(max_length=255, null=True, verbose_name='Department')), - ('language', models.CharField(max_length=255, null=True, verbose_name='Language')), - ('do_not_call', models.BooleanField(default=False)), - ('description', models.TextField(blank=True, null=True)), - ('linked_in_url', models.URLField(blank=True, null=True)), - ('facebook_url', models.URLField(blank=True, null=True)), - ('twitter_username', models.CharField(max_length=255, null=True)), - ('is_active', models.BooleanField(default=False)), - ('country', models.CharField(blank=True, choices=[('GB', 'United Kingdom'), ('AF', 'Afghanistan'), ('AX', 'Aland Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('CV', 'Cape Verde'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo, The Democratic Republic of the'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "Cote d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands (Malvinas)'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See (Vatican City State)'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran, Islamic Republic of'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', "Lao People's Democratic Republic"), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libyan Arab Jamahiriya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia, The Former Yugoslav Republic of'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia, Federated States of'), ('MD', 'Moldova'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('AN', 'Netherlands Antilles'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestinian Territory, Occupied'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('BL', 'Saint Barthelemy'), ('SH', 'Saint Helena'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syrian Arab Republic'), ('TW', 'Taiwan, Province of China'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania, United Republic of'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('US', 'United States'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Viet Nam'), ('VG', 'Virgin Islands, British'), ('VI', 'Virgin Islands, U.S.'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], max_length=3, null=True)), - ('address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='adress_contacts', to='common.address')), - ('assigned_to', models.ManyToManyField(related_name='contact_assigned_users', to='common.profile')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contact_created_by', to='common.profile')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.org')), - ('teams', models.ManyToManyField(related_name='contact_teams', to='teams.teams')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Contact', - 'verbose_name_plural': 'Contacts', - 'db_table': 'contacts', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/contacts/migrations/0002_alter_contact_created_by.py b/contacts/migrations/0002_alter_contact_created_by.py deleted file mode 100644 index 117678bb..00000000 --- a/contacts/migrations/0002_alter_contact_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-29 07:31 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contacts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/contacts/migrations/0003_alter_contact_secondary_number_and_more.py b/contacts/migrations/0003_alter_contact_secondary_number_and_more.py deleted file mode 100644 index 227439f1..00000000 --- a/contacts/migrations/0003_alter_contact_secondary_number_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-21 11:23 - -from django.db import migrations, models -import phonenumber_field.modelfields - - -class Migration(migrations.Migration): - - dependencies = [ - ('contacts', '0002_alter_contact_created_by'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='secondary_number', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), - ), - migrations.AlterField( - model_name='contact', - name='twitter_username', - field=models.CharField(blank=True, max_length=255, null=True), - ), - ] diff --git a/contacts/migrations/0004_alter_contact_address.py b/contacts/migrations/0004_alter_contact_address.py deleted file mode 100644 index 2f4ab9d6..00000000 --- a/contacts/migrations/0004_alter_contact_address.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-23 05:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contacts', '0003_alter_contact_secondary_number_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='address', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/contacts/migrations/0005_alter_contact_address.py b/contacts/migrations/0005_alter_contact_address.py deleted file mode 100644 index b0234246..00000000 --- a/contacts/migrations/0005_alter_contact_address.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-23 05:59 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0007_org_is_active'), - ('contacts', '0004_alter_contact_address'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='address', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='adress_contacts', to='common.address'), - ), - ] diff --git a/contacts/models.py b/contacts/models.py deleted file mode 100644 index 4ac0944f..00000000 --- a/contacts/models.py +++ /dev/null @@ -1,83 +0,0 @@ -import arrow -from django.db import models -from django.utils.translation import gettext_lazy as _ -from phonenumber_field.modelfields import PhoneNumberField - -from common.models import Address, Org, Profile -from common.base import BaseModel -from common.utils import COUNTRIES -from teams.models import Teams - - -class Contact(BaseModel): - salutation = models.CharField( - _("Salutation"), max_length=255, default="", blank=True - ) - first_name = models.CharField(_("First name"), max_length=255) - last_name = models.CharField(_("Last name"), max_length=255) - date_of_birth = models.DateField(null=True, blank=True) - organization = models.CharField(_("Organization"), max_length=255, null=True) - title = models.CharField(_("Title"), max_length=255, default="", blank=True) - primary_email = models.EmailField(unique=True) - secondary_email = models.EmailField(default="", blank=True) - mobile_number = PhoneNumberField(null=True, unique=True) - secondary_number = PhoneNumberField(null=True,blank=True) - department = models.CharField(_("Department"), max_length=255, null=True) - language = models.CharField(_("Language"), max_length=255, null=True) - do_not_call = models.BooleanField(default=False) - address = models.ForeignKey( - Address, - related_name="adress_contacts", - on_delete=models.CASCADE, - blank=True, - null=True, - ) - description = models.TextField(blank=True, null=True) - linked_in_url = models.URLField(blank=True, null=True) - facebook_url = models.URLField(blank=True, null=True) - twitter_username = models.CharField(max_length=255, blank=True,null=True) - # created_by = models.ForeignKey( - # Profile, related_name="contact_created_by", on_delete=models.SET_NULL, null=True - # ) - is_active = models.BooleanField(default=False) - assigned_to = models.ManyToManyField(Profile, related_name="contact_assigned_users") - teams = models.ManyToManyField(Teams, related_name="contact_teams") - org = models.ForeignKey(Org, on_delete=models.SET_NULL, null=True, blank=True) - country = models.CharField(max_length=3, choices=COUNTRIES, blank=True, null=True) - - class Meta: - verbose_name = "Contact" - verbose_name_plural = "Contacts" - db_table = "contacts" - ordering = ("-created_at",) - - def __str__(self): - return self.first_name - - @property - def created_on_arrow(self): - return arrow.get(self.created_at).humanize() - - @property - def created_on(self): - return self.created_at - - - @property - def get_team_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return Profile.objects.filter(id__in=team_user_ids) - - @property - def get_team_and_assigned_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = team_user_ids + assigned_user_ids - return Profile.objects.filter(id__in=user_ids) - - @property - def get_assigned_users_not_in_teams(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = set(assigned_user_ids) - set(team_user_ids) - return Profile.objects.filter(id__in=list(user_ids)) diff --git a/contacts/swagger_params1.py b/contacts/swagger_params1.py deleted file mode 100644 index ecc52d5e..00000000 --- a/contacts/swagger_params1.py +++ /dev/null @@ -1,61 +0,0 @@ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter - -organization_params_in_header = organization_params_in_header = OpenApiParameter( - "org", OpenApiTypes.STR, OpenApiParameter.HEADER -) - -organization_params = [ - organization_params_in_header, -] - -contact_list_get_params = [ - organization_params_in_header, - OpenApiParameter("name", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("city", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("assigned_to", OpenApiTypes.STR,OpenApiParameter.QUERY), -] - -contact_create_post_params = [ - organization_params_in_header, - OpenApiParameter( - "salutation", OpenApiParameter.QUERY, OpenApiTypes.STR - ), - OpenApiParameter( - "first_name", OpenApiParameter.QUERY, OpenApiTypes.STR - ), - OpenApiParameter( - "last_name", OpenApiParameter.QUERY, OpenApiTypes.STR - ), - OpenApiParameter( - "date_of_birth", - OpenApiParameter.QUERY, - OpenApiTypes.STR - ), - OpenApiParameter("organization", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter( - "title", OpenApiParameter.QUERY, OpenApiTypes.STR - ), - OpenApiParameter("primary_email", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("secondary_email", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("mobile_number", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("secondary_number", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("department", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("language", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("do_not_call", OpenApiParameter.QUERY, OpenApiTypes.BOOL), - OpenApiParameter( - "address_line", OpenApiParameter.QUERY, OpenApiTypes.STR - ), - OpenApiParameter("street", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("city", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("state", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("pincode", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("country", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("description", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("linked_in_url", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("facebook_url", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("twitter_username", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("teams", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("assigned_to", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("contact_attachment", OpenApiParameter.QUERY, OpenApiTypes.BINARY), -] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 7828c851..00000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: '3' -services: - crm-app: - image: djcrm:1 - container_name: crm-app - environment: - - DBNAME=crm - - DBUSER=crm - - DBPASSWORD=crm - - DBHOST=crm-db - - DBPORT=5432 - - ENV_TYPE=development - - DEFAULT_FROM_EMAIL=your_email@example.com - - ADMIN_EMAIL=your_admin_email@example.com - - CELERY_BROKER_URL="celery.url" - - CELERY_RESULT_BACKEND="celery.backend.url" - - DOMAIN_NAME="DOMAIN.NAME" - - SWAGGER_ROOT_URL="SWAGGER.ROOT.URL" - depends_on: - - crm-db - ports: - - 8000:8000 - networks: - - nw - - crm-db: - image: postgres:12-bookworm - container_name: crm-db - environment: - - POSTGRES_DB=crm - - POSTGRES_USER=crm - - POSTGRES_PASSWORD=crm - ports: - - 5432:5432 - volumes: - - /var/run/postgresql/:/var/run/postgresql - networks: - - nw - -networks: - nw: {} diff --git a/docker/dockerfile b/docker/dockerfile deleted file mode 100644 index 0f7af4bb..00000000 --- a/docker/dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM ubuntu:22.04 - -ARG DEBIAN_FRONTEND=noninteractive -ARG PIP_EXTRA_INDEX_URL - -WORKDIR /app - -# Intall dependencies -COPY requirements.txt /app - -RUN apt update -RUN apt install -y git ruby-dev ruby-ffi postgresql-client redis-server wkhtmltopdf -RUN apt clean -RUN gem install sass -RUN gem install compass -# install nvm/npm -# RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash -RUN apt install nodejs npm -y -RUN npm -g install less -RUN apt install -y python3-pip -RUN python3 -m pip install --no-cache-dir -r requirements.txt -RUN python3 -m pip install --no-cache-dir redis - -RUN python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' > /app/secret_key.txt -ENV SECRET_KEY \ - $(cat /app/secret_key.txt) -RUN rm /app/secret_key.txt - - -COPY . /app -COPY docker/entrypoint.sh /app -COPY docker/wait-for-postgres.sh /app - - -RUN chmod +x entrypoint.sh -RUN chmod +x wait-for-postgres.sh -ENTRYPOINT ["./entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100644 index b3125dfe..00000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Execute startup scripts -./wait-for-postgres.sh "$DBHOST" -python3 manage.py collectstatic --noinput -python3 manage.py migrate -python3 manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/docker/wait-for-postgres.sh b/docker/wait-for-postgres.sh deleted file mode 100644 index 49a49799..00000000 --- a/docker/wait-for-postgres.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# wait-for-postgres.sh - -set -e - -host="$1" -shift -cmd=( "$@" ) - -echo "Testing Postgres connection with psql ..." -until PGPASSWORD="$DBPASSWORD" psql -h "$host" -U "$DBUSER" -c "\q"; do - >&2 echo -n . - sleep 1 -done - ->&2 echo "Postgres is up - executing command" -exec $cmd diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 182d33ac..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MicroSite.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MicroSite.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/MicroSite" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MicroSite" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/source/Accounts.rst b/docs/source/Accounts.rst deleted file mode 100644 index 5e644f4c..00000000 --- a/docs/source/Accounts.rst +++ /dev/null @@ -1,38 +0,0 @@ -Accounts -******** - -| These have the user roles i.e Admin and User. User Role will have permissions where Admin can access. - -| Create your Accounts - new account can be created for different users and contacts and leads can create in accounts itself in their view pages - - - - -| **Fig** Accounts page - -| This page view describes us about the accounts available and basic information. - -| There are 2 types of accounts i.e Open/Closed accounts. - -| We can Edit/Delete accounts here - -| To create an account we have a option "Add New Account" at the top right corner On Clicking it you will be redirected to the below page. - -| **Optional:** We can filter the users based on name,city,tags . - - -| **Fig** Account create page - -| Here while we are creating an account, it is mandatory to have minimum one contact and one lead. - -| **Note:** Fields having ``*`` are mandatory - - - - - -.. | Rules to follow:- - -.. 1.writing test cases for the code - -.. 2.test cases coverage percent should be above 90% diff --git a/docs/source/Cases.rst b/docs/source/Cases.rst deleted file mode 100644 index 12777f15..00000000 --- a/docs/source/Cases.rst +++ /dev/null @@ -1,18 +0,0 @@ -Cases -***** - - - -| **Fig** cases Home Page - -| The above figure shows the cases view, this page gives details and list view of leads -| You can filter the results by name, status, priority, account. - -| Create a new Lead using the button on right corner ``+ Add New case`` - - -| **Fig** cases Create Page - -| **Note:** Fields having ``*`` are mandatory. - -| **Note:** To create a cases user,accounts are required. \ No newline at end of file diff --git a/docs/source/Change Password.rst b/docs/source/Change Password.rst deleted file mode 100644 index 77ba43c5..00000000 --- a/docs/source/Change Password.rst +++ /dev/null @@ -1,10 +0,0 @@ -Change Password -*************** - - - -| **Fig** Password Change Page - -| The above figure shows the password change view, here enter the old password in "old password field" and enter new password in "new password field" and retype the new password in "confirm new password" field. - -| Click "Change Password" button to change the password successfully, otherwise cancel to cancel the process. diff --git a/docs/source/Contacts.rst b/docs/source/Contacts.rst deleted file mode 100644 index 02871320..00000000 --- a/docs/source/Contacts.rst +++ /dev/null @@ -1,21 +0,0 @@ -Contacts -******** - -| Contacts can be created and can be assigned to Accounts - - - - -| **Fig** Contacts Home Page - -| The above figure shows the contacts view, this page gives details and list view of leads -| You can filter the results by name, city, assigned user. - -| Create a new Lead using the button on right corner ``+ Add New contact`` - - -| **Fig** Contacts Create Page - -| **Note:** Fields having ``*`` are mandatory. - -| **Note:** To create a contacts lead is required. \ No newline at end of file diff --git a/docs/source/Documents.rst b/docs/source/Documents.rst deleted file mode 100644 index c6c9a524..00000000 --- a/docs/source/Documents.rst +++ /dev/null @@ -1,16 +0,0 @@ -Documents -********* - - - -| **Fig** documents Home Page - -| The above figure shows the documents view, this page gives details and list view of leads -| You can filter the results by title, status, shared to. - -| Create a new Document using the button on right corner ``+ Add New Document`` - - -| **Fig** Documents Create Page - -| **Note:** Fields having ``*`` are mandatory. diff --git a/docs/source/Forget Password.rst b/docs/source/Forget Password.rst deleted file mode 100644 index c03a0561..00000000 --- a/docs/source/Forget Password.rst +++ /dev/null @@ -1,37 +0,0 @@ -Forget Password -*************** - - -| **Fig** Forget Password Page View - -| Enter the registered email id and click submit, then you will be sent a email to the registered email address, and redirect to the acknowledge page as below figure - - -| **Fig** Email Sent acknowledgement view - -| When you check the mail you will receive a email similar to the below figure. - - -| **Fig** Email recieved - -| When you click on the link in the mail, you will get a page as below - - -| **Fig** Password Reset view - - -| After you successful change the password you will get below page - - -| **Fig** Password Reset Successful view - -| Now click on login and enter your credentials - -| After successful login you will be redirected to Dashboard which looks similiar to the below figure - - -| **Fig** Dashboard view - -| The home page/Dashboard have access to all the modules of the CRM - -| **Note:** The numbers below accounts, contacts, leads, opportunities represent the total count of each module. \ No newline at end of file diff --git a/docs/source/Leads.rst b/docs/source/Leads.rst deleted file mode 100644 index 11098fa8..00000000 --- a/docs/source/Leads.rst +++ /dev/null @@ -1,16 +0,0 @@ -Leads -***** - - - -| **Fig** Accounts Home Page - -| This is Account Home Page, this page gives details and list view of leads -| You can filter the results by name, source, assigned user, status, tags - -| Create a new Lead using the button on right corner ``+ Add New Lead`` - - -| **Fig** Leads Create Page - -| **Note:** Fields having ``*`` are mandatory diff --git a/docs/source/Login.rst b/docs/source/Login.rst deleted file mode 100644 index 798dbc2c..00000000 --- a/docs/source/Login.rst +++ /dev/null @@ -1,5 +0,0 @@ -Login -***** - - -| **Fig** Login Page diff --git a/docs/source/Opportunity.rst b/docs/source/Opportunity.rst deleted file mode 100644 index 80ea57d2..00000000 --- a/docs/source/Opportunity.rst +++ /dev/null @@ -1,21 +0,0 @@ -Opportunity -*********** - -| New Opportunities can be added and can be assigned to accounts and contacts. - - - -| **Fig** Opportunity Home Page - -| The above figure shows the Opportunity view, this page gives details and list view of leads -| They are two types of leads open and closed -| You can filter the results by name, stage, account, lead source - -| Create a new Lead using the button on right corner ``+ Add New Opportunity`` - - -| **Fig** Opportunity Create Page - -| **Note:** Fields having ``*`` are mandatory. - -| **Note:** To create a Opportunity lead is required. \ No newline at end of file diff --git a/docs/source/Profile.rst b/docs/source/Profile.rst deleted file mode 100644 index 7328d17e..00000000 --- a/docs/source/Profile.rst +++ /dev/null @@ -1,78 +0,0 @@ -Profile -******* - -Dropdown menu for user settings - - - -| **Fig** Dropdown menu - -Here in this drop down all the essential options for the user profile management are available i.e. - -Users -~~~~~ - -| On clicking users, the following page is shown - - -| **Fig:** Users view - -| Here the user can activate/deactivate, edit/delete user data. - -| To create new user click on `` + Add New User``. - -| One clicking `` + Add New User`` the following page is shown: - - -| **Fig** User create page - -| **Note:** Fields having ``*`` are mandatory. - - - -| **Fig:** User Edit User page - -Settings -~~~~~~~~~ -| On clicking settings, the following page is shown: - -.. image:: screenshots/settingshome.png - :align: center - -| **Fig:** User Settings page - -| Click on `` + Add New Setting`` - - - -| **Fig:** User Create Setting page - - -Change Password -~~~~~~~~~~~~~~~ - -| On clicking settings, the following page is shown: - - - -| **Fig:** User Change password page - -Profile -~~~~~~~ - -| On clicking settings, the following page is shown: - - - -| **Fig:** User profile page - - - -| **Fig:** User edit profile page - -| Here user can edit his personal details. - - -Logout -~~~~~~~ -On Clicking logout you will be logged out from the account and redirected to Login page. \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index b398bfbd..00000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,265 +0,0 @@ -# -*- coding: utf-8 -*- -# -# django-blog-it documentation build configuration file, created by -# sphinx-quickstart on Mon Dec 29 20:41:44 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -from recommonmark.parser import CommonMarkParser - -source_parsers = { - ".md": CommonMarkParser, -} - - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.napoleon"] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["ntemplates"] - -# The suffix of source filenames. -# source_suffix = '.rst' -source_suffix = [".rst", ".md"] - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = "Django-CRM" -copyright = "2017, MicroPyramid" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = "0.7.0" -# The full version, including alpha/beta/rc tags. -release = "0.7.0" - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "default" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["nstatic"] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = "Django-CRM-doc" - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - #'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ("index", "Django-CRM.tex", "Django-CRM Documentation", "Django-CRM", "manual"), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [("index", "Django-CRM", "Django-CRM Documentation", ["Django-CRM"], 1)] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - "index", - "Django-CRM", - "Django-CRM Documentation", - "Django-CRM", - "Django-CRM", - "One line description of project.", - "Miscellaneous", - ), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 36145d99..00000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,127 +0,0 @@ -========== -Django CRM -========== - -Introduction: -============= - -Django CRM is an Open Source CRM developed on the Django framework. It has all the basic features of CRM to start with. We welcome code contributions and feature requests via Github. - -Source Code is available in Micropyramid Repository() `Link `_. - -Tech stack used: -================ - -* Python >= 3.4 -* Django >= 2.0 -* Redis -* django-simple-pagination - -Modules available in Django-CRM: -================================ - -User functionalites: -******************** -.. toctree:: - :maxdepth: 2 - - Login - Forget Password - Profile - Change Password - - -Modules in crm: -*************** - -.. toctree:: - :maxdepth: 2 - - Accounts - Contacts - Leads - Opportunity - Cases - Documents - -Rules to follow:- -================= -| 1.writing test cases for the code -| 2.test cases coverage percent should be above 90% - - - -Setup On Local System -===================== - -On Windows -********** - -On Ubuntu -********* -| First, create a Virtual Environment in a local directory. -| Install Pip to install python packages. -| Then clone the source code from the repository `click here `_. -| Activate Virtual environment, then install the requirements.txt using the command - -:: - - pip install -r requirements.txt - -| Then execute - -:: - - python manage.py runserver - -Now, open your browser and enter the url ``http://127.0.0.1:8000`` - -Installation - Requirements -=========================== - - -Ubuntu 64bit - 16.04 -********************* - -| $ sudo apt-get update && apt-get upgrade -y -| $ sudo apt-get install -y curl wget libpq-dev python3-dev gem ruby ruby-dev build-essential libssl-dev libffi-dev python-dev python-virtualenv python-pip git redis-server libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev tcl8.6-dev tk8.6-dev python-tk -| $ sudo gem install sass - -Visit our Django web development page [Here](https://micropyramid.com/django-ecommerce-development/) - - -Docker -********************* - -To install using Docker, you need to have Docker installed in your system. - -1. Clone the repository into your system. - -2. Then run the following commands to build and run the container: - - | $ docker build -t djcrm:1 -f docker/Dockerfile . - | $ docker-compose -f docker/docker-compose.yml up - - The first line will build your container image and the second one will run it. - - There you will be able to see what is going on inside the container. - - If you head to localhost:8000/admin you will get to the admin panel of Wagtail. - - 3. In order to be able to login to the admin panel you have to create an admin user. If you are using VSCode, go to the bottom left corner and click on it, there go to "Attach to running container" and you will see two options, click on the one called "crm-app". Once inside the container, then cd into the app directory and run: - - | $ python3 manage.py createsuperuser - - After this you will be able to log into the admin panel with the user you just created. - -We welcome your feedback and support, raise github ticket if you want to report a bug or need new feature. - -Need additional support? `Contact us here`_ - -.. _contact us here: https://micropyramid.com/contact-us/ - - or - -mailto:: "hello@micropyramid.com" - - diff --git a/emails/admin.py b/emails/admin.py deleted file mode 100644 index 56b8b709..00000000 --- a/emails/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin - -from emails.models import Email - -# Register your models here. -admin.site.register(Email) diff --git a/emails/apps.py b/emails/apps.py deleted file mode 100644 index 4290ce3a..00000000 --- a/emails/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class EmailsConfig(AppConfig): - name = "emails" diff --git a/emails/forms.py b/emails/forms.py deleted file mode 100644 index 903defb7..00000000 --- a/emails/forms.py +++ /dev/null @@ -1,15 +0,0 @@ -from django import forms - -from .models import Email - - -class EmailForm(forms.ModelForm): - - from_email = forms.EmailField(max_length=200, required=True) - to_email = forms.EmailField(max_length=200, required=True) - subject = forms.CharField(max_length=200, required=True) - message = forms.CharField(max_length=200, required=True) - - class Meta: - model = Email - fields = ("from_email", "to_email", "subject", "message") diff --git a/emails/migrations/0001_initial.py b/emails/migrations/0001_initial.py deleted file mode 100644 index 346d070a..00000000 --- a/emails/migrations/0001_initial.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Email', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('from_email', models.EmailField(max_length=200)), - ('to_email', models.EmailField(max_length=200)), - ('subject', models.CharField(max_length=200)), - ('message', models.CharField(max_length=200)), - ('file', models.FileField(null=True, upload_to='files/')), - ('send_time', models.DateTimeField(auto_now_add=True)), - ('status', models.CharField(default='sent', max_length=200)), - ('important', models.BooleanField(default=False, max_length=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Email', - 'verbose_name_plural': 'Emails', - 'db_table': 'email', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/emails/models.py b/emails/models.py deleted file mode 100644 index 0d608f76..00000000 --- a/emails/models.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.db import models -from common.base import BaseModel - -# Create your models here. - - -class Email(BaseModel): - - from_email = models.EmailField(max_length=200) - to_email = models.EmailField(max_length=200) - subject = models.CharField(max_length=200) - message = models.CharField(max_length=200) - file = models.FileField(null=True, upload_to="files/") - # send_time = models.DateTimeField(default=datetime.now) - send_time = models.DateTimeField(auto_now_add=True) - status = models.CharField(max_length=200, default="sent") - important = models.BooleanField(max_length=10, default=False) - - class Meta: - verbose_name = "Email" - verbose_name_plural = "Emails" - db_table = "email" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.message_subject}" \ No newline at end of file diff --git a/emails/serializer.py b/emails/serializer.py deleted file mode 100644 index 0cecab1e..00000000 --- a/emails/serializer.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework import serializers - -from emails.models import Email - - -class EmailSerailizer(serializers.ModelSerializer): - class Meta: - model = Email - fields = "__all__" diff --git a/emails/templates/create_mail.html b/emails/templates/create_mail.html deleted file mode 100644 index 37fa7339..00000000 --- a/emails/templates/create_mail.html +++ /dev/null @@ -1,156 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% block breadcrumb %} - -
- - - - -
-
-
-
-
- Compose email -
-
-
-
-
-
- - - -
- {{ form.from_email.errors }} -
-
-
- - -
-
-
-
- - -
- {{ form.to_email.errors }} -
-
-
- - -
-
-
-
- - -
- {{ form.subject.errors }} -
-
-
-
-
- -
-
- -
- -
- -
-
-
-
-
-
- - -
-
-
-
- - -
- {{ form.message.errors }} -
-
-
-
-
-
-
-
-
- -
-
- - -
-
- -
-
-
- - - -{% endblock %} - - - - - - -{% block js_block %} - -{% endblock js_block %} - - \ No newline at end of file diff --git a/emails/templates/emails.html b/emails/templates/emails.html deleted file mode 100644 index d766a7c8..00000000 --- a/emails/templates/emails.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - -
- -
- -
- -
- -
- -
- - - - - - - - - - -{% for sent in email_sent %} - - - - - - - - -{% endfor %} -
TO : {{sent.to_email}}{{sent.subject}}{{sent.message}}{{sent.send_time}}{{sent.file}}delete
- - -{% for trash in email_trash %} - - - - - - - - -{% endfor %} -
TO : {{trash.to_email}}{{trash.subject}}{{trash.message}}{{trash.send_time}}{{trash.file}}delete
- - -{% for draft in email_draft %} - - - - - - - - -{% endfor %} -
TO : {{draft.to_email}}{{draft.subject}}{{draft.message}}{{draft.send_time}}{{draft.file}}delete
- - - - - - - diff --git a/emails/templates/mail_all.html b/emails/templates/mail_all.html deleted file mode 100644 index 484b0d05..00000000 --- a/emails/templates/mail_all.html +++ /dev/null @@ -1,122 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load paginate %} -{% block breadcrumb %} - -
- -
- {% include 'mail_center.html' %} -
- -
- -
-
-
- -
-
-
- -
- -
-
-
- filters -
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
-
-
-
-
-
-
- - - - - - - - - - Total Mails :{{filter_list.count}} - - - - {% paginate 10 filter_list as filter_list %} - {% for fil in filter_list %} - - - - - - - - - {% endfor %} - -
ToSubjectMessageDate SentStatus
{{fil.to_email}}{{fil.subject}}{{fil.message|stringformat:".5s"}}{{fil.send_time}}{{fil.status}}
- {% ifequal filter_list|length 0 %} -

-
No Mails Found
-

- {% else %} -
- -
- {%endifequal%} -
-
- -
-
-
-
- - -{% endblock %} -{% block js_block %} - -{% endblock js_block %} - - \ No newline at end of file diff --git a/emails/templates/mail_center.html b/emails/templates/mail_center.html deleted file mode 100644 index 5fbfcc0b..00000000 --- a/emails/templates/mail_center.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% block breadcrumb %} - -
- -
-
Mail Center
-
-
-
-
-
- - -
- -
- -
-
-
-
-
- - - -
-
-
-
- -
-
- - - -{% endblock %} -{% block js_block %} - - -{% endblock %} \ No newline at end of file diff --git a/emails/templates/mail_drafts.html b/emails/templates/mail_drafts.html deleted file mode 100644 index 6ac8a0fb..00000000 --- a/emails/templates/mail_drafts.html +++ /dev/null @@ -1,130 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load paginate %} -{% block breadcrumb %} - -
- -
- {% include 'mail_center.html' %} -
- -
- -
-
-
- -
-
-
- -
- -
-
-
- filters -
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
-
-
-
- -
-
- - - - - - - - - - Total Drafts : {{filter_list.count}} - - - - {% paginate 10 filter_list as filter_list %} - {% for draft in filter_list %} - - - - - - - - - - {% endfor %} - -
ToSubjectMessageDate SentStatus
{{draft.to_email}}{{draft.subject}}{{draft.message|stringformat:".5s"}}{{draft.send_time}}{{draft.status}} - -
- {% ifequal filter_list|length 0 %} -

-
No Mails Found
-

- {% else %} -
- -
- {%endifequal%} -
-
-
-
-
-
- -
-{% endblock %} -{% block js_block %} - -{% endblock js_block %} \ No newline at end of file diff --git a/emails/templates/mail_important.html b/emails/templates/mail_important.html deleted file mode 100644 index 441de305..00000000 --- a/emails/templates/mail_important.html +++ /dev/null @@ -1,130 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load paginate %} -{% block breadcrumb %} - -
- -
- {% include 'mail_center.html' %} -
- -
- -
-
-
- -
-
-
- -
-
-
-
- filters -
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
-
-
-
-
-
- - - - - - - - - - - Total Mails : {{filter_list.count}} - - - - {% paginate 10 filter_list as filter_list %} - {% for imp in filter_list %} - - - - - - - - - - - {% endfor %} - -
ToSubjectMessageDate SentStatusimportant
{{imp.to_email}}{{imp.subject}}{{imp.message|stringformat:".5s"}}{{imp.send_time}}{{imp.status}}{{imp.important}} - -
- {% ifequal filter_list|length 0 %} -

-
No Mails Found
-

- {% else %} -
- -
- {%endifequal%} -
-
-
-
-
-
- -
-{% endblock %} -{% block js_block %} - -{% endblock js_block %} - - \ No newline at end of file diff --git a/emails/templates/mail_sent.html b/emails/templates/mail_sent.html deleted file mode 100644 index ab5e7387..00000000 --- a/emails/templates/mail_sent.html +++ /dev/null @@ -1,133 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load paginate %} -{% block breadcrumb %} - -
- -
- {% include 'mail_center.html' %} -
- -
- -
-
-
- -
-
-
- - -
-
-
-
- filters -
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
-
-
-
-
- -
-
- - - - - - - - - - Total Sent Mails : {{filter_list.count}} - - - - {% paginate 10 filter_list as filter_list %} - {% for sent in filter_list %} - - - - - - - - - {% endfor %} - -
TOSubjectMessageDate SentStatus
{{sent.to_email}}{{sent.subject}}{{sent.message|stringformat:".5s"}}{{sent.send_time}}{{sent.status}} - -
- {% ifequal filter_list|length 0 %} -

-
No Mails Found
-

- {% else %} -
- -
- {%endifequal%} -
-
-
-
-
-
- - -{% endblock %} -{% block js_block %} - -{% endblock js_block %} - - \ No newline at end of file diff --git a/emails/templates/mail_trash.html b/emails/templates/mail_trash.html deleted file mode 100644 index 1952b08c..00000000 --- a/emails/templates/mail_trash.html +++ /dev/null @@ -1,130 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load paginate %} -{% block breadcrumb %} - -
- -
- {% include 'mail_center.html' %} -
- -
- -
-
-
- -
-
-
- -
- -
-
-
- filters -
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
-
-
-
- -
-
- - - - - - - - - Total Trash : {{filter_list.count}} - - - - {% paginate 10 filter_list as filter_list %} - {% for trash in filter_list %} - - - - - - - - - {% endfor %} - -
TOSubjectMessageDate SentStatus
{{trash.to_email}}{{trash.subject}}{{trash.message|stringformat:".5s"}}{{trash.send_time}}{{trash.status}} - -
- {% ifequal filter_list|length 0 %} -

-
No Mails Found
-

- {% else %} -
- -
- {%endifequal%} -
-
-
-
-
-
- -
-{% endblock %} -{% block js_block %} - -{% endblock js_block %} - - \ No newline at end of file diff --git a/emails/tests.py b/emails/tests.py deleted file mode 100644 index 1d0d3c02..00000000 --- a/emails/tests.py +++ /dev/null @@ -1,177 +0,0 @@ -from django.test import Client, TestCase - -from common.models import Org, User -from emails.forms import EmailForm -from emails.models import Email - - -class UserCreation(TestCase): - def setUp(self): - self.client = Client() - self.org, _ = Org.objects.get_or_create( - name="test org", address="IN", country="IN" - ) - self.user = User.objects.create( - first_name="janeEmail@example.com", - username="jane", - email="janeEmail@example.com", - role="ADMIN", - org=self.org, - ) - self.user.set_password("password") - self.user.save() - self.email = Email.objects.create( - from_email="admin@micropyramid.com", - to_email="janeEmail@example.com", - subject="subject ", - message="message", - important=False, - ) - self.client.login(username="janeEmail@example.com", password="password") - - -class EmailSentEdit(UserCreation, TestCase): - def test_edit_form_valid(self): - form = EmailForm( - data={ - "from_email": "john@doe.com", - "to_email": "jane@doe.com", - "subject": "test subject", - "message": "test message", - } - ) - # print('yes') - self.assertTrue(form.is_valid()) - - def test_edit_form_invalid(self): - form = EmailForm( - data={ - "from_email": "john@doe.com", - "to_email": "", - "subject": "test subject", - "message": "test message", - } - ) - # print('yes2') - self.assertFalse(form.is_valid()) - - -class EmailListTestCase(UserCreation, TestCase): - def test_email_list(self): - url = "/emails/list/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class EmailTestCase(UserCreation, TestCase): - def test_email_compose(self): - url = "/emails/compose/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_email_trash_get(self): - url = "/emails/email_trash_delete/" + str(self.email.pk) + "/" - # print(url) - response = self.client.get(url) - self.assertEqual(response.status_code, 302) - - def test_email_send_fail(self): - url = "/emails/compose/" - data = { - "from_email": "john@doe.com", - "to_email": "", - "subject": "sample subject", - "message": "sample message", - } - response = self.client.post(url, data) - self.assertEqual(response.status_code, 200) - - # email_trash_delete/ - - def test_email_send(self): - url = "/emails/compose/" - data = { - "from_email": "john@doe.com", - "to_email": "jane@doe.com", - "subject": "sample subject", - "message": "sample message", - } - response = self.client.post(url, data) - get_email = Email.objects.get(subject="sample subject") - # boo = Email.objects.get(important=True) - # print('yes') - self.assertFalse(get_email.important) - self.assertEqual(get_email.subject, get_email.__str__()) - - self.assertEqual(response.status_code, 302) - - def test_email_sent(self): - url = "/emails/email_sent/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_email_trash(self): - url = "/emails/email_trash/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_email_draft(self): - url = "/emails/email_draft/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_email_draft_delete(self): - url = "/emails/email_draft_delete/" + str(self.email.pk) + "/" - response = self.client.get(url) - self.assertEqual(response.status_code, 302) - - def test_email_delete(self): - url = "/emails/email_delete/" + str(self.email.pk) + "/" - response = self.client.get(url) - self.assertEqual(response.status_code, 302) - - def test_email_view(self): - url = "/emails/email_view/" + str(self.email.pk) + "/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_email_sent_edit_get(self): - url = "/emails/email_sent_edit/" + str(self.email.pk) + "/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_email_sent_edit_post(self): - url = "/emails/email_sent_edit/" + str(self.email.pk) + "/" - data = { - "from_email": "john@doe.com", - "to_email": "jane@doe.com", - "subject": "subject", - "message": "message", - } - data1 = { - "from_email": "john@doe.com", - "to_email": "", - "subject": "subject", - "message": "message", - } - response = self.client.post(url, data) - response1 = self.client.post(url, data1) - self.assertEqual(response.status_code, 302) - self.assertEqual(response1.status_code, 200) - - def test_email_imp_list(self): - url = "/emails/email_imp_list/" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - # def test_email_move_to_trash(self): - # url = "/emails/email_move_to_trash/" + str(self.email.pk) + "/" - # response = self.client.get(url) - # self.assertEqual(response.status_code, 302) - # response = self.client.post(reverse('302'), {}, HTTP_REFERER=url) - - # def test_email_trash_del(self): - # url = "/emails/trash_delete/"+str(self.email.pk)+"/$" - # print(url) - # response = self.client.get(url) - # self.assertEqual(response.status_code,200) diff --git a/emails/urls.py b/emails/urls.py deleted file mode 100644 index 14bcf41d..00000000 --- a/emails/urls.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.conf import settings -from django.conf.urls import url -from django.conf.urls.static import static - -from . import views - -app_name = "emails" - - -urlpatterns = [ - url(r"^list/", views.emails_list, name="list"), - url(r"^compose/", views.email, name="compose"), - url(r"^email_sent/", views.email_sent, name="email_sent"), - url( - r"^email_move_to_trash/(?P\d+)/$", - views.email_move_to_trash, - name="email_move_to_trash", - ), - url(r"^email_delete/(?P\d+)/$", views.email_delete, name="email_delete"), - url(r"^email_trash/", views.email_trash, name="email_trash"), - url( - r"^email_trash_delete/(?P\d+)/$", - views.email_trash_delete, - name="email_trash_delete", - ), - url(r"^email_draft/", views.email_draft, name="email_draft"), - url( - r"^email_draft_delete/(?P\d+)/$", - views.email_draft_delete, - name="email_draft_delete", - ), - url(r"^email_imp/(?P\d+)/$", views.email_imp, name="email_imp"), - url(r"^email_imp_list/", views.email_imp_list, name="email_imp_list"), - url( - r"^email_sent_edit/(?P\d+)/$", views.email_sent_edit, name="email_sent_edit" - ), - url(r"^email_unimp/(?P\d+)/$", views.email_unimp, name="email_unimp"), - url(r"^email_view/(?P\d+)/$", views.email_view, name="email_view"), -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/emails/views.py b/emails/views.py deleted file mode 100644 index 90564927..00000000 --- a/emails/views.py +++ /dev/null @@ -1,195 +0,0 @@ -from datetime import datetime, timedelta - -from django.core.mail import EmailMessage -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404, render -from django.urls import reverse - -from emails.forms import EmailForm -from emails.models import Email - - -def emails_list(request): - filter_list = Email.objects.all() - if request.GET.get("from_date", ""): - from_date = request.GET.get("from_date", "") - fd = datetime.strptime(from_date, "%Y-%m-%d").date() - filter_list = filter_list.filter(send_time__gte=fd) - if request.GET.get("to_date", ""): - to_date = request.GET.get("to_date", "") - td = datetime.strptime(to_date, "%Y-%m-%d") - td = td + timedelta(seconds=(24 * 60 * 60 - 1)) - filter_list = filter_list.filter(send_time__lte=td) - if request.GET.get("name", ""): - name = request.GET.get("name", "") - filter_list = filter_list.filter(to_email__startswith=name) - return render(request, "mail_all.html", {"filter_list": filter_list}) - - -def email(request): - if request.method == "POST": - form = EmailForm(request.POST, request.FILES) - if form.is_valid(): - subject = request.POST.get("subject", "") - message = request.POST.get("message", "") - from_email = request.POST.get("from_email", "") - to_email = request.POST.get("to_email", "") - file = request.FILES.get("files", None) - status = request.POST.get("email_draft", "") - email = EmailMessage(subject, message, from_email, [to_email]) - email.content_subtype = "html" - f = form.save() - if file is not None: - email.attach(file.name, file.read(), file.content_type) - f.file = file - if status: - f.status = "draft" - else: - email.send(fail_silently=False) - f.save() - return HttpResponseRedirect(reverse("emails:list")) - else: - return render(request, "create_mail.html", {"form": form}) - else: - form = EmailForm() - return render(request, "create_mail.html", {"form": form}) - - -def email_sent(request): - filter_list = Email.objects.filter(status="sent") - if request.GET.get("from_date", ""): - from_date = request.GET.get("from_date", "") - fd = datetime.strptime(from_date, "%Y-%m-%d").date() - filter_list = filter_list.filter(send_time__gte=fd) - if request.GET.get("to_date", ""): - to_date = request.GET.get("to_date", "") - td = datetime.strptime(to_date, "%Y-%m-%d") - td = td + timedelta(seconds=(24 * 60 * 60 - 1)) - filter_list = filter_list.filter(send_time__lte=td) - if request.GET.get("name", ""): - name = request.GET.get("name", "") - filter_list = filter_list.filter(to_email__startswith=name) - return render(request, "mail_sent.html", {"filter_list": filter_list}) - - -def email_trash(request): - filter_list = Email.objects.filter(status="trash") - if request.GET.get("from_date", ""): - from_date = request.GET.get("from_date", "") - fd = datetime.strptime(from_date, "%Y-%m-%d").date() - filter_list = filter_list.filter(send_time__gte=fd) - if request.GET.get("to_date", ""): - to_date = request.GET.get("to_date", "") - td = datetime.strptime(to_date, "%Y-%m-%d") - td = td + timedelta(seconds=(24 * 60 * 60 - 1)) - filter_list = filter_list.filter(send_time__lte=td) - if request.GET.get("name", ""): - name = request.GET.get("name", "") - filter_list = filter_list.filter(to_email__startswith=name) - return render(request, "mail_trash.html", {"filter_list": filter_list}) - - -def email_trash_delete(request, pk): - get_object_or_404(Email, id=pk).delete() - return HttpResponseRedirect(reverse("emails:email_trash")) - - -def email_draft(request): - filter_list = Email.objects.filter(status="draft") - if request.GET.get("from_date", ""): - from_date = request.GET.get("from_date", "") - fd = datetime.strptime(from_date, "%Y-%m-%d").date() - filter_list = filter_list.filter(send_time__gte=fd) - if request.GET.get("to_date", ""): - to_date = request.GET.get("to_date", "") - td = datetime.strptime(to_date, "%Y-%m-%d") - td = td + timedelta(seconds=(24 * 60 * 60 - 1)) - filter_list = filter_list.filter(send_time__lte=td) - if request.GET.get("name", ""): - name = request.GET.get("name", "") - filter_list = filter_list.filter(to_email__startswith=name) - return render(request, "mail_drafts.html", {"filter_list": filter_list}) - - -def email_draft_delete(request, pk): - get_object_or_404(Email, id=pk).delete() - return HttpResponseRedirect(reverse("emails:email_draft")) - - -def email_delete(request, pk): - get_object_or_404(Email, id=pk).delete() - return HttpResponseRedirect(reverse("emails:email_sent")) - - -def email_move_to_trash(request, pk): - trashitem = get_object_or_404(Email, id=pk) - trashitem.status = "trash" - trashitem.save() - return HttpResponseRedirect(request.META["HTTP_REFERER"]) - - -def email_imp(request, pk): - impitem = get_object_or_404(Email, id=pk) - impitem.important = True - impitem.save() - return HttpResponseRedirect(request.META["HTTP_REFERER"]) - - -def email_imp_list(request): - filter_list = Email.objects.filter(important="True") - if request.GET.get("from_date", ""): - from_date = request.GET.get("from_date", "") - fd = datetime.strptime(from_date, "%Y-%m-%d").date() - filter_list = filter_list.filter(send_time__gte=fd) - - if request.GET.get("to_date", ""): - to_date = request.GET.get("to_date", "") - td = datetime.strptime(to_date, "%Y-%m-%d") - td = td + timedelta(seconds=(24 * 60 * 60 - 1)) - filter_list = filter_list.filter(send_time__lte=td) - if request.GET.get("name", ""): - name = request.GET.get("name", "") - filter_list = filter_list.filter(to_email__startswith=name) - return render(request, "mail_important.html", {"filter_list": filter_list}) - - -def email_sent_edit(request, pk): - em = get_object_or_404(Email, pk=pk) - if request.method == "POST": - form = EmailForm(request.POST, instance=em) - if form.is_valid(): - subject = request.POST.get("subject", "") - message = request.POST.get("message", "") - from_email = request.POST.get("from_email", "") - to_email = request.POST.get("to_email", "") - file = request.FILES.get("files", None) - status = request.POST.get("email_draft", "") - email = EmailMessage(subject, message, from_email, [to_email]) - email.content_subtype = "html" - f = form.save() - if file is not None: - email.attach(file.name, file.read(), file.content_type) - f.file = file - if status: - f.status = "draft" - else: - email.send(fail_silently=False) - f.status = "sent" - f.save() - return HttpResponseRedirect(reverse("emails:list")) - return render(request, "create_mail.html", {"form": form, "em": em}) - form = EmailForm() - return render(request, "create_mail.html", {"form": form, "em": em}) - - -def email_unimp(request, pk): - unimpitem = get_object_or_404(Email, id=pk) - unimpitem.important = False - unimpitem.save() - return HttpResponseRedirect(request.META["HTTP_REFERER"]) - - -def email_view(request, pk): - email_view = get_object_or_404(Email, pk=pk) - x = EmailForm(instance=email_view) - return render(request, "create_mail.html", {"x": x}) diff --git a/events/admin.py b/events/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/events/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/events/apps.py b/events/apps.py deleted file mode 100644 index 60b376ce..00000000 --- a/events/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class EventsConfig(AppConfig): - name = "events" diff --git a/events/migrations/0001_initial.py b/events/migrations/0001_initial.py deleted file mode 100644 index 0371fea6..00000000 --- a/events/migrations/0001_initial.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('contacts', '0001_initial'), - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('teams', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Event', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=64, verbose_name='Event')), - ('event_type', models.CharField(choices=[('Recurring', 'Recurring'), ('Non-Recurring', 'Non-Recurring')], max_length=20)), - ('status', models.CharField(blank=True, choices=[('Planned', 'Planned'), ('Held', 'Held'), ('Not Held', 'Not Held'), ('Not Started', 'Not Started'), ('Started', 'Started'), ('Completed', 'Completed'), ('Canceled', 'Canceled'), ('Deferred', 'Deferred')], max_length=64, null=True)), - ('start_date', models.DateField(default=None)), - ('start_time', models.TimeField(default=None)), - ('end_date', models.DateField(default=None)), - ('end_time', models.TimeField(blank=True, default=None, null=True)), - ('description', models.TextField(blank=True, null=True)), - ('created_on', models.DateTimeField(auto_now_add=True, verbose_name='Created on')), - ('is_active', models.BooleanField(default=True)), - ('disabled', models.BooleanField(default=False)), - ('date_of_meeting', models.DateField(blank=True, null=True)), - ('assigned_to', models.ManyToManyField(blank=True, related_name='event_assigned', to='common.profile')), - ('contacts', models.ManyToManyField(blank=True, related_name='event_contact', to='contacts.contact')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='event_created_by_user', to='common.profile')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.org')), - ('teams', models.ManyToManyField(related_name='event_teams', to='teams.teams')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Event', - 'verbose_name_plural': 'Events', - 'db_table': 'event', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/events/models.py b/events/models.py deleted file mode 100644 index 8b01c326..00000000 --- a/events/models.py +++ /dev/null @@ -1,90 +0,0 @@ -import arrow -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from common.models import Org, Profile -from common.base import BaseModel -from contacts.models import Contact -from teams.models import Teams - -# Create your models here. - - -class Event(BaseModel): - EVENT_TYPE = ( - ("Recurring", "Recurring"), - ("Non-Recurring", "Non-Recurring"), - # ("Call", "Call"), - # ('Meeting', 'Meeting'), - # ('Task', 'Task') - ) - EVENT_STATUS = ( - ("Planned", "Planned"), - ("Held", "Held"), - ("Not Held", "Not Held"), - ("Not Started", "Not Started"), - ("Started", "Started"), - ("Completed", "Completed"), - ("Canceled", "Canceled"), - ("Deferred", "Deferred"), - ) - name = models.CharField(_("Event"), max_length=64) - event_type = models.CharField(max_length=20, choices=EVENT_TYPE) - status = models.CharField( - choices=EVENT_STATUS, max_length=64, blank=True, null=True - ) - contacts = models.ManyToManyField(Contact, blank=True, related_name="event_contact") - assigned_to = models.ManyToManyField( - Profile, blank=True, related_name="event_assigned" - ) - start_date = models.DateField(default=None) - start_time = models.TimeField(default=None) - end_date = models.DateField(default=None) - end_time = models.TimeField(default=None, blank=True, null=True) - description = models.TextField(blank=True, null=True) - created_on = models.DateTimeField(_("Created on"), auto_now_add=True) - created_by = models.ForeignKey( - Profile, - related_name="event_created_by_user", - null=True, - on_delete=models.SET_NULL, - ) - is_active = models.BooleanField(default=True) - disabled = models.BooleanField(default=False) - date_of_meeting = models.DateField(blank=True, null=True) - teams = models.ManyToManyField(Teams, related_name="event_teams") - org = models.ForeignKey(Org, on_delete=models.SET_NULL, null=True, blank=True) - - # tags = models.ManyToManyField(Tag) - - class Meta: - verbose_name = "Event" - verbose_name_plural = "Events" - db_table = "event" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.name}" - - @property - def created_on_arrow(self): - return arrow.get(self.created_on).humanize() - - @property - def get_team_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return Profile.objects.filter(id__in=team_user_ids) - - @property - def get_team_and_assigned_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = team_user_ids + assigned_user_ids - return Profile.objects.filter(id__in=user_ids) - - @property - def get_assigned_users_not_in_teams(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = set(assigned_user_ids) - set(team_user_ids) - return Profile.objects.filter(id__in=list(user_ids)) diff --git a/events/serializer.py b/events/serializer.py deleted file mode 100644 index 3810cccd..00000000 --- a/events/serializer.py +++ /dev/null @@ -1,158 +0,0 @@ -from datetime import datetime, timedelta - -from rest_framework import serializers - -from common.serializer import ( - AttachmentsSerializer, - CommentSerializer, - OrganizationSerializer, - ProfileSerializer, - UserSerializer -) -from contacts.serializer import ContactSerializer -from events.models import Event -from teams.serializer import TeamsSerializer - - -class EventSerializer(serializers.ModelSerializer): - created_by = UserSerializer() - assigned_to = ProfileSerializer(read_only=True, many=True) - contacts = ContactSerializer(read_only=True, many=True) - teams = TeamsSerializer(read_only=True, many=True) - event_attachment = AttachmentsSerializer(read_only=True, many=True) - event_comments = CommentSerializer(read_only=True, many=True) - org = OrganizationSerializer() - - class Meta: - model = Event - fields = ( - "id", - "name", - "event_type", - "status", - "is_active", - "disabled", - "start_date", - "start_time", - "end_date", - "end_time", - "description", - "date_of_meeting", - "created_by", - "created_at", - "contacts", - "teams", - "assigned_to", - "event_attachment", - "event_comments", - "org", - ) - - -class EventCreateSerializer(serializers.ModelSerializer): - def __init__(self, *args, **kwargs): - request_obj = kwargs.pop("request_obj", None) - super(EventCreateSerializer, self).__init__(*args, **kwargs) - self.fields["event_type"].required = True - self.org = request_obj.profile.org - - def validate_name(self, name): - if self.instance: - if ( - Event.objects.filter(name__iexact=name, org=self.org) - .exclude(id=self.instance.id) - .exists() - ): - raise serializers.ValidationError("Event already exists with this name") - else: - if Event.objects.filter(name__iexact=name, org=self.org).exists(): - raise serializers.ValidationError("Event already exists with this name") - return name - - def validate_event_type(self, event_type): - """This Validation Is For Keeping The Field Readonly While Editing or Updating""" - event_type = self.initial_data.get("event_type") - if self.instance: - return self.instance.event_type - else: - return event_type - - def validate_start_date(self, start_date): - if start_date: - if self.instance: - return self.instance.start_date - else: - return start_date - else: - raise serializers.ValidationError("Enter a valid Start date.") - - def validate_end_date(self, end_date): - end_date = self.initial_data.get("end_date") - start_date = self.initial_data.get("start_date") - end_date = datetime.strptime(end_date, "%Y-%m-%d").date() - start_date = datetime.strptime(start_date, "%Y-%m-%d").date() - event_type = self.initial_data.get("event_type") - if event_type == "Recurring": - if start_date == end_date: - raise serializers.ValidationError( - "Start Date and End Date cannot be equal for recurring events" - ) - if start_date > end_date: - raise serializers.ValidationError("End Date cannot be less than start date") - return end_date - - def validate_end_time(self, end_time): - end_time = self.initial_data.get("end_time") - start_time = self.initial_data.get("start_time") - end_time = datetime.strptime(end_time, "%H:%M:%S").time() - start_time = datetime.strptime(start_time, "%H:%M:%S").time() - if not start_time: - raise serializers.ValidationError("Enter a valid start time.") - if start_time > end_time: - raise serializers.ValidationError("End Time cannot be less than Start Time") - return end_time - - class Meta: - model = Event - fields = ( - "id", - "name", - "event_type", - "start_date", - "start_time", - "end_date", - "end_time", - "status", - "is_active", - "disabled", - "description", - "created_by", - "created_at", - "org", - ) - -class EventCreateSwaggerSerializer(serializers.ModelSerializer): - - recurring_days = serializers.CharField() - class Meta: - model = Event - fields = ( - "name", - "event_type", - "contacts", - "start_date", - "start_time", - "end_date", - "end_time", - "teams", - "assigned_to", - "description", - "recurring_days" - ) - -class EventDetailEditSwaggerSerializer(serializers.Serializer): - comment = serializers.CharField() - event_attachment = serializers.FileField() - -class EventCommentEditSwaggerSerializer(serializers.Serializer): - comment = serializers.CharField() diff --git a/events/swagger_params1.py b/events/swagger_params1.py deleted file mode 100644 index ec4b994c..00000000 --- a/events/swagger_params1.py +++ /dev/null @@ -1,37 +0,0 @@ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter - -organization_params_in_header = organization_params_in_header = OpenApiParameter( - "org", OpenApiTypes.STR, OpenApiParameter.HEADER -) - -organization_params = [ - organization_params_in_header, -] - -event_list_get_params = [ - organization_params_in_header, - OpenApiParameter("name", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("created_by", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("assigned_users", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter( - "date_of_meeting", - OpenApiParameter.QUERY, - OpenApiTypes.DATE - ), -] - -event_detail_post_params = [ - organization_params_in_header, - OpenApiParameter( - "event_attachment", - OpenApiParameter.QUERY, - OpenApiTypes.BINARY - ), - OpenApiParameter("comment", OpenApiTypes.STR,OpenApiParameter.QUERY), -] - -event_comment_edit_params = [ - organization_params_in_header, - OpenApiParameter("comment", OpenApiTypes.STR,OpenApiParameter.QUERY), -] diff --git a/events/tasks.py b/events/tasks.py deleted file mode 100644 index f92cd717..00000000 --- a/events/tasks.py +++ /dev/null @@ -1,89 +0,0 @@ -from celery import Celery -from django.conf import settings -from django.core.mail import EmailMessage -from django.template.loader import render_to_string - -from common.models import Profile -from events.models import Event - -app = Celery("redis://") - - -@app.task -def send_email(event_id, recipients): - event = Event.objects.filter(id=event_id).first() - subject = " Invitation for an event." - context = {} - context["event"] = event.name - context["event_id"] = event_id - context["event_created_by"] = event.created_by - context["event_date_of_meeting"] = event.date_of_meeting - context["url"] = settings.DOMAIN_NAME - # recipients = event.assigned_to.filter(is_active=True) - for profile_id in recipients: - recipients_list = [] - profile = Profile.objects.filter(id=profile_id, is_active=True).first() - if profile: - recipients_list.append(profile.user.email) - event_members = event.assigned_to.filter(is_active=True) - - context["other_members"] = list( - event_members.exclude(id=profile.id).values_list( - "user__email", flat=True - ) - ) - if len(context["other_members"]) > 0: - context["other_members"] = ", ".join(context["other_members"]) - else: - context["other_members"] = "" - context["user"] = profile.user.email - html_content = render_to_string( - "assigned_to_email_template_event.html", context=context - ) - msg = EmailMessage(subject=subject, body=html_content, to=recipients_list) - msg.content_subtype = "html" - msg.send() - - # if recipients.count() > 0: - # for recipient in recipients: - # context['other_members'] = list(recipients.exclude( - # id=recipient.id).values_list('email', flat=True)) - # if len(context['other_members']) > 0: - # context['other_members'] = ', '.join(context['other_members']) - # else: - # context['other_members'] = '' - # context['user'] = recipient.email - # html_content = render_to_string( - # 'assigned_to_email_template_event.html', context=context) - # msg = EmailMessage( - # subject=subject, body=html_content, to=[recipient.email, ]) - # msg.content_subtype = "html" - # msg.send() - - # if recipients.count() > 0: - # for recipient in recipients: - # context['other_members'] = list(recipients.exclude( - # id=recipient.id).values_list('email', flat=True)) - # if len(context['other_members']) > 0: - # context['other_members'] = ', '.join(context['other_members']) - # else: - # context['other_members'] = '' - # context['user'] = recipient.email - # html_content = render_to_string( - # 'assigned_to_email_template_event.html', context=context) - # msg = EmailMessage( - # subject=subject, body=html_content, to=[recipient.email, ]) - # msg.content_subtype = "html" - # msg.send() - - # might need to add contacts to TODO - # recipients = event.contacts.all() - # if recipients.count() > 0: - # for recipient in recipients: - # context['user'] = recipient.email - # html_content = render_to_string( - # 'assigned_to_email_template.html', context=context) - # msg = EmailMessage( - # subject=subject, body=html_content, to=[recipient.email, ]) - # msg.content_subtype = "html" - # msg.send diff --git a/events/templates/assigned_to_email_template_event.html b/events/templates/assigned_to_email_template_event.html deleted file mode 100644 index 374e721b..00000000 --- a/events/templates/assigned_to_email_template_event.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends 'root_email_template_new.html' %} - -{% block heading %} -Hello {{ user }} -{% endblock heading %} - - -{% block content_body %} -You have been invited to the following event {{event_name}} created by {{event_created_by}} -{% endblock content_body %} - -{% block extra_content %} -

- When : {{event_date_of_meeting}} -

- -{% if other_members %} -

- Who : {{other_members}} -

-{% endif %} - -{% endblock extra_content %} - - -{% block button_link %} - -{% endblock button_link %} \ No newline at end of file diff --git a/events/tests_celery_tasks.py b/events/tests_celery_tasks.py deleted file mode 100644 index 5d9b4a99..00000000 --- a/events/tests_celery_tasks.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.test import TestCase -from django.test.utils import override_settings - -from events.tasks import send_email -from events.tests import EventObjectTest - - -class TestEventCeleryTasks(EventObjectTest, TestCase): - @override_settings( - CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, - CELERY_ALWAYS_EAGER=True, - BROKER_BACKEND="memory", - ) - def test_event_celery_tasks(self): - task = send_email.apply((self.event.id, [self.user.id, self.user1.id])) - self.assertEqual("SUCCESS", task.state) diff --git a/events/urls.py b/events/urls.py deleted file mode 100644 index d2a27ac6..00000000 --- a/events/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path - -from events import views - -app_name = "api_events" - -urlpatterns = [ - path("", views.EventListView.as_view()), - path("/", views.EventDetailView.as_view()), - path("comment//", views.EventCommentView.as_view()), - path("attachment//", views.EventAttachmentView.as_view()), -] diff --git a/events/views.py b/events/views.py deleted file mode 100644 index ee0ecd4b..00000000 --- a/events/views.py +++ /dev/null @@ -1,537 +0,0 @@ -import json -from datetime import datetime, timedelta - -from django.db.models import Q -from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema - -from rest_framework import status -from rest_framework.pagination import LimitOffsetPagination -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView - -from common.models import Attachments, Comment, Profile, User - -#from common.external_auth import CustomDualAuthentication -from common.serializer import ( - AttachmentsSerializer, - CommentSerializer, - ProfileSerializer -) -from contacts.models import Contact -from contacts.serializer import ContactSerializer -from events import swagger_params1 -from events.models import Event -from events.serializer import EventCreateSerializer, EventSerializer, EventCreateSwaggerSerializer, EventDetailEditSwaggerSerializer, EventCommentEditSwaggerSerializer -from events.tasks import send_email -from teams.models import Teams -from teams.serializer import TeamsSerializer - -WEEKDAYS = ( - ("Monday", "Monday"), - ("Tuesday", "Tuesday"), - ("Wednesday", "Wednesday"), - ("Thursday", "Thursday"), - ("Friday", "Friday"), - ("Saturday", "Saturday"), - ("Sunday", "Sunday"), -) - - -class EventListView(APIView, LimitOffsetPagination): - model = Event - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_context_data(self, **kwargs): - params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") - contacts = Contact.objects.filter(org=self.request.profile.org) - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - queryset = queryset.filter( - Q(assigned_to__in=[self.request.profile]) - | Q(created_by=self.request.profile.user) - ) - contacts = contacts.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) - ).distinct() - - if params: - if params.get("name"): - queryset = queryset.filter(name__icontains=params.get("name")) - if params.get("created_by"): - queryset = queryset.filter(created_by=params.get("created_by")) - if params.getlist("assigned_users"): - queryset = queryset.filter( - assigned_to__id__in=params.get("assigned_users") - ) - if params.get("date_of_meeting"): - queryset = queryset.filter( - date_of_meeting=params.get("date_of_meeting") - ) - context = {} - results_events = self.paginate_queryset(queryset, self.request, view=self) - events = EventSerializer(results_events, many=True).data - if results_events: - offset = queryset.filter(id__gte=results_events[-1].id).count() - if offset == queryset.count(): - offset = None - else: - offset = 0 - context.update({"events_count": self.count, "offset": offset}) - context["events"] = events - context["recurring_days"] = WEEKDAYS - context["contacts_list"] = ContactSerializer(contacts, many=True).data - return context - - @extend_schema( - tags=["Events"], parameters=swagger_params1.event_list_get_params - ) - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) - return Response(context) - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params,request=EventCreateSwaggerSerializer - ) - def post(self, request, *args, **kwargs): - params = request.data - data = {} - serializer = EventCreateSerializer(data=params, request_obj=request) - if serializer.is_valid(): - start_date = params.get("start_date") - end_date = params.get("end_date") - recurring_days = json.dumps(params.get("recurring_days")) - if params.get("event_type") == "Non-Recurring": - event_obj = serializer.save( - created_by=request.profile.user, - date_of_meeting=params.get("start_date"), - is_active=True, - disabled=False, - org=request.profile.org, - ) - - if params.get("contacts"): - obj_contact = Contact.objects.filter( - id=params.get("contacts"), org=request.profile.org - ) - event_obj.contacts.add(obj_contact) - - if params.get("teams"): - teams_list = params.get("teams") - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - event_obj.teams.add(*teams) - - if params.get("assigned_to"): - assinged_to_list = params.get("assigned_to") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org - ) - event_obj.assigned_to.add(*profiles) - - assigned_to_list = list( - event_obj.assigned_to.all().values_list("id", flat=True) - ) - send_email.delay( - event_obj.id, - assigned_to_list, - ) - if params.get("event_type") == "Recurring": - recurring_days = params.get("recurring_days") - if not recurring_days: - return Response( - {"error": True, "errors": "Choose atleast one recurring day"}, - status=status.HTTP_400_BAD_REQUEST, - ) - end_date = datetime.strptime(end_date, "%Y-%m-%d").date() - start_date = datetime.strptime(start_date, "%Y-%m-%d").date() - - delta = end_date - start_date - required_dates = [] - - for day in range(delta.days + 1): - each_date = start_date + timedelta(days=day) - if each_date.strftime("%A") in recurring_days: - required_dates.append(each_date) - - for each in required_dates: - each = datetime.strptime(str(each), "%Y-%m-%d").date() - data = serializer.validated_data - - event = Event.objects.create( - created_by=request.profile.user, - start_date=start_date, - end_date=end_date, - name=data["name"], - event_type=data["event_type"], - description=data["description"], - start_time=data["start_time"], - end_time=data["end_time"], - date_of_meeting=each, - org=request.profile.org, - ) - - if params.get("contacts"): - obj_contact = Contact.objects.filter( - id=params.get("contacts"), org=request.profile.org - ) - event.contacts.add(obj_contact) - - if params.get("teams"): - teams_list = params.get("teams") - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - event.teams.add(*teams) - - if params.get("assigned_to"): - assinged_to_list = params.get("assigned_to") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org - ) - event.assigned_to.add(*profiles) - - assigned_to_list = list( - event.assigned_to.all().values_list("id", flat=True) - ) - send_email.delay( - event.id, - assigned_to_list, - ) - return Response( - {"error": False, "message": "Event Created Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class EventDetailView(APIView): - model = Event - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_object(self, pk): - return Event.objects.get(pk=pk) - - def get_context_data(self, **kwargs): - context = {} - user_assgn_list = [ - assigned_to.id for assigned_to in self.event_obj.assigned_to.all() - ] - if self.request.profile == self.event_obj.created_by: - user_assgn_list.append(self.request.profile.id) - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - if self.request.profile.id not in user_assgn_list: - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - comments = Comment.objects.filter(event=self.event_obj).order_by("-id") - attachments = Attachments.objects.filter(event=self.event_obj).order_by("-id") - assigned_data = self.event_obj.assigned_to.values("id", "user__email") - if self.request.profile.is_admin or self.request.profile.role == "ADMIN": - users_mention = list( - Profile.objects.filter( - is_active=True, - ).values("user__email") - ) - elif self.request.profile != self.event_obj.created_by: - users_mention = [{"username": self.event_obj.created_by.user.email}] - else: - users_mention = list( - self.event_obj.assigned_to.all().values("user__email") - ) - profile_list = Profile.objects.filter(is_active=True, org=self.request.profile.org) - if self.request.profile.role == "ADMIN" or self.request.profile.is_admin: - profiles = profile_list.order_by("user__email") - else: - profiles = profile_list.filter(role="ADMIN").order_by("user__email") - - if self.request.profile == self.event_obj.created_by: - user_assgn_list.append(self.request.profile.id) - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - if self.request.profile.id not in user_assgn_list: - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - team_ids = [user.id for user in self.event_obj.get_team_users] - all_user_ids = profiles.values_list("id", flat=True) - users_excluding_team_id = set(all_user_ids) - set(team_ids) - users_excluding_team = Profile.objects.filter(id__in=users_excluding_team_id) - - selected_recurring_days = Event.objects.filter( - name=self.event_obj.name - ).values_list("date_of_meeting", flat=True) - selected_recurring_days = set( - [day.strftime("%A") for day in selected_recurring_days] - ) - context.update( - { - "event_obj": EventSerializer(self.event_obj).data, - "attachments": AttachmentsSerializer(attachments, many=True).data, - "comments": CommentSerializer(comments, many=True).data, - "selected_recurring_days": selected_recurring_days, - "users_mention": users_mention, - "assigned_data": assigned_data, - } - ) - - context["users"] = ProfileSerializer(profiles, many=True).data - context["users_excluding_team"] = ProfileSerializer( - users_excluding_team, many=True - ).data - context["teams"] = TeamsSerializer(Teams.objects.all(), many=True).data - return context - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params - ) - def get(self, request, pk, **kwargs): - self.event_obj = self.get_object(pk) - if self.event_obj.org != request.profile.org: - return Response( - {"error": True, "errors": "User company doesnot match with header...."}, - status=status.HTTP_403_FORBIDDEN, - ) - context = self.get_context_data(**kwargs) - return Response(context) - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params,request=EventDetailEditSwaggerSerializer - ) - def post(self, request, pk, **kwargs): - params = request.data - context = {} - self.event_obj = Event.objects.get(pk=pk) - if self.event_obj.org != request.profile.org: - return Response( - { - "error": True, - "errors": "User company does not match with header....", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - if not ( - (self.request.profile == self.event_obj.created_by) - or (self.request.profile in self.event_obj.assigned_to.all()) - ): - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - comment_serializer = CommentSerializer(data=params) - if comment_serializer.is_valid(): - if params.get("comment"): - comment_serializer.save( - event_id=self.event_obj.id, - commented_by_id=self.request.profile.id, - ) - - if self.request.FILES.get("event_attachment"): - attachment = Attachments() - attachment.created_by = self.request.profile.user - attachment.file_name = self.request.FILES.get("event_attachment").name - attachment.event = self.event_obj - attachment.attachment = self.request.FILES.get("event_attachment") - attachment.save() - - comments = Comment.objects.filter(event__id=self.event_obj.id).order_by("-id") - attachments = Attachments.objects.filter(event__id=self.event_obj.id).order_by( - "-id" - ) - context.update( - { - "event_obj": EventSerializer(self.event_obj).data, - "attachments": AttachmentsSerializer(attachments, many=True).data, - "comments": CommentSerializer(comments, many=True).data, - } - ) - return Response(context) - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params,request=EventCreateSwaggerSerializer - ) - def put(self, request, pk, **kwargs): - params = request.data - data = {} - self.event_obj = self.get_object(pk) - if self.event_obj.org != request.profile.org: - return Response( - {"error": True, "errors": "User company doesnot match with header...."}, - status=status.HTTP_403_FORBIDDEN, - ) - serializer = EventCreateSerializer( - data=params, - instance=self.event_obj, - request_obj=request, - ) - if serializer.is_valid(): - event_obj = serializer.save() - previous_assigned_to_users = list( - event_obj.assigned_to.all().values_list("id", flat=True) - ) - if params.get("event_type") == "Non-Recurring": - event_obj.date_of_meeting = event_obj.start_date - - event_obj.contacts.clear() - if params.get("contacts"): - obj_contact = Contact.objects.filter( - id=params.get("contacts"), org=request.profile.org - ) - event_obj.contacts.add(obj_contact) - - event_obj.teams.clear() - if params.get("teams"): - teams_list = params.get("teams") - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - event_obj.teams.add(*teams) - - event_obj.assigned_to.clear() - if params.get("assigned_to"): - assinged_to_list = params.get("assigned_to") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org - ) - event_obj.assigned_to.add(*profiles) - - assigned_to_list = list( - event_obj.assigned_to.all().values_list("id", flat=True) - ) - recipients = list(set(assigned_to_list) - set(previous_assigned_to_users)) - send_email.delay( - event_obj.id, - recipients, - ) - return Response( - {"error": False, "message": "Event updated Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, **kwargs): - self.object = self.get_object(pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == self.object.created_by - ) and self.object.org == request.profile.org: - self.object.delete() - return Response( - {"error": False, "message": "Event deleted Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": "you don't have permission to delete this event"}, - status=status.HTTP_403_FORBIDDEN, - ) - - -class EventCommentView(APIView): - model = Comment - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_object(self, pk): - return self.model.objects.get(pk=pk) - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params,request=EventCommentEditSwaggerSerializer - ) - def put(self, request, pk, format=None): - params = request.data - obj = self.get_object(pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == obj.commented_by - ): - serializer = CommentSerializer(obj, data=params) - if serializer.is_valid(): - serializer.save() - return Response( - {"error": False, "message": "Comment Submitted"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, format=None): - self.object = self.get_object(pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == self.object.commented_by - ): - self.object.delete() - return Response( - {"error": False, "message": "Comment Deleted Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - -class EventAttachmentView(APIView): - model = Attachments - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - @extend_schema( - tags=["Events"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, format=None): - self.object = self.model.objects.get(pk=pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == self.object.created_by - ): - self.object.delete() - return Response( - {"error": False, "message": "Attachment Deleted Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..8e69bac7 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,9 @@ +# Google OAuth (client_id only - secret is on backend) +GOOGLE_CLIENT_ID="your-google-client-id-here" +GOOGLE_LOGIN_DOMAIN="http://localhost:5173" + +# Django API URL (PUBLIC_ prefix makes it available in both server and browser) +PUBLIC_DJANGO_API_URL=http://localhost:8000 + +# Environment +NODE_ENV=development diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..43ea859a --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,27 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +generated/* +src/generated/* +.github/prompts/* diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 00000000..6562bcbb --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,6 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..7ebb855b --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..246e92f2 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,229 @@ +# BottleCRM: Free and Open Source Customer Relationship Management + +
+

Powerful, Modern Multi-Tenant CRM for Everyone

+
+ +BottleCRM is a free, open-source Customer Relationship Management solution designed to help small and medium businesses effectively manage their customer relationships. Built with modern technologies and enterprise-grade multi-tenancy, it offers a comprehensive set of features without the enterprise price tag. + +## ✨ Key Highlights + +- **Multi-Tenant Architecture**: Secure organization-based data isolation +- **Role-Based Access Control**: Granular permissions for users and admins +- **Modern Technology Stack**: Built with SvelteKit 2.x, Svelte 5.x, and PostgreSQL +- **Mobile-First Design**: Responsive interface optimized for all devices + +## 🚀 Core Features + +### Sales & Lead Management + +- **Lead Management**: Track and nurture leads from initial contact to conversion +- **Account Management**: Maintain detailed records of customer accounts and organizations +- **Contact Management**: Store and organize all your customer contact information +- **Opportunity Management**: Track deals through your sales pipeline with customizable stages + +### Customer Support + +- **Case Management**: Handle customer support cases and track resolution +- **Solution Knowledge Base**: Maintain searchable solutions for common issues +- **Multi-Channel Support**: Handle cases from various origins (email, web, phone) + +### Productivity & Collaboration + +- **Task Management**: Never miss a follow-up with built-in task tracking +- **Event Management**: Schedule and manage meetings and activities +- **Board Management**: Trello-like kanban boards for project tracking +- **Comment System**: Collaborate with team members on records + +### Sales Tools + +- **Quote Management**: Generate professional quotes with line items +- **Product Catalog**: Maintain product inventory with pricing +- **Sales Pipeline**: Visual opportunity tracking with probability scoring + +### Administrative Features + +- **User Management**: Add team members with appropriate role assignments +- **Organization Management**: Multi-tenant structure with data isolation +- **Audit Logging**: Complete activity tracking for compliance +- **Super Admin Panel**: Platform-wide management for system administrators + +## 🔮 Coming Soon + +- **Invoice Management**: Create, send, and track invoices (in development) +- **Email Integration**: Connect your email accounts for seamless communication +- **Analytics Dashboard**: Make data-driven decisions with powerful reporting tools +- **API Integration**: REST API for third-party integrations + +## 🖥️ Technology Stack + +- **Frontend**: SvelteKit 2.x, Svelte 5.x, TailwindCSS 4.x +- **Backend**: Node.js with Prisma ORM +- **Database**: PostgreSQL (recommended) with multi-tenant schema +- **Authentication**: Session-based authentication with organization membership +- **Icons**: Lucide Svelte icon library +- **Validation**: Zod for type-safe form validation + +## 🚀 Getting Started + +### Prerequisites + +- **Node.js**: v22.13.0 (use nvm for version management) +- **Package Manager**: pnpm (recommended) +- **Database**: PostgreSQL (required for multi-tenancy features) + +### Installation + +1. **Clone the repository:** + +```bash +git clone https://github.com/micropyramid/svelte-crm.git +cd svelte-crm +``` + +2. **Set up Node.js version:** + +```bash +nvm use 22.13.0 +``` + +3. **Install dependencies:** + +```bash +pnpm install +``` + +4. **Configure environment variables:** + Create a `.env` file based on the following template: + +```env +# Database Configuration +DATABASE_URL="postgresql://postgres:password@localhost:5432/bottlecrm?schema=public" + +# JWT Secret (required for authentication) +# Generate a secure secret using openssl: +# openssl rand -base64 32 +JWT_SECRET="" + +# Google OAuth (Optional) +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" +GOOGLE_LOGIN_DOMAIN="http://localhost:5173" +``` + +5. **Set up the database:** + +```bash +# Generate Prisma client +npx prisma generate + +# Run database migrations +npx prisma migrate dev + +# (Optional) Open Prisma Studio to view data +npx prisma studio +``` + +6. **Start the development server:** + +```bash +pnpm run dev +``` + +### Development Workflow + +Before committing code, ensure quality checks pass: + +```bash +# Type checking +pnpm run check + +# Linting and formatting +pnpm run lint + +# Build verification +pnpm run build +``` + +### Production Deployment + +```bash +# Set Node.js version +nvm use 22.13.0 + +# Generate Prisma client +npx prisma generate + +# Run production migrations +npx prisma migrate deploy + +# Build application +pnpm run build + +# Start production server +pnpm run preview +``` + +## 🏗️ Architecture & Security + +### Multi-Tenant Design + +- **Organization Isolation**: Complete data separation between organizations +- **Role-Based Access**: Users can have different roles across organizations +- **Session Management**: Secure cookie-based authentication with organization context + +### User Roles + +- **User**: Standard access to organization data +- **Admin**: Organization-level administrative privileges +- **Super Admin**: Platform-wide access (requires @micropyramid.com email) + +### Data Security + +- All database queries are organization-scoped +- Strict permission validation on all routes +- Audit logging for compliance and tracking + +## 📁 Project Structure + +``` +src/ +├── routes/ +│ ├── (site)/ # Public marketing pages +│ ├── (no-layout)/ # Authentication pages +│ ├── (app)/ # Main CRM application +│ └── (admin)/ # Super admin panel +├── lib/ +│ ├── stores/ # Svelte stores for state management +│ ├── data/ # Static data and configurations +│ └── utils/ # Utility functions +└── hooks.server.js # Authentication and route protection +``` + +## 💬 Community and Feedback + +We love to hear from our users! Please share your feedback, report bugs, or suggest new features: + +- **Issues**: Open an issue on GitHub for bugs and feature requests +- **Discussions**: Join community discussions for general questions +- **Pull Requests**: Contribute code improvements and new features + +## 🤝 Contributing + +We welcome contributions of all kinds! See our [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started. + +### Development Guidelines + +- Follow existing code patterns and conventions +- Ensure all forms have proper accessibility (labels associated with controls) +- Never use `$app` imports from SvelteKit (see packaging best practices) +- Always filter database queries by organization membership +- Add appropriate error handling and validation + +## 📄 License + +BottleCRM is open source software [licensed as MIT](LICENSE). + +--- + +_Built with ❤️ for small businesses everywhere. We believe quality CRM software should be accessible to everyone._ diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..81b0525d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,26 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import { includeIgnoreFile } from '@eslint/compat'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default [ + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + } + }, + { + files: ['**/*.svelte', '**/*.svelte.js'], + languageOptions: { parserOptions: { svelteConfig } } + } +]; diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 00000000..e6092646 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "noImplicitAny": false, + "useUnknownInCatchVariables": false, + "strictNullChecks": false, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..b03d320e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,53 @@ +{ + "name": "svelte-crm", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint ." + }, + "devDependencies": { + "@eslint/compat": "^2.0.0", + "@eslint/js": "^9.39.1", + "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/kit": "^2.48.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.17", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.13.0", + "globals": "^16.5.0", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.1", + "svelte": "^5.43.12", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "vite": "^7.2.2" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] + }, + "dependencies": { + "@lucide/svelte": "^0.554.0", + "axios": "^1.13.2", + "date-fns": "^4.1.0", + "dotenv": "^17.2.3", + "libphonenumber-js": "^1.12.27", + "marked": "^17.0.0", + "svelte-meta-tags": "^4.5.0", + "uuid": "^13.0.0", + "zod": "^4.1.12" + } +} diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 00000000..8b958409 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,4 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/typography'; + +@variant dark (&:where(.dark, .dark *)); diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 00000000..e4ad273d --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,17 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + interface Locals { + user?: any; // You might want to replace 'any' with a more specific type for user + org?: any; // You might want to replace 'any' with a more specific type for org + org_name?: string; + } + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 00000000..98b93cfd --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,23 @@ + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/hooks.server.js b/frontend/src/hooks.server.js new file mode 100644 index 00000000..87b7dc86 --- /dev/null +++ b/frontend/src/hooks.server.js @@ -0,0 +1,288 @@ +/** + * SvelteKit Server Hooks with JWT Authentication + * + * Authentication Flow: + * 1. JWT tokens stored in cookies (httpOnly for security) + * 2. Decode JWT locally to check org context (no API call needed) + * 3. Only call switch-org if token doesn't have correct org + * 4. Routes protected based on authentication and org membership + */ + +import { redirect } from '@sveltejs/kit'; +import axios from 'axios'; +import { env } from '$env/dynamic/public'; + +const API_BASE_URL = `${env.PUBLIC_DJANGO_API_URL}/api`; + +/** + * @typedef {{ org_id?: string, user_id?: string, exp?: number, iat?: number }} JWTPayload + * @typedef {{ id: string, name: string }} OrgInfo + * @typedef {{ org: OrgInfo, role?: string }} ProfileInfo + * @typedef {{ organizations?: Array<{ id: string, name: string }> }} UserInfo + * @typedef {{ access_token: string, refresh_token: string, current_org?: OrgInfo }} SwitchOrgResult + */ + +/** + * Decode JWT payload without verification (for reading claims only) + * @param {string} token - JWT token + * @returns {JWTPayload|null} Decoded payload or null if invalid + */ +function decodeJwtPayload(token) { + try { + const parts = token.split('.'); + if (parts.length !== 3) return null; + + const payload = parts[1]; + // Handle base64url encoding + const base64 = payload.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = Buffer.from(base64, 'base64').toString('utf8'); + return /** @type {JWTPayload} */ (JSON.parse(jsonPayload)); + } catch (error) { + return null; + } +} + +/** + * Check if JWT token has the specified org_id in its payload + * @param {string} token - JWT token + * @param {string} orgId - Organization UUID to check + * @returns {boolean} True if token has the org context + */ +function tokenHasOrgContext(token, orgId) { + const payload = decodeJwtPayload(token); + return Boolean(payload && payload.org_id === orgId); +} + +/** + * Verify JWT token with Django backend + * @param {string} accessToken - JWT access token + * @returns {Promise} User data or null if invalid + */ +async function verifyToken(accessToken) { + try { + const response = await axios.get(`${API_BASE_URL}/auth/me/`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + return response.data; + } catch (error) { + console.error('Token verification failed:', error); + return null; + } +} + +/** + * Refresh access token using refresh token + * @param {string} refreshToken - JWT refresh token + * @returns {Promise} New access token or null if refresh failed + */ +async function refreshAccessToken(refreshToken) { + try { + const response = await axios.post(`${API_BASE_URL}/auth/refresh-token/`, { + refresh: refreshToken + }); + + return response.data.access; + } catch (error) { + console.error('Token refresh failed:', error); + return null; + } +} + +/** + * Get profile for specific organization + * @param {string} accessToken - JWT access token + * @returns {Promise} Profile data or null if invalid + */ +async function getProfile(accessToken) { + try { + const response = await axios.get(`${API_BASE_URL}/auth/profile/`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + return response.data; + } catch (error) { + console.error('Profile fetch failed:', error); + return null; + } +} + +/** + * Switch organization and get new tokens with org context + * @param {string} accessToken - Current JWT access token + * @param {string} orgId - Organization UUID to switch to + * @returns {Promise} New tokens and org data or null if failed + */ +async function switchOrg(accessToken, orgId) { + try { + const response = await axios.post( + `${API_BASE_URL}/auth/switch-org/`, + { org_id: orgId }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return response.data; + } catch (error) { + console.error('Org switch failed:', error); + return null; + } +} + +/** @type {import('@sveltejs/kit').Handle} */ +export async function handle({ event, resolve }) { + // Get tokens from cookies + /** @type {string | undefined} */ + let accessToken = event.cookies.get('jwt_access'); + const refreshToken = event.cookies.get('jwt_refresh'); + const orgId = event.cookies.get('org'); + + /** @type {UserInfo | null} */ + let user = null; + + // Try to authenticate user + if (accessToken) { + user = await verifyToken(accessToken); + + // If access token expired, try to refresh + if (!user && refreshToken) { + const newAccessToken = await refreshAccessToken(refreshToken); + if (newAccessToken) { + // Update cookie with new access token (use jwt_access for consistency with login) + event.cookies.set('jwt_access', newAccessToken, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 // 1 day + }); + accessToken = newAccessToken; + user = await verifyToken(newAccessToken); + } else { + // Refresh failed, clear cookies + event.cookies.delete('jwt_access', { path: '/' }); + event.cookies.delete('jwt_refresh', { path: '/' }); + event.cookies.delete('org', { path: '/' }); + } + } + } + + // Set user in locals + if (user) { + event.locals.user = user; + + // Check if org cookie is set + if (orgId) { + // Verify user has access to this organization + const userOrg = user.organizations?.find((org) => org.id === orgId); + + if (userOrg) { + // accessToken must be defined here since we're inside if (user) block + const token = /** @type {string} */ (accessToken); + // OPTIMIZATION: Check if token already has correct org context + // This avoids unnecessary API calls on every request + if (tokenHasOrgContext(token, orgId)) { + // Token already has org context, just get profile + const profile = await getProfile(token); + + if (profile) { + event.locals.org = profile.org; + /** @type {any} */ (event.locals).profile = profile; + event.locals.org_name = profile.org.name; + } else { + // Profile fetch failed, clear org cookie + event.cookies.delete('org', { path: '/' }); + } + } else { + // Token doesn't have org context, need to switch + const switchResult = await switchOrg(token, orgId); + + if (switchResult) { + // Update cookies with new tokens that have org context + event.cookies.set('jwt_access', switchResult.access_token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 // 1 day + }); + event.cookies.set('jwt_refresh', switchResult.refresh_token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 365 // 1 year + }); + + // Update accessToken for profile fetch + accessToken = switchResult.access_token; + + // Get profile using the new token + const profile = await getProfile(accessToken); + + if (profile) { + event.locals.org = profile.org; + /** @type {any} */ (event.locals).profile = profile; + event.locals.org_name = switchResult.current_org?.name || profile.org.name; + } else { + // Profile fetch failed, clear org cookie + event.cookies.delete('org', { path: '/' }); + } + } else { + // Org switch failed, clear org cookie + event.cookies.delete('org', { path: '/' }); + } + } + } else { + // User doesn't have access to this organization, clear org cookie + event.cookies.delete('org', { path: '/' }); + // Don't throw error here, just redirect + throw redirect(307, '/org'); + } + } + } + + // Route protection + const pathname = event.url.pathname; + + // Define public routes (no auth required) + const PUBLIC_ROUTES = ['/login', '/logout', '/bounce']; + + // Define semi-protected routes (auth required, but no org) + const AUTH_ONLY_ROUTES = ['/org']; + + // Check if public route + const isPublicRoute = PUBLIC_ROUTES.some( + (route) => pathname === route || pathname.startsWith(route + '/') + ); + + // Check if auth-only route + const isAuthOnlyRoute = AUTH_ONLY_ROUTES.some( + (route) => pathname === route || pathname.startsWith(route + '/') + ); + + if (isAuthOnlyRoute) { + // Auth-only route - require user + if (!user) { + throw redirect(307, '/login'); + } + } else if (!isPublicRoute) { + // Protected route - require user + org + if (!user) { + throw redirect(307, '/login'); + } + if (!event.locals.org) { + throw redirect(307, '/org'); + } + } + + return resolve(event); +} diff --git a/frontend/src/lib/api-helpers.js b/frontend/src/lib/api-helpers.js new file mode 100644 index 00000000..cc61b1df --- /dev/null +++ b/frontend/src/lib/api-helpers.js @@ -0,0 +1,233 @@ +/** + * API Helper Functions + * + * Shared utilities for making API requests to Django backend. + * Used by SvelteKit server files during migration from Prisma to API. + */ + +import { env } from '$env/dynamic/public'; + +const API_BASE_URL = `${env.PUBLIC_DJANGO_API_URL}/api`; + +/** + * @typedef {import('@sveltejs/kit').Cookies} Cookies + */ + +/** + * Make authenticated API request to Django backend + * + * This helper is designed for SvelteKit server-side use, where cookies + * are managed by hooks and locals contains the user and org context. + * + * @param {string} endpoint - API endpoint (e.g., '/accounts/', '/leads/123/') + * @param {{ method?: string, body?: Record, headers?: Record }} options - Fetch options + * @param {{ cookies?: Cookies, org?: { id: string } } | Cookies} locals - SvelteKit locals object or cookies directly + * @returns {Promise} Response data + * @throws {Error} If request fails + */ +export async function apiRequest(endpoint, options = {}, locals) { + const { method = 'GET', body, headers = {} } = options; + + // Get access token from cookies (server-side) + // In server load functions, cookies is passed directly + /** @type {Cookies | undefined} */ + const cookies = /** @type {Cookies | undefined} */ ( + /** @type {{ cookies?: Cookies }} */ (locals).cookies || locals + ); + // Support both naming conventions (jwt_access from login, access_token from legacy) + const accessToken = cookies?.get?.('jwt_access') || cookies?.get?.('access_token'); + + // Build request headers + /** @type {Record} */ + const requestHeaders = { + 'Content-Type': 'application/json', + ...headers + }; + + // Add authentication + // Note: Organization context is now embedded in JWT token, not sent as header + if (accessToken) { + requestHeaders['Authorization'] = `Bearer ${accessToken}`; + } + + // Build request options + /** @type {RequestInit} */ + const requestOptions = { + method, + headers: requestHeaders + }; + + // Add body for non-GET requests + if (body && method !== 'GET') { + requestOptions.body = JSON.stringify(body); + } + + const url = `${API_BASE_URL}${endpoint}`; + + try { + const response = await fetch(url, requestOptions); + + // Handle error responses + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + + // Extract meaningful error message from various Django error formats + let errorMessage = `HTTP ${response.status}: ${response.statusText}`; + + if (errorData.detail) { + errorMessage = errorData.detail; + } else if (errorData.error && typeof errorData.error === 'string') { + errorMessage = errorData.error; + } else if (errorData.errors && typeof errorData.errors === 'object') { + // Handle field-level errors + const fieldErrors = Object.entries(errorData.errors) + .map(([field, msgs]) => `${field}: ${Array.isArray(msgs) ? msgs.join(', ') : msgs}`) + .join('; '); + errorMessage = fieldErrors || errorMessage; + } else if (errorData.non_field_errors) { + errorMessage = Array.isArray(errorData.non_field_errors) + ? errorData.non_field_errors.join(', ') + : errorData.non_field_errors; + } else if (typeof errorData === 'object' && Object.keys(errorData).length > 0) { + // Handle field-level validation errors from DRF serializers + const fieldErrors = Object.entries(errorData) + .filter(([key, value]) => key !== 'error' || typeof value === 'string') + .map(([field, msgs]) => { + if (Array.isArray(msgs)) { + return `${field}: ${msgs.join(', ')}`; + } else if (typeof msgs === 'string') { + return `${field}: ${msgs}`; + } + return null; + }) + .filter(Boolean) + .join('; '); + if (fieldErrors) { + errorMessage = fieldErrors; + } + } + + console.error(`API Error Response:`, errorData); + throw new Error(errorMessage); + } + + // Handle 204 No Content + if (response.status === 204) { + return null; + } + + // Return JSON response + return await response.json(); + } catch (error) { + console.error(`API request failed: ${method} ${endpoint}`, error); + throw error; + } +} + +/** + * Transform Django field names to camelCase for SvelteKit + * + * @param {any} obj - Django object with snake_case fields + * @returns {any} Object with camelCase fields + */ +export function transformFromDjango(obj) { + if (!obj) return obj; + if (Array.isArray(obj)) return obj.map(transformFromDjango); + if (typeof obj !== 'object') return obj; + + /** @type {Record} */ + const transformed = {}; + for (const [key, value] of Object.entries(obj)) { + const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); + transformed[camelKey] = typeof value === 'object' ? transformFromDjango(value) : value; + } + return transformed; +} + +/** + * Transform camelCase field names to Django snake_case + * + * @param {any} obj - SvelteKit object with camelCase fields + * @returns {any} Object with snake_case fields + */ +export function transformToDjango(obj) { + if (!obj) return obj; + if (Array.isArray(obj)) return obj.map(transformToDjango); + if (typeof obj !== 'object') return obj; + + /** @type {Record} */ + const transformed = {}; + for (const [key, value] of Object.entries(obj)) { + const snakeKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + transformed[snakeKey] = typeof value === 'object' ? transformToDjango(value) : value; + } + return transformed; +} + +/** + * Build Django query parameters from filter object + * + * @param {Object} filters - Filter object + * @param {number} [filters.page] - Page number (1-indexed) + * @param {number} [filters.limit] - Items per page + * @param {string} [filters.sort] - Sort field name + * @param {string} [filters.order] - Sort order ('asc' or 'desc') + * @returns {URLSearchParams} Query parameters for Django API + */ +export function buildQueryParams(filters = {}) { + const { page = 1, limit = 10, sort, order = 'asc', ...otherFilters } = filters; + + const params = new URLSearchParams(); + + // Pagination + params.append('limit', limit.toString()); + params.append('offset', ((page - 1) * limit).toString()); + + // Sorting (Django uses - prefix for descending) + if (sort) { + const ordering = order === 'desc' ? `-${sort}` : sort; + params.append('ordering', ordering); + } + + // Other filters + for (const [key, value] of Object.entries(otherFilters)) { + if (value !== null && value !== undefined && value !== '') { + params.append(key, value.toString()); + } + } + + return params; +} + +/** + * Extract pagination info from Django response + * + * @param {{ count?: number, next?: string | null, previous?: string | null, results?: unknown[] }} response - Django paginated response + * @param {number} [limit=10] - Items per page + * @returns {{ page: number, limit: number, total: number, totalPages: number, hasNext: boolean, hasPrevious: boolean }} Pagination info compatible with SvelteKit + */ +export function extractPagination(response, limit = 10) { + const total = response.count || 0; + const totalPages = Math.ceil(total / limit); + + // Determine current page from offset in next/previous URLs + let currentPage = 1; + if (response.next) { + const url = new URL(response.next, 'http://localhost'); + const offset = parseInt(url.searchParams.get('offset') || '0'); + currentPage = Math.floor(offset / limit); + } else if (response.previous) { + const url = new URL(response.previous, 'http://localhost'); + const offset = parseInt(url.searchParams.get('offset') || '0'); + currentPage = Math.floor(offset / limit) + 2; + } + + return { + page: currentPage, + limit, + total, + totalPages, + hasNext: response.next !== null, + hasPrevious: response.previous !== null + }; +} diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js new file mode 100644 index 00000000..534cfaf8 --- /dev/null +++ b/frontend/src/lib/api.js @@ -0,0 +1,470 @@ +/** + * API Client for Django REST Framework Backend + * + * This module provides a centralized API client for making requests to the Django backend. + * It handles JWT authentication, organization context, and common API patterns. + * + * @module lib/api + */ + +import { env } from '$env/dynamic/public'; +import { goto } from '$app/navigation'; + +// API Base URL from environment variables +// Note: VITE_ prefix is required for client-side env vars +const API_BASE_URL = env.PUBLIC_DJANGO_API_URL + ? `${env.PUBLIC_DJANGO_API_URL}/api` + : 'http://localhost:8000/api'; + +/** + * Storage keys for tokens and org + */ +const STORAGE_KEYS = { + ACCESS_TOKEN: 'access_token', + REFRESH_TOKEN: 'refresh_token', + ORG_ID: 'org_id', + USER: 'user' +}; + +/** + * Get token from localStorage (client-side) or return null (server-side) + * @param {string} key - Storage key + * @returns {string|null} + */ +function getFromStorage(key) { + if (typeof window === 'undefined') return null; + return localStorage.getItem(key); +} + +/** + * Set token in localStorage (client-side only) + * @param {string} key - Storage key + * @param {string} value - Value to store + */ +function setInStorage(key, value) { + if (typeof window !== 'undefined') { + localStorage.setItem(key, value); + } +} + +/** + * Remove token from localStorage (client-side only) + * @param {string} key - Storage key + */ +function removeFromStorage(key) { + if (typeof window !== 'undefined') { + localStorage.removeItem(key); + } +} + +/** + * Clear all auth data from storage + */ +export function clearAuthData() { + removeFromStorage(STORAGE_KEYS.ACCESS_TOKEN); + removeFromStorage(STORAGE_KEYS.REFRESH_TOKEN); + removeFromStorage(STORAGE_KEYS.ORG_ID); + removeFromStorage(STORAGE_KEYS.USER); +} + +/** + * Get current access token + * @returns {string|null} + */ +export function getAccessToken() { + return getFromStorage(STORAGE_KEYS.ACCESS_TOKEN); +} + +/** + * Get current refresh token + * @returns {string|null} + */ +export function getRefreshToken() { + return getFromStorage(STORAGE_KEYS.REFRESH_TOKEN); +} + +/** + * Get current organization ID + * @returns {string|null} + */ +export function getOrgId() { + return getFromStorage(STORAGE_KEYS.ORG_ID); +} + +/** + * Set organization ID + * @param {string} orgId - Organization UUID + */ +export function setOrgId(orgId) { + setInStorage(STORAGE_KEYS.ORG_ID, orgId); +} + +/** + * Get current user data + * @returns {Object|null} + */ +export function getCurrentUser() { + const userData = getFromStorage(STORAGE_KEYS.USER); + return userData ? JSON.parse(userData) : null; +} + +/** + * Refresh the access token using refresh token + * @returns {Promise} New access token or null if refresh failed + */ +async function refreshAccessToken() { + const refreshToken = getRefreshToken(); + if (!refreshToken) { + return null; + } + + try { + const response = await fetch(`${API_BASE_URL}/auth/refresh-token/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ refresh: refreshToken }) + }); + + if (response.ok) { + const data = await response.json(); + setInStorage(STORAGE_KEYS.ACCESS_TOKEN, data.access); + return data.access; + } + + // Refresh token expired or invalid + clearAuthData(); + if (typeof window !== 'undefined') { + goto('/login'); + } + return null; + } catch (error) { + console.error('Token refresh failed:', error); + clearAuthData(); + return null; + } +} + +/** + * Make an authenticated API request + * + * @param {string} endpoint - API endpoint (e.g., '/accounts/', '/leads/123/') + * @param {{ method?: string, body?: Record | null, headers?: Record, requiresAuth?: boolean }} [options] - Fetch options + * @returns {Promise} Response data + * @throws {Error} If request fails + */ +export async function apiRequest(endpoint, options = {}) { + const { method = 'GET', body = null, headers = {}, requiresAuth = true } = options; + + const url = `${API_BASE_URL}${endpoint}`; + + // Build headers + /** @type {Record} */ + const requestHeaders = { + 'Content-Type': 'application/json', + ...headers + }; + + // Add authentication if required + // Note: Organization context is now embedded in JWT token, not sent as header + if (requiresAuth) { + const accessToken = getAccessToken(); + if (accessToken) { + requestHeaders['Authorization'] = `Bearer ${accessToken}`; + } + } + + // Build fetch options + /** @type {RequestInit} */ + const fetchOptions = { + method, + headers: requestHeaders + }; + + if (body && method !== 'GET') { + fetchOptions.body = JSON.stringify(body); + } + + try { + let response = await fetch(url, fetchOptions); + + // If unauthorized and we have a refresh token, try to refresh + if (response.status === 401 && requiresAuth) { + const newAccessToken = await refreshAccessToken(); + if (newAccessToken) { + // Retry request with new token + requestHeaders['Authorization'] = `Bearer ${newAccessToken}`; + fetchOptions.headers = requestHeaders; + response = await fetch(url, fetchOptions); + } + } + + // Handle response + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.detail || errorData.error || `HTTP ${response.status}: ${response.statusText}` + ); + } + + // Return JSON response + return await response.json(); + } catch (error) { + console.error(`API request failed: ${method} ${endpoint}`, error); + throw error; + } +} + +/** + * Authentication API methods + */ +export const auth = { + /** + * Login with email and password + * @param {string} email - User email + * @param {string} password - User password + * @param {string} [orgId] - Optional organization ID to login to + * @returns {Promise} User data with tokens and current_org + */ + async login(email, password, orgId = undefined) { + /** @type {Record} */ + const body = { email, password }; + if (orgId) { + body.org_id = orgId; + } + + /** @type {any} */ + const data = await apiRequest('/auth/login/', { + method: 'POST', + body, + requiresAuth: false + }); + + // Store tokens and user data + setInStorage(STORAGE_KEYS.ACCESS_TOKEN, data.access_token); + setInStorage(STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token); + setInStorage(STORAGE_KEYS.USER, JSON.stringify(data.user)); + + // Store current org if provided + if (data.current_org) { + setInStorage(STORAGE_KEYS.ORG_ID, data.current_org.id); + } + + return data; + }, + + /** + * Register a new user + * @param {string} email - User email + * @param {string} password - User password + * @param {string} confirmPassword - Password confirmation + * @returns {Promise} Registration response + */ + async register(email, password, confirmPassword) { + return await apiRequest('/auth/register/', { + method: 'POST', + body: { email, password, confirm_password: confirmPassword }, + requiresAuth: false + }); + }, + + /** + * Switch to a different organization + * This issues new JWT tokens with the new org context + * @param {string} orgId - Organization UUID to switch to + * @returns {Promise} New tokens and org data + */ + async switchOrg(orgId) { + /** @type {any} */ + const data = await apiRequest('/auth/switch-org/', { + method: 'POST', + body: { org_id: orgId } + }); + + // Update tokens with new org context + setInStorage(STORAGE_KEYS.ACCESS_TOKEN, data.access_token); + setInStorage(STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token); + setInStorage(STORAGE_KEYS.ORG_ID, data.current_org.id); + + return data; + }, + + /** + * Get current user data + * @returns {Promise} Current user with organizations + */ + async me() { + const data = await apiRequest('/auth/me/'); + + // Update stored user data + setInStorage(STORAGE_KEYS.USER, JSON.stringify(data)); + + return data; + }, + + /** + * Get profile for current organization + * @returns {Promise} Profile data for current org + */ + async profile() { + return await apiRequest('/auth/profile/'); + }, + + /** + * Logout (clear local data) + * Note: Django simplejwt doesn't have server-side logout by default + * Token blacklisting can be added later if needed + */ + logout() { + clearAuthData(); + if (typeof window !== 'undefined') { + goto('/login'); + } + } +}; + +/** + * Generic CRUD API factory + * Creates standard CRUD methods for an entity + * + * @param {string} entityPath - Entity path (e.g., 'accounts', 'leads') + * @returns {Object} CRUD methods + */ +function createCrudApi(entityPath) { + return { + /** + * List entities with pagination and filters + * @param {Record} [params] - Query parameters + * @returns {Promise} Paginated list of entities + */ + async list(params = {}) { + const queryString = new URLSearchParams(params).toString(); + const endpoint = `/${entityPath}/${queryString ? `?${queryString}` : ''}`; + return await apiRequest(endpoint); + }, + + /** + * Get a single entity by ID + * @param {string} id - Entity UUID + * @returns {Promise} Entity data + */ + async get(id) { + return await apiRequest(`/${entityPath}/${id}/`); + }, + + /** + * Create a new entity + * @param {Record} data - Entity data + * @returns {Promise} Created entity + */ + async create(data) { + return await apiRequest(`/${entityPath}/`, { + method: 'POST', + body: data + }); + }, + + /** + * Update an entity + * @param {string} id - Entity UUID + * @param {Record} data - Updated entity data + * @returns {Promise} Updated entity + */ + async update(id, data) { + return await apiRequest(`/${entityPath}/${id}/`, { + method: 'PUT', + body: data + }); + }, + + /** + * Partially update an entity + * @param {string} id - Entity UUID + * @param {Record} data - Partial entity data + * @returns {Promise} Updated entity + */ + async patch(id, data) { + return await apiRequest(`/${entityPath}/${id}/`, { + method: 'PATCH', + body: data + }); + }, + + /** + * Delete an entity + * @param {string} id - Entity UUID + * @returns {Promise} + */ + async delete(id) { + return await apiRequest(`/${entityPath}/${id}/`, { + method: 'DELETE' + }); + }, + + /** + * Get comments for an entity + * @param {string} id - Entity UUID + * @returns {Promise} List of comments + */ + async getComments(id) { + return await apiRequest(`/${entityPath}/comment/${id}/`); + }, + + /** + * Add comment to an entity + * @param {string} id - Entity UUID + * @param {string} comment - Comment text + * @returns {Promise} Created comment + */ + async addComment(id, comment) { + return await apiRequest(`/${entityPath}/comment/${id}/`, { + method: 'POST', + body: { comment } + }); + }, + + /** + * Get attachments for an entity + * @param {string} id - Entity UUID + * @returns {Promise} List of attachments + */ + async getAttachments(id) { + return await apiRequest(`/${entityPath}/attachment/${id}/`); + } + }; +} + +/** + * CRM Entity APIs + */ +export const accounts = createCrudApi('accounts'); +export const leads = createCrudApi('leads'); +export const contacts = createCrudApi('contacts'); +export const opportunities = createCrudApi('opportunity'); +export const cases = createCrudApi('cases'); +export const tasks = createCrudApi('tasks'); +export const events = createCrudApi('events'); +export const invoices = createCrudApi('invoices'); + +/** + * Export all as default + */ +export default { + auth, + accounts, + leads, + contacts, + opportunities, + cases, + tasks, + events, + invoices, + apiRequest, + getAccessToken, + getRefreshToken, + getOrgId, + setOrgId, + getCurrentUser, + clearAuthData +}; diff --git a/frontend/src/lib/assets/images/banner.png b/frontend/src/lib/assets/images/banner.png new file mode 100644 index 00000000..641cc18e Binary files /dev/null and b/frontend/src/lib/assets/images/banner.png differ diff --git a/frontend/src/lib/assets/images/google.svg b/frontend/src/lib/assets/images/google.svg new file mode 100644 index 00000000..6f1cfcc2 --- /dev/null +++ b/frontend/src/lib/assets/images/google.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/lib/assets/images/img_login.png b/frontend/src/lib/assets/images/img_login.png new file mode 100644 index 00000000..e3c9702c Binary files /dev/null and b/frontend/src/lib/assets/images/img_login.png differ diff --git a/frontend/src/lib/assets/images/logo.png b/frontend/src/lib/assets/images/logo.png new file mode 100644 index 00000000..1433123f Binary files /dev/null and b/frontend/src/lib/assets/images/logo.png differ diff --git a/frontend/src/lib/data/accountOwnership.js b/frontend/src/lib/data/accountOwnership.js new file mode 100644 index 00000000..d888c120 --- /dev/null +++ b/frontend/src/lib/data/accountOwnership.js @@ -0,0 +1,9 @@ +export const accountOwnership = [ + ['', 'Select Ownership'], + ['PUBLIC', 'Public'], + ['PRIVATE', 'Private'], + ['SUBSIDIARY', 'Subsidiary'], + ['NON_PROFIT', 'Non-Profit'], + ['GOVERNMENT', 'Government'], + ['OTHER', 'Other'] +]; diff --git a/frontend/src/lib/data/accountTypes.js b/frontend/src/lib/data/accountTypes.js new file mode 100644 index 00000000..2fb1af07 --- /dev/null +++ b/frontend/src/lib/data/accountTypes.js @@ -0,0 +1,9 @@ +export const accountTypes = [ + ['', 'Select Type'], + ['CUSTOMER', 'Customer'], + ['PARTNER', 'Partner'], + ['PROSPECT', 'Prospect'], + ['VENDOR', 'Vendor'], + ['COMPETITOR', 'Competitor'], + ['OTHER', 'Other'] +]; diff --git a/frontend/src/lib/data/countries.js b/frontend/src/lib/data/countries.js new file mode 100644 index 00000000..e777dfed --- /dev/null +++ b/frontend/src/lib/data/countries.js @@ -0,0 +1,44 @@ +export const countries = [ + ['', 'Select Country'], + ['US', 'United States'], + ['UK', 'United Kingdom'], + ['CA', 'Canada'], + ['AU', 'Australia'], + ['IN', 'India'], + ['DE', 'Germany'], + ['FR', 'France'], + ['JP', 'Japan'], + ['CN', 'China'], + ['BR', 'Brazil'], + ['MX', 'Mexico'], + ['IT', 'Italy'], + ['ES', 'Spain'], + ['NL', 'Netherlands'], + ['SE', 'Sweden'], + ['NO', 'Norway'], + ['DK', 'Denmark'], + ['FI', 'Finland'], + ['CH', 'Switzerland'], + ['AT', 'Austria'], + ['BE', 'Belgium'], + ['IE', 'Ireland'], + ['PL', 'Poland'], + ['RU', 'Russia'], + ['KR', 'South Korea'], + ['SG', 'Singapore'], + ['TH', 'Thailand'], + ['MY', 'Malaysia'], + ['ID', 'Indonesia'], + ['PH', 'Philippines'], + ['VN', 'Vietnam'], + ['NZ', 'New Zealand'], + ['ZA', 'South Africa'], + ['EG', 'Egypt'], + ['NG', 'Nigeria'], + ['KE', 'Kenya'], + ['AR', 'Argentina'], + ['CL', 'Chile'], + ['CO', 'Colombia'], + ['PE', 'Peru'], + ['OTHER', 'Other'] +]; diff --git a/frontend/src/lib/data/index.js b/frontend/src/lib/data/index.js new file mode 100644 index 00000000..f5efe2fc --- /dev/null +++ b/frontend/src/lib/data/index.js @@ -0,0 +1,8 @@ +// Re-export all data constants for easy importing +export { industries } from './industries.js'; +export { accountTypes } from './accountTypes.js'; +export { accountOwnership } from './accountOwnership.js'; +export { ratings } from './ratings.js'; +export { countries } from './countries.js'; +export { leadSources } from './leadSources.js'; +export { leadStatuses } from './leadStatuses.js'; diff --git a/frontend/src/lib/data/industries.js b/frontend/src/lib/data/industries.js new file mode 100644 index 00000000..c2c76b74 --- /dev/null +++ b/frontend/src/lib/data/industries.js @@ -0,0 +1,16 @@ +export const industries = [ + ['', 'Select Industry'], + ['TECHNOLOGY', 'Technology'], + ['HEALTHCARE', 'Healthcare'], + ['FINANCE', 'Finance'], + ['EDUCATION', 'Education'], + ['RETAIL', 'Retail'], + ['MANUFACTURING', 'Manufacturing'], + ['ENERGY', 'Energy'], + ['REAL_ESTATE', 'Real Estate'], + ['CONSTRUCTION', 'Construction'], + ['TRANSPORTATION', 'Transportation'], + ['HOSPITALITY', 'Hospitality'], + ['AGRICULTURE', 'Agriculture'], + ['OTHER', 'Other'] +]; diff --git a/frontend/src/lib/data/leadSources.js b/frontend/src/lib/data/leadSources.js new file mode 100644 index 00000000..01f5d618 --- /dev/null +++ b/frontend/src/lib/data/leadSources.js @@ -0,0 +1,11 @@ +// Lead sources matching Django LEAD_SOURCE choices +export const leadSources = [ + ['', 'Select Source'], + ['call', 'Call'], + ['email', 'Email'], + ['existing customer', 'Existing Customer'], + ['partner', 'Partner'], + ['public relations', 'Public Relations'], + ['compaign', 'Campaign'], + ['other', 'Other'] +]; diff --git a/frontend/src/lib/data/leadStatuses.js b/frontend/src/lib/data/leadStatuses.js new file mode 100644 index 00000000..b8cfad50 --- /dev/null +++ b/frontend/src/lib/data/leadStatuses.js @@ -0,0 +1,8 @@ +// Lead statuses matching Django LEAD_STATUS choices +export const leadStatuses = [ + ['assigned', 'Assigned'], + ['in process', 'In Process'], + ['converted', 'Converted'], + ['recycled', 'Recycled'], + ['closed', 'Closed'] +]; diff --git a/frontend/src/lib/data/ratings.js b/frontend/src/lib/data/ratings.js new file mode 100644 index 00000000..8903d9aa --- /dev/null +++ b/frontend/src/lib/data/ratings.js @@ -0,0 +1,6 @@ +export const ratings = [ + ['', 'Select Rating'], + ['HOT', '🔥 Hot'], + ['WARM', '🟡 Warm'], + ['COLD', '🟦 Cold'] +]; diff --git a/frontend/src/lib/stores/auth.js b/frontend/src/lib/stores/auth.js new file mode 100644 index 00000000..565cfda5 --- /dev/null +++ b/frontend/src/lib/stores/auth.js @@ -0,0 +1,17 @@ +// src/lib/stores/auth.js +import { writable } from 'svelte/store'; + +export const auth = writable({ + isAuthenticated: false, + user: null +}); + +// Helper to get the current session user from event.locals (SvelteKit convention) +/** + * @param {any} event + */ +export function getSessionUser(event) { + // If you use event.locals.user for authentication, return it + // You can adjust this logic if your user is stored differently + return event.locals?.user || null; +} diff --git a/frontend/src/lib/utils/phone.js b/frontend/src/lib/utils/phone.js new file mode 100644 index 00000000..819dcbd1 --- /dev/null +++ b/frontend/src/lib/utils/phone.js @@ -0,0 +1,154 @@ +import { isValidPhoneNumber, parsePhoneNumber, isPossiblePhoneNumber } from 'libphonenumber-js'; + +/** + * Validates a phone number and returns validation result + * @param {string} phoneNumber - The phone number to validate + * @param {string} [defaultCountry] - Default country code (e.g., 'US', 'IN') + * @returns {{ isValid: boolean, formatted?: string, error?: string }} + */ +export function validatePhoneNumber(phoneNumber, defaultCountry) { + if (!phoneNumber || phoneNumber.trim() === '') { + return { isValid: true }; // Allow empty phone numbers + } + + const trimmed = phoneNumber.trim(); + + try { + // If the number starts with +, validate as international + if (trimmed.startsWith('+')) { + const isValid = isValidPhoneNumber(trimmed); + if (!isValid) { + return { + isValid: false, + error: 'Please enter a valid phone number with country code' + }; + } + const parsed = parsePhoneNumber(trimmed); + return { + isValid: true, + formatted: parsed.formatInternational() + }; + } + + // For numbers without country code, try with default country if provided + if (defaultCountry) { + // @ts-ignore - defaultCountry is a valid CountryCode + const isValid = isValidPhoneNumber(trimmed, defaultCountry); + if (isValid) { + // @ts-ignore + const parsed = parsePhoneNumber(trimmed, defaultCountry); + return { + isValid: true, + formatted: parsed.formatInternational() + }; + } + } + + // Try common countries for numbers without + prefix + const commonCountries = ['IN', 'US', 'GB', 'AU', 'CA', 'DE', 'FR']; + for (const country of commonCountries) { + try { + // @ts-ignore + if (isValidPhoneNumber(trimmed, country)) { + // @ts-ignore + const parsed = parsePhoneNumber(trimmed, country); + return { + isValid: true, + formatted: parsed.formatInternational() + }; + } + } catch { + continue; + } + } + + // If nothing worked, check if it's at least a possible phone number + // (has right length, starts with valid digits, etc.) + if (isPossiblePhoneNumber(trimmed, 'IN') || isPossiblePhoneNumber(trimmed, 'US')) { + return { + isValid: true, + formatted: trimmed + }; + } + + return { + isValid: false, + error: + 'Please enter a valid phone number (include country code like +91 for India or +1 for US)' + }; + } catch (error) { + return { + isValid: false, + error: 'Please enter a valid phone number' + }; + } +} + +/** + * Formats a phone number for display + * @param {string} phoneNumber - The phone number to format + * @param {string} defaultCountry - Default country code + * @returns {string} Formatted phone number or original if invalid + */ +export function formatPhoneNumber(phoneNumber, defaultCountry = 'US') { + if (!phoneNumber) return ''; + + try { + // @ts-ignore - defaultCountry is a valid CountryCode + const parsed = parsePhoneNumber(phoneNumber, { defaultCountry }); + return parsed.formatInternational(); + } catch { + return phoneNumber; // Return original if parsing fails + } +} + +/** + * Formats a phone number for storage (E.164 format) + * @param {string} phoneNumber - The phone number to format + * @param {string} [defaultCountry] - Default country code + * @returns {string} E.164 formatted phone number or original if invalid + */ +export function formatPhoneForStorage(phoneNumber, defaultCountry) { + if (!phoneNumber) return ''; + + const trimmed = phoneNumber.trim(); + + try { + // If starts with +, parse as international + if (trimmed.startsWith('+')) { + const parsed = parsePhoneNumber(trimmed); + return parsed.format('E.164'); + } + + // Try with default country first + if (defaultCountry) { + try { + // @ts-ignore + const parsed = parsePhoneNumber(trimmed, defaultCountry); + if (parsed.isValid()) { + return parsed.format('E.164'); + } + } catch { + // Continue to try other countries + } + } + + // Try common countries + const commonCountries = ['IN', 'US', 'GB', 'AU', 'CA', 'DE', 'FR']; + for (const country of commonCountries) { + try { + // @ts-ignore + const parsed = parsePhoneNumber(trimmed, country); + if (parsed.isValid()) { + return parsed.format('E.164'); + } + } catch { + continue; + } + } + + return phoneNumber; // Return original if parsing fails + } catch { + return phoneNumber; // Return original if parsing fails + } +} diff --git a/frontend/src/lib/utils/pkce.js b/frontend/src/lib/utils/pkce.js new file mode 100644 index 00000000..0efa37e6 --- /dev/null +++ b/frontend/src/lib/utils/pkce.js @@ -0,0 +1,51 @@ +/** + * PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0 + * + * PKCE adds security to the authorization code flow by: + * 1. Generating a random code_verifier + * 2. Creating a code_challenge from the verifier (SHA256) + * 3. Sending challenge with authorization request + * 4. Sending verifier with token exchange request + * + * This prevents authorization code interception attacks. + */ + +import crypto from 'crypto'; + +/** + * Generate a cryptographically random code verifier + * RFC 7636: 43-128 characters from [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" + * @returns {string} Base64URL encoded random string (43 chars from 32 bytes) + */ +export function generateCodeVerifier() { + const buffer = crypto.randomBytes(32); + return base64URLEncode(buffer); +} + +/** + * Generate code challenge from verifier using SHA256 + * @param {string} verifier - The code verifier + * @returns {string} Base64URL encoded SHA256 hash + */ +export function generateCodeChallenge(verifier) { + const hash = crypto.createHash('sha256').update(verifier).digest(); + return base64URLEncode(hash); +} + +/** + * Generate a cryptographically random state parameter for CSRF protection + * @returns {string} Base64URL encoded random string (32 bytes) + */ +export function generateState() { + const buffer = crypto.randomBytes(32); + return base64URLEncode(buffer); +} + +/** + * Base64URL encode a buffer per RFC 4648 Section 5 + * @param {Buffer} buffer - Buffer to encode + * @returns {string} Base64URL encoded string (no padding) + */ +function base64URLEncode(buffer) { + return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} diff --git a/frontend/src/routes/(app)/+layout.server.js b/frontend/src/routes/(app)/+layout.server.js new file mode 100644 index 00000000..933e0678 --- /dev/null +++ b/frontend/src/routes/(app)/+layout.server.js @@ -0,0 +1,10 @@ +/** @type {import('./$types').LayoutServerLoad} */ +export async function load({ locals }) { + // console.log("locals", locals.user); + return { + user: locals.user, + org_name: locals.org_name || 'BottleCRM' + }; +} + +export const ssr = false; diff --git a/frontend/src/routes/(app)/+layout.svelte b/frontend/src/routes/(app)/+layout.svelte new file mode 100644 index 00000000..8dd72f63 --- /dev/null +++ b/frontend/src/routes/(app)/+layout.svelte @@ -0,0 +1,33 @@ + + + + + +
+ +
+
+ {@render children()} +
+
+
diff --git a/frontend/src/routes/(app)/+page.server.js b/frontend/src/routes/(app)/+page.server.js new file mode 100644 index 00000000..4645edad --- /dev/null +++ b/frontend/src/routes/(app)/+page.server.js @@ -0,0 +1,151 @@ +/** + * Dashboard Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/dashboard/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const userId = locals.user?.id; + const org = locals.org; + + if (!userId || !org) { + return { + error: 'User not authenticated' + }; + } + + try { + // Fetch dashboard data from Django API + const dashboardResponse = await apiRequest('/dashboard/', {}, { cookies, org }); + + // Django returns: + // { + // accounts_count, contacts_count, leads_count, opportunities_count, + // accounts: [], contacts: [], leads: [], opportunities: [] + // } + + // Transform recent leads + const recentLeads = (dashboardResponse.leads || []).slice(0, 5).map((lead) => ({ + id: lead.id, + firstName: lead.first_name, + lastName: lead.last_name, + company: lead.company?.name || null, + status: lead.status, + createdAt: lead.created_on + })); + + // Transform recent opportunities + const recentOpportunities = (dashboardResponse.opportunities || []).slice(0, 5).map((opp) => ({ + id: opp.id, + name: opp.name, + amount: opp.amount ? parseFloat(opp.amount) : null, + stage: opp.stage, + probability: opp.probability ? parseFloat(opp.probability) : null, + createdAt: opp.created_on, + account: opp.account + ? { + name: opp.account.name + } + : null + })); + + // Calculate opportunity revenue (sum of all opportunity amounts) + const opportunityRevenue = (dashboardResponse.opportunities || []).reduce( + (sum, opp) => sum + (opp.amount ? parseFloat(opp.amount) : 0), + 0 + ); + + // Fetch tasks and activities separately + let upcomingTasks = []; + let recentActivities = []; + + try { + // Fetch tasks separately - filter for current user's pending tasks + const tasksResponse = await apiRequest('/tasks/', {}, { cookies, org }); + // Django TaskListView returns { tasks: [...], tasks_count: ..., ... } + const allTasks = tasksResponse.tasks || tasksResponse.results || []; + + // Filter for user's pending tasks with due dates + upcomingTasks = allTasks + .filter((task) => { + const isAssignedToUser = task.assigned_to && task.assigned_to.some((id) => id === userId); + const isNotCompleted = task.status !== 'Completed'; + const hasDueDate = task.due_date; + return isAssignedToUser && isNotCompleted && hasDueDate; + }) + .sort((a, b) => { + const dateA = new Date(a.due_date); + const dateB = new Date(b.due_date); + return dateA.getTime() - dateB.getTime(); + }) + .slice(0, 5) + .map((task) => ({ + id: task.id, + subject: task.title, + status: task.status, + priority: task.priority, + dueDate: task.due_date + })); + } catch (err) { + console.error('Error fetching tasks for dashboard:', err); + } + + try { + // Fetch recent activities from Django API + const activitiesResponse = await apiRequest('/activities/?limit=10', {}, { cookies, org }); + + // Transform activities to match frontend format + // Django returns: { error: false, count: N, activities: [...] } + recentActivities = (activitiesResponse.activities || []).map((activity) => ({ + id: activity.id, + user: { + name: activity.user?.name || activity.user?.email?.split('@')[0] || 'Unknown' + }, + action: activity.action, + entityType: activity.entity_type, + entityId: activity.entity_id, + entityName: activity.entity_name, + description: + activity.description || + `${activity.action_display} ${activity.entity_type}: ${activity.entity_name}`, + timestamp: activity.timestamp, + humanizedTime: activity.humanized_time + })); + } catch (err) { + console.error('Error fetching activities for dashboard:', err); + } + + // Count pending tasks for the user + const pendingTasks = upcomingTasks.length; + + return { + metrics: { + totalLeads: dashboardResponse.leads_count || 0, + totalOpportunities: dashboardResponse.opportunities_count || 0, + totalAccounts: dashboardResponse.accounts_count || 0, + totalContacts: dashboardResponse.contacts_count || 0, + pendingTasks: pendingTasks, + opportunityRevenue: opportunityRevenue + }, + recentData: { + leads: recentLeads, + opportunities: recentOpportunities, + tasks: upcomingTasks, + activities: recentActivities + } + }; + } catch (error) { + console.error('Dashboard load error:', error); + return { + error: 'Failed to load dashboard data' + }; + } +} diff --git a/frontend/src/routes/(app)/+page.svelte b/frontend/src/routes/(app)/+page.svelte new file mode 100644 index 00000000..a894e2f3 --- /dev/null +++ b/frontend/src/routes/(app)/+page.svelte @@ -0,0 +1,464 @@ + + + + Dashboard - BottleCRM + + +
+ +
+
+

Dashboard

+

+ Welcome back! Here's what's happening with your CRM. +

+
+
+ + {#if data.error} +
+ + {data.error} +
+ {:else} + +
+
+
+
+

Active Leads

+

{metrics.totalLeads}

+
+
+ +
+
+
+ +
+
+
+

Opportunities

+

+ {metrics.totalOpportunities} +

+
+
+ +
+
+
+ +
+
+
+

Accounts

+

{metrics.totalAccounts}

+
+
+ +
+
+
+ +
+
+
+

Contacts

+

{metrics.totalContacts}

+
+
+ +
+
+
+ +
+
+
+

Pending Tasks

+

{metrics.pendingTasks}

+
+
+ +
+
+
+ +
+
+
+

Pipeline Value

+

+ {formatCurrency(metrics.opportunityRevenue)} +

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

Recent Leads

+ +
+
+
+ {#if recentData.leads?.length > 0} +
+ {#each recentData.leads as lead} +
+
+

+ {lead.firstName} + {lead.lastName} +

+

+ {lead.company || 'No company'} +

+
+
+ + {lead.status} + +

+ {formatDate(lead.createdAt)} +

+
+
+ {/each} +
+ {:else} +

No recent leads

+ {/if} +
+
+ + +
+
+
+

+ Recent Opportunities +

+ +
+
+
+ {#if recentData.opportunities?.length > 0} +
+ {#each recentData.opportunities as opportunity} +
+
+

{opportunity.name}

+

+ {opportunity.account?.name || 'No account'} +

+
+
+ {#if opportunity.amount} +

+ {formatCurrency(opportunity.amount)} +

+ {/if} +

+ {formatDate(opportunity.createdAt)} +

+
+
+ {/each} +
+ {:else} +

No recent opportunities

+ {/if} +
+
+ + +
+
+
+

Upcoming Tasks

+ +
+
+
+ {#if recentData.tasks?.length > 0} +
+ {#each recentData.tasks as task} +
+
+

{task.subject}

+

{task.status}

+
+
+ + {task.priority} + + {#if task.dueDate} +

+ {formatDate(task.dueDate)} +

+ {/if} +
+
+ {/each} +
+ {:else} +

No upcoming tasks

+ {/if} +
+
+
+ + +
+
+
+

Recent Activities

+ +
+
+
+ {#if recentData.activities?.length > 0} +
+ {#each recentData.activities as activity} + {@const style = getActivityStyle(activity.action)} +
+
+ {#if style.icon === 'plus'} + + {:else if style.icon === 'pencil'} + + {:else if style.icon === 'trash'} + + {:else if style.icon === 'eye'} + + {:else if style.icon === 'message'} + + {:else if style.icon === 'user-plus'} + + {:else} + + {/if} +
+
+

+ {activity.user?.name || 'Someone'} + {' '}{getActionVerb(activity.action)}{' '} + {activity.entityType.toLowerCase()} + {#if activity.entityName && activity.action !== 'DELETE'} + + {activity.entityName} + + {:else if activity.entityName} + {activity.entityName} + {/if} +

+

+ {activity.humanizedTime || formatDate(activity.timestamp)} +

+
+
+ {/each} +
+ {:else} +

No recent activities

+ {/if} +
+
+ {/if} +
diff --git a/frontend/src/routes/(app)/Sidebar.svelte b/frontend/src/routes/(app)/Sidebar.svelte new file mode 100644 index 00000000..646cf3e4 --- /dev/null +++ b/frontend/src/routes/(app)/Sidebar.svelte @@ -0,0 +1,447 @@ + + + + + diff --git a/frontend/src/routes/(app)/accounts/+page.server.js b/frontend/src/routes/(app)/accounts/+page.server.js new file mode 100644 index 00000000..4b3ac302 --- /dev/null +++ b/frontend/src/routes/(app)/accounts/+page.server.js @@ -0,0 +1,158 @@ +/** + * Accounts List Page - API Version + * + * This is the migrated version that uses Django REST API instead of Prisma. + * Once tested and working, activate by: + * 1. mv +page.server.js +page.server.prisma.js + * 2. mv +page.server.api.js +page.server.js + * + * Migration completed: 2025-11-19 + * Django endpoint: GET /api/accounts/ + */ + +import { error } from '@sveltejs/kit'; +import { apiRequest, buildQueryParams } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, url, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + // Extract URL parameters (same as Prisma version) + const page = parseInt(url.searchParams.get('page') || '1'); + const limit = parseInt(url.searchParams.get('limit') || '10'); + const sort = url.searchParams.get('sort') || 'name'; + const order = url.searchParams.get('order') || 'asc'; + const status = url.searchParams.get('status'); // 'open' or 'closed' + + try { + // Build query parameters for Django + const queryParams = buildQueryParams({ + page, + limit, + sort, + order + }); + + // Add filters + const nameFilter = url.searchParams.get('name'); + const cityFilter = url.searchParams.get('city'); + const industryFilter = url.searchParams.get('industry'); + const tagsFilter = url.searchParams.get('tags'); + + if (nameFilter) queryParams.append('name', nameFilter); + if (cityFilter) queryParams.append('city', cityFilter); + if (industryFilter) queryParams.append('industry', industryFilter); + if (tagsFilter) queryParams.append('tags', tagsFilter); + + // Make API request + const response = await apiRequest(`/accounts/?${queryParams.toString()}`, {}, { cookies, org }); + + // Django returns complex structure with active_accounts and closed_accounts + // We need to extract based on status filter + let accounts = []; + let total = 0; + + if (response.active_accounts && response.closed_accounts) { + // Django endpoint returns separate lists + if (status === 'open' || !status) { + accounts = response.active_accounts.open_accounts || []; + total = accounts.length; // Django doesn't provide count in this format + } else if (status === 'closed') { + accounts = response.closed_accounts.close_accounts || []; + total = accounts.length; + } + } else if (response.results) { + // Standard Django pagination response + accounts = response.results; + total = response.count || 0; + } + + // Transform Django accounts to match SvelteKit expectations + const transformedAccounts = accounts.map((account) => { + // Calculate counts from related data + const opportunityCount = account.opportunities?.length || 0; + const contactCount = account.contacts?.length || 0; + const taskCount = account.tasks?.length || 0; + + const openOpportunities = + account.opportunities?.filter((opp) => !['CLOSED_WON', 'CLOSED_LOST'].includes(opp.stage)) + .length || 0; + + const totalOpportunityValue = + account.opportunities?.reduce((sum, opp) => sum + (parseFloat(opp.amount) || 0), 0) || 0; + + return { + id: account.id, + name: account.name, + email: account.email, + phone: account.phone, + website: account.website, + industry: account.industry, + description: account.description, + isActive: account.status === 'open' || account.is_active === true, + createdAt: account.created_at, + updatedAt: account.updated_at || account.created_at, + + // Address fields (Django uses separate fields) + billingAddressLine: account.billing_address_line, + billingStreet: account.billing_street, + billingCity: account.billing_city, + billingState: account.billing_state, + billingPostcode: account.billing_postcode, + billingCountry: account.billing_country, + + // Owner - Django returns assigned_to array + owner: + account.assigned_to && account.assigned_to.length > 0 + ? { + id: account.assigned_to[0].id, + name: account.assigned_to[0].user_details?.email || 'Unknown', + email: account.assigned_to[0].user_details?.email, + profilePhoto: account.assigned_to[0].user_details?.profile_pic + } + : account.created_by + ? { + id: account.created_by.id, + name: account.created_by.email, + email: account.created_by.email, + profilePhoto: account.created_by.profile_pic + } + : null, + + // Aggregated data + opportunityCount, + contactCount, + taskCount, + openOpportunities, + totalOpportunityValue, + + // Top contacts for display + topContacts: + account.contacts?.slice(0, 3).map((contact) => ({ + id: contact.id, + name: `${contact.first_name || ''} ${contact.last_name || ''}`.trim() + })) || [], + + // Tags + tags: account.tags || [] + }; + }); + + return { + accounts: transformedAccounts, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1 + } + }; + } catch (err) { + console.error('Error fetching accounts from API:', err); + throw error(500, `Failed to fetch accounts: ${err.message}`); + } +} diff --git a/frontend/src/routes/(app)/accounts/+page.svelte b/frontend/src/routes/(app)/accounts/+page.svelte new file mode 100644 index 00000000..5a4f1202 --- /dev/null +++ b/frontend/src/routes/(app)/accounts/+page.svelte @@ -0,0 +1,549 @@ + + +
+ +
+
+
+

Accounts

+

+ Manage all your customer accounts and business relationships +

+
+ + +
+ +
+ + + debounceSearch(/** @type {HTMLInputElement} */ (e.target).value)} + /> +
+ + +
+ + + +
+ + + + + New Account + +
+
+
+ + +
+
+
+
+ +
+
+

Total Accounts

+

{pagination.total}

+
+
+
+ +
+
+
+ +
+
+

Active

+

+ {accounts.filter((a) => a.isActive).length} +

+
+
+
+ +
+
+
+ +
+
+

Total Contacts

+

+ {accounts.reduce((sum, a) => sum + (a.contactCount || 0), 0)} +

+
+
+
+ +
+
+
+ +
+
+

Opportunities

+

+ {accounts.reduce((sum, a) => sum + (a.opportunityCount || 0), 0)} +

+
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + {#if isLoading} + + + + {:else if accounts.length === 0} + + + + {:else} + {#each accounts as account (account.id)} + + + + + + + + + + + {/each} + {/if} + +
toggleSort('name')} + > +
+ + Account Name + {#if sortField === 'name'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + {/if} +
+
Actions
+
+
+

Loading accounts...

+
+
+
+ +
+

+ No accounts found +

+

+ Get started by creating your first account +

+
+ + + Create Account + +
+
+ + + +
+
+
+ + + {#if pagination.totalPages > 1} +
+
+ Showing {(pagination.page - 1) * pagination.limit + 1} to + {Math.min(pagination.page * pagination.limit, pagination.total)} + of + {pagination.total} accounts +
+
+ + + + {pagination.page} of {pagination.totalPages} + + + +
+
+ {/if} +
diff --git a/frontend/src/routes/(app)/accounts/[accountId]/+page.server.js b/frontend/src/routes/(app)/accounts/[accountId]/+page.server.js new file mode 100644 index 00000000..6d6de18a --- /dev/null +++ b/frontend/src/routes/(app)/accounts/[accountId]/+page.server.js @@ -0,0 +1,273 @@ +/** + * Account Detail Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/accounts/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, url, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + const accountId = params.accountId; + + // If ?commentsOnly=1, return just comments as JSON (for live updates) + if (url.searchParams.get('commentsOnly') === '1') { + const response = await apiRequest(`/accounts/${accountId}/`, {}, { cookies, org }); + + const comments = (response.comments || []).map((comment) => ({ + id: comment.id, + body: comment.comment, + createdAt: comment.created_at, + author: comment.commented_by + ? { + id: comment.commented_by.id, + name: comment.commented_by.user_details?.email || comment.commented_by.email + } + : null + })); + + return new Response(JSON.stringify({ comments }), { + headers: { 'Content-Type': 'application/json' } + }); + } + + // Fetch full account details from Django + const response = await apiRequest(`/accounts/${accountId}/`, {}, { cookies, org }); + + if (!response.account_obj) { + throw error(404, 'Account not found'); + } + + const accountData = response.account_obj; + + // Transform account data + const account = { + id: accountData.id, + name: accountData.name, + email: accountData.email, + phone: accountData.phone, + website: accountData.website, + industry: accountData.industry, + description: accountData.description, + billingAddress: accountData.billing_address_line, + billingCity: accountData.billing_city, + billingState: accountData.billing_state, + billingPostcode: accountData.billing_postcode, + billingCountry: accountData.billing_country, + status: accountData.status, + isActive: accountData.status === 'open' || accountData.is_active, + createdAt: accountData.created_at, + updatedAt: accountData.updated_at, + closedAt: accountData.closed_at || null, + closureReason: accountData.closure_reason || null, + organizationId: accountData.org?.id || org.id, + ownerId: accountData.created_by?.id || null + }; + + // Transform contacts + const contacts = (response.contacts || []).map((contact) => ({ + id: contact.id, + firstName: contact.first_name, + lastName: contact.last_name, + email: contact.email, + phone: contact.phone, + title: contact.title, + isPrimary: contact.is_primary || false, + role: contact.role || null + })); + + // Transform opportunities + const opportunities = (response.opportunity_list || []).map((opp) => ({ + id: opp.id, + name: opp.name, + amount: opp.amount ? Number(opp.amount) : null, + stage: opp.stage, + probability: opp.probability, + closedOn: opp.closed_on + })); + + // Transform comments + const comments = (response.comments || []).map((comment) => ({ + id: comment.id, + body: comment.comment, + createdAt: comment.created_at, + author: comment.commented_by + ? { + id: comment.commented_by.id, + name: comment.commented_by.user_details?.email || comment.commented_by.email + } + : null + })); + + // Transform quotes/invoices + const quotes = (response.invoices || []).map((invoice) => ({ + id: invoice.id, + name: invoice.invoice_title || invoice.name, + total: invoice.total_amount ? Number(invoice.total_amount) : null, + status: invoice.status + })); + + // Transform tasks + const tasks = (response.tasks || []).map((task) => ({ + id: task.id, + title: task.title, + status: task.status, + priority: task.priority, + dueDate: task.due_date, + owner: + task.assigned_to && task.assigned_to.length > 0 + ? { + id: task.assigned_to[0].id, + name: task.assigned_to[0].user_details?.email || task.assigned_to[0].email + } + : null + })); + + // Transform cases + const cases = (response.cases || []).map((caseItem) => ({ + id: caseItem.id, + subject: caseItem.name, + status: caseItem.status, + priority: caseItem.priority, + closedOn: caseItem.closed_on + })); + + // Transform users + const users = (response.users || []).map((user) => ({ + id: user.id, + name: user.user_details?.email || user.email, + email: user.user_details?.email || user.email + })); + + return { + account, + contacts, + opportunities, + comments, + quotes, + tasks, + cases, + users, + meta: { + title: account.name, + description: `Account details for ${account.name}` + } + }; + } catch (err) { + console.error('Error loading account data:', err); + const errorMessage = err instanceof Error ? err.message : 'Error loading account data'; + const statusCode = + err && typeof err === 'object' && 'status' in err + ? typeof err.status === 'number' + ? err.status + : 500 + : 500; + throw error(statusCode, errorMessage); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + closeAccount: async ({ request, locals, params, cookies }) => { + try { + const user = locals.user; + const org = locals.org; + const { accountId } = params; + const formData = await request.formData(); + const closureReason = formData.get('closureReason')?.toString(); + + if (!closureReason) { + return fail(400, { + success: false, + message: 'Please provide a reason for closing this account' + }); + } + + // Close account via API (Django may not have dedicated endpoint, use PATCH) + await apiRequest( + `/accounts/${accountId}/`, + { + method: 'PATCH', + body: { + status: 'close', + closure_reason: closureReason, + is_active: false + } + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error closing account:', err); + return fail(500, { success: false, message: 'An unexpected error occurred' }); + } + }, + + reopenAccount: async ({ request, locals, params, cookies }) => { + try { + const user = locals.user; + const org = locals.org; + const { accountId } = params; + + // Reopen account via API + await apiRequest( + `/accounts/${accountId}/`, + { + method: 'PATCH', + body: { + status: 'open', + closure_reason: null, + is_active: true + } + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error reopening account:', err); + return fail(500, { success: false, message: 'An unexpected error occurred' }); + } + }, + + comment: async ({ request, locals, params, cookies }) => { + try { + const org = locals.org; + const form = await request.formData(); + const body = form.get('body')?.toString().trim(); + + if (!body) { + return fail(400, { error: 'Comment cannot be empty.' }); + } + + // Add comment via API + await apiRequest( + `/accounts/comment/${params.accountId}/`, + { + method: 'POST', + body: { comment: body } + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error adding comment:', err); + return fail(500, { error: 'Failed to add comment' }); + } + } +}; diff --git a/frontend/src/routes/(app)/accounts/[accountId]/+page.svelte b/frontend/src/routes/(app)/accounts/[accountId]/+page.svelte new file mode 100644 index 00000000..aa4fd680 --- /dev/null +++ b/frontend/src/routes/(app)/accounts/[accountId]/+page.svelte @@ -0,0 +1,1099 @@ + + +
+ +
+
+
+
+ + + Back to Accounts + +
+

{account.name}

+
+ + {account.isActive ? 'Active' : 'Inactive'} + + {#if account.type} + + {account.type} + + {/if} +
+
+
+ +
+ {#if account.closedAt} +
+ +
+ {:else} + + + Edit + + + {/if} +
+
+
+
+ +
+
+ +
+ +
+
+

Account Information

+
+
+
+
+
+ Name +

{account.name || 'N/A'}

+
+
+ Industry +

+ {account.industry || 'N/A'} +

+
+
+ Website + {#if account.website} + + + {account.website} + + + {:else} +

N/A

+ {/if} +
+
+ Phone + {#if account.phone} + + + {account.phone} + + {:else} +

N/A

+ {/if} +
+
+ Email + {#if account.email} + + + {account.email} + + {:else} +

N/A

+ {/if} +
+
+ +
+
+ Annual Revenue +

+ {account.annualRevenue ? formatCurrency(account.annualRevenue) : 'N/A'} +

+
+
+ Employees +

+ {account.numberOfEmployees ? account.numberOfEmployees.toLocaleString() : 'N/A'} +

+
+
+ Ownership +

+ {account.accountOwnership || 'N/A'} +

+
+
+ Rating +

+ {account.rating || 'N/A'} +

+
+
+ SIC Code +

+ {account.sicCode || 'N/A'} +

+
+
+
+ + {#if account.street || account.city || account.state || account.country} +
+ Address +
+ +
+ {account.street || ''}
+ {account.city || ''}{account.city && account.state ? ', ' : ''}{account.state || + ''} + {account.postalCode || ''}
+ {account.country || ''} +
+
+
+ {/if} + + {#if account.description} +
+ Description +

+ {account.description} +

+
+ {/if} + + {#if account.closedAt} +
+
+
+ +
+

+ This account was closed on {formatDate(account.closedAt)}. +

+

+ Reason: {account.closureReason || 'No reason provided'} +

+
+
+
+
+ {/if} + +
+
+ Created +

{formatDate(account.createdAt)}

+
+
+ Last Updated +

{formatDate(account.updatedAt)}

+
+
+
+
+ + +
+ +
+ +
+ + +
+ {#if activeTab === 'contacts'} + {#if contacts.length === 0} +
+ +

+ No contacts found for this account +

+ + + Add Contact + +
+ {:else} +
+ + + + + + + + + + + + + {#each contacts as contact (contact.id)} + + + + + + + + + {/each} + +
NameTitleRoleActions
+ + {contact.firstName} + {contact.lastName} + + {#if contact.isPrimary} + + Primary + + {/if} + {contact.title || 'N/A'}{contact.role || 'N/A'} + View +
+
+ {/if} + {/if} + + {#if activeTab === 'opportunities'} + {#if opportunities.length === 0} +
+ +

+ No opportunities found for this account +

+ + + Add Opportunity + +
+ {:else} +
+ + + + + + + + + + + + + {#each opportunities as opportunity (opportunity.id)} + + + + + + + + + {/each} + +
NameValueStageActions
+ + {opportunity.name} + + {formatCurrency(opportunity.amount)} + + {opportunity.stage || 'Unknown'} + + + View +
+
+ {/if} + {/if} + + {#if activeTab === 'tasks'} + {#if tasks.length === 0} +
+ +

+ No tasks found for this account +

+ + + Add Task + +
+ {:else} +
+ + + + + + + + + + + + + {#each tasks as task (task.id)} + + + + + + + + + {/each} + +
SubjectStatusPriorityActions
+ + {task.subject} + + + + {task.status} + + + + {task.priority} + + + View +
+
+ {/if} + {/if} + + {#if activeTab === 'cases'} + {#if cases.length === 0} +
+ +

+ No cases found for this account +

+ + + Open Case + +
+ {:else} +
+ + + + + + + + + + + + + {#each cases as caseItem (caseItem.id)} + + + + + + + + + {/each} + +
Case NumberSubjectStatusPriorityActions
+ + {caseItem.caseNumber} + + {caseItem.subject} + + {caseItem.status} + + + + {caseItem.priority} + + + View +
+
+ {/if} + {/if} + + {#if activeTab === 'notes'} +
+ +
+ + + {#if commentError} +

{commentError}

+ {/if} +
+ +
+
+ + + {#if comments.length === 0} +
+ +

+ No notes found for this account +

+
+ {:else} +
+ {#each comments as comment (comment.id)} +
+
+
+ {comment.author?.name || 'Unknown'} + + {formatDate(comment.createdAt)} + + {#if comment.isPrivate} + + Private + + {/if} +
+
+

+ {comment.body} +

+
+ {/each} +
+ {/if} +
+ {/if} +
+
+
+ + +
+ +
+
+

Overview

+
+
+
+
+

Contacts

+

{contacts.length}

+
+ +
+ +
+
+

Opportunities

+

+ {opportunities.length} +

+
+ +
+ +
+
+

Pipeline Value

+

+ {formatCurrency( + opportunities.reduce( + /** @param {number} sum @param {any} opp */ (sum, opp) => + sum + (opp.amount || 0), + 0 + ) + )} +

+
+ +
+ +
+
+

Open Cases

+

+ {cases.filter(/** @param {any} c */ (c) => c.status !== 'CLOSED').length} +

+
+ +
+
+
+ + + +
+
+
+ + + {#if showCloseModal} +
+
+
(showCloseModal = false)} + onkeydown={(e) => e.key === 'Escape' && (showCloseModal = false)} + >
+ +
+
+
+
+
+ +
+
+

+ Close Account +

+
+

+ You are about to close the account "{account.name}". This action will mark the + account as closed but will retain all account data for historical purposes. +

+ +
+ + + {#if closeError} +

{closeError}

+ {/if} +
+
+
+
+
+
+ + +
+
+
+
+
+ {/if} +
diff --git a/frontend/src/routes/(app)/accounts/[accountId]/edit/+page.server.js b/frontend/src/routes/(app)/accounts/[accountId]/edit/+page.server.js new file mode 100644 index 00000000..1a02efec --- /dev/null +++ b/frontend/src/routes/(app)/accounts/[accountId]/edit/+page.server.js @@ -0,0 +1,92 @@ +/** + * Account Edit Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: PUT /api/accounts/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, redirect, error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + const response = await apiRequest(`/accounts/${params.accountId}/`, {}, { cookies, org }); + + if (!response.account_obj) { + throw error(404, 'Account not found'); + } + + const accountData = response.account_obj; + + // Transform to Prisma structure + const account = { + id: accountData.id, + name: accountData.name, + industry: accountData.industry, + type: accountData.account_type || accountData.type, + website: accountData.website, + phone: accountData.phone + }; + + return { account }; + } catch (err) { + console.error('Error loading account:', err); + throw error(500, `Failed to load account: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, params, locals, cookies }) => { + const org = locals.org; + const form = await request.formData(); + const name = form.get('name')?.toString(); + const industry = form.get('industry')?.toString() || null; + const type = form.get('type')?.toString() || null; + const website = form.get('website')?.toString() || null; + const phone = form.get('phone')?.toString() || null; + + if (!name) { + return fail(400, { name, missing: true }); + } + + try { + // Transform to Django field names + const djangoData = { + name, + industry, + account_type: type, + website, + phone + }; + + // Update account via API + await apiRequest( + `/accounts/${params.accountId}/`, + { + method: 'PUT', + body: djangoData + }, + { cookies, org } + ); + + throw redirect(303, `/accounts/${params.accountId}`); + } catch (err) { + console.error('Error updating account:', err); + return fail(500, { + error: 'Failed to update account: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + } +}; diff --git a/frontend/src/routes/(app)/accounts/[accountId]/edit/+page.svelte b/frontend/src/routes/(app)/accounts/[accountId]/edit/+page.svelte new file mode 100644 index 00000000..113ad1f7 --- /dev/null +++ b/frontend/src/routes/(app)/accounts/[accountId]/edit/+page.svelte @@ -0,0 +1,83 @@ + + +
+

Edit Account

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Cancel +
+
+
diff --git a/frontend/src/routes/(app)/accounts/new/+page.server.js b/frontend/src/routes/(app)/accounts/new/+page.server.js new file mode 100644 index 00000000..7bee718d --- /dev/null +++ b/frontend/src/routes/(app)/accounts/new/+page.server.js @@ -0,0 +1,143 @@ +/** + * Account Create Page - Modern CRM Version + * + * Streamlined account creation based on Twenty CRM and Salesforce patterns. + * Django endpoint: POST /api/accounts/ + */ + +import { fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; +import { industries, countries } from '$lib/data/index.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const user = locals.user; + const org = locals.org; + + try { + // Fetch users and teams for assignment dropdowns + const [usersResponse, teamsResponse] = await Promise.all([ + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/teams/', {}, { cookies, org }) + ]); + + // Extract active users + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((profile) => ({ + id: profile.id, + name: profile.user_details?.email?.split('@')[0] || 'Unknown', + email: profile.user_details?.email || '' + })); + + // Extract teams + const teams = (teamsResponse.teams || teamsResponse.results || []).map((team) => ({ + id: team.id, + name: team.name + })); + + return { + data: { + industries, + countries, + users, + teams + } + }; + } catch (err) { + console.error('Error loading account form data:', err); + return { + data: { + industries, + countries, + users: [], + teams: [] + } + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals, cookies }) => { + const user = locals.user; + const org = locals.org; + + const formData = await request.formData(); + + // Extract and validate required fields + const name = formData.get('name')?.toString().trim(); + + if (!name) { + return fail(400, { error: 'Account name is required' }); + } + + // Validate phone number if provided + let formattedPhone = null; + const phone = formData.get('phone')?.toString(); + if (phone && phone.trim().length > 0) { + const phoneValidation = validatePhoneNumber(phone.trim()); + if (!phoneValidation.isValid) { + return fail(400, { error: phoneValidation.error || 'Please enter a valid phone number' }); + } + formattedPhone = formatPhoneForStorage(phone.trim()); + } + + // Build Django data object + const djangoData = { + // Core Account Information + name, + email: formData.get('email')?.toString().trim() || null, + phone: formattedPhone, + website: formData.get('website')?.toString().trim() || null, + + // Business Information + industry: formData.get('industry')?.toString() || null, + number_of_employees: formData.get('number_of_employees') + ? parseInt(formData.get('number_of_employees')?.toString() || '0') + : null, + annual_revenue: formData.get('annual_revenue') + ? parseFloat(formData.get('annual_revenue')?.toString() || '0') + : null, + + // Address + address_line: formData.get('address_line')?.toString().trim() || null, + city: formData.get('city')?.toString().trim() || null, + state: formData.get('state')?.toString().trim() || null, + postcode: formData.get('postcode')?.toString().trim() || null, + country: formData.get('country')?.toString() || null, + + // Assignment + assigned_to: formData.getAll('assigned_to').map((id) => id.toString()), + teams: formData.getAll('teams').map((id) => id.toString()), + + // Notes + description: formData.get('description')?.toString().trim() || null + }; + + try { + const response = await apiRequest( + '/accounts/', + { + method: 'POST', + body: djangoData + }, + { cookies, org } + ); + + return { + status: 'success', + message: 'Account created successfully', + account: { + id: response.id || response.account_obj?.id, + name: response.name || response.account_obj?.name + } + }; + } catch (err) { + console.error('Error creating account:', err); + return fail(500, { + error: 'Failed to create account: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + } +}; diff --git a/frontend/src/routes/(app)/accounts/new/+page.svelte b/frontend/src/routes/(app)/accounts/new/+page.svelte new file mode 100644 index 00000000..592b490b --- /dev/null +++ b/frontend/src/routes/(app)/accounts/new/+page.svelte @@ -0,0 +1,706 @@ + + + +
+ + {#if showToast} +
+
+
+ {#if toastType === 'success'} + + {:else} + + {/if} +
+
+

+ {toastMessage} +

+
+ +
+
+ {/if} + +
+ +
+
+
+
+

+ + Create New Account +

+

+ Add a new company or organization to your CRM +

+
+
+
+
+ + + {#if form?.error} +
+
+ + Error: + {form.error} +
+
+ {/if} + + +
{ + if (!validateForm()) { + cancel(); + return; + } + + isSubmitting = true; + + return async ({ result }) => { + isSubmitting = false; + + if (result.type === 'success') { + showNotification('Account created successfully!', 'success'); + resetForm(); + setTimeout(() => goto('/accounts'), 1500); + } else if (result.type === 'failure') { + const errorMessage = + result.data && + typeof result.data === 'object' && + 'error' in result.data && + typeof result.data.error === 'string' + ? result.data.error + : 'Failed to create account'; + showNotification(errorMessage, 'error'); + } + }; + }} + class="space-y-6" + > + +
+
+

+ + Account Information +

+
+
+
+ +
+ + + {#if errors.name} +

{errors.name}

+ {/if} +
+ + +
+ + +
+ + +
+ + + {#if errors.phone} +

{errors.phone}

+ {/if} +
+ + +
+ + + {#if errors.website} +

{errors.website}

+ {/if} +
+
+
+
+ + +
+
+

+ + Business Information +

+
+
+
+ +
+ + +
+ + +
+ + + {#if errors.number_of_employees} +

+ {errors.number_of_employees} +

+ {/if} +
+ + +
+ + + {#if errors.annual_revenue} +

+ {errors.annual_revenue} +

+ {/if} +
+
+
+
+ + +
+
+

+ + Assignment +

+
+
+
+ +
+ + +

+ Hold Ctrl/Cmd to select multiple +

+
+ + +
+ + +

+ Hold Ctrl/Cmd to select multiple +

+
+
+
+
+ + +
+
+

+ + Address +

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

+ + Notes +

+
+
+ +
+
+ + +
+
+
+ + +
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/cases/+page.server.js b/frontend/src/routes/(app)/cases/+page.server.js new file mode 100644 index 00000000..ad01e503 --- /dev/null +++ b/frontend/src/routes/(app)/cases/+page.server.js @@ -0,0 +1,272 @@ +/** + * Cases List Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/cases/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, redirect, error } from '@sveltejs/kit'; +import { apiRequest, buildQueryParams } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ url, locals, cookies }) { + const org = locals.org; + const user = locals.user; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Get filters from query params + const status = url.searchParams.get('status') || undefined; + const assigned = url.searchParams.get('assigned') || undefined; + const account = url.searchParams.get('account') || undefined; + + // Build query parameters + const queryParams = buildQueryParams({ + page: 1, + limit: 1000, // Django doesn't paginate list by default + sort: 'created_at', + order: 'desc' + }); + + // Add filters + if (status) queryParams.append('status', status); + if (account) queryParams.append('account', account); + // Django doesn't have direct owner name filter, we'd need to filter by assigned_to ID + + // Fetch cases, users, and accounts in parallel + const [casesResponse, usersResponse, accountsResponse] = await Promise.all([ + apiRequest(`/cases/?${queryParams.toString()}`, {}, { cookies, org }), + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/accounts/', {}, { cookies, org }) + ]); + + // Extract cases from response + let cases = []; + if (casesResponse.cases) { + cases = casesResponse.cases; + } else if (Array.isArray(casesResponse)) { + cases = casesResponse; + } else if (casesResponse.results) { + cases = casesResponse.results; + } + + // Transform Django cases to Prisma structure + const transformedCases = cases.map((caseItem) => ({ + id: caseItem.id, + subject: caseItem.name, + description: caseItem.description, + status: caseItem.status, + priority: caseItem.priority, + caseType: caseItem.case_type, + closedOn: caseItem.closed_on, + createdAt: caseItem.created_at, + updatedAt: caseItem.updated_at, + + // Owner (first assigned user) + owner: + caseItem.assigned_to && caseItem.assigned_to.length > 0 + ? { + id: caseItem.assigned_to[0].id, + name: caseItem.assigned_to[0].user_details?.email || caseItem.assigned_to[0].email + } + : null, + + // Account + account: caseItem.account + ? { + id: caseItem.account.id, + name: caseItem.account.name + } + : null, + + // Comments (Django might not return by default) + comments: (caseItem.comments || []).map((comment) => ({ + id: comment.id, + body: comment.comment, + createdAt: comment.created_at, + author: comment.commented_by + ? { + id: comment.commented_by.id, + name: comment.commented_by.user_details?.email || comment.commented_by.email + } + : null + })) + })); + + // Transform users list + // Django UsersListView returns { active_users: { active_users: [...] }, ... } + const activeUsersList = usersResponse.active_users?.active_users || []; + const allUsers = activeUsersList.map((user) => ({ + id: user.id, + name: user.user_details?.email || user.email + })); + + // Transform accounts list + let allAccounts = []; + if (accountsResponse.active_accounts?.open_accounts) { + allAccounts = accountsResponse.active_accounts.open_accounts; + } else if (accountsResponse.results) { + allAccounts = accountsResponse.results; + } else if (Array.isArray(accountsResponse)) { + allAccounts = accountsResponse; + } + + const transformedAccounts = allAccounts.map((account) => ({ + id: account.id, + name: account.name + })); + + // Status options from Django + const statusOptions = ['New', 'Assigned', 'In Progress', 'Closed']; + + return { + cases: transformedCases, + allUsers, + allAccounts: transformedAccounts, + statusOptions + }; + } catch (err) { + console.error('Error loading cases from API:', err); + throw error(500, `Failed to load cases: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + create: async ({ request, locals, cookies }) => { + try { + const form = await request.formData(); + const name = form.get('title')?.toString().trim(); + const description = form.get('description')?.toString().trim(); + const accountId = form.get('accountId')?.toString(); + const dueDateValue = form.get('dueDate'); + const closedOn = dueDateValue ? dueDateValue.toString() : null; + const priority = form.get('priority')?.toString() || 'Normal'; + const ownerId = form.get('assignedId')?.toString(); + + if (!name || !accountId || !ownerId) { + return fail(400, { error: 'Missing required fields.' }); + } + + // Create case via API + const caseData = { + name, + description, + account: accountId, + closed_on: closedOn, + priority, + assigned_to: [ownerId], + status: 'New' + }; + + const newCase = await apiRequest( + '/cases/', + { + method: 'POST', + body: caseData + }, + { cookies, org: locals.org } + ); + + throw redirect(303, `/cases/${newCase.id}`); + } catch (err) { + console.error('Error creating case:', err); + return fail(500, { error: 'Failed to create case' }); + } + }, + + update: async ({ request, locals, cookies }) => { + try { + const form = await request.formData(); + const name = form.get('title')?.toString().trim(); + const description = form.get('description')?.toString().trim(); + const accountId = form.get('accountId')?.toString(); + const dueDateValue = form.get('dueDate'); + const closedOn = dueDateValue ? dueDateValue.toString() : null; + const priority = form.get('priority')?.toString() || 'Normal'; + const ownerId = form.get('assignedId')?.toString(); + const caseId = form.get('caseId')?.toString(); + + if (!name || !accountId || !ownerId || !caseId) { + return fail(400, { error: 'Missing required fields.' }); + } + + // Update case via API + const caseData = { + name, + description, + account: accountId, + closed_on: closedOn, + priority, + assigned_to: [ownerId] + }; + + await apiRequest( + `/cases/${caseId}/`, + { + method: 'PUT', + body: caseData + }, + { cookies, org: locals.org } + ); + + throw redirect(303, `/cases/${caseId}`); + } catch (err) { + console.error('Error updating case:', err); + return fail(500, { error: 'Failed to update case' }); + } + }, + + delete: async ({ request, locals, cookies }) => { + try { + const form = await request.formData(); + const caseId = form.get('caseId')?.toString(); + + if (!caseId) { + return fail(400, { error: 'Case ID is required.' }); + } + + // Delete via API + await apiRequest(`/cases/${caseId}/`, { method: 'DELETE' }, { cookies, org: locals.org }); + + throw redirect(303, '/cases'); + } catch (err) { + console.error('Error deleting case:', err); + return fail(500, { error: 'Failed to delete case' }); + } + }, + + comment: async ({ request, locals, cookies }) => { + try { + const form = await request.formData(); + const body = form.get('body')?.toString().trim(); + const caseId = form.get('caseId')?.toString(); + + if (!body || !caseId) { + return fail(400, { error: 'Comment and case ID are required.' }); + } + + // Create comment via API + await apiRequest( + `/cases/comment/${caseId}/`, + { + method: 'POST', + body: { comment: body } + }, + { cookies, org: locals.org } + ); + + return { success: true }; + } catch (err) { + console.error('Error creating comment:', err); + return fail(500, { error: 'Failed to create comment' }); + } + } +}; diff --git a/frontend/src/routes/(app)/cases/+page.svelte b/frontend/src/routes/(app)/cases/+page.svelte new file mode 100644 index 00000000..56035bf9 --- /dev/null +++ b/frontend/src/routes/(app)/cases/+page.svelte @@ -0,0 +1,381 @@ + + +
+
+ +
+
+
+ +
+
+

Cases

+

+ Manage customer support cases and issues +

+
+
+ + + New Case + +
+ + +
+
+ +

Filters

+ {#if hasActiveFilters} + + {[statusFilter, assignedFilter, accountFilter].filter(Boolean).length} active + + {/if} +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + {#if hasActiveFilters} +
+ +
+ {/if} +
+
+ + +
+ {#if filteredCases.length} + + + + +
+ {#each filteredCases as c} +
+
+ + {c.subject} + + + {c.status.replace('_', ' ')} + +
+ + {#if c.description} +

+ {c.description} +

+ {/if} + +
+
+ Account: + {c.account?.name || '-'} +
+
+ Priority: + + {c.priority} + +
+
+ Assigned: + {c.owner?.name || 'Unassigned'} +
+
+ Due: + + {c.dueDate ? new Date(c.dueDate).toLocaleDateString() : '-'} + +
+
+ + +
+ {/each} +
+ {:else} +
+
+ +
+

No cases found

+

+ {hasActiveFilters + ? 'No cases match your current filters.' + : 'Get started by creating your first case.'} +

+ {#if hasActiveFilters} + + {:else} + + + Create Case + + {/if} +
+ {/if} +
+
+
diff --git a/frontend/src/routes/(app)/cases/[caseId]/+page.server.js b/frontend/src/routes/(app)/cases/[caseId]/+page.server.js new file mode 100644 index 00000000..966b3a1d --- /dev/null +++ b/frontend/src/routes/(app)/cases/[caseId]/+page.server.js @@ -0,0 +1,113 @@ +/** + * Case Detail Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/cases/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + const caseId = params.caseId; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch case details from Django + const response = await apiRequest(`/cases/${caseId}/`, {}, { cookies, org }); + + if (!response.cases_obj) { + throw error(404, 'Case not found'); + } + + const caseData = response.cases_obj; + + // Transform case data + const caseItem = { + id: caseData.id, + subject: caseData.name, + description: caseData.description, + status: caseData.status, + priority: caseData.priority, + caseType: caseData.case_type, + closedOn: caseData.closed_on, + createdAt: caseData.created_at, + updatedAt: caseData.updated_at, + + // Owner + owner: + caseData.assigned_to && caseData.assigned_to.length > 0 + ? { + id: caseData.assigned_to[0].id, + name: caseData.assigned_to[0].user_details?.email || caseData.assigned_to[0].email + } + : null, + + // Account + account: caseData.account + ? { + id: caseData.account.id, + name: caseData.account.name + } + : null, + + // Comments + comments: (response.comments || []).map((comment) => ({ + id: comment.id, + body: comment.comment, + createdAt: comment.created_at, + author: comment.commented_by + ? { + id: comment.commented_by.id, + name: comment.commented_by.user_details?.email || comment.commented_by.email + } + : null + })) + }; + + return { caseItem }; + } catch (err) { + console.error('Error loading case data:', err); + throw error(500, `Failed to load case: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + comment: async ({ request, params, locals, cookies }) => { + const org = locals.org; + + try { + const form = await request.formData(); + const body = form.get('body')?.toString().trim(); + + if (!body) { + return fail(400, { error: 'Comment cannot be empty.' }); + } + + // Add comment via API + await apiRequest( + `/cases/comment/${params.caseId}/`, + { + method: 'POST', + body: { comment: body } + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error adding comment:', err); + return fail(500, { error: 'Failed to add comment' }); + } + } +}; diff --git a/frontend/src/routes/(app)/cases/[caseId]/+page.svelte b/frontend/src/routes/(app)/cases/[caseId]/+page.svelte new file mode 100644 index 00000000..82cc8621 --- /dev/null +++ b/frontend/src/routes/(app)/cases/[caseId]/+page.svelte @@ -0,0 +1,335 @@ + + +
+
+ +
+
+
+
+ +
+
+

+ {data.caseItem.subject} +

+

Case #{data.caseItem.caseNumber}

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

Case Information

+ + {#if data.caseItem.description} +
+

Description

+

+ {data.caseItem.description} +

+
+ {/if} + +
+
+
+ +
+

Account

+

+ {data.caseItem.account?.name || 'Not assigned'} +

+
+
+ +
+ +
+

Assigned to

+

+ {data.caseItem.owner?.name || 'Unassigned'} +

+
+
+
+ +
+ {#if data.caseItem.dueDate} +
+ +
+

Due Date

+

+ {new Date(data.caseItem.dueDate).toLocaleDateString()} +

+
+
+ {/if} + +
+ +
+

Created

+

+ {new Date(data.caseItem.createdAt).toLocaleDateString()} +

+
+
+
+
+
+ + +
+
+ +

Comments

+ ({data.caseItem.comments?.length || 0}) +
+ + +
+ {#if data.caseItem.comments && data.caseItem.comments.length} + {#each data.caseItem.comments as c} +
+
+
+ + {c.author?.name?.[0]?.toUpperCase() || 'U'} + +
+
+
+

+ {c.author?.name || 'Unknown User'} +

+ • +

+ {new Date(c.createdAt).toLocaleDateString()} +

+
+

{c.body}

+
+
+
+ {/each} + {:else} +
+ +

No comments yet. Be the first to add one!

+
+ {/if} +
+ + +
+
+ + +
+ {#if errorMsg} +

{errorMsg}

+ {/if} +
+
+
+ + +
+ +
+

+ Status & Priority +

+ +
+
+

Status

+
+ + + {data.caseItem.status.replace('_', ' ')} + +
+
+ +
+

Priority

+ + {data.caseItem.priority} + +
+
+
+ + +
+

+ Activity Timeline +

+ +
+
+
+
+

Case Created

+

+ {new Date(data.caseItem.createdAt).toLocaleDateString()} +

+
+
+ + {#if data.caseItem.updatedAt && data.caseItem.updatedAt !== data.caseItem.createdAt} +
+
+
+

Last Updated

+

+ {new Date(data.caseItem.updatedAt).toLocaleDateString()} +

+
+
+ {/if} + + {#if data.caseItem.closedAt} +
+
+
+

Case Closed

+

+ {new Date(data.caseItem.closedAt).toLocaleDateString()} +

+
+
+ {/if} +
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/cases/[caseId]/edit/+page.server.js b/frontend/src/routes/(app)/cases/[caseId]/edit/+page.server.js new file mode 100644 index 00000000..94c69aa1 --- /dev/null +++ b/frontend/src/routes/(app)/cases/[caseId]/edit/+page.server.js @@ -0,0 +1,229 @@ +/** + * Case Edit Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: PUT/PATCH /api/cases/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, redirect, error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch case, users, and accounts in parallel + const [caseResponse, usersResponse, accountsResponse] = await Promise.all([ + apiRequest(`/cases/${params.caseId}/`, {}, { cookies, org }), + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/accounts/', {}, { cookies, org }) + ]); + + if (!caseResponse.cases_obj) { + throw error(404, 'Case not found'); + } + + const caseData = caseResponse.cases_obj; + + // Transform case to Prisma structure + const caseItem = { + id: caseData.id, + subject: caseData.name, + description: caseData.description, + accountId: caseData.account?.id || null, + dueDate: caseData.closed_on || null, + priority: caseData.priority || 'Normal', + ownerId: caseData.assigned_to?.[0]?.id || caseData.assigned_to?.[0] || null, + status: caseData.status || 'New', + closedAt: caseData.closed_on || null, + caseType: caseData.case_type || null, + // Include related data + owner: caseData.assigned_to_details?.[0] + ? { + id: caseData.assigned_to_details[0].id, + name: + caseData.assigned_to_details[0].user_details?.email || + caseData.assigned_to_details[0].email + } + : null, + account: caseData.account + ? { + id: caseData.account.id, + name: caseData.account.name + } + : null + }; + + // Transform users + // Django UsersListView returns { active_users: { active_users: [...] }, ... } + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((user) => ({ + id: user.id, + name: user.user_details?.email || user.email + })); + + // Transform accounts + let accounts = []; + if (accountsResponse.results) { + accounts = accountsResponse.results; + } else if (accountsResponse.active_accounts?.open_accounts) { + accounts = accountsResponse.active_accounts.open_accounts; + } + + const transformedAccounts = accounts.map((acc) => ({ + id: acc.id, + name: acc.name + })); + + return { + caseItem, + users, + accounts: transformedAccounts + }; + } catch (err) { + console.error('Error loading case for edit:', err); + throw error(500, `Failed to load case: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + update: async ({ request, params, locals, cookies }) => { + const org = locals.org; + const form = await request.formData(); + + // Extract form fields + const subject = form.get('title')?.toString().trim(); + const description = form.get('description')?.toString().trim(); + const accountId = form.get('accountId')?.toString(); + const dueDateRaw = form.get('dueDate'); + const dueDate = dueDateRaw ? dueDateRaw.toString() : null; + const priority = form.get('priority')?.toString() || 'Normal'; + const ownerId = form.get('assignedId')?.toString(); + const status = form.get('status')?.toString() || 'New'; + + // Validation + const errors = {}; + + if (!subject || subject.length === 0) { + errors.subject = 'Subject is required'; + } + + if (!accountId) { + errors.accountId = 'Account is required'; + } + + if (!ownerId) { + errors.ownerId = 'Owner is required'; + } + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + values: { subject, description, accountId, dueDate, priority, ownerId } + }); + } + + try { + // Transform to Django field names + const djangoData = { + name: subject, + description: description || null, + account: accountId, + closed_on: dueDate, + priority: priority, + status: status, + assigned_to: [ownerId] + }; + + // Update case via API + await apiRequest( + `/cases/${params.caseId}/`, + { + method: 'PUT', + body: djangoData + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error updating case:', err); + return fail(500, { + error: 'Failed to update case: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { subject, description, accountId, dueDate, priority, ownerId } + }); + } + }, + + close: async ({ params, locals, cookies }) => { + const org = locals.org; + + try { + // Close case via API + await apiRequest( + `/cases/${params.caseId}/`, + { + method: 'PATCH', + body: { + status: 'Closed', + closed_on: new Date().toISOString() + } + }, + { cookies, org } + ); + + throw redirect(303, `/cases/${params.caseId}`); + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error closing case:', err); + return fail(500, { + error: 'Failed to close case: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + }, + + reopen: async ({ params, locals, cookies }) => { + const org = locals.org; + + try { + // Reopen case via API + await apiRequest( + `/cases/${params.caseId}/`, + { + method: 'PATCH', + body: { + status: 'New', + closed_on: null + } + }, + { cookies, org } + ); + + throw redirect(303, `/cases/${params.caseId}`); + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error reopening case:', err); + return fail(500, { + error: 'Failed to reopen case: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + } +}; diff --git a/frontend/src/routes/(app)/cases/[caseId]/edit/+page.svelte b/frontend/src/routes/(app)/cases/[caseId]/edit/+page.svelte new file mode 100644 index 00000000..7e7af414 --- /dev/null +++ b/frontend/src/routes/(app)/cases/[caseId]/edit/+page.svelte @@ -0,0 +1,426 @@ + + +
+
+ +
+
+
+

Edit Case

+

+ Update case details and assignment +

+
+
+ +
+
+
+ + +
+
{ + loading = true; + errorMsg = ''; + successMsg = ''; + return async ({ result, update }) => { + loading = false; + if (result.type === 'failure') { + errorMsg = (result.data as any)?.error || 'An error occurred'; + } else if (result.type === 'success') { + successMsg = 'Case updated successfully!'; + // Update the data and reset form values from fresh data + await update(); + // Re-initialize form values from updated data + title = data.caseItem.subject; + description = data.caseItem.description || ''; + accountId = data.caseItem.accountId; + if (data.caseItem.dueDate) { + const dueDateValue: any = data.caseItem.dueDate; + if (typeof dueDateValue === 'string') { + dueDate = dueDateValue.split('T')[0]; + } else if (dueDateValue instanceof Date) { + dueDate = dueDateValue.toISOString().split('T')[0]; + } + } else { + dueDate = ''; + } + assignedId = data.caseItem.ownerId; + priority = data.caseItem.priority || 'Normal'; + } else { + await update(); + } + }; + }} + > + + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ + +
+ + + {#if errorMsg} +
+ +
{errorMsg}
+
+ {/if} + + {#if successMsg} +
+
+
+
+
{successMsg}
+
+ {/if} +
+ + +
+ + + {#if data.caseItem.status === 'CLOSED'} + + {:else} + + {/if} +
+
+
+ + + + + + + + + {#if showCloseConfirmation} +
+
+
+
+
+ +
+
+

Close Case

+

+ Are you sure you want to close this case? +

+
+
+

+ This action will mark the case as closed. You can still view the case details, but it + will no longer be active. +

+
+ + +
+
+
+
+ {/if} + + + {#if showReopenConfirmation} +
+
+
+
+
+ +
+
+

Reopen Case

+

+ Are you sure you want to reopen this case? +

+
+
+

+ This action will mark the case as active again and allow you to continue working on + it. +

+
+ + +
+
+
+
+ {/if} +
+
diff --git a/frontend/src/routes/(app)/cases/new/+page.server.js b/frontend/src/routes/(app)/cases/new/+page.server.js new file mode 100644 index 00000000..7d6a3d6b --- /dev/null +++ b/frontend/src/routes/(app)/cases/new/+page.server.js @@ -0,0 +1,157 @@ +/** + * Case Create Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: POST /api/cases/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, url, cookies }) { + const org = locals.org; + const preSelectedAccountId = url.searchParams.get('accountId'); + + try { + // Fetch accounts and users in parallel + const [accountsResponse, usersResponse] = await Promise.all([ + apiRequest('/accounts/', {}, { cookies, org }), + apiRequest('/users/', {}, { cookies, org }) + ]); + + // Transform accounts + let accounts = []; + if (accountsResponse.results) { + accounts = accountsResponse.results; + } else if (accountsResponse.active_accounts?.open_accounts) { + accounts = accountsResponse.active_accounts.open_accounts; + } + + const transformedAccounts = accounts.map((acc) => ({ + id: acc.id, + name: acc.name + })); + + // Transform users + // Django UsersListView returns { active_users: { active_users: [...] }, ... } + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((user) => ({ + user: { + id: user.id, + name: user.user_details?.email || user.email + } + })); + + return { + accounts: transformedAccounts, + users, + preSelectedAccountId + }; + } catch (err) { + console.error('Error loading case create page:', err); + return { + accounts: [], + users: [], + preSelectedAccountId + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + create: async ({ request, locals, cookies }) => { + const org = locals.org; + const form = await request.formData(); + + const subject = form.get('title')?.toString().trim(); + const description = form.get('description')?.toString().trim(); + const accountId = form.get('accountId')?.toString(); + const dueDateValue = form.get('dueDate'); + const dueDate = dueDateValue ? dueDateValue.toString() : null; + const priority = form.get('priority')?.toString() || 'Normal'; + const ownerId = form.get('assignedId')?.toString(); + + // Validation + const errors = {}; + + if (!subject || subject.length === 0) { + errors.subject = 'Subject is required'; + } + + if (!accountId) { + errors.accountId = 'Account is required'; + } + + if (!ownerId) { + errors.ownerId = 'Owner is required'; + } + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + values: { + subject, + description, + accountId, + dueDate, + priority, + ownerId + } + }); + } + + try { + // Transform to Django field names + const djangoData = { + name: subject, + description: description || null, + account: accountId, + closed_on: dueDate, + priority: priority, + assigned_to: [ownerId], + status: 'New' + }; + + // Create case via API + const response = await apiRequest( + '/cases/', + { + method: 'POST', + body: djangoData + }, + { cookies, org } + ); + + const caseId = response.id || response.cases_obj?.id; + + if (caseId) { + throw redirect(303, `/cases/${caseId}`); + } else { + throw redirect(303, '/cases'); + } + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error creating case:', err); + return fail(500, { + error: 'Failed to create case: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { + subject, + description, + accountId, + dueDate, + priority, + ownerId + } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/cases/new/+page.svelte b/frontend/src/routes/(app)/cases/new/+page.svelte new file mode 100644 index 00000000..5c3922a2 --- /dev/null +++ b/frontend/src/routes/(app)/cases/new/+page.svelte @@ -0,0 +1,195 @@ + + +
+
+ +
+
+
+ +
+

Create New Case

+
+

+ Create and assign a new support case to track customer issues and requests. +

+
+ + +
+
+ +
+
+

+ Case Details +

+

Basic information about the case

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

+ Assignment & Priority +

+

Set ownership and urgency level

+
+ +
+ +
+ + +
+ + +
+ + +
+
+ + +
+ + +
+
+ + + {#if errorMsg} +
+

{errorMsg}

+
+ {/if} + + +
+ + + Cancel + +
+
+
+
+
diff --git a/frontend/src/routes/(app)/contacts/+page.server.js b/frontend/src/routes/(app)/contacts/+page.server.js new file mode 100644 index 00000000..34817c9b --- /dev/null +++ b/frontend/src/routes/(app)/contacts/+page.server.js @@ -0,0 +1,164 @@ +/** + * Contacts List Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/contacts/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail } from '@sveltejs/kit'; +import { apiRequest, buildQueryParams } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ url, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + const page = parseInt(url.searchParams.get('page') || '1'); + const limit = parseInt(url.searchParams.get('limit') || '20'); + const search = url.searchParams.get('search') || ''; + const ownerId = url.searchParams.get('owner') || ''; + + // Build query parameters for Django + const queryParams = buildQueryParams({ + page, + limit, + sort: 'created_at', + order: 'desc' + }); + + // Add search filter (Django typically uses 'search' param) + if (search) { + queryParams.append('search', search); + } + + // Add owner filter (Django uses assigned_to) + if (ownerId) { + queryParams.append('assigned_to', ownerId); + } + + // Fetch contacts and owners in parallel + const [contactsResponse, ownersResponse] = await Promise.all([ + apiRequest(`/contacts/?${queryParams.toString()}`, {}, { cookies, org }), + // Get users for owner filter dropdown + apiRequest('/users/', {}, { cookies, org }) + ]); + + // Handle Django response format + let contacts = []; + let totalCount = 0; + + if (contactsResponse.results) { + // Standard Django pagination + contacts = contactsResponse.results; + totalCount = contactsResponse.count || 0; + } else if (Array.isArray(contactsResponse)) { + // Direct array response + contacts = contactsResponse; + totalCount = contacts.length; + } + + // Transform Django contacts to match Prisma structure + const transformedContacts = contacts.map((contact) => ({ + id: contact.id, + firstName: contact.first_name, + lastName: contact.last_name, + email: contact.email, + phone: contact.phone, + title: contact.title, + department: contact.department, + description: contact.description, + createdAt: contact.created_at, + updatedAt: contact.updated_at, + + // Owner info - Django returns assigned_to array + owner: + contact.assigned_to && contact.assigned_to.length > 0 + ? { + id: contact.assigned_to[0].id, + name: contact.assigned_to[0].user_details?.email || 'Unknown', + email: contact.assigned_to[0].user_details?.email + } + : contact.created_by + ? { + id: contact.created_by.id, + name: contact.created_by.email, + email: contact.created_by.email + } + : null, + + // Related accounts + relatedAccounts: + contact.accounts?.map((account) => ({ + account: { + id: account.id, + name: account.name + } + })) || [], + + // Counts - Django may not provide _count, use 0 as default + _count: { + tasks: contact.task_count || 0, + events: contact.event_count || 0, + opportunities: contact.opportunity_count || 0, + cases: contact.case_count || 0 + } + })); + + // Transform owners list + // Django UsersListView returns { active_users: { active_users: [...] }, inactive_users: { ... } } + const activeUsers = ownersResponse.active_users?.active_users || []; + const owners = activeUsers.map((user) => ({ + id: user.id, + name: user.user_details?.email || user.email, + email: user.user_details?.email || user.email + })); + + return { + contacts: transformedContacts, + totalCount, + currentPage: page, + totalPages: Math.ceil(totalCount / limit), + limit, + search, + ownerId, + owners + }; + } catch (err) { + console.error('Error loading contacts from API:', err); + throw error(500, `Failed to load contacts: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + delete: async ({ request, locals, cookies }) => { + try { + const data = await request.formData(); + const contactId = data.get('contactId')?.toString(); + + if (!contactId) { + return fail(400, { error: 'Contact ID is required' }); + } + + // Delete via API + await apiRequest( + `/contacts/${contactId}/`, + { method: 'DELETE' }, + { cookies, org: locals.org } + ); + + return { success: true }; + } catch (err) { + console.error('Error deleting contact:', err); + return fail(500, { error: 'Failed to delete contact' }); + } + } +}; diff --git a/frontend/src/routes/(app)/contacts/+page.svelte b/frontend/src/routes/(app)/contacts/+page.svelte new file mode 100644 index 00000000..76a3fad5 --- /dev/null +++ b/frontend/src/routes/(app)/contacts/+page.svelte @@ -0,0 +1,520 @@ + + + + Contacts - BottleCRM + + +
+ +
+
+
+
+
+ +
+
+

Contacts

+

+ {data.totalCount} total contacts +

+
+
+
+ + + + Add Contact + +
+
+ + +
+
+ + e.key === 'Enter' && handleSearch()} + class="w-full rounded-lg border border-gray-300 bg-white py-3 pr-4 pl-10 text-gray-900 placeholder-gray-500 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" + /> + {#if searchQuery} + + {/if} +
+
+ + + {#if showFilters} +
+
+
+ + +
+
+
+ + +
+
+ {/if} +
+
+ + +
+ {#if data.contacts.length === 0} +
+ +

No contacts found

+

+ {data.search + ? 'Try adjusting your search criteria.' + : 'Get started by creating your first contact.'} +

+ {#if !data.search} + + + Add Contact + + {/if} +
+ {:else} + + + + +
+ {#each data.contacts as contact} +
+
+ + +
+ +
+ {#if contact.email} +
+ + {contact.email} +
+ {/if} + {#if contact.phone} +
+ + {formatPhone(contact.phone)} +
+ {/if} + {#if contact.relatedAccounts.length > 0} +
+ + {contact.relatedAccounts[0].account.name} +
+ {/if} +
+ +
+
+ {#if contact._count.tasks > 0} + + {contact._count.tasks} tasks + + {/if} + {#if contact._count.opportunities > 0} + + {contact._count.opportunities} opps + + {/if} +
+
+ + +
+
+
+ {/each} +
+ + + {#if data.totalPages > 1} +
+
+ Showing {(data.currentPage - 1) * data.limit + 1} to {Math.min( + data.currentPage * data.limit, + data.totalCount + )} of {data.totalCount} contacts +
+
+ + + {#each Array.from({ length: Math.min(5, data.totalPages) }, (_, i) => { + const start = Math.max(1, data.currentPage - 2); + return start + i; + }) as pageNum} + {#if pageNum <= data.totalPages} + + {/if} + {/each} + + +
+
+ {/if} + {/if} +
+
diff --git a/frontend/src/routes/(app)/contacts/[contactId]/+page.server.js b/frontend/src/routes/(app)/contacts/[contactId]/+page.server.js new file mode 100644 index 00000000..387fae5f --- /dev/null +++ b/frontend/src/routes/(app)/contacts/[contactId]/+page.server.js @@ -0,0 +1,161 @@ +/** + * Contact Detail Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/contacts/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch contact details from Django + const response = await apiRequest(`/contacts/${params.contactId}/`, {}, { cookies, org }); + + if (!response.contact_obj) { + return { + status: 404, + error: new Error('Contact not found') + }; + } + + const contactData = response.contact_obj; + + // Transform contact data + const contact = { + id: contactData.id, + firstName: contactData.first_name, + lastName: contactData.last_name, + email: contactData.email, + phone: contactData.phone, + title: contactData.title, + department: contactData.department, + description: contactData.description, + createdAt: contactData.created_at, + updatedAt: contactData.updated_at, + organizationId: contactData.org?.id || org.id, + ownerId: contactData.created_by?.id || null, + + // Address fields (from Django address or direct fields) + street: contactData.address?.street || contactData.street || null, + city: contactData.address?.city || contactData.city || null, + state: contactData.address?.state || contactData.state || null, + country: contactData.address?.country || contactData.country || null, + postalCode: contactData.address?.postcode || contactData.postal_code || null, + + // Owner info + owner: + contactData.assigned_to && contactData.assigned_to.length > 0 + ? { + id: contactData.assigned_to[0].id, + name: + contactData.assigned_to[0].user_details?.email || contactData.assigned_to[0].email, + email: + contactData.assigned_to[0].user_details?.email || contactData.assigned_to[0].email + } + : contactData.created_by + ? { + id: contactData.created_by.id, + name: contactData.created_by.email, + email: contactData.created_by.email + } + : null + }; + + // Transform account relationships + const accountRelationships = (contactData.accounts || response.accounts_list || []).map( + (account) => ({ + account: { + id: account.id, + name: account.name, + industry: account.industry, + email: account.email + }, + isPrimary: account.is_primary || false, + role: account.role || null, + startDate: account.start_date || null + }) + ); + + // Transform opportunities + const opportunities = (response.opportunity_list || []).map((opp) => ({ + id: opp.id, + name: opp.name, + amount: opp.amount ? Number(opp.amount) : null, + stage: opp.stage, + probability: opp.probability, + closedOn: opp.closed_on, + createdAt: opp.created_at, + account: opp.account + ? { + id: opp.account.id, + name: opp.account.name + } + : null + })); + + // Transform tasks + const tasks = (response.tasks || []) + .map((task) => ({ + id: task.id, + title: task.title, + status: task.status, + priority: task.priority, + dueDate: task.due_date, + createdAt: task.created_at, + owner: + task.assigned_to && task.assigned_to.length > 0 + ? { + id: task.assigned_to[0].id, + name: task.assigned_to[0].user_details?.email || task.assigned_to[0].email + } + : null + })) + .slice(0, 5); + + // Transform events (Django may return these) + const events = (response.events || []) + .map((event) => ({ + id: event.id, + name: event.name, + startDate: event.start_date, + startTime: event.start_time, + endDate: event.end_date, + status: event.status, + createdAt: event.created_at, + owner: + event.assigned_to && event.assigned_to.length > 0 + ? { + id: event.assigned_to[0].id, + name: event.assigned_to[0].user_details?.email || event.assigned_to[0].email + } + : null + })) + .slice(0, 5); + + return { + contact: { + ...contact, + accountRelationships, + opportunities, + tasks, + events + } + }; + } catch (err) { + console.error('Error loading contact data:', err); + throw error(500, `Failed to load contact: ${err.message}`); + } +} diff --git a/frontend/src/routes/(app)/contacts/[contactId]/+page.svelte b/frontend/src/routes/(app)/contacts/[contactId]/+page.svelte new file mode 100644 index 00000000..f9350489 --- /dev/null +++ b/frontend/src/routes/(app)/contacts/[contactId]/+page.svelte @@ -0,0 +1,473 @@ + + +
+ +
+
+
+
+ {#if primaryAccountRel} + + + Back to {primaryAccountRel.account.name} + + {:else} + + + Back to Contacts + + {/if} +
+
+ {contact.firstName?.[0]}{contact.lastName?.[0]} +
+
+

+ {contact.firstName} + {contact.lastName} +

+

{contact.title || 'Contact'}

+
+ {#if primaryAccountRel?.isPrimary} + + + Primary + + {/if} + {#if hasMultipleAccounts} + + + {contact.accountRelationships.length} Accounts + + {/if} +
+
+ +
+
+
+ +
+
+ +
+ +
+

+ + Contact Information +

+
+
+
+ Email + {#if contact.email} + + + {contact.email} + + {:else} +

N/A

+ {/if} +
+
+ Phone + {#if contact.phone} + + + {contact.phone} + + {:else} +

N/A

+ {/if} +
+
+ Department +

{contact.department || 'N/A'}

+
+
+
+
+ Title +

{contact.title || 'N/A'}

+
+
+ Owner +

{contact.owner?.name || 'N/A'}

+
+
+ Created +

+ + {formatDate(contact.createdAt)} +

+
+
+
+ {#if contact.description} +
+ Description +

{contact.description}

+
+ {/if} +
+ + + {#if contact.accountRelationships && contact.accountRelationships.length > 0} +
+

+ + Account Relationships + ({contact.accountRelationships.length}) +

+
+ {#each contact.accountRelationships as relationship} +
+
+
+ + + {relationship.account.name} + + {#if relationship.isPrimary} + + + Primary + + {/if} +
+
+ {#if relationship.role} + + + {relationship.role} + + {/if} + + + Since {formatDate(relationship.startDate)} + +
+ {#if relationship.description} +

+ {relationship.description} +

+ {/if} +
+
+ + {relationship.account.type || 'Account'} + +
+
+ {/each} +
+
+ {/if} + + + {#if contact.street || contact.city || contact.state || contact.country} +
+

+ + Address +

+
+ {#if contact.street}

{contact.street}

{/if} +

+ {contact.city || ''}{contact.city && contact.state ? ', ' : ''}{contact.state || ''} + {contact.postalCode || ''} +

+ {#if contact.country}

{contact.country}

{/if} +
+
+ {/if} + + + {#if contact.opportunities && contact.opportunities.length > 0} +
+
+

+ + Opportunities +

+ + View all + + +
+
+ {#each contact.opportunities as opp} +
+
+ + {opp.name} + +

{opp.account?.name}

+
+
+

+ + {formatCurrency(opp.amount || 0)} +

+ + {opp.stage.replace('_', ' ')} + +
+
+ {/each} +
+
+ {/if} +
+ + +
+ + {#if contact.tasks && contact.tasks.length > 0} +
+
+

+ + Recent Tasks +

+ View all +
+
+ {#each contact.tasks as task} +
+ {#if task.status === 'Completed'} + + {:else} + + {/if} +
+

+ {task.subject} +

+
+ + {task.priority} + + {#if task.dueDate} + + + {formatDate(task.dueDate)} + + {/if} +
+
+
+ {/each} +
+
+ {/if} + + + {#if contact.events && contact.events.length > 0} +
+
+

+ + Recent Events +

+ View all +
+
+ {#each contact.events as event} +
+

{event.subject}

+

+ + {formatDateTime(event.startDate)} +

+ {#if event.location} +

+ + {event.location} +

+ {/if} +
+ {/each} +
+
+ {/if} +
+
+
+
diff --git a/frontend/src/routes/(app)/contacts/[contactId]/edit/+page.server.js b/frontend/src/routes/(app)/contacts/[contactId]/edit/+page.server.js new file mode 100644 index 00000000..af6ac1a0 --- /dev/null +++ b/frontend/src/routes/(app)/contacts/[contactId]/edit/+page.server.js @@ -0,0 +1,191 @@ +/** + * Contact Edit Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: PUT/PATCH /api/contacts/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + const response = await apiRequest(`/contacts/${params.contactId}/`, {}, { cookies, org }); + + if (!response.contact_obj) { + throw error(404, 'Contact not found'); + } + + const contactData = response.contact_obj; + + // Transform to Prisma structure + const contact = { + id: contactData.id, + firstName: contactData.first_name, + lastName: contactData.last_name, + email: contactData.email, + phone: contactData.phone, + title: contactData.title, + department: contactData.department, + street: contactData.address?.address_line || null, + city: contactData.address?.city || null, + state: contactData.address?.state || null, + postalCode: contactData.address?.postcode || null, + country: contactData.address?.country || null, + description: contactData.description + }; + + // Note: Account relationship handling + // Django may return account info differently + // This might require additional API call or different endpoint + const account = contactData.account + ? { + id: contactData.account.id, + name: contactData.account.name + } + : null; + + return { + contact, + account, + isPrimary: contactData.is_primary || false, + role: contactData.role || '' + }; + } catch (err) { + console.error('Error loading contact for edit:', err); + throw error(500, `Failed to load contact: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, params, locals, cookies }) => { + const org = locals.org; + const formData = await request.formData(); + + // Extract form fields + const firstName = formData.get('firstName')?.toString().trim(); + const lastName = formData.get('lastName')?.toString().trim(); + const email = formData.get('email')?.toString().trim() || null; + const phone = formData.get('phone')?.toString().trim() || null; + const title = formData.get('title')?.toString().trim() || null; + const department = formData.get('department')?.toString().trim() || null; + const street = formData.get('street')?.toString().trim() || null; + const city = formData.get('city')?.toString().trim() || null; + const state = formData.get('state')?.toString().trim() || null; + const postalCode = formData.get('postalCode')?.toString().trim() || null; + const country = formData.get('country')?.toString().trim() || null; + const description = formData.get('description')?.toString().trim() || null; + + // Validation + const errors = {}; + + if (!firstName || firstName.length === 0) { + errors.firstName = 'First name is required'; + } + + if (!lastName || lastName.length === 0) { + errors.lastName = 'Last name is required'; + } + + // Validate email format if provided + if (email && email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + errors.email = 'Please enter a valid email address'; + } + + // Validate phone number if provided + let formattedPhone = null; + if (phone && phone.length > 0) { + const phoneValidation = validatePhoneNumber(phone); + if (!phoneValidation.isValid) { + errors.phone = phoneValidation.error || 'Please enter a valid phone number'; + } else { + formattedPhone = formatPhoneForStorage(phone); + } + } + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + values: { + firstName, + lastName, + email, + phone, + title, + department, + street, + city, + state, + postalCode, + country, + description + } + }); + } + + try { + // Transform to Django field names + const djangoData = { + first_name: firstName, + last_name: lastName, + email: email, + phone: formattedPhone, + title: title, + department: department, + address: { + address_line: street, + city: city, + state: state, + postcode: postalCode, + country: country + }, + description: description + }; + + // Update contact via API + await apiRequest( + `/contacts/${params.contactId}/`, + { + method: 'PUT', + body: djangoData + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error updating contact:', err); + return fail(500, { + error: + 'Failed to update contact: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { + firstName, + lastName, + email, + phone, + title, + department, + street, + city, + state, + postalCode, + country, + description + } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/contacts/[contactId]/edit/+page.svelte b/frontend/src/routes/(app)/contacts/[contactId]/edit/+page.svelte new file mode 100644 index 00000000..254d78e2 --- /dev/null +++ b/frontend/src/routes/(app)/contacts/[contactId]/edit/+page.svelte @@ -0,0 +1,461 @@ + + +
+
+ +
+
+ +
+

Edit Contact

+

+ Update contact information and details +

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

Basic Information

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

Contact Information

+
+
+
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+ {#if phoneError} +

{phoneError}

+ {/if} +
+
+
+
+ + +
+
+
+
+ +
+

Address Information

+
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ + + {#if account} +
+
+
+
+ +
+

+ Account Relationship +

+
+
+
+
+
+
Account
+
{account.name}
+
+ {#if role} +
+
Role
+
{role}
+
+ {/if} + {#if isPrimary} +
+
+ + Primary Contact +
+
+ {/if} +
+
+
+ {/if} + + +
+
+
+
+ +
+

+ Additional Information +

+
+
+
+
+ + +
+
+
+ + + {#if errorMsg} +
+
+
+ +
+
+

{errorMsg}

+
+
+
+ {/if} + + +
+ + +
+
+
+
diff --git a/frontend/src/routes/(app)/contacts/new/+page.server.js b/frontend/src/routes/(app)/contacts/new/+page.server.js new file mode 100644 index 00000000..25dd3e4b --- /dev/null +++ b/frontend/src/routes/(app)/contacts/new/+page.server.js @@ -0,0 +1,146 @@ +/** + * Contact Create Page - Modern CRM Version + * + * Streamlined contact creation based on Twenty CRM and Salesforce patterns. + * Django endpoint: POST /api/contacts/ + */ + +import { fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; +import { countries } from '$lib/data/index.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const user = locals.user; + const org = locals.org; + + try { + // Fetch users and teams for assignment dropdowns + const [usersResponse, teamsResponse] = await Promise.all([ + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/teams/', {}, { cookies, org }) + ]); + + // Extract active users + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((profile) => ({ + id: profile.id, + name: profile.user_details?.email?.split('@')[0] || 'Unknown', + email: profile.user_details?.email || '' + })); + + // Extract teams + const teams = (teamsResponse.teams || teamsResponse.results || []).map((team) => ({ + id: team.id, + name: team.name + })); + + return { + data: { + countries, + users, + teams + } + }; + } catch (err) { + console.error('Error loading contact form data:', err); + return { + data: { + countries, + users: [], + teams: [] + } + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals, cookies }) => { + const user = locals.user; + const org = locals.org; + + const formData = await request.formData(); + + // Extract and validate required fields + const firstName = formData.get('first_name')?.toString().trim(); + const lastName = formData.get('last_name')?.toString().trim(); + + if (!firstName) { + return fail(400, { error: 'First name is required' }); + } + + if (!lastName) { + return fail(400, { error: 'Last name is required' }); + } + + // Validate phone number if provided + let formattedPhone = null; + const phone = formData.get('phone')?.toString(); + if (phone && phone.trim().length > 0) { + const phoneValidation = validatePhoneNumber(phone.trim()); + if (!phoneValidation.isValid) { + return fail(400, { error: phoneValidation.error || 'Please enter a valid phone number' }); + } + formattedPhone = formatPhoneForStorage(phone.trim()); + } + + // Build Django data object + const djangoData = { + // Core Contact Information + first_name: firstName, + last_name: lastName, + email: formData.get('email')?.toString().trim() || null, + phone: formattedPhone, + + // Professional Information + organization: formData.get('organization')?.toString().trim() || null, + title: formData.get('title')?.toString().trim() || null, + department: formData.get('department')?.toString().trim() || null, + + // Communication Preferences + do_not_call: formData.get('do_not_call') === 'on', + linked_in_url: formData.get('linked_in_url')?.toString().trim() || null, + + // Address + address_line: formData.get('address_line')?.toString().trim() || null, + city: formData.get('city')?.toString().trim() || null, + state: formData.get('state')?.toString().trim() || null, + postcode: formData.get('postcode')?.toString().trim() || null, + country: formData.get('country')?.toString() || null, + + // Assignment + assigned_to: formData.getAll('assigned_to').map((id) => id.toString()), + teams: formData.getAll('teams').map((id) => id.toString()), + + // Notes + description: formData.get('description')?.toString().trim() || null + }; + + try { + const response = await apiRequest( + '/contacts/', + { + method: 'POST', + body: djangoData + }, + { cookies, org } + ); + + return { + status: 'success', + message: 'Contact created successfully', + contact: { + id: response.id || response.contact_obj?.id, + name: `${firstName} ${lastName}` + } + }; + } catch (err) { + console.error('Error creating contact:', err); + return fail(500, { + error: 'Failed to create contact: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + } +}; diff --git a/frontend/src/routes/(app)/contacts/new/+page.svelte b/frontend/src/routes/(app)/contacts/new/+page.svelte new file mode 100644 index 00000000..8759dc72 --- /dev/null +++ b/frontend/src/routes/(app)/contacts/new/+page.svelte @@ -0,0 +1,567 @@ + + + + New Contact - CRM + + +
+ +
+
+
+
+ + + Back to Contacts + +
+

New Contact

+

Add a new contact to your CRM

+
+
+
+
+
+ + +
+ + {#if form?.error} +
+

{form.error}

+
+ {/if} + + + {#if form?.status === 'success'} +
+

{form.message}

+
+ {/if} + +
+ +
+
+
+ +

Contact Information

+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + {#if phoneError} +

{phoneError}

+ {/if} +
+
+ + +
+ + +
+
+
+ + +
+
+
+ +

+ Professional Information +

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

Assignment

+
+
+
+
+
+ + +

+ Hold Ctrl/Cmd to select multiple +

+
+
+ + +

+ Hold Ctrl/Cmd to select multiple +

+
+
+
+
+ + +
+
+
+ +

Address

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

Notes

+
+
+
+
+ + +
+
+
+ + +
+ + Cancel + + +
+
+
+
diff --git a/frontend/src/routes/(app)/invoices/+page.server.js b/frontend/src/routes/(app)/invoices/+page.server.js new file mode 100644 index 00000000..5d21d142 --- /dev/null +++ b/frontend/src/routes/(app)/invoices/+page.server.js @@ -0,0 +1,138 @@ +/** + * Invoices List Page - API Version + * + * This is the migrated version that uses Django REST API instead of Prisma. + * Once tested and working, activate by: + * 1. mv +page.server.js +page.server.prisma.js + * 2. mv +page.server.api.js +page.server.js + * + * Migration completed: 2025-11-19 + * Django endpoint: GET /api/invoices/ + * + * NOTE: Django's Invoice model with InvoiceLineItem maps to SvelteKit's Quote with QuoteLineItem + */ + +import { error, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + if (!locals.user || !locals.org) { + throw redirect(302, '/login'); + } + + try { + // Get invoices from Django API + // Django returns: { results: [...], count: N } + const response = await apiRequest('/invoices/', {}, { cookies, org: locals.org }); + + // Transform Django Invoice to match SvelteKit Quote structure + const invoices = (response.results || []).map((invoice) => transformInvoiceToQuote(invoice)); + + return { + invoices + }; + } catch (err) { + console.error('Failed to load invoices:', err); + throw error(500, `Failed to load invoices: ${err.message}`); + } +} + +/** + * Transform Django Invoice to SvelteKit Quote format + * + * Django structure: + * - invoice_title, invoice_number, status, due_date, total_amount + * - line_items: [{ product, quantity, unit_price, total }] + * - accounts: [{ id, name }] + * + * SvelteKit structure: + * - name (from invoice_title), quoteNumber, status, expirationDate (from due_date), grandTotal + * - lineItems: [{ product, quantity, unitPrice, totalPrice }] + * - account: { id, name } + * + * @param {Object} invoice - Django invoice object + * @returns {Object} SvelteKit-compatible quote object + */ +function transformInvoiceToQuote(invoice) { + return { + id: invoice.id, + quoteNumber: invoice.invoice_number || `INV-${invoice.id.substring(0, 8)}`, + name: invoice.invoice_title || invoice.name || 'Untitled Invoice', + status: mapInvoiceStatusToQuoteStatus(invoice.status), + description: invoice.details || null, + expirationDate: invoice.due_date ? new Date(invoice.due_date) : null, + + // Amounts - Django uses total_amount, amount_due, amount_paid + // SvelteKit uses subtotal, discountAmount, taxAmount, grandTotal + subtotal: invoice.total_amount || '0.00', + discountAmount: '0.00', // Not in Django Invoice model + taxAmount: invoice.tax || '0.00', + grandTotal: invoice.total_amount || '0.00', + + // Billing address - Django has Address model, SvelteKit has inline fields + billingStreet: invoice.from_address?.street || null, + billingCity: invoice.from_address?.city || null, + billingState: invoice.from_address?.state || null, + billingPostalCode: invoice.from_address?.postcode || null, + billingCountry: invoice.from_address?.country || null, + + // Shipping address + shippingStreet: invoice.to_address?.street || null, + shippingCity: invoice.to_address?.city || null, + shippingState: invoice.to_address?.state || null, + shippingPostalCode: invoice.to_address?.postcode || null, + shippingCountry: invoice.to_address?.country || null, + + // Timestamps + createdAt: new Date(invoice.created_at), + updatedAt: new Date(invoice.created_at), // Django doesn't have updated_at on Invoice + + // Relationships + account: invoice.accounts?.[0] + ? { + id: invoice.accounts[0].id, + name: invoice.accounts[0].name + } + : null, + + // Line items transformation + lineItems: + invoice.line_items?.map((item) => ({ + id: item.id, + quantity: parseFloat(item.quantity) || 1, + listPrice: item.unit_price || '0.00', + unitPrice: item.unit_price || '0.00', + discount: '0.00', // Not in Django InvoiceLineItem + totalPrice: item.total || '0.00', + description: item.description || null, + product: item.product + ? { + id: item.product.id, + name: item.product_name || item.product.name + } + : null + })) || [] + }; +} + +/** + * Map Django Invoice status to SvelteKit Quote status + * + * Django: Draft, Sent, Paid, Pending, Cancelled + * SvelteKit: DRAFT, NEEDS_REVIEW, IN_REVIEW, APPROVED, REJECTED, PRESENTED, ACCEPTED + * + * @param {string} djangoStatus - Django invoice status + * @returns {string} SvelteKit quote status + */ +function mapInvoiceStatusToQuoteStatus(djangoStatus) { + const statusMap = { + Draft: 'DRAFT', + Sent: 'PRESENTED', + Paid: 'ACCEPTED', + Pending: 'IN_REVIEW', + Cancelled: 'REJECTED' + }; + + return statusMap[djangoStatus] || 'DRAFT'; +} diff --git a/frontend/src/routes/(app)/invoices/+page.svelte b/frontend/src/routes/(app)/invoices/+page.svelte new file mode 100644 index 00000000..856946d7 --- /dev/null +++ b/frontend/src/routes/(app)/invoices/+page.svelte @@ -0,0 +1,190 @@ + + + +
+
+
+

Invoices

+ + New Invoice +
+ + +
+ +
+ + + + + + +
+ + +
+ + + + + + +
+ + +
+ + + + + + +
+
+ + +
+ {#each data.invoices as invoice} +
+
+
+ {invoice.status.toLowerCase()} + Due: {invoice.expirationDate + ? new Date(invoice.expirationDate).toLocaleDateString() + : 'N/A'} +
+
+

{invoice.quoteNumber}

+

{invoice.account.name}

+
+ {#each invoice.lineItems as item} +
+ {item.description || item.product?.name} + {formatCurrency(Number(item.totalPrice))} +
+ {/each} +
+
+
+
+
+ {formatCurrency(Number(invoice.grandTotal))} +
+
+ View + Edit +
+
+ +
+
+ {/each} + + + {#if data.invoices.length === 0} +
+
📄
+

No invoices yet

+

Create your first invoice to get started

+ + Create Invoice + +
+ {/if} +
+
+
diff --git a/frontend/src/routes/(app)/invoices/[invoiceId]/+page.server.js b/frontend/src/routes/(app)/invoices/[invoiceId]/+page.server.js new file mode 100644 index 00000000..612ba168 --- /dev/null +++ b/frontend/src/routes/(app)/invoices/[invoiceId]/+page.server.js @@ -0,0 +1,153 @@ +/** + * Invoice Detail Page - API Version + * + * This is the migrated version that uses Django REST API instead of Prisma. + * Once tested and working, activate by: + * 1. mv +page.server.js +page.server.prisma.js + * 2. mv +page.server.api.js +page.server.js + * + * Migration completed: 2025-11-19 + * Django endpoint: GET /api/invoices/{id}/ + * + * NOTE: Django's Invoice model with InvoiceLineItem maps to SvelteKit's Quote with QuoteLineItem + */ + +import { error, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + if (!locals.user || !locals.org) { + throw redirect(302, '/login'); + } + + try { + // Get invoice detail from Django API + const invoice = await apiRequest( + `/invoices/${params.invoiceId}/`, + {}, + { cookies, org: locals.org } + ); + + if (!invoice) { + throw error(404, 'Invoice not found'); + } + + // Transform to SvelteKit Quote format + const transformedInvoice = transformInvoiceToQuote(invoice); + + return { + invoice: transformedInvoice + }; + } catch (err) { + if (err.message?.includes('404') || err.message?.includes('not found')) { + throw error(404, 'Invoice not found'); + } + console.error('Failed to load invoice:', err); + throw error(500, `Failed to load invoice: ${err.message}`); + } +} + +/** + * Transform Django Invoice to SvelteKit Quote format + * + * @param {Object} invoice - Django invoice object + * @returns {Object} SvelteKit-compatible quote object + */ +function transformInvoiceToQuote(invoice) { + return { + id: invoice.id, + quoteNumber: invoice.invoice_number || `INV-${invoice.id.substring(0, 8)}`, + name: invoice.invoice_title || invoice.name || 'Untitled Invoice', + status: mapInvoiceStatusToQuoteStatus(invoice.status), + description: invoice.details || null, + expirationDate: invoice.due_date ? new Date(invoice.due_date) : null, + + // Amounts + subtotal: invoice.total_amount || '0.00', + discountAmount: '0.00', + taxAmount: invoice.tax || '0.00', + grandTotal: invoice.total_amount || '0.00', + + // Billing address - Django has Address model + billingStreet: invoice.from_address?.street || null, + billingCity: invoice.from_address?.city || null, + billingState: invoice.from_address?.state || null, + billingPostalCode: invoice.from_address?.postcode || null, + billingCountry: invoice.from_address?.country || null, + + // Shipping address + shippingStreet: invoice.to_address?.street || null, + shippingCity: invoice.to_address?.city || null, + shippingState: invoice.to_address?.state || null, + shippingPostalCode: invoice.to_address?.postcode || null, + shippingCountry: invoice.to_address?.country || null, + + // Timestamps + createdAt: new Date(invoice.created_at), + updatedAt: new Date(invoice.created_at), + + // Account relationship + account: invoice.accounts?.[0] + ? { + id: invoice.accounts[0].id, + name: invoice.accounts[0].name, + // Address fields might be in the Account object + street: invoice.accounts[0].billing_address?.street || null, + city: invoice.accounts[0].billing_address?.city || null, + state: invoice.accounts[0].billing_address?.state || null, + postalCode: invoice.accounts[0].billing_address?.postcode || null, + country: invoice.accounts[0].billing_address?.country || null + } + : null, + + // Contact - Django doesn't have direct contact on Invoice + // It's linked through Account + contact: null, // TODO: May need to fetch separately if needed + + // Line items + lineItems: + invoice.line_items?.map((item) => ({ + id: item.id, + quantity: parseFloat(item.quantity) || 1, + listPrice: item.unit_price || '0.00', + unitPrice: item.unit_price || '0.00', + discount: '0.00', + totalPrice: item.total || '0.00', + description: item.description || null, + product: item.product + ? { + id: item.product.id, + name: item.product_name || item.product.name, + code: item.product.sku || null + } + : null + })) || [], + + // Prepared by - Django has created_by + preparedBy: invoice.created_by + ? { + name: invoice.created_by.username || invoice.created_by.email, + email: invoice.created_by.email + } + : null + }; +} + +/** + * Map Django Invoice status to SvelteKit Quote status + * + * @param {string} djangoStatus - Django invoice status + * @returns {string} SvelteKit quote status + */ +function mapInvoiceStatusToQuoteStatus(djangoStatus) { + const statusMap = { + Draft: 'DRAFT', + Sent: 'PRESENTED', + Paid: 'ACCEPTED', + Pending: 'IN_REVIEW', + Cancelled: 'REJECTED' + }; + + return statusMap[djangoStatus] || 'DRAFT'; +} diff --git a/frontend/src/routes/(app)/invoices/[invoiceId]/+page.svelte b/frontend/src/routes/(app)/invoices/[invoiceId]/+page.svelte new file mode 100644 index 00000000..d56c398b --- /dev/null +++ b/frontend/src/routes/(app)/invoices/[invoiceId]/+page.svelte @@ -0,0 +1,185 @@ + + + +
+
+
+
+

Invoice

+
{invoice.quoteNumber}
+
+
+
Prepared by:
+
{invoice.preparedBy.name}
+
{invoice.preparedBy.email}
+
+
+ +
+
+
From:
+
{invoice.account.name}
+
+ {#if invoice.account.street} + {invoice.account.street}
+ {/if} + {#if invoice.account.city} + {invoice.account.city}{#if invoice.account.state}, {invoice.account.state}{/if} + {invoice.account.postalCode}
+ {/if} + {#if invoice.account.country} + {invoice.account.country} + {/if} +
+
+
+
To:
+
+ {#if invoice.contact} + {invoice.contact.firstName} {invoice.contact.lastName} + {:else} + {invoice.account.name} + {/if} +
+
+ {#if invoice.contact && invoice.contact.email} + {invoice.contact.email} + {/if} +
+
+
+
+ Status: + + {invoice.status.toLowerCase()} + +
+
+ Created:{new Date(invoice.createdAt).toLocaleDateString()} +
+
+ Due Date:{invoice.expirationDate + ? new Date(invoice.expirationDate).toLocaleDateString() + : 'N/A'} +
+
+
+ +
+ + + + + + + + + + + {#each invoice.lineItems as item} + + + + + + + {/each} + + + + + + + + + + + +
DescriptionQuantityRateTotal
{item.description || item.product?.name || 'N/A'}{item.quantity}{formatCurrency(Number(item.unitPrice))}{formatCurrency(Number(item.totalPrice))}
Subtotal:{formatCurrency(Number(invoice.subtotal))}
Total:{formatCurrency(Number(invoice.grandTotal))}
+
+ + {#if invoice.description} +
+
Notes:
+
+ {invoice.description} +
+
+ {/if} + + + + +
+
+
+
diff --git a/frontend/src/routes/(app)/invoices/[invoiceId]/edit/+page.server.js b/frontend/src/routes/(app)/invoices/[invoiceId]/edit/+page.server.js new file mode 100644 index 00000000..f0a155e3 --- /dev/null +++ b/frontend/src/routes/(app)/invoices/[invoiceId]/edit/+page.server.js @@ -0,0 +1,220 @@ +/** + * Edit Invoice Page - API Version + * + * This is the migrated version that uses Django REST API instead of Prisma. + * Once tested and working, activate by: + * 1. mv +page.server.js +page.server.prisma.js + * 2. mv +page.server.api.js +page.server.js + * + * Migration completed: 2025-11-19 + * Django endpoint: PUT /api/invoices/{id}/ + * + * NOTE: Django's Invoice model with InvoiceLineItem maps to SvelteKit's Quote with QuoteLineItem + */ + +import { error, fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + if (!locals.user || !locals.org) { + throw redirect(302, '/login'); + } + + try { + // Get invoice and accounts in parallel + const [invoice, accountsResponse] = await Promise.all([ + apiRequest(`/invoices/${params.invoiceId}/`, {}, { cookies, org: locals.org }), + apiRequest('/accounts/?status=open&limit=1000', {}, { cookies, org: locals.org }) + ]); + + if (!invoice) { + throw error(404, 'Invoice not found'); + } + + // Transform invoice to Quote format + const transformedInvoice = transformInvoiceToQuote(invoice); + + // Extract accounts from Django response + let accounts = []; + if (accountsResponse.active_accounts?.open_accounts) { + accounts = accountsResponse.active_accounts.open_accounts; + } else if (accountsResponse.results) { + accounts = accountsResponse.results.filter((acc) => !acc.status || acc.status === 'open'); + } + + const transformedAccounts = accounts.map((acc) => ({ + id: acc.id, + name: acc.name + })); + + return { + invoice: transformedInvoice, + accounts: transformedAccounts + }; + } catch (err) { + if (err.message?.includes('404') || err.message?.includes('not found')) { + throw error(404, 'Invoice not found'); + } + console.error('Failed to load invoice:', err); + throw error(500, `Failed to load invoice: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, params, locals, cookies }) => { + if (!locals.user || !locals.org) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + + const accountId = String(formData.get('account_id') || ''); + const invoiceDate = String(formData.get('invoice_date') || ''); + const dueDate = String(formData.get('due_date') || ''); + const status = String(formData.get('status') || 'DRAFT'); + const notes = String(formData.get('notes') || ''); + + // Validation + if (!accountId || !invoiceDate || !dueDate) { + return fail(400, { + error: 'Account, invoice date, and due date are required' + }); + } + + try { + // First, verify the invoice exists + const existingInvoice = await apiRequest( + `/invoices/${params.invoiceId}/`, + {}, + { cookies, org: locals.org } + ); + + if (!existingInvoice) { + return fail(404, { error: 'Invoice not found' }); + } + + // Transform status + const djangoStatus = mapQuoteStatusToInvoiceStatus(status); + + // Update invoice via Django API + const updateData = { + accounts: [accountId], + status: djangoStatus, + details: notes, + due_date: dueDate + }; + + await apiRequest( + `/invoices/${params.invoiceId}/`, + { + method: 'PUT', + body: updateData + }, + { cookies, org: locals.org } + ); + + // Redirect to invoice detail page + throw redirect(303, `/invoices/${params.invoiceId}`); + } catch (err) { + if (err instanceof Response) throw err; // Re-throw redirects + + console.error('Error updating invoice:', err); + + if (err.message.includes('404') || err.message.includes('not found')) { + return fail(404, { error: 'Invoice not found' }); + } + + return fail(500, { + error: 'Failed to update invoice. Please try again.' + }); + } + } +}; + +/** + * Transform Django Invoice to SvelteKit Quote format + * + * @param {Object} invoice - Django invoice object + * @returns {Object} SvelteKit-compatible quote object + */ +function transformInvoiceToQuote(invoice) { + return { + id: invoice.id, + quoteNumber: invoice.invoice_number || `INV-${invoice.id.substring(0, 8)}`, + name: invoice.invoice_title || invoice.name || 'Untitled Invoice', + status: mapInvoiceStatusToQuoteStatus(invoice.status), + description: invoice.details || null, + expirationDate: invoice.due_date ? new Date(invoice.due_date) : null, + + // Account relationship + account: invoice.accounts?.[0] + ? { + id: invoice.accounts[0].id, + name: invoice.accounts[0].name + } + : null, + + // Line items + lineItems: + invoice.line_items?.map((item) => ({ + id: item.id, + quantity: parseFloat(item.quantity) || 1, + listPrice: item.unit_price || '0.00', + unitPrice: item.unit_price || '0.00', + discount: '0.00', + totalPrice: item.total || '0.00', + description: item.description || null, + product: item.product + ? { + id: item.product.id, + name: item.product_name || item.product.name, + code: item.product.sku || null + } + : null + })) || [], + + // Timestamps + createdAt: new Date(invoice.created_at), + updatedAt: new Date(invoice.created_at) + }; +} + +/** + * Map SvelteKit Quote status to Django Invoice status + * + * @param {string} quoteStatus - SvelteKit quote status + * @returns {string} Django invoice status + */ +function mapQuoteStatusToInvoiceStatus(quoteStatus) { + const statusMap = { + DRAFT: 'Draft', + NEEDS_REVIEW: 'Pending', + IN_REVIEW: 'Pending', + APPROVED: 'Sent', + REJECTED: 'Cancelled', + PRESENTED: 'Sent', + ACCEPTED: 'Paid' + }; + + return statusMap[quoteStatus] || 'Draft'; +} + +/** + * Map Django Invoice status to SvelteKit Quote status + * + * @param {string} djangoStatus - Django invoice status + * @returns {string} SvelteKit quote status + */ +function mapInvoiceStatusToQuoteStatus(djangoStatus) { + const statusMap = { + Draft: 'DRAFT', + Sent: 'PRESENTED', + Paid: 'ACCEPTED', + Pending: 'IN_REVIEW', + Cancelled: 'REJECTED' + }; + + return statusMap[djangoStatus] || 'DRAFT'; +} diff --git a/opportunity/migrations/__init__.py b/frontend/src/routes/(app)/invoices/[invoiceId]/edit/+page.svelte similarity index 100% rename from opportunity/migrations/__init__.py rename to frontend/src/routes/(app)/invoices/[invoiceId]/edit/+page.svelte diff --git a/frontend/src/routes/(app)/invoices/new/+page.server.js b/frontend/src/routes/(app)/invoices/new/+page.server.js new file mode 100644 index 00000000..95f6df38 --- /dev/null +++ b/frontend/src/routes/(app)/invoices/new/+page.server.js @@ -0,0 +1,147 @@ +/** + * Create Invoice Page - API Version + * + * This is the migrated version that uses Django REST API instead of Prisma. + * Once tested and working, activate by: + * 1. mv +page.server.js +page.server.prisma.js + * 2. mv +page.server.api.js +page.server.js + * + * Migration completed: 2025-11-19 + * Django endpoint: POST /api/invoices/ + * + * NOTE: Django's Invoice model with InvoiceLineItem maps to SvelteKit's Quote with QuoteLineItem + */ + +import { fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + if (!locals.user || !locals.org) { + throw redirect(302, '/login'); + } + + try { + // Get accounts for the dropdown + const response = await apiRequest( + '/accounts/?status=open&limit=1000', + {}, + { cookies, org: locals.org } + ); + + // Extract accounts from Django response + let accounts = []; + if (response.active_accounts?.open_accounts) { + accounts = response.active_accounts.open_accounts; + } else if (response.results) { + accounts = response.results.filter((acc) => !acc.status || acc.status === 'open'); + } + + // Transform to match SvelteKit format + const transformedAccounts = accounts.map((acc) => ({ + id: acc.id, + name: acc.name + })); + + return { + accounts: transformedAccounts + }; + } catch (err) { + console.error('Failed to load accounts:', err); + return { + accounts: [] + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals, cookies }) => { + if (!locals.user || !locals.org) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + + const invoiceNumber = String(formData.get('invoice_number') || ''); + const accountId = String(formData.get('account_id') || ''); + const invoiceDate = String(formData.get('invoice_date') || ''); + const dueDate = String(formData.get('due_date') || ''); + const status = String(formData.get('status') || 'DRAFT'); + const notes = String(formData.get('notes') || ''); + + // Validation + if (!invoiceNumber || !accountId || !invoiceDate || !dueDate) { + return fail(400, { + error: 'Invoice number, account, invoice date, and due date are required' + }); + } + + try { + // Create invoice via Django API + // Transform SvelteKit Quote status to Django Invoice status + const djangoStatus = mapQuoteStatusToInvoiceStatus(status); + + const invoiceData = { + invoice_title: `Invoice ${invoiceNumber}`, + invoice_number: invoiceNumber, + status: djangoStatus, + due_date: dueDate, + details: notes, + accounts: [accountId], // Django expects array of account IDs + currency: 'USD', + total_amount: '0.00', // Will be updated when line items are added + amount_due: '0.00', + amount_paid: '0.00' + }; + + const invoice = await apiRequest( + '/invoices/', + { + method: 'POST', + body: invoiceData + }, + { cookies, org: locals.org } + ); + + // Redirect to invoice detail page + throw redirect(303, `/invoices/${invoice.id}`); + } catch (error) { + console.error('Error creating invoice:', error); + + // Check for validation errors + if (error.message.includes('already exists')) { + return fail(400, { + error: 'An invoice with this number already exists' + }); + } + + return fail(500, { + error: 'Failed to create invoice. Please try again.' + }); + } + } +}; + +/** + * Map SvelteKit Quote status to Django Invoice status + * + * SvelteKit: DRAFT, NEEDS_REVIEW, IN_REVIEW, APPROVED, REJECTED, PRESENTED, ACCEPTED + * Django: Draft, Sent, Paid, Pending, Cancelled + * + * @param {string} quoteStatus - SvelteKit quote status + * @returns {string} Django invoice status + */ +function mapQuoteStatusToInvoiceStatus(quoteStatus) { + const statusMap = { + DRAFT: 'Draft', + NEEDS_REVIEW: 'Pending', + IN_REVIEW: 'Pending', + APPROVED: 'Sent', + REJECTED: 'Cancelled', + PRESENTED: 'Sent', + ACCEPTED: 'Paid' + }; + + return statusMap[quoteStatus] || 'Draft'; +} diff --git a/frontend/src/routes/(app)/invoices/new/+page.svelte b/frontend/src/routes/(app)/invoices/new/+page.svelte new file mode 100644 index 00000000..938d8a98 --- /dev/null +++ b/frontend/src/routes/(app)/invoices/new/+page.svelte @@ -0,0 +1,292 @@ + + + +
+
+
+
+

New Invoice

+

Create a professional invoice for your client

+
+
+ DRAFT +
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

Line Items

+ +
+ +
+ + + + + + + + + + + + {#each lineItems as item, index (index)} + + + + + + + + {/each} + + + + + + + + + + + + + +
DescriptionQuantityRateTotal
+ { + const target = e.target; + if (target instanceof HTMLInputElement) { + updateLineItem(index, 'description', target.value); + } + }} + class="w-full rounded border-none bg-transparent px-2 py-1 focus:ring-2 focus:ring-blue-500" + placeholder="Description" + /> + + { + const target = e.target; + if (target instanceof HTMLInputElement) { + updateLineItem(index, 'quantity', Number(target.value)); + } + }} + class="w-20 rounded border-none bg-transparent px-2 py-1 text-right focus:ring-2 focus:ring-blue-500" + min="1" + /> + + { + const target = e.target; + if (target instanceof HTMLInputElement) { + updateLineItem(index, 'rate', Number(target.value)); + } + }} + class="w-24 rounded border-none bg-transparent px-2 py-1 text-right focus:ring-2 focus:ring-blue-500" + min="0" + step="0.01" + /> + + ${item.total.toFixed(2)} + + +
Subtotal:${subtotal.toFixed(2)}
Total:${grandTotal.toFixed(2)}
+
+
+ + +
+ + +
+ + +
+ + +
+
+
+
diff --git a/frontend/src/routes/(app)/leads/[lead_id]/+page.server.js b/frontend/src/routes/(app)/leads/[lead_id]/+page.server.js new file mode 100644 index 00000000..32302419 --- /dev/null +++ b/frontend/src/routes/(app)/leads/[lead_id]/+page.server.js @@ -0,0 +1,233 @@ +/** + * Lead Detail Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/leads/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + * + * Note: Lead conversion is complex in Django and may require custom endpoint + */ + +import { error, fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const lead_id = params.lead_id; + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch lead details from Django + const response = await apiRequest(`/leads/${lead_id}/`, {}, { cookies, org }); + + if (!response.lead_obj) { + throw error(404, 'Lead not found'); + } + + const leadData = response.lead_obj; + + // Transform lead data + const lead = { + id: leadData.id, + firstName: leadData.first_name, + lastName: leadData.last_name, + company: leadData.company?.name || null, + email: leadData.email, + phone: leadData.phone, + title: leadData.title, + industry: leadData.industry, + website: leadData.website, + description: leadData.description, + leadSource: leadData.source, + status: leadData.status, + rating: leadData.rating || null, + isConverted: leadData.is_converted || leadData.status === 'converted', + convertedAt: leadData.converted_at || null, + convertedContactId: leadData.converted_contact_id || null, + convertedAccountId: leadData.converted_account_id || null, + convertedOpportunityId: leadData.converted_opportunity_id || null, + createdAt: leadData.created_at, + updatedAt: leadData.updated_at, + organizationId: leadData.org?.id || org.id, + ownerId: leadData.created_by?.id || null, + + // Owner info + owner: + leadData.assigned_to && leadData.assigned_to.length > 0 + ? { + id: leadData.assigned_to[0].id, + name: leadData.assigned_to[0].user_details?.email || leadData.assigned_to[0].email, + email: leadData.assigned_to[0].user_details?.email || leadData.assigned_to[0].email + } + : leadData.created_by + ? { + id: leadData.created_by.id, + name: leadData.created_by.email, + email: leadData.created_by.email + } + : null, + + // Contact info (if converted) + contact: leadData.contact + ? { + id: leadData.contact.id, + firstName: leadData.contact.first_name, + lastName: leadData.contact.last_name, + email: leadData.contact.email + } + : null + }; + + // Transform tasks + const tasks = (response.tasks || leadData.tasks || []).map((task) => ({ + id: task.id, + title: task.title, + status: task.status, + priority: task.priority, + dueDate: task.due_date, + createdAt: task.created_at + })); + + // Transform events (Django may return these separately) + const events = (response.events || leadData.events || []).map((event) => ({ + id: event.id, + name: event.name, + startDate: event.start_date, + startTime: event.start_time, + endDate: event.end_date, + status: event.status + })); + + // Transform comments + const comments = (response.comments || []).map((comment) => ({ + id: comment.id, + body: comment.comment, + createdAt: comment.created_at, + author: comment.commented_by + ? { + id: comment.commented_by.id, + name: comment.commented_by.user_details?.email || comment.commented_by.email, + email: comment.commented_by.user_details?.email || comment.commented_by.email + } + : null + })); + + return { + lead: { + ...lead, + tasks, + events, + comments + } + }; + } catch (err) { + console.error('Error loading lead data:', err); + throw error(500, `Failed to load lead: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + convert: async ({ params, locals, cookies }) => { + const lead_id = params.lead_id; + const org = locals.org; + + try { + console.log('Starting lead conversion for lead:', lead_id); + + const response = await apiRequest( + `/leads/${lead_id}/`, + { + method: 'PATCH', + body: { + status: 'converted', + is_converted: true + } + }, + { cookies, org } + ); + + // Return success with account ID for redirect + return { + status: 'success', + message: response.message || 'Lead converted successfully', + accountId: response.account_id, + contactId: response.contact_id, + redirectTo: response.account_id ? `/accounts/${response.account_id}` : null + }; + } catch (err) { + console.error('Error converting lead:', err); + + let errorMessage = 'Failed to convert lead'; + if (err instanceof Error) { + errorMessage = err.message; + } + + return fail(500, { + status: 'error', + message: `Error converting lead: ${errorMessage}` + }); + } + }, + + addComment: async ({ params, request, locals, cookies }) => { + const lead_id = params.lead_id; + const org = locals.org; + + try { + const data = await request.formData(); + const comment = data.get('comment')?.toString().trim(); + + if (!comment || comment.length === 0) { + return fail(400, { status: 'error', message: 'Comment cannot be empty' }); + } + + if (comment.length > 1000) { + return fail(400, { status: 'error', message: 'Comment too long (max 1000 characters)' }); + } + + // Add comment via API + await apiRequest( + `/leads/comment/${lead_id}/`, + { + method: 'POST', + body: { comment } + }, + { cookies, org } + ); + + // Fetch updated comments + const response = await apiRequest(`/leads/${lead_id}/`, {}, { cookies, org }); + + const comments = (response.comments || []).map((comment) => ({ + id: comment.id, + body: comment.comment, + createdAt: comment.created_at, + author: comment.commented_by + ? { + id: comment.commented_by.id, + name: comment.commented_by.user_details?.email || comment.commented_by.email, + email: comment.commented_by.user_details?.email || comment.commented_by.email + } + : null + })); + + return { + status: 'success', + message: 'Comment added successfully', + commentAdded: true, + comments + }; + } catch (err) { + console.error('Error adding comment:', err); + return fail(500, { status: 'error', message: 'Failed to add comment' }); + } + } +}; diff --git a/frontend/src/routes/(app)/leads/[lead_id]/+page.svelte b/frontend/src/routes/(app)/leads/[lead_id]/+page.svelte new file mode 100644 index 00000000..10592698 --- /dev/null +++ b/frontend/src/routes/(app)/leads/[lead_id]/+page.svelte @@ -0,0 +1,862 @@ + + + +{#if showConfirmModal} +
+
+
+
+ +
+
+

Convert Lead

+

This action cannot be undone

+
+
+ +

+ Are you sure you want to convert {getFullName(lead)} into an account and contact? + This will create new records and mark the lead as converted. +

+ +
+ + +
+
+
+{/if} + + +{#if showToast} +
+
+
+ {#if toastType === 'success'} +
+ +
+ {:else} +
+ +
+ {/if} +
+

{toastMessage}

+ +
+
+{/if} + +
+
+ + + +
+ +
+ +
+
+
+
+
+ {getInitials(lead)} +
+
+
+

+ {getFullName(lead)} +

+
+ + {lead.status} + + {#if lead.company} +
+ + {lead.company} +
+ {/if} + {#if lead.title} +
+ + {lead.title} +
+ {/if} +
+ + +
+ {#if lead.email} +
+
+ +
+
+

+ Email +

+ + {lead.email} + +
+ +
+ {/if} + + {#if lead.phone} +
+
+ +
+
+

+ Phone +

+ + {lead.phone} + +
+ +
+ {/if} +
+ + +
+ {#if lead.email} + + + Send Email + + {/if} + {#if lead.phone} + + + Call + + {/if} +
+
+ + +
+ {#if lead.status !== 'CONVERTED'} +
+ +
+ + {/if} + + + Edit Lead + +
+
+
+
+ + +
+
+

+
+ +
+ Lead Information +

+ +
+ + {#if lead.leadSource} +
+
+ + Lead Source +
+

+ {getLeadSourceDisplay(lead.leadSource)} +

+
+ {/if} + + + {#if lead.industry} +
+
+ + Industry +
+

+ {lead.industry} +

+
+ {/if} + + + {#if lead.rating} +
+
+ + Rating +
+
+ {#each Array(parseInt(lead.rating) || 0) as _, i} + + {/each} + {#each Array(5 - (parseInt(lead.rating) || 0)) as _, i} + + {/each} + {lead.rating}/5 +
+
+ {/if} + + +
+
+ + Lead Owner +
+

+ {lead.owner?.name || 'Unassigned'} +

+
+ + +
+
+ + Created +
+

+ {formatDateShort(lead.createdAt)} +

+
+
+ + + {#if lead.description} +
+
+ + Description +
+
+
+ {@html lead.description} +
+
+
+ {/if} + + + {#if lead.isConverted} +
+
+
+
+ +
+

+ Lead Converted +

+
+ {#if lead.convertedAt} +

+ Converted on {formatDate(lead.convertedAt)} +

+ {/if} +
+
+ {/if} +
+
+
+ + +
+ +
+
+

+
+ +
+ Activity & Notes +

+
+ +
+ +
+
+ +
+ +
+
+
+ + +
+ {#if lead.comments && lead.comments.length > 0} + {#each lead.comments as comment, i (comment.id || i)} +
+
+
+ +
+
+
+
+

+ {comment.author?.name || 'Unknown User'} +

+

+ {formatDate(comment.createdAt)} +

+
+

+ {comment.body} +

+
+
+ {/each} + {:else} +
+
+ +
+

+ No activity yet +

+

+ Be the first to add a note or log an interaction. +

+
+ {/if} +
+
+
+ + + {#if lead.convertedContactId && lead.contact} +
+
+

+
+ +
+ Related Contact +

+
+ +
+
+
+
+ +
+
+

+ {lead.contact.firstName} + {lead.contact.lastName} +

+

Contact

+
+
+ + {#if lead.contact.email} +
+ + {lead.contact.email} +
+ {/if} + + {#if lead.contact.phone} +
+ + {lead.contact.phone} +
+ {/if} + + + + View Contact + +
+
+
+ {/if} + + +
+
+

+
+ +
+ Quick Stats +

+
+ +
+
+ Comments + {lead.comments?.length || 0} +
+
+ Days Since Created + + {Math.floor( + (new Date().getTime() - new Date(lead.createdAt).getTime()) / + (1000 * 60 * 60 * 24) + )} + +
+ {#if lead.convertedAt} +
+ Days to Convert + + {Math.floor( + (new Date(lead.convertedAt).getTime() - new Date(lead.createdAt).getTime()) / + (1000 * 60 * 60 * 24) + )} + +
+ {/if} +
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/leads/[lead_id]/edit/+page.server.js b/frontend/src/routes/(app)/leads/[lead_id]/edit/+page.server.js new file mode 100644 index 00000000..97f32e75 --- /dev/null +++ b/frontend/src/routes/(app)/leads/[lead_id]/edit/+page.server.js @@ -0,0 +1,220 @@ +/** + * Lead Edit Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: PUT/PATCH /api/leads/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch lead and users in parallel + const [leadResponse, usersResponse] = await Promise.all([ + apiRequest(`/leads/${params.lead_id}/`, {}, { cookies, org }), + apiRequest('/users/', {}, { cookies, org }) + ]); + + if (!leadResponse.lead_obj) { + throw error(404, 'Lead not found'); + } + + const leadData = leadResponse.lead_obj; + + // Transform lead to form structure + // Note: assigned_to is an array of ProfileSerializer objects + const assignedTo = leadData.assigned_to || []; + const firstAssigned = assignedTo[0]; + + const lead = { + id: leadData.id, + firstName: leadData.first_name || '', + lastName: leadData.last_name || '', + email: leadData.email, + phone: leadData.phone, + company: leadData.company?.name || null, + title: leadData.title, + industry: leadData.industry, + rating: leadData.rating || null, + description: leadData.description, + status: leadData.status || 'assigned', // Django default status + leadSource: leadData.source || null, + ownerId: firstAssigned?.id || null, // Profile ID + // Include owner info if available + owner: firstAssigned + ? { + id: firstAssigned.id, + name: firstAssigned.user_details?.email || 'Unknown', + email: firstAssigned.user_details?.email + } + : null + }; + + // Transform users + // Django UsersListView returns { active_users: { active_users: [...] }, ... } + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((user) => ({ + id: user.id, + user: { + id: user.id, + name: user.user_details?.email || user.email, + email: user.user_details?.email || user.email + } + })); + + return { + lead, + users + }; + } catch (err) { + console.error('Error loading lead for edit:', err); + throw error(500, `Failed to load lead: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, params, locals, cookies }) => { + const org = locals.org; + const formData = await request.formData(); + + // Extract form fields + const firstName = formData.get('firstName')?.toString().trim(); + const lastName = formData.get('lastName')?.toString().trim(); + const email = formData.get('email')?.toString().trim(); + const phone = formData.get('phone')?.toString().trim(); + const company = formData.get('company')?.toString().trim(); + const title = formData.get('title')?.toString().trim(); + const industry = formData.get('industry')?.toString().trim(); + const rating = formData.get('rating')?.toString().trim(); + const description = formData.get('description')?.toString().trim(); + const ownerId = formData.get('ownerId')?.toString(); + const status = formData.get('status')?.toString() || 'assigned'; + const leadSource = formData.get('leadSource')?.toString(); + + // Validation + const errors = {}; + + // Note: Django requires 'title' field, not firstName/lastName + // But we'll keep the UI friendly by using first/last name + if (!firstName || firstName.length === 0) { + errors.firstName = 'First name is required'; + } + + if (!lastName || lastName.length === 0) { + errors.lastName = 'Last name is required'; + } + + if (!ownerId) { + errors.ownerId = 'Owner is required'; + } + + // Title is required by Django - generate from first + last name if not provided + if (!title && firstName && lastName) { + // Will generate title below + } + + // Validate email format if provided + if (email && email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + errors.email = 'Please enter a valid email address'; + } + + // Validate phone number if provided + let formattedPhone = null; + if (phone && phone.length > 0) { + const phoneValidation = validatePhoneNumber(phone); + if (!phoneValidation.isValid) { + errors.phone = phoneValidation.error || 'Please enter a valid phone number'; + } else { + formattedPhone = formatPhoneForStorage(phone); + } + } + + // Status and source validation not needed here since form options + // now match Django LEAD_STATUS and LEAD_SOURCE choices + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + values: { + firstName, + lastName, + email, + phone, + company, + title, + industry, + rating, + description, + ownerId, + status, + leadSource + } + }); + } + + try { + // Transform to Django field names + // Note: Django requires 'title' field - use provided title or generate from name + const leadTitle = title || `${firstName} ${lastName}`.trim(); + + const djangoData = { + first_name: firstName, + last_name: lastName, + email: email || null, + phone: formattedPhone, + title: leadTitle, // Required by Django + industry: industry || null, + rating: rating || null, + description: description || null, + status: status, + source: leadSource || null, + assigned_to: ownerId ? [ownerId] : [] + }; + + // Update lead via API + await apiRequest( + `/leads/${params.lead_id}/`, + { + method: 'PUT', + body: djangoData + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error updating lead:', err); + return fail(500, { + error: 'Failed to update lead: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { + firstName, + lastName, + email, + phone, + company, + title, + industry, + rating, + description, + ownerId, + status, + leadSource + } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/leads/[lead_id]/edit/+page.svelte b/frontend/src/routes/(app)/leads/[lead_id]/edit/+page.svelte new file mode 100644 index 00000000..c61594df --- /dev/null +++ b/frontend/src/routes/(app)/leads/[lead_id]/edit/+page.svelte @@ -0,0 +1,531 @@ + + +
+ +
+
+
+
+ +
+

Edit Lead

+

+ Editing {lead.firstName} + {lead.lastName} +

+
+
+
+
+
+ +
+ + {#if formSubmitted && !errorMessage} +
+
+
+ + + +
+
+

+ Lead updated successfully! +

+
+
+
+ {/if} + + {#if errorMessage} +
+
+ +
+

{errorMessage}

+
+
+
+ {/if} + + +
{ + const isValid = validateForm(formData); + if (!isValid) return; + + isSubmitting = true; + return async ({ result, update }) => { + isSubmitting = false; + formSubmitted = true; + + if (result.type === 'success') { + if (result.data?.success) { + await update(); + setTimeout(() => { + goto(`/leads/${lead.id}`); + }, 1500); + } else if (result.data?.error) { + errorMessage = result.data.error as string; + } + } else { + errorMessage = 'An unexpected error occurred'; + } + }; + }} + class="space-y-8" + > + +
+
+
+ +

+ Personal Information +

+
+
+
+
+
+ + + {#if errors.firstName} +

{errors.firstName}

+ {/if} +
+ +
+ + + {#if errors.lastName} +

{errors.lastName}

+ {/if} +
+ +
+ + + {#if errors.email} +

{errors.email}

+ {/if} +
+ +
+ + +
+
+
+
+ + +
+
+
+ +

Company Information

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

Lead Details

+
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+ + +
+
+
+
diff --git a/frontend/src/routes/(app)/leads/new/+page.server.js b/frontend/src/routes/(app)/leads/new/+page.server.js new file mode 100644 index 00000000..f2bd1560 --- /dev/null +++ b/frontend/src/routes/(app)/leads/new/+page.server.js @@ -0,0 +1,167 @@ +/** + * Lead Create Page - Modern CRM Version + * + * Streamlined lead creation based on Twenty CRM and Salesforce patterns. + * Django endpoint: POST /api/leads/ + */ + +import { fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; +import { industries, leadSources, leadStatuses, countries } from '$lib/data/index.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const user = locals.user; + const org = locals.org; + + try { + // Fetch users and teams for assignment dropdowns + const [usersResponse, teamsResponse] = await Promise.all([ + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/teams/', {}, { cookies, org }) + ]); + + // Extract active users + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((profile) => ({ + id: profile.id, + name: profile.user_details?.email?.split('@')[0] || 'Unknown', + email: profile.user_details?.email || '' + })); + + // Extract teams + const teams = (teamsResponse.teams || teamsResponse.results || []).map((team) => ({ + id: team.id, + name: team.name + })); + + return { + data: { + industries, + statuses: leadStatuses, + sources: leadSources, + countries, + users, + teams + } + }; + } catch (err) { + console.error('Error loading lead form data:', err); + return { + data: { + industries, + statuses: leadStatuses, + sources: leadSources, + countries, + users: [], + teams: [] + } + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals, cookies }) => { + const user = locals.user; + const org = locals.org; + + const formData = await request.formData(); + + // Extract and validate required fields + const title = formData.get('title')?.toString().trim(); + const firstName = formData.get('first_name')?.toString().trim(); + const lastName = formData.get('last_name')?.toString().trim(); + + if (!title) { + return fail(400, { error: 'Lead title is required' }); + } + + if (!firstName) { + return fail(400, { error: 'First name is required' }); + } + + if (!lastName) { + return fail(400, { error: 'Last name is required' }); + } + + // Validate phone number if provided + let formattedPhone = null; + const phone = formData.get('phone')?.toString(); + if (phone && phone.trim().length > 0) { + const phoneValidation = validatePhoneNumber(phone.trim()); + if (!phoneValidation.isValid) { + return fail(400, { error: phoneValidation.error || 'Please enter a valid phone number' }); + } + formattedPhone = formatPhoneForStorage(phone.trim()); + } + + // Build Django data object + const djangoData = { + // Core Lead Information + title, + first_name: firstName, + last_name: lastName, + email: formData.get('email')?.toString().trim() || null, + phone: formattedPhone, + contact_title: formData.get('contact_title')?.toString().trim() || null, + website: formData.get('website')?.toString().trim() || null, + linkedin_url: formData.get('linkedin_url')?.toString().trim() || null, + + // Sales Pipeline + status: formData.get('status')?.toString() || 'assigned', + source: formData.get('source')?.toString() || null, + industry: formData.get('industry')?.toString() || null, + rating: formData.get('rating')?.toString() || null, + opportunity_amount: formData.get('opportunity_amount') + ? parseFloat(formData.get('opportunity_amount')?.toString() || '0') + : null, + probability: formData.get('probability') + ? parseInt(formData.get('probability')?.toString() || '0') + : null, + close_date: formData.get('close_date')?.toString() || null, + + // Address + address_line: formData.get('address_line')?.toString().trim() || null, + city: formData.get('city')?.toString().trim() || null, + state: formData.get('state')?.toString().trim() || null, + postcode: formData.get('postcode')?.toString().trim() || null, + country: formData.get('country')?.toString() || null, + + // Assignment + assigned_to: formData.getAll('assigned_to').map((id) => id.toString()), + teams: formData.getAll('teams').map((id) => id.toString()), + + // Activity + last_contacted: formData.get('last_contacted')?.toString() || null, + next_follow_up: formData.get('next_follow_up')?.toString() || null, + description: formData.get('description')?.toString().trim() || null + }; + + try { + const response = await apiRequest( + '/leads/', + { + method: 'POST', + body: djangoData + }, + { cookies, org } + ); + + return { + status: 'success', + message: 'Lead created successfully', + lead: { + id: response.id || response.lead_obj?.id, + name: `${firstName} ${lastName}` + } + }; + } catch (err) { + console.error('Error creating lead:', err); + return fail(500, { + error: 'Failed to create lead: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + } +}; diff --git a/frontend/src/routes/(app)/leads/new/+page.svelte b/frontend/src/routes/(app)/leads/new/+page.svelte new file mode 100644 index 00000000..939cb822 --- /dev/null +++ b/frontend/src/routes/(app)/leads/new/+page.svelte @@ -0,0 +1,824 @@ + + +
+ {#if showToast} +
+
+
+ {#if toastType === 'success'} + + {:else} + + {/if} +
+
+

+ {toastMessage} +

+
+ +
+
+ {/if} + +
+ +
+

+ + Create Lead +

+

Capture new sales opportunity

+
+ + {#if form?.error} +
+
+ + {form.error} +
+
+ {/if} + +
{ + if (!validateForm()) { + cancel(); + return; + } + isSubmitting = true; + return async ({ result }) => { + isSubmitting = false; + if (result.type === 'success') { + showNotification('Lead created successfully!', 'success'); + resetForm(); + setTimeout(() => goto('/leads/open'), 1500); + } else if (result.type === 'failure') { + const errorMessage = result.data?.error || 'Failed to create lead'; + showNotification(errorMessage, 'error'); + } + }; + }} + class="space-y-6" + > + +
+
+

+ + Contact Information +

+
+
+
+ + + {#if errors.title}

{errors.title}

{/if} +
+ +
+ + + {#if errors.first_name}

{errors.first_name}

{/if} +
+ +
+ + + {#if errors.last_name}

{errors.last_name}

{/if} +
+ +
+ + + {#if errors.email}

{errors.email}

{/if} +
+ +
+ + + {#if errors.phone || phoneError}

+ {errors.phone || phoneError} +

{/if} +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+

+ + Sales Pipeline +

+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + {#if errors.probability}

+ {errors.probability} +

{/if} +
+ +
+ + +
+
+
+ + +
+
+

+ + Assignment +

+
+
+
+ + +

Ctrl/Cmd + click to select multiple

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

+ + Address +

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

+ + Activity & Notes +

+
+
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+ + +
+ + +
+
+
+
diff --git a/frontend/src/routes/(app)/leads/open/+page.server.js b/frontend/src/routes/(app)/leads/open/+page.server.js new file mode 100644 index 00000000..8dff5953 --- /dev/null +++ b/frontend/src/routes/(app)/leads/open/+page.server.js @@ -0,0 +1,90 @@ +/** + * Open Leads Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/leads/ + * + * Django LeadListView returns leads separated into: + * - open_leads: { leads_count, open_leads: [...] } - excludes status='closed' + * - close_leads: { leads_count, close_leads: [...] } - only status='closed' + * + * Valid Django LEAD_STATUS values: assigned, in process, converted, recycled, closed + */ + +import { error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ cookies, locals }) { + const user = locals.user; + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Django leads endpoint - no status filter needed + // Django LeadListView already separates leads into open_leads and close_leads + // Valid Django status values: assigned, in process, converted, recycled, closed + const response = await apiRequest(`/leads/`, {}, { cookies, org }); + + // Handle Django response format + // Django returns: { open_leads: { leads_count, open_leads: [...] }, close_leads: {...}, ... } + let leads = []; + + if (response.open_leads && response.open_leads.open_leads) { + // Django returns nested structure: open_leads.open_leads + leads = response.open_leads.open_leads || []; + } else if (response.results) { + // Standard Django pagination + leads = response.results; + } else if (Array.isArray(response)) { + // Direct array response + leads = response; + } + + // Transform Django leads to match Prisma structure + const transformedLeads = leads.map((lead) => ({ + id: lead.id, + firstName: lead.first_name, + lastName: lead.last_name, + title: lead.title, + company: lead.company, + email: lead.email, + phone: lead.phone, + website: lead.website, + status: lead.status ? lead.status.toUpperCase().replace(' ', '_') : 'NEW', + leadSource: lead.source, // Map Django 'source' to Prisma 'leadSource' + rating: lead.rating, // Include rating for filtering + description: lead.description, + isConverted: lead.status === 'converted', + createdAt: lead.created_at, + updatedAt: lead.updated_at || lead.created_at, + + // Owner info - Django returns assigned_to array with ProfileSerializer + owner: + lead.assigned_to && lead.assigned_to.length > 0 + ? { + name: + lead.assigned_to[0].user?.email || + lead.assigned_to[0].user_details?.email || + 'Unknown', + email: lead.assigned_to[0].user?.email || lead.assigned_to[0].user_details?.email + } + : lead.created_by + ? { + name: lead.created_by.email, + email: lead.created_by.email + } + : null + })); + + return { + leads: transformedLeads + }; + } catch (err) { + console.error('Error fetching leads from API:', err); + throw error(500, `Failed to fetch leads: ${err.message}`); + } +} diff --git a/frontend/src/routes/(app)/leads/open/+page.svelte b/frontend/src/routes/(app)/leads/open/+page.svelte new file mode 100644 index 00000000..90c87a10 --- /dev/null +++ b/frontend/src/routes/(app)/leads/open/+page.svelte @@ -0,0 +1,684 @@ + + +
+ +
+
+
+
+
+ +
+
+

+ Open Leads +

+

+ {filteredLeads.length} of {leads.length} leads +

+
+
+ + + New Lead + +
+
+
+ + +
+
+ +
+
+ + + +
+ +
+ + + {#if showFilters} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ {/if} +
+
+ + +
+ {#if isLoading} +
+
+
+ {:else if filteredLeads.length === 0} +
+
📭
+

No leads found

+

+ Try adjusting your search criteria or create a new lead. +

+ + + Create New Lead + +
+ {:else} +
+ + + + +
+ {#each filteredLeads as lead, i} + {@const statusConfig = getStatusConfig(lead.status)} + {@const ratingConfig = getRatingConfig(lead.rating || '')} +
+ +
+
+
+ {lead.firstName.charAt(0)}{lead.lastName.charAt(0)} +
+
+ + {getFullName(lead)} + + {#if lead.title} +

{lead.title}

+ {/if} +
+
+
+ {#snippet statusIcon(/** @type {any} */ config)} + {@const StatusIcon = config.icon} + + {/snippet} + {@render statusIcon(statusConfig)} + + {lead.status} + +
+
+ + +
+ {#if lead.company} +
+ + {lead.company} +
+ {/if} + + {#if lead.email} + + + {lead.email} + + {/if} + + {#if lead.phone} + + + {lead.phone} + + {/if} + +
+
+ + {formatDate(lead.createdAt)} +
+ + {#if lead.rating} +
+ {#each Array(ratingConfig.dots) as _, i} +
+ {/each} + {lead.rating} +
+ {/if} +
+ + {#if lead.owner?.name} +
+ + Owned by {lead.owner.name} +
+ {/if} +
+ + + +
+ {/each} +
+
+ {/if} +
+
diff --git a/frontend/src/routes/(app)/opportunities/+page.server.js b/frontend/src/routes/(app)/opportunities/+page.server.js new file mode 100644 index 00000000..5eaeb44d --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/+page.server.js @@ -0,0 +1,148 @@ +/** + * Opportunities List Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/opportunities/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail } from '@sveltejs/kit'; +import { apiRequest, buildQueryParams } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const userId = locals.user?.id; + const org = locals.org; + + if (!userId) { + return { + opportunities: [], + stats: { + total: 0, + totalValue: 0, + wonValue: 0, + pipeline: 0 + } + }; + } + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch opportunities from Django API + const response = await apiRequest('/opportunities/', {}, { cookies, org }); + + // Handle Django response structure + let opportunities = []; + if (response.opportunities) { + opportunities = response.opportunities; + } else if (Array.isArray(response)) { + opportunities = response; + } else if (response.results) { + opportunities = response.results; + } + + // Transform Django opportunities to Prisma structure + const transformedOpportunities = opportunities.map((opp) => ({ + id: opp.id, + name: opp.name, + amount: opp.amount ? Number(opp.amount) : null, + expectedRevenue: opp.expected_revenue ? Number(opp.expected_revenue) : null, + stage: opp.stage, + probability: opp.probability, + leadSource: opp.lead_source, + description: opp.description, + closedOn: opp.closed_on, + createdAt: opp.created_at, + updatedAt: opp.updated_at, + + // Account information + account: opp.account + ? { + id: opp.account.id, + name: opp.account.name, + type: opp.account.type || opp.account.account_type + } + : null, + + // Owner information (assigned_to in Django) + owner: + opp.assigned_to && opp.assigned_to.length > 0 + ? { + id: opp.assigned_to[0].id, + name: opp.assigned_to[0].user_details?.email || opp.assigned_to[0].email, + email: opp.assigned_to[0].user_details?.email || opp.assigned_to[0].email + } + : opp.created_by + ? { + id: opp.created_by.id, + name: opp.created_by.email, + email: opp.created_by.email + } + : null, + + // Contacts + contacts: (opp.contacts || []).map((contact) => ({ + id: contact.id, + firstName: contact.first_name, + lastName: contact.last_name, + email: contact.email + })), + + // Counts + _count: { + tasks: opp.task_count || 0, + events: opp.event_count || 0 + } + })); + + // Calculate stats + const stats = { + total: transformedOpportunities.length, + totalValue: transformedOpportunities.reduce((sum, opp) => sum + (opp.amount || 0), 0), + wonValue: transformedOpportunities + .filter((opp) => opp.stage === 'CLOSED WON') + .reduce((sum, opp) => sum + (opp.amount || 0), 0), + pipeline: transformedOpportunities + .filter((opp) => !['CLOSED WON', 'CLOSED LOST'].includes(opp.stage)) + .reduce((sum, opp) => sum + (opp.amount || 0), 0) + }; + + return { + opportunities: transformedOpportunities, + stats + }; + } catch (err) { + console.error('Error loading opportunities from API:', err); + throw error(500, `Failed to load opportunities: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + delete: async ({ request, locals, cookies }) => { + try { + const formData = await request.formData(); + const opportunityId = formData.get('opportunityId')?.toString(); + const userId = locals.user?.id; + const org = locals.org; + + if (!opportunityId || !userId || !org) { + return fail(400, { message: 'Missing required data' }); + } + + // Delete via API + await apiRequest(`/opportunities/${opportunityId}/`, { method: 'DELETE' }, { cookies, org }); + + return { success: true, message: 'Opportunity deleted successfully' }; + } catch (err) { + console.error('Error deleting opportunity:', err); + return fail(500, { message: 'Failed to delete opportunity' }); + } + } +}; diff --git a/frontend/src/routes/(app)/opportunities/+page.svelte b/frontend/src/routes/(app)/opportunities/+page.svelte new file mode 100644 index 00000000..52b6ce6d --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/+page.svelte @@ -0,0 +1,656 @@ + + + + Opportunities - BottleCRM + + +
+ + {#if form?.success} +
+ +
+ {/if} + + {#if form?.message && !form?.success} +
+ +
+ {/if} + + +
+
+
+
+

Opportunities

+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+ Total Opportunities +
+
+ {data.stats.total} +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ Total Value +
+
+ {formatCurrency(data.stats.totalValue)} +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ Pipeline Value +
+
+ {formatCurrency(data.stats.pipeline)} +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ Won Value +
+
+ {formatCurrency(data.stats.wonValue)} +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+ +
+ +
+ +
+
+ + +
+ + +
+ + + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + {#each filteredOpportunities as opportunity (opportunity.id)} + {@const config = stageConfig[opportunity.stage] || stageConfig.PROSPECTING} + + + + + + + + + + + {/each} + +
toggleSort('name')} + > +
+ Opportunity + +
+
toggleSort('account.name')} + > +
+ Account + +
+
+ Stage + toggleSort('amount')} + > +
+ Amount + +
+
toggleSort('closeDate')} + > +
+ Close Date + +
+
toggleSort('owner.name')} + > +
+ Owner + +
+
+ Activities + + Actions +
+
+
+
+ {opportunity.name || 'Unnamed Opportunity'} +
+ {#if opportunity.type} +
+ {opportunity.type} +
+ {/if} +
+
+
+
+ +
+
+ {opportunity.account?.name || 'No Account'} +
+ {#if opportunity.account?.type} +
+ {opportunity.account.type} +
+ {/if} +
+
+
+ + {#if config.icon} + {@const IconComponent = config.icon} + + {/if} + {config.label} + + + {formatCurrency(opportunity.amount)} + {#if opportunity.probability} +
+ {opportunity.probability}% probability +
+ {/if} +
+
+ + {formatDate(opportunity.closeDate)} +
+
+
+ +
+ {opportunity.owner?.name || opportunity.owner?.email || 'No Owner'} +
+
+
+
+ {#if opportunity._count?.tasks > 0} + + + {opportunity._count.tasks} + + {/if} + {#if opportunity._count?.events > 0} + + + {opportunity._count.events} + + {/if} +
+
+
+ + + + + + + +
+
+
+ + {#if filteredOpportunities.length === 0} +
+ +

No opportunities

+

+ {searchTerm || selectedStage !== 'all' + ? 'No opportunities match your current filters.' + : 'Get started by creating a new opportunity.'} +

+ {#if !searchTerm && selectedStage === 'all'} + + {/if} +
+ {/if} +
+
+
+ + +{#if showDeleteModal && opportunityToDelete} + +{/if} diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/+page.server.js b/frontend/src/routes/(app)/opportunities/[opportunityId]/+page.server.js new file mode 100644 index 00000000..c5b32ef6 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/+page.server.js @@ -0,0 +1,92 @@ +/** + * Opportunity Detail Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/opportunities/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch opportunity details from Django + const response = await apiRequest( + `/opportunities/${params.opportunityId}/`, + {}, + { cookies, org } + ); + + if (!response.opportunity_obj) { + throw error(404, 'Opportunity not found'); + } + + const oppData = response.opportunity_obj; + + // Transform opportunity data + const opportunity = { + id: oppData.id, + name: oppData.name, + amount: oppData.amount ? Number(oppData.amount) : null, + expectedRevenue: oppData.expected_revenue ? Number(oppData.expected_revenue) : null, + stage: oppData.stage, + probability: oppData.probability, + leadSource: oppData.lead_source, + description: oppData.description, + closedOn: oppData.closed_on, + createdAt: oppData.created_at, + updatedAt: oppData.updated_at, + + // Account + account: oppData.account + ? { + id: oppData.account.id, + name: oppData.account.name, + email: oppData.account.email, + phone: oppData.account.phone, + industry: oppData.account.industry + } + : null, + + // Owner + owner: + oppData.assigned_to && oppData.assigned_to.length > 0 + ? { + id: oppData.assigned_to[0].id, + name: oppData.assigned_to[0].user_details?.email || oppData.assigned_to[0].email, + email: oppData.assigned_to[0].user_details?.email || oppData.assigned_to[0].email + } + : oppData.created_by + ? { + id: oppData.created_by.id, + name: oppData.created_by.email, + email: oppData.created_by.email + } + : null + }; + + // Account data (for compatibility) + const account = opportunity.account; + const owner = opportunity.owner; + + return { + opportunity, + account, + owner + }; + } catch (err) { + console.error('Error loading opportunity data:', err); + throw error(500, `Failed to load opportunity: ${err.message}`); + } +} diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/+page.svelte b/frontend/src/routes/(app)/opportunities/[opportunityId]/+page.svelte new file mode 100644 index 00000000..feb7243d --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/+page.svelte @@ -0,0 +1,340 @@ + + +
+
+ +
+ + +
+

{opportunity.name}

+
+ + {opportunity.stage.replace('_', ' ')} + + {#if opportunity.probability} +
+ + {opportunity.probability}% probability +
+ {/if} +
+
+ + + {#if opportunity.stage !== 'CLOSED_LOST'} +
+
+ Progress + {Math.round(getStageProgress(opportunity.stage))}% +
+
+
+
+
+ {/if} +
+ + +
+ +
+ +
+
+ +

Financial Details

+
+
+
+
Amount
+
+ {formatCurrency(opportunity.amount)} +
+
+
+
Expected Revenue
+
+ {formatCurrency(opportunity.expectedRevenue)} +
+
+
+
+ + +
+
+ +

+ Opportunity Information +

+
+
+
+
Type
+
{opportunity.type || 'Not specified'}
+
+
+
Lead Source
+
+ {opportunity.leadSource || 'Not specified'} +
+
+
+
Forecast Category
+
+ {opportunity.forecastCategory || 'Not specified'} +
+
+
+
Close Date
+
+ + {formatDate(opportunity.closeDate)} +
+
+
+
+ + + {#if opportunity.nextStep} +
+
+ +

Next Steps

+
+
{opportunity.nextStep}
+
+ {/if} + + + {#if opportunity.description} +
+
+ +

Description

+
+
+ {opportunity.description} +
+
+ {/if} +
+ + +
+ +
+
+ +

Key Metrics

+
+
+
+ Probability + + {opportunity.probability ? `${opportunity.probability}%` : 'N/A'} + +
+
+ Days to Close + + {opportunity.closeDate + ? Math.ceil( + (new Date(opportunity.closeDate).getTime() - new Date().getTime()) / + (1000 * 60 * 60 * 24) + ) + : 'N/A'} + +
+
+
+ + +
+
+ +

Related Records

+
+
+
+
Account
+ {#if account} + + {account.name} + + {:else} + No account + {/if} +
+
+
Owner
+
+ + {owner?.name ?? 'Unassigned'} +
+
+
+
+ + +
+
+ +

System Information

+
+
+
+
Created
+
+ {formatDateTime(opportunity.createdAt)} +
+
+
+
Last Updated
+
+ {formatDateTime(opportunity.updatedAt)} +
+
+
+
+
+
+
+
+ + diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/close/+page.server.js b/frontend/src/routes/(app)/opportunities/[opportunityId]/close/+page.server.js new file mode 100644 index 00000000..7791a9e1 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/close/+page.server.js @@ -0,0 +1,145 @@ +/** + * Opportunity Close Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: PATCH /api/opportunities/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(403, 'Organization access required'); + } + + try { + const response = await apiRequest( + `/opportunities/${params.opportunityId}/`, + {}, + { cookies, org } + ); + + if (!response.opportunity_obj) { + throw error(404, 'Opportunity not found'); + } + + const oppData = response.opportunity_obj; + + // Transform to Prisma structure + const opportunity = { + id: oppData.id, + name: oppData.name, + stage: oppData.stage, + account: oppData.account + ? { + id: oppData.account.id, + name: oppData.account.name + } + : null + }; + + return { opportunity }; + } catch (err) { + console.error('Error loading opportunity for close:', err); + throw error(500, `Failed to load opportunity: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, params, locals, cookies }) => { + const org = locals.org; + + if (!org) { + return fail(403, { error: 'Organization access required' }); + } + + const formData = await request.formData(); + const status = formData.get('status')?.toString(); + const closeDate = formData.get('closeDate')?.toString(); + const closeReason = formData.get('closeReason')?.toString(); + + // Validation + const errors = {}; + + if (!status) { + errors.status = 'Status is required'; + } + + if (!closeDate) { + errors.closeDate = 'Close date is required'; + } + + // Validate status + const validCloseStatuses = ['CLOSED_WON', 'CLOSED_LOST']; + if (status && !validCloseStatuses.includes(status)) { + errors.status = 'Invalid status selected'; + } + + if (Object.keys(errors).length > 0) { + return fail(400, { errors, values: { status, closeDate, closeReason } }); + } + + try { + // First, get the current opportunity + const currentResponse = await apiRequest( + `/opportunities/${params.opportunityId}/`, + {}, + { cookies, org } + ); + + if (!currentResponse.opportunity_obj) { + return fail(404, { error: 'Opportunity not found' }); + } + + const currentOpp = currentResponse.opportunity_obj; + + // Prepare description with close reason appended + let updatedDescription = currentOpp.description || ''; + if (closeReason) { + updatedDescription = updatedDescription + ? `${updatedDescription}\n\nClose Reason: ${closeReason}` + : `Close Reason: ${closeReason}`; + } + + // Transform to Django field names + const djangoData = { + stage: status, // CLOSED_WON or CLOSED_LOST + closed_on: closeDate, + description: updatedDescription + }; + + // Update opportunity via API + await apiRequest( + `/opportunities/${params.opportunityId}/`, + { + method: 'PATCH', + body: djangoData + }, + { cookies, org } + ); + + throw redirect(303, `/opportunities/${params.opportunityId}`); + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error closing opportunity:', err); + return fail(500, { + error: + 'Failed to close opportunity: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { status, closeDate, closeReason } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/close/+page.svelte b/frontend/src/routes/(app)/opportunities/[opportunityId]/close/+page.svelte new file mode 100644 index 00000000..9f4372b0 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/close/+page.svelte @@ -0,0 +1,227 @@ + + +
+
+ +
+
+

+
+ +
+ Close Opportunity +

+

+ Update the status and close details for this opportunity +

+
+
+ + +
+
+

Opportunity Details

+
+
+

Name

+

{opportunity.name}

+
+
+

Amount

+

+ {opportunity.amount ? `$${opportunity.amount.toLocaleString()}` : 'Not specified'} +

+
+
+

Current Stage

+

+ {opportunity.stage.replace('_', ' ')} +

+
+
+

Probability

+

+ {opportunity.probability ? `${opportunity.probability}%` : 'Not specified'} +

+
+
+
+
+ + +
+
+

Close Opportunity

+
+ +
{ + return async ({ update }) => { + isSubmitting = true; + await update(); + isSubmitting = false; + }; + }} + class="space-y-6 p-6" + > + {#if form?.error} +
+
+ + Error +
+

{form.error}

+
+ {/if} + + +
+ +
+ {#each statusOptions as option} + + {/each} +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + Cancel + +
+
+
+
+
+ + diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/delete/+page.server.js b/frontend/src/routes/(app)/opportunities/[opportunityId]/delete/+page.server.js new file mode 100644 index 00000000..4d9347a0 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/delete/+page.server.js @@ -0,0 +1,99 @@ +/** + * Opportunity Delete Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: DELETE /api/opportunities/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const userId = locals.user?.id; + const org = locals.org; + + if (!userId || !org) { + throw error(401, 'Unauthorized'); + } + + try { + const response = await apiRequest( + `/opportunities/${params.opportunityId}/`, + {}, + { cookies, org } + ); + + if (!response.opportunity_obj) { + throw error(404, 'Opportunity not found'); + } + + const oppData = response.opportunity_obj; + + // Transform to Prisma structure + const opportunity = { + id: oppData.id, + name: oppData.name, + amount: oppData.amount ? parseFloat(oppData.amount) : null, + stage: oppData.stage, + account: oppData.account + ? { + id: oppData.account.id, + name: oppData.account.name + } + : null, + owner: oppData.assigned_to_details?.[0] + ? { + id: oppData.assigned_to_details[0].id, + name: + oppData.assigned_to_details[0].user_details?.email || + oppData.assigned_to_details[0].email, + email: + oppData.assigned_to_details[0].user_details?.email || + oppData.assigned_to_details[0].email + } + : null + }; + + return { opportunity }; + } catch (err) { + console.error('Error loading opportunity for delete:', err); + throw error(500, `Failed to load opportunity: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ params, locals, cookies }) => { + try { + const userId = locals.user?.id; + const org = locals.org; + + if (!userId || !org) { + return fail(401, { message: 'Unauthorized' }); + } + + // Delete the opportunity via API + await apiRequest( + `/opportunities/${params.opportunityId}/`, + { + method: 'DELETE' + }, + { cookies, org } + ); + + // Return success response - let client handle redirect + return { success: true, message: 'Opportunity deleted successfully' }; + } catch (err) { + console.error('Error deleting opportunity:', err); + return fail(500, { + message: + 'Failed to delete opportunity: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + } +}; diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/delete/+page.svelte b/frontend/src/routes/(app)/opportunities/[opportunityId]/delete/+page.svelte new file mode 100644 index 00000000..582f14c0 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/delete/+page.svelte @@ -0,0 +1,130 @@ + + + + Delete Opportunity - BottleCRM + + +
+ +
+
+
+
+ + + +

Delete Opportunity

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

+ Confirm Deletion +

+

+ This action cannot be undone. +

+
+
+ +
+

+ You are about to delete: +

+
+
Opportunity: {data.opportunity.name}
+
Account: {data.opportunity.account?.name || 'N/A'}
+
+ Amount: + {data.opportunity.amount + ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + data.opportunity.amount + ) + : 'N/A'} +
+
Stage: {data.opportunity.stage}
+
+
+ +
+

+ Warning: Deleting this opportunity will also remove all associated: +

+
    +
  • Tasks and activities
  • +
  • Events and meetings
  • +
  • Comments and notes
  • +
  • Quote associations
  • +
+
+ +
+ + Cancel + + +
{ + deleteLoading = true; + return ({ result }) => { + deleteLoading = false; + if (result.type === 'success') { + // Navigate to opportunities list on successful deletion + goto('/opportunities'); + } else if (result.type === 'failure') { + // Handle error case - you could show a toast notification here + console.error('Failed to delete opportunity:', result.data?.message); + alert(result.data?.message || 'Failed to delete opportunity'); + } + }; + }} + > + +
+
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/edit/+page.server.js b/frontend/src/routes/(app)/opportunities/[opportunityId]/edit/+page.server.js new file mode 100644 index 00000000..1afc96a5 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/edit/+page.server.js @@ -0,0 +1,218 @@ +/** + * Opportunity Edit Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: PUT/PATCH /api/opportunities/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error, fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(403, 'Organization access required'); + } + + try { + const response = await apiRequest( + `/opportunities/${params.opportunityId}/`, + {}, + { cookies, org } + ); + + if (!response.opportunity_obj) { + throw error(404, 'Opportunity not found'); + } + + const oppData = response.opportunity_obj; + + // Transform to Prisma structure + const opportunity = { + id: oppData.id, + name: oppData.name, + amount: oppData.amount ? parseFloat(oppData.amount) : null, + expectedRevenue: oppData.expected_revenue ? parseFloat(oppData.expected_revenue) : null, + stage: oppData.stage || 'PROSPECTING', + probability: oppData.probability ? parseFloat(oppData.probability) : null, + closeDate: oppData.closed_on || null, + leadSource: oppData.lead_source || null, + forecastCategory: oppData.forecast_category || null, + type: oppData.opportunity_type || null, + nextStep: oppData.next_step || null, + description: oppData.description || null + }; + + // Extract account and owner info + const account = oppData.account + ? { + id: oppData.account.id, + name: oppData.account.name + } + : null; + + const owner = oppData.assigned_to_details?.[0] + ? { + id: oppData.assigned_to_details[0].id, + name: + oppData.assigned_to_details[0].user_details?.email || + oppData.assigned_to_details[0].email + } + : null; + + return { + opportunity, + account, + owner + }; + } catch (err) { + console.error('Error loading opportunity for edit:', err); + throw error(500, `Failed to load opportunity: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, params, locals, cookies }) => { + const org = locals.org; + + if (!org) { + return fail(403, { error: 'Organization access required' }); + } + + const form = await request.formData(); + + // Extract form fields + const name = form.get('name')?.toString().trim(); + const amountStr = form.get('amount')?.toString(); + const amount = amountStr ? parseFloat(amountStr) : null; + const expectedRevenueStr = form.get('expectedRevenue')?.toString(); + const expectedRevenue = expectedRevenueStr ? parseFloat(expectedRevenueStr) : null; + const stage = form.get('stage')?.toString(); + const probabilityStr = form.get('probability')?.toString(); + const probability = probabilityStr ? parseFloat(probabilityStr) : null; + const closeDateValue = form.get('closeDate')?.toString(); + const closeDate = closeDateValue || null; + const leadSource = form.get('leadSource')?.toString() || null; + const forecastCategory = form.get('forecastCategory')?.toString() || null; + const type = form.get('type')?.toString() || null; + const nextStep = form.get('nextStep')?.toString() || null; + const description = form.get('description')?.toString() || null; + + // Validation + const errors = {}; + + if (!name || name.length === 0) { + errors.name = 'Opportunity name is required'; + } + + if (!stage) { + errors.stage = 'Stage is required'; + } + + // Validate stage is a valid enum value + const validStages = [ + 'PROSPECTING', + 'QUALIFICATION', + 'PROPOSAL', + 'NEGOTIATION', + 'CLOSED_WON', + 'CLOSED_LOST' + ]; + if (stage && !validStages.includes(stage)) { + errors.stage = 'Invalid stage selected'; + } + + // Validate probability range + if (probability !== null && (probability < 0 || probability > 100)) { + errors.probability = 'Probability must be between 0 and 100'; + } + + // Validate amounts are not negative + if (amount !== null && amount < 0) { + errors.amount = 'Amount cannot be negative'; + } + + if (expectedRevenue !== null && expectedRevenue < 0) { + errors.expectedRevenue = 'Expected revenue cannot be negative'; + } + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + values: { + name, + amount, + expectedRevenue, + stage, + probability, + closeDate, + leadSource, + forecastCategory, + type, + nextStep, + description + } + }); + } + + try { + // Transform to Django field names + const djangoData = { + name: name, + amount: amount, + expected_revenue: expectedRevenue, + stage: stage, + probability: probability, + closed_on: closeDate, + lead_source: leadSource, + forecast_category: forecastCategory, + opportunity_type: type, + next_step: nextStep, + description: description + }; + + // Update opportunity via API + await apiRequest( + `/opportunities/${params.opportunityId}/`, + { + method: 'PUT', + body: djangoData + }, + { cookies, org } + ); + + throw redirect(303, `/opportunities/${params.opportunityId}`); + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error updating opportunity:', err); + return fail(500, { + error: + 'Failed to update opportunity: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { + name, + amount, + expectedRevenue, + stage, + probability, + closeDate, + leadSource, + forecastCategory, + type, + nextStep, + description + } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/opportunities/[opportunityId]/edit/+page.svelte b/frontend/src/routes/(app)/opportunities/[opportunityId]/edit/+page.svelte new file mode 100644 index 00000000..a06823f8 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/[opportunityId]/edit/+page.svelte @@ -0,0 +1,372 @@ + + +
+
+ +
+
+
+ + + +
+

Edit Opportunity

+

+ Update opportunity details and track progress +

+
+
+
+
+ + +
+
+ +
+

+ + Basic Information +

+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + + {#if error} +
+
+ +

{error}

+
+
+ {/if} + + +
+ + Cancel + + +
+
+
+
+
diff --git a/frontend/src/routes/(app)/opportunities/new/+page.server.js b/frontend/src/routes/(app)/opportunities/new/+page.server.js new file mode 100644 index 00000000..9518b2e1 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/new/+page.server.js @@ -0,0 +1,223 @@ +/** + * Opportunity Create Page - Modern CRM Version + * + * Streamlined opportunity creation based on Twenty CRM and Salesforce patterns. + * Django endpoint: POST /api/opportunities/ + */ + +import { fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, url, cookies }) { + const org = locals.org; + const preSelectedAccountId = url.searchParams.get('accountId'); + + // Stages matching Django STAGES choices + const stages = [ + { value: 'PROSPECTING', label: 'Prospecting' }, + { value: 'QUALIFICATION', label: 'Qualification' }, + { value: 'PROPOSAL', label: 'Proposal' }, + { value: 'NEGOTIATION', label: 'Negotiation' }, + { value: 'CLOSED_WON', label: 'Closed Won' }, + { value: 'CLOSED_LOST', label: 'Closed Lost' } + ]; + + // Opportunity types matching Django OPPORTUNITY_TYPES choices + const opportunityTypes = [ + { value: 'NEW_BUSINESS', label: 'New Business' }, + { value: 'EXISTING_BUSINESS', label: 'Existing Business' }, + { value: 'RENEWAL', label: 'Renewal' }, + { value: 'UPSELL', label: 'Upsell' }, + { value: 'CROSS_SELL', label: 'Cross-sell' } + ]; + + // Lead sources matching Django SOURCES choices + const leadSources = [ + { value: 'NONE', label: 'None' }, + { value: 'CALL', label: 'Call' }, + { value: 'EMAIL', label: 'Email' }, + { value: 'EXISTING CUSTOMER', label: 'Existing Customer' }, + { value: 'PARTNER', label: 'Partner' }, + { value: 'PUBLIC RELATIONS', label: 'Public Relations' }, + { value: 'CAMPAIGN', label: 'Campaign' }, + { value: 'WEBSITE', label: 'Website' }, + { value: 'OTHER', label: 'Other' } + ]; + + try { + // Fetch accounts, contacts, users, and teams in parallel + const [accountsResponse, contactsResponse, usersResponse, teamsResponse] = await Promise.all([ + apiRequest('/accounts/', {}, { cookies, org }), + apiRequest('/contacts/', {}, { cookies, org }), + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/teams/', {}, { cookies, org }) + ]); + + // Transform accounts + let accountsList = []; + if (accountsResponse.results) { + accountsList = accountsResponse.results; + } else if (accountsResponse.active_accounts?.open_accounts) { + accountsList = accountsResponse.active_accounts.open_accounts; + } + + const accounts = accountsList.map((acc) => ({ + id: acc.id, + name: acc.name + })); + + // Transform contacts + const contacts = (contactsResponse.results || contactsResponse || []).map((contact) => ({ + id: contact.id, + firstName: contact.first_name, + lastName: contact.last_name, + email: contact.email + })); + + // Extract active users + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((profile) => ({ + id: profile.id, + name: profile.user_details?.email?.split('@')[0] || 'Unknown', + email: profile.user_details?.email || '' + })); + + // Extract teams + const teams = (teamsResponse.teams || teamsResponse.results || []).map((team) => ({ + id: team.id, + name: team.name + })); + + // Get pre-selected account details if provided + let preSelectedAccount = null; + if (preSelectedAccountId) { + preSelectedAccount = accounts.find((account) => account.id === preSelectedAccountId); + } + + return { + data: { + accounts, + contacts, + users, + teams, + stages, + opportunityTypes, + leadSources, + preSelectedAccountId, + preSelectedAccountName: preSelectedAccount?.name || null + } + }; + } catch (err) { + console.error('Error loading opportunity form data:', err); + return { + data: { + accounts: [], + contacts: [], + users: [], + teams: [], + stages, + opportunityTypes, + leadSources, + preSelectedAccountId: null, + preSelectedAccountName: null + } + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals, cookies }) => { + const org = locals.org; + + const formData = await request.formData(); + + // Extract and validate required fields + const name = formData.get('name')?.toString().trim(); + const accountId = formData.get('account')?.toString(); + const stage = formData.get('stage')?.toString(); + + if (!name) { + return fail(400, { error: 'Opportunity name is required' }); + } + + if (!stage) { + return fail(400, { error: 'Stage is required' }); + } + + // Validate probability + const probabilityStr = formData.get('probability')?.toString(); + let probability = null; + if (probabilityStr && probabilityStr.trim() !== '') { + probability = parseInt(probabilityStr); + if (isNaN(probability) || probability < 0 || probability > 100) { + return fail(400, { error: 'Probability must be between 0 and 100' }); + } + } + + // Validate amount + const amountStr = formData.get('amount')?.toString(); + let amount = null; + if (amountStr && amountStr.trim() !== '') { + amount = parseFloat(amountStr); + if (isNaN(amount) || amount < 0) { + return fail(400, { error: 'Amount must be a valid positive number' }); + } + } + + // Build Django data object + const djangoData = { + // Core Opportunity Information + name, + account: accountId || null, + stage, + opportunity_type: formData.get('opportunity_type')?.toString() || null, + + // Financial Information + currency: formData.get('currency')?.toString() || null, + amount, + probability, + closed_on: formData.get('closed_on')?.toString() || null, + + // Source & Context + lead_source: formData.get('lead_source')?.toString() || null, + + // Relationships + contacts: formData.getAll('contacts').map((id) => id.toString()), + + // Assignment + assigned_to: formData.getAll('assigned_to').map((id) => id.toString()), + teams: formData.getAll('teams').map((id) => id.toString()), + + // Notes + description: formData.get('description')?.toString().trim() || null + }; + + try { + const response = await apiRequest( + '/opportunities/', + { + method: 'POST', + body: djangoData + }, + { cookies, org } + ); + + return { + status: 'success', + message: 'Opportunity created successfully', + opportunity: { + id: response.id || response.opportunity_obj?.id, + name: response.name || response.opportunity_obj?.name + } + }; + } catch (err) { + console.error('Error creating opportunity:', err); + return fail(500, { + error: + 'Failed to create opportunity: ' + (err instanceof Error ? err.message : 'Unknown error') + }); + } + } +}; diff --git a/frontend/src/routes/(app)/opportunities/new/+page.svelte b/frontend/src/routes/(app)/opportunities/new/+page.svelte new file mode 100644 index 00000000..3a5702d0 --- /dev/null +++ b/frontend/src/routes/(app)/opportunities/new/+page.svelte @@ -0,0 +1,529 @@ + + + + New Opportunity - CRM + + +
+ +
+
+
+
+ +
+
+ +
+
+

New Opportunity

+

Create a new sales opportunity

+
+
+
+
+
+
+ + +
+ {#if form?.error} +
+

{form.error}

+
+ {/if} + + {#if form?.status === 'success'} +
+

{form.message}

+
+ + {/if} + +
{ + isSubmitting = true; + return async ({ result, update }) => { + isSubmitting = false; + await update(); + if (result.type === 'success' && result.data?.status === 'success') { + handleSuccess(); + } + }; + }} + class="space-y-8" + > + +
+

+ + Opportunity Information +

+ +
+ +
+ + +
+ + +
+ + + {#if data.data.preSelectedAccountId} +

+ Account pre-selected from {data.data.preSelectedAccountName} +

+ {/if} +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + +
+

+ + Financial Information +

+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ Expected Revenue +
+
+ ${calculateExpectedRevenue()} +
+

Amount × Probability / 100

+
+
+
+ + +
+

+ + Assignment +

+ +
+ +
+ + +

+ Hold Ctrl/Cmd to select multiple +

+
+ + +
+ + +

+ Hold Ctrl/Cmd to select multiple +

+
+
+
+ + + {#if data.data.contacts.length > 0} +
+

+ + Related Contacts +

+ +
+ + +

+ Hold Ctrl/Cmd to select multiple contacts +

+
+
+ {/if} + + +
+

+ + Notes +

+ +
+ + +
+
+ + +
+ + +
+
+
+
diff --git a/frontend/src/routes/(app)/profile/+page.server.js b/frontend/src/routes/(app)/profile/+page.server.js new file mode 100644 index 00000000..6c2be3d8 --- /dev/null +++ b/frontend/src/routes/(app)/profile/+page.server.js @@ -0,0 +1,126 @@ +/** + * Profile Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET/PATCH /api/profile/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + if (!locals.user) { + throw redirect(307, '/login'); + } + + const org = locals.org; + + try { + // Fetch user profile from Django API + const response = await apiRequest('/profile/', {}, { cookies, org }); + + // Django profile response structure may vary + const profileData = response.user_obj || response; + + // Get user details + const userDetails = profileData.user_details || profileData.user || {}; + + // Transform to Prisma structure + const user = { + id: profileData.id || locals.user.id, + user_id: profileData.user_id || userDetails.id, + email: userDetails.email || profileData.email || locals.user.email, + name: userDetails.name || profileData.name || userDetails.email, + profilePhoto: + profileData.profile_photo || userDetails.profile_photo || userDetails.profile_pic || null, + phone: profileData.phone || userDetails.phone || null, + isActive: profileData.is_active !== undefined ? profileData.is_active : true, + lastLogin: userDetails.last_login || null, + createdAt: + profileData.created_at || profileData.created_on || userDetails.date_joined || null, + organizations: [] // Django might not return this in profile endpoint + }; + + return { user }; + } catch (err) { + console.error('Error loading profile:', err); + throw redirect(307, '/login'); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + updateProfile: async ({ request, locals, cookies }) => { + if (!locals.user) { + throw redirect(307, '/login'); + } + + const org = locals.org; + const formData = await request.formData(); + const name = formData.get('name')?.toString(); + const phone = formData.get('phone')?.toString(); + + // Validation + const errors = {}; + + if (!name || name.trim().length === 0) { + errors.name = 'Name is required'; + } else if (name.trim().length < 2) { + errors.name = 'Name must be at least 2 characters long'; + } + + // Validate phone if provided + let formattedPhone = null; + if (phone && phone.trim().length > 0) { + const phoneValidation = validatePhoneNumber(phone.trim()); + if (!phoneValidation.isValid) { + errors.phone = phoneValidation.error || 'Please enter a valid phone number'; + } else { + formattedPhone = formatPhoneForStorage(phone.trim()); + } + } + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + data: { name, phone } + }); + } + + try { + // Transform to Django field names + const djangoData = { + name: name.trim(), + phone: formattedPhone + }; + + // Update profile via API + await apiRequest( + '/profile/', + { + method: 'PATCH', + body: djangoData + }, + { cookies, org } + ); + + return { + success: true, + message: 'Profile updated successfully' + }; + } catch (err) { + console.error('Error updating profile:', err); + return fail(500, { + error: + 'Failed to update profile: ' + (err instanceof Error ? err.message : 'Unknown error'), + data: { name, phone } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/profile/+page.svelte b/frontend/src/routes/(app)/profile/+page.svelte new file mode 100644 index 00000000..071cae37 --- /dev/null +++ b/frontend/src/routes/(app)/profile/+page.svelte @@ -0,0 +1,427 @@ + + + + Profile - BottleCRM + + +
+ +
+
+
+

Profile

+

+ Manage your personal information and account settings +

+
+ +
+
+ + + {#if form?.success} +
+
+
+ + + +
+
+

+ {form.message} +

+
+
+
+ {/if} + + {#if form?.error} +
+
+
+ + + +
+
+

+ {form.error} +

+
+
+
+ {/if} + +
+ +
+
+
+ +
+ {#if data.user.profilePhoto} + {data.user.name + {:else} +
+ + {getInitials(data.user.name)} + +
+ {/if} +
+ +

+ {data.user.name || 'Unnamed User'} +

+

{data.user.email}

+
+ + +
+ + {data.user.isActive ? 'Active' : 'Inactive'} + +
+
+
+ + +
+
+ {#if isEditing} + +
+
+

+ Edit Profile Information +

+ +
+ +
+ + +
+ + +
+ + + {#if phoneError} +

+ {phoneError} +

+ {/if} +
+
+
+ +
+
+ + +
+
+
+ {:else} + +
+

+ Profile Information +

+ +
+ +
+
+ + Email Address +
+
{data.user.email}
+
+ + +
+
+ + Phone Number +
+
+ {data.user.phone ? formatPhoneNumber(data.user.phone) : 'Not provided'} +
+
+ + +
+
+ + Last Login +
+
+ {formatDate(data.user.lastLogin)} +
+
+ + +
+
+ + Member Since +
+
+ {formatDate(data.user.createdAt)} +
+
+
+
+ {/if} +
+ + + {#if data.user.organizations && data.user.organizations.length > 0} +
+

Organizations

+
+ {#each data.user.organizations as userOrg} +
+
+
+ +
+
+

+ {userOrg.organization.name} +

+

+ Joined {formatDate(userOrg.joinedAt)} +

+
+
+ + {userOrg.role} + +
+ {/each} +
+
+ {/if} +
+
+
diff --git a/frontend/src/routes/(app)/support/+page.svelte b/frontend/src/routes/(app)/support/+page.svelte new file mode 100644 index 00000000..07f982f7 --- /dev/null +++ b/frontend/src/routes/(app)/support/+page.svelte @@ -0,0 +1,187 @@ + + +
+
+ +
+

BottleCRM Support

+

+ Empowering startups with free, open-source CRM solutions. Say goodbye to expensive + subscription fees. +

+
+ + +
+
+
+ +
+
+

Our Mission

+

+ BottleCRM addresses the high subscription costs of commercial CRM alternatives by + providing a completely free, open-source, and highly customizable solution. Clone it, + self-host it, and make it yours - forever free. +

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

Community Support

+
+

+ Join our open-source community for free support, discussions, and collaboration. +

+ + + Visit GitHub Repository + +
+ + +
+
+
+ +
+

Professional Support

+
+

+ Get priority support, hosting assistance, and custom development services. +

+ + + Contact for Paid Support + +
+
+ + +
+
+
+ +
+

+ Feature Requests & Ideas +

+
+

+ Have an idea to make BottleCRM better? We'd love to hear from you! Share your feature + requests and help shape the future of open-source CRM. +

+ + + Request Feature + +
+ + +
+
+
+ +
+

Bug Reports

+
+

+ Found a bug? Help us improve BottleCRM by reporting issues. Your feedback helps make the + platform more stable for everyone. +

+ + + Report Bug + +
+ + +
+
+
+ +
+

Security Issues

+
+

+ Security is our priority. If you discover any security vulnerabilities, please + report them privately. Do not create public GitHub issues for security concerns. +

+ + + Report Security Issue + +
+ + +
+
+
+ +
+

Custom CRM Development

+
+

+ Need BottleCRM tailored to your specific business needs? We offer professional customization + services including hosting, custom features, integrations, and ongoing support. +

+ +
+
+
diff --git a/frontend/src/routes/(app)/tasks/+page.server.js b/frontend/src/routes/(app)/tasks/+page.server.js new file mode 100644 index 00000000..31ffe2a5 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/+page.server.js @@ -0,0 +1,101 @@ +/** + * Tasks Main Page (Kanban Boards List) - API Version + * + * Migrated from Prisma to Django REST API + * Displays user's Kanban boards and allows creating new boards + * + * ⚠️ NOTE: This page requires Django Boards API (Phase 3 feature) + * Django Endpoints (to be implemented): + * - GET /api/boards/ - List user's boards + * - POST /api/boards/ - Create new board with default columns + * + * Until boards API is implemented, this returns empty list. + */ + +import { fail } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const user = locals.user; + const org = locals.org; + + try { + // Fetch boards from Django API (Phase 3 feature - now implemented) + const data = await apiRequest('/boards/', {}, { cookies, org }); + + // Transform Django response to match Prisma format + const boards = (data.results || []).map((board) => ({ + id: board.id, + name: board.name, + createdAt: new Date(board.created_at), + ownerId: board.owner?.id || user.id, + // Additional fields from Django + isArchived: board.is_archived || false, + _count: { + columns: board.columns?.length || 0, + members: board.members?.length || 0 + } + })); + + return { boards }; + } catch (err) { + console.error('Error loading boards:', err); + + // Graceful fallback if API has issues + return { + boards: [], + error: err.message + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + /** + * Create new board with default columns + */ + create: async ({ request, locals, cookies }) => { + const user = locals.user; + const org = locals.org; + + try { + const formData = await request.formData(); + const name = formData.get('name')?.toString(); + + if (!name) { + return fail(400, { error: 'Board name is required' }); + } + + // Create board via Django API + // Django will automatically create default columns (To Do, In Progress, Done) + const boardData = { + name + // owner is set automatically from request.profile in Django + // default columns are created automatically in Django backend + }; + + await apiRequest( + '/boards/', + { + method: 'POST', + body: boardData + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error creating board:', err); + + if (err.message.includes('404') || err.message.includes('Not Found')) { + return fail(404, { + error: 'Boards API endpoint not found', + details: err.message + }); + } + + return fail(500, { error: err.message || 'Failed to create board' }); + } + } +}; diff --git a/frontend/src/routes/(app)/tasks/+page.svelte b/frontend/src/routes/(app)/tasks/+page.svelte new file mode 100644 index 00000000..6fc16e2c --- /dev/null +++ b/frontend/src/routes/(app)/tasks/+page.svelte @@ -0,0 +1,138 @@ + + +
+

Boards

+
+ {#if data?.boards?.length} + {#each data.boards as board} + +
{board.name}
+
+ {/each} + {:else} +

No boards found.

+ {/if} +
+
+
+ + + +
+
+
+ + diff --git a/frontend/src/routes/(app)/tasks/[task_id]/+page.server.js b/frontend/src/routes/(app)/tasks/[task_id]/+page.server.js new file mode 100644 index 00000000..b2ebdfa3 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/[task_id]/+page.server.js @@ -0,0 +1,148 @@ +/** + * Task Detail Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/tasks/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const { task_id } = params; + const org = locals.org; + const user = locals.user; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch task details from Django + const response = await apiRequest(`/tasks/${task_id}/`, {}, { cookies, org }); + + if (!response.task_obj) { + return fail(404, { message: 'Task not found or you do not have permission to view it.' }); + } + + const taskData = response.task_obj; + + // Transform task data + const task = { + id: taskData.id, + title: taskData.title, + status: taskData.status, + priority: taskData.priority, + dueDate: taskData.due_date ? new Date(taskData.due_date).toISOString().split('T')[0] : null, + createdAt: taskData.created_at, + updatedAt: taskData.updated_at, + + // Owner + owner: + taskData.assigned_to && taskData.assigned_to.length > 0 + ? { + id: taskData.assigned_to[0].id, + name: taskData.assigned_to[0].user_details?.email || taskData.assigned_to[0].email, + profilePhoto: taskData.assigned_to[0].profile_photo || null + } + : null, + + // Account + account: taskData.account + ? { + id: taskData.account.id, + name: taskData.account.name + } + : null, + + // Comments + comments: (response.comments || []).map((comment) => ({ + id: comment.id, + body: comment.comment, + createdAt: comment.created_at, + author: comment.commented_by + ? { + id: comment.commented_by.id, + name: comment.commented_by.user_details?.email || comment.commented_by.email, + profilePhoto: comment.commented_by.profile_photo || null + } + : null + })) + }; + + // Transform users list + const users = (response.users || []).map((user) => ({ + id: user.id, + name: user.user_details?.email || user.email, + profilePhoto: user.profile_photo || null + })); + + // Transform accounts list + let accounts = []; + if (response.accounts_list) { + accounts = response.accounts_list; + } else if (response.accounts) { + accounts = response.accounts; + } + + const transformedAccounts = accounts.map((account) => ({ + id: account.id, + name: account.name + })); + + // Logged in user info + const loggedInUser = user + ? { + id: user.id, + name: user.name, + profilePhoto: user.profilePhoto || null + } + : null; + + return { + task, + users, + accounts: transformedAccounts, + loggedInUser + }; + } catch (err) { + console.error('Error loading task data:', err); + throw error(500, `Failed to load task: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + addComment: async ({ request, params, locals, cookies }) => { + const org = locals.org; + const formData = await request.formData(); + const commentBody = formData.get('commentBody')?.toString(); + const { task_id } = params; + + if (!commentBody || commentBody.trim() === '') { + return fail(400, { error: true, message: 'Comment body cannot be empty.', commentBody }); + } + + try { + // Add comment via API - comments are added via TaskDetailView.post() + await apiRequest( + `/tasks/${task_id}/`, + { + method: 'POST', + body: { comment: commentBody } + }, + { cookies, org } + ); + + return { success: true, message: 'Comment added successfully.' }; + } catch (err) { + console.error('Error adding comment:', err); + return fail(500, { error: true, message: 'Failed to add comment.' }); + } + } +}; diff --git a/frontend/src/routes/(app)/tasks/[task_id]/+page.svelte b/frontend/src/routes/(app)/tasks/[task_id]/+page.svelte new file mode 100644 index 00000000..2c76df3b --- /dev/null +++ b/frontend/src/routes/(app)/tasks/[task_id]/+page.svelte @@ -0,0 +1,303 @@ + + +
+
+ +
+
+
+ +
+

+ Task Details +

+

View and manage task information

+
+
+ +
+
+ + + {#if task} +
+ +
+
+ + {task.status} + + + {task.priority} + +
+ + Due {formatDate(task.dueDate)} +
+
+ +

+ {task.title} +

+ + {#if task.description} +

+ {task.description} +

+ {:else} +

No description provided

+ {/if} +
+ + +
+
+ +
+
+ + Task Owner +
+
+ {#if task.owner?.profilePhoto} + {task.owner.name} + {:else} +
+ + {task.owner?.name?.charAt(0) || 'U'} + +
+ {/if} +
+
+ {task.owner?.name || 'Unassigned'} +
+
Owner
+
+
+
+ + +
+
+ + Related Account +
+
+
+ +
+
+
+ {task.account?.name || 'No account assigned'} +
+
Account
+
+
+
+
+
+
+ + +
+
+
+ +

Comments

+ {#if task.comments && task.comments.length > 0} + ({task.comments.length}) + {/if} +
+
+ +
+ {#if form?.message} +
+

{form.message}

+
+ {/if} + + {#if form?.fieldError && Array.isArray(form.fieldError) && form.fieldError.includes('commentBody')} + {@const formData = /** @type {any} */ (form)} + {#if 'commentBody' in formData} + {@const _ = newComment = /** @type {string} */ (formData.commentBody || '')} + {/if} + {/if} + + +
+ {#if task.comments && task.comments.length > 0} + {#each task.comments as c (c.id || c.createdAt)} +
+ {#if c.author.profilePhoto} + {c.author.name} + {:else} +
+ + {c.author?.name?.charAt(0) || 'U'} + +
+ {/if} +
+
+ {c.author.name} + {new Date(c.createdAt).toLocaleString()} +
+
+ {c.body} +
+
+
+ {/each} + {:else} +
+ +

No comments yet

+

+ Be the first to add a comment +

+
+ {/if} +
+ + +
+
+ + +
+
+ +
+
+
+
+ {/if} +
+
diff --git a/frontend/src/routes/(app)/tasks/[task_id]/edit/+page.server.js b/frontend/src/routes/(app)/tasks/[task_id]/edit/+page.server.js new file mode 100644 index 00000000..7b0b1889 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/[task_id]/edit/+page.server.js @@ -0,0 +1,190 @@ +/** + * Task Edit Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: PUT/PATCH /api/tasks/{id}/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, redirect, error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Fetch task, users, and accounts in parallel + const [taskResponse, usersResponse, accountsResponse] = await Promise.all([ + apiRequest(`/tasks/${params.task_id}/`, {}, { cookies, org }), + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/accounts/', {}, { cookies, org }) + ]); + + if (!taskResponse.task_obj) { + throw redirect(303, '/tasks/list'); + } + + const taskData = taskResponse.task_obj; + + // Format dueDate for input[type=date] + let formattedDueDate = null; + if (taskData.due_date) { + try { + formattedDueDate = new Date(taskData.due_date).toISOString().split('T')[0]; + } catch (e) { + formattedDueDate = taskData.due_date; + } + } + + // Transform task to Prisma structure + const task = { + id: taskData.id, + subject: taskData.title, + description: taskData.description, + status: taskData.status || 'New', + priority: taskData.priority || 'Medium', + dueDate: formattedDueDate, + ownerId: taskData.assigned_to?.[0]?.id || null, + accountId: taskData.account || null + }; + + // Transform users + // Django UsersListView returns { active_users: { active_users: [...] }, ... } + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((user) => ({ + id: user.id, + name: user.user_details?.email || user.email + })); + + // Transform accounts + let accounts = []; + if (accountsResponse.results) { + accounts = accountsResponse.results; + } else if (accountsResponse.active_accounts?.open_accounts) { + accounts = accountsResponse.active_accounts.open_accounts; + } + + const transformedAccounts = accounts.map((acc) => ({ + id: acc.id, + name: acc.name + })); + + return { + task, + users, + accounts: transformedAccounts + }; + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error loading task for edit:', err); + throw error(500, `Failed to load task: ${err.message}`); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + update: async ({ request, params, locals, cookies }) => { + const formData = await request.formData(); + const org = locals.org; + + const subject = formData.get('subject')?.toString(); + const description = formData.get('description')?.toString(); + const status = formData.get('status')?.toString(); + const priority = formData.get('priority')?.toString(); + let dueDate = formData.get('dueDate')?.toString(); + const ownerId = formData.get('ownerId')?.toString(); + let accountId = formData.get('accountId')?.toString(); + + // Validation + const errors = {}; + + if (!subject || subject.trim().length === 0) { + errors.subject = 'Subject is required'; + } + + if (!ownerId) { + errors.ownerId = 'Owner is required'; + } + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + values: { + subject, + description, + status, + priority, + dueDate, + ownerId, + accountId + } + }); + } + + try { + // Clean up accountId + if (accountId === '' || accountId === 'null') { + accountId = null; + } + + // Clean up dueDate + if (!dueDate || dueDate.trim() === '') { + dueDate = null; + } + + // Transform to Django field names + const djangoData = { + title: subject.trim(), + description: description ? description.trim() : null, + status: status || 'New', + priority: priority || 'Medium', + due_date: dueDate, + assigned_to: ownerId ? [ownerId] : [], + account: accountId + }; + + // Update task via API + await apiRequest( + `/tasks/${params.task_id}/`, + { + method: 'PUT', + body: djangoData + }, + { cookies, org } + ); + + throw redirect(303, `/tasks/${params.task_id}`); + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error updating task:', err); + return fail(500, { + error: 'Failed to update task: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { + subject, + description, + status, + priority, + dueDate, + ownerId, + accountId + } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/tasks/[task_id]/edit/+page.svelte b/frontend/src/routes/(app)/tasks/[task_id]/edit/+page.svelte new file mode 100644 index 00000000..d6cdf86d --- /dev/null +++ b/frontend/src/routes/(app)/tasks/[task_id]/edit/+page.svelte @@ -0,0 +1,274 @@ + + +
+
+ +
+
+
+
+
+ +
+
+

Edit Task

+

+ Update task details and settings +

+
+
+ +
+
+
+ + +
+ + {#if form?.message || form?.fieldError} +
+
+
+ +
+ {#if form?.message} +

{form.message}

+ {/if} + {#if form?.fieldError} +

+ Error with field '{form.fieldError[0]}': {form.fieldError[1]} +

+ {/if} +
+
+
+
+ {/if} + + +
+
+ +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/tasks/board/[boardId]/+page.server.js b/frontend/src/routes/(app)/tasks/board/[boardId]/+page.server.js new file mode 100644 index 00000000..8f78da38 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/board/[boardId]/+page.server.js @@ -0,0 +1,256 @@ +/** + * Board Detail Page - Server-side Loader + * + * Fetches board data including columns and tasks from Django API. + * + * Django Endpoints: + * - GET /api/boards/{id}/ - Get board with columns + * - GET /api/boards/{id}/columns/ - List columns with tasks + * - POST /api/boards/{id}/columns/ - Create column + * - POST /api/boards/columns/{id}/tasks/ - Create task in column + * - PUT /api/boards/tasks/{id}/ - Update task + * - DELETE /api/boards/tasks/{id}/ - Delete task + */ + +import { fail, error } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals, cookies }) { + const { boardId } = params; + const user = locals.user; + const org = locals.org; + + try { + // Fetch board details with columns + const board = await apiRequest(`/boards/${boardId}/`, {}, { cookies, org }); + + // Fetch columns with tasks + const columns = await apiRequest(`/boards/${boardId}/columns/`, {}, { cookies, org }); + + return { + board: { + id: board.id, + name: board.name, + description: board.description, + isArchived: board.is_archived, + owner: board.owner, + createdAt: new Date(board.created_at), + updatedAt: new Date(board.updated_at) + }, + columns: columns.map((col) => ({ + id: col.id, + name: col.name, + order: col.order, + color: col.color, + limit: col.limit, + taskCount: col.task_count, + tasks: (col.tasks || []).map((task) => ({ + id: task.id, + title: task.title, + description: task.description, + order: task.order, + priority: task.priority, + dueDate: task.due_date ? new Date(task.due_date) : null, + completedAt: task.completed_at ? new Date(task.completed_at) : null, + isCompleted: task.is_completed, + isOverdue: task.is_overdue, + assignedTo: task.assigned_to || [], + accountId: task.account, + contactId: task.contact, + opportunityId: task.opportunity + })) + })), + user + }; + } catch (err) { + console.error('Error loading board:', err); + + if (err.message.includes('404') || err.message.includes('not found')) { + throw error(404, 'Board not found'); + } + + throw error(500, err.message || 'Failed to load board'); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + /** + * Create a new column + */ + createColumn: async ({ params, request, locals, cookies }) => { + const { boardId } = params; + const org = locals.org; + + try { + const formData = await request.formData(); + const name = formData.get('name')?.toString(); + const color = formData.get('color')?.toString() || '#6B7280'; + + if (!name) { + return fail(400, { error: 'Column name is required' }); + } + + await apiRequest( + `/boards/${boardId}/columns/`, + { + method: 'POST', + body: { name, color } + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error creating column:', err); + return fail(500, { error: err.message || 'Failed to create column' }); + } + }, + + /** + * Create a new task in a column + */ + createTask: async ({ params, request, locals, cookies }) => { + const { boardId } = params; + const org = locals.org; + + try { + const formData = await request.formData(); + const columnId = formData.get('columnId')?.toString(); + const title = formData.get('title')?.toString(); + const description = formData.get('description')?.toString() || ''; + const priority = formData.get('priority')?.toString() || 'medium'; + + if (!columnId || !title) { + return fail(400, { error: 'Column ID and title are required' }); + } + + await apiRequest( + `/boards/columns/${columnId}/tasks/`, + { + method: 'POST', + body: { + title, + description, + priority, + column: columnId + } + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error creating task:', err); + return fail(500, { error: err.message || 'Failed to create task' }); + } + }, + + /** + * Update a task (move to different column, update details) + */ + updateTask: async ({ request, locals, cookies }) => { + const org = locals.org; + + try { + const formData = await request.formData(); + const taskId = formData.get('taskId')?.toString(); + const title = formData.get('title')?.toString(); + const description = formData.get('description')?.toString(); + const columnId = formData.get('columnId')?.toString(); + const order = formData.get('order')?.toString(); + const priority = formData.get('priority')?.toString(); + + if (!taskId) { + return fail(400, { error: 'Task ID is required' }); + } + + /** @type {Record} */ + const updateData = {}; + if (title) updateData.title = title; + if (description !== undefined) updateData.description = description; + if (columnId) updateData.column = columnId; + if (order) updateData.order = parseInt(order); + if (priority) updateData.priority = priority; + + await apiRequest( + `/boards/tasks/${taskId}/`, + { + method: 'PUT', + body: updateData + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error updating task:', err); + return fail(500, { error: err.message || 'Failed to update task' }); + } + }, + + /** + * Delete a task + */ + deleteTask: async ({ request, locals, cookies }) => { + const org = locals.org; + + try { + const formData = await request.formData(); + const taskId = formData.get('taskId')?.toString(); + + if (!taskId) { + return fail(400, { error: 'Task ID is required' }); + } + + await apiRequest( + `/boards/tasks/${taskId}/`, + { + method: 'DELETE' + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error deleting task:', err); + return fail(500, { error: err.message || 'Failed to delete task' }); + } + }, + + /** + * Move task to a different column (drag and drop) + */ + moveTask: async ({ request, locals, cookies }) => { + const org = locals.org; + + try { + const formData = await request.formData(); + const taskId = formData.get('taskId')?.toString(); + const columnId = formData.get('columnId')?.toString(); + const order = parseInt(formData.get('order')?.toString() || '0'); + + if (!taskId || !columnId) { + return fail(400, { error: 'Task ID and column ID are required' }); + } + + await apiRequest( + `/boards/tasks/${taskId}/`, + { + method: 'PUT', + body: { + column: columnId, + order + } + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error moving task:', err); + return fail(500, { error: err.message || 'Failed to move task' }); + } + } +}; diff --git a/frontend/src/routes/(app)/tasks/board/[boardId]/+page.svelte b/frontend/src/routes/(app)/tasks/board/[boardId]/+page.svelte new file mode 100644 index 00000000..438c3c8b --- /dev/null +++ b/frontend/src/routes/(app)/tasks/board/[boardId]/+page.svelte @@ -0,0 +1,549 @@ + + +
+
+ Back to Boards +

{data.board.name}

+ {#if data.board.description} +

{data.board.description}

+ {/if} +
+ +
+ {#each data.columns as column (column.id)} +
handleDragOver(e, column.id)} + on:dragleave={handleDragLeave} + on:drop={(e) => handleDrop(e, column.id)} + > +
+

{column.name}

+ {column.tasks.length} +
+ +
+ {#each column.tasks as task (task.id)} +
handleDragStart(e, task, column.id)} + on:dragend={handleDragEnd} + > +
{task.title}
+ {#if task.description} +
{task.description}
+ {/if} +
+ + {task.priority} + + {#if task.dueDate} + + {formatDate(task.dueDate)} + + {/if} +
+ {#if task.assignedTo && task.assignedTo.length > 0} +
+ {#each task.assignedTo.slice(0, 3) as assignee} + + {assignee.user?.email?.charAt(0).toUpperCase() || '?'} + + {/each} + {#if task.assignedTo.length > 3} + +{task.assignedTo.length - 3} + {/if} +
+ {/if} +
+ {/each} + + {#if addingTaskToColumn === column.id} +
{ + return async ({ result }) => { + if (result.type === 'success') { + cancelAddingTask(); + await invalidateAll(); + } + }; + }} + > + + + + +
+ + +
+
+ {:else} + + {/if} +
+
+ {/each} + + +
+ {#if showAddColumn} +
{ + return async ({ result }) => { + if (result.type === 'success') { + showAddColumn = false; + newColumnName = ''; + await invalidateAll(); + } + }; + }} + > + + +
+ + +
+
+ {:else} + + {/if} +
+
+
+ + diff --git a/frontend/src/routes/(app)/tasks/calendar/+page.server.js b/frontend/src/routes/(app)/tasks/calendar/+page.server.js new file mode 100644 index 00000000..89844355 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/calendar/+page.server.js @@ -0,0 +1,69 @@ +/** + * Calendar Page (Tasks with Due Dates) - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/tasks/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error } from '@sveltejs/kit'; +import { apiRequest, buildQueryParams } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const user = locals.user; + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Build query parameters - filter tasks with due dates + const queryParams = buildQueryParams({ + page: 1, + limit: 1000, + sort: 'due_date', + order: 'asc' + }); + + // Fetch tasks from Django API + const response = await apiRequest(`/tasks/?${queryParams.toString()}`, {}, { cookies, org }); + + // Handle Django response structure + // Django TaskListView returns { tasks: [...], tasks_count: ..., ... } + let allTasks = []; + if (response.tasks) { + allTasks = response.tasks; + } else if (Array.isArray(response)) { + allTasks = response; + } else if (response.results) { + allTasks = response.results; + } + + // Filter tasks with due dates only (Django might return all tasks) + const tasksWithDueDates = allTasks.filter((task) => task.due_date !== null); + + // Transform Django tasks to Prisma structure + const transformedTasks = tasksWithDueDates.map((task) => ({ + id: task.id, + subject: task.title, + description: task.description || '', + dueDate: task.due_date, + status: task.status, + priority: task.priority, + createdAt: task.created_at, + updatedAt: task.updated_at + })); + + return { + tasks: transformedTasks + }; + } catch (err) { + console.error('Error loading calendar tasks from API:', err); + throw error(500, `Failed to load calendar tasks: ${err.message}`); + } +} diff --git a/frontend/src/routes/(app)/tasks/calendar/+page.svelte b/frontend/src/routes/(app)/tasks/calendar/+page.svelte new file mode 100644 index 00000000..52294336 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/calendar/+page.svelte @@ -0,0 +1,390 @@ + + +
+
+ +
+
+ +

Task Calendar

+
+

Manage and track your tasks with ease

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

+ {monthNames[month]} + {year} +

+ +
+ +
+
+ + +
+ {#each ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as day} +
+ {day} +
+ {/each} +
+ + +
+ {#each calendar as date, i} + {#if date} + + {:else} +
+ {/if} + {/each} +
+
+
+ + +
+
+
+

+ Tasks for {new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} +

+
+ {selectedTasks.length} task{selectedTasks.length !== 1 ? 's' : ''} +
+
+ + +
+ + +
+

This Month

+
+
+ Total Tasks + + {totalMonthlyTasks} + +
+
+ Days with Tasks + {monthlyTasks.length} +
+
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/tasks/list/+page.server.js b/frontend/src/routes/(app)/tasks/list/+page.server.js new file mode 100644 index 00000000..63143572 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/list/+page.server.js @@ -0,0 +1,88 @@ +/** + * Tasks List Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/tasks/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { error } from '@sveltejs/kit'; +import { apiRequest, buildQueryParams } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const user = locals.user; + const org = locals.org; + + if (!org) { + throw error(401, 'Organization context required'); + } + + try { + // Build query parameters for tasks + const queryParams = buildQueryParams({ + page: 1, + limit: 1000, // Get all tasks without pagination + sort: 'created_at', + order: 'desc' + }); + + // Fetch tasks from Django API + const response = await apiRequest(`/tasks/?${queryParams.toString()}`, {}, { cookies, org }); + + // Handle Django response structure + // Django TaskListView returns { tasks: [...], tasks_count: ..., ... } + let tasks = []; + if (response.tasks) { + tasks = response.tasks; + } else if (Array.isArray(response)) { + tasks = response; + } else if (response.results) { + tasks = response.results; + } + + // Transform Django tasks to Prisma structure + const transformedTasks = tasks.map((task) => ({ + id: task.id, + subject: task.title, + description: task.description, + status: task.status, + priority: task.priority, + dueDate: task.due_date, + createdAt: task.created_at, + updatedAt: task.updated_at, + + // Owner (first assigned user) + owner: + task.assigned_to && task.assigned_to.length > 0 + ? { + id: task.assigned_to[0].id, + name: task.assigned_to[0].user_details?.email || task.assigned_to[0].email + } + : task.created_by + ? { + id: task.created_by.id, + name: task.created_by.email + } + : null, + + // Account + account: task.account + ? { + id: task.account.id, + name: task.account.name + } + : null + })); + + return { + tasks: transformedTasks + }; + } catch (err) { + console.error('Error loading tasks from API:', err); + throw error(500, `Failed to load tasks: ${err.message}`); + } +} diff --git a/frontend/src/routes/(app)/tasks/list/+page.svelte b/frontend/src/routes/(app)/tasks/list/+page.svelte new file mode 100644 index 00000000..9af21e26 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/list/+page.svelte @@ -0,0 +1,426 @@ + + +
+
+ +
+
+
+

Tasks

+

+ Manage and track your team's tasks +

+
+ + + New Task + +
+
+ + +
+ {#if data.tasks.length === 0} +
+
+ +
+

No tasks yet

+

+ Get started by creating your first task +

+ + + Create Task + +
+ {:else} + + + + +
+ {#each data.tasks as task (task.id)} +
+
+
+ + {task.subject} + + {#if task.description} +

+ {task.description} +

+ {/if} +
+
+ + + + +
+
+ +
+
+ {#snippet statusIconCard(/** @type {string} */ status)} + {@const StatusIcon = getStatusIcon(status)} + + {/snippet} + {@render statusIconCard(task.status)} + + {task.status || 'N/A'} + +
+ +
+ {#snippet priorityIconCard(/** @type {string} */ priority)} + {@const PriorityIcon = getPriorityIcon(priority)} + + {/snippet} + {@render priorityIconCard(task.priority)} + + {task.priority || 'Medium'} + +
+
+ +
+
+ + Due: {formatDate(task.dueDate)} +
+
+ + {task.owner?.name || 'Unassigned'} +
+ {#if task.account?.name} +
+ + {task.account.name} +
+ {/if} +
+
+ {/each} +
+ {/if} +
+
+
+ + diff --git a/frontend/src/routes/(app)/tasks/new/+page.server.js b/frontend/src/routes/(app)/tasks/new/+page.server.js new file mode 100644 index 00000000..61415004 --- /dev/null +++ b/frontend/src/routes/(app)/tasks/new/+page.server.js @@ -0,0 +1,178 @@ +/** + * Task Create Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: POST /api/tasks/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { fail, redirect } from '@sveltejs/kit'; +import { apiRequest } from '$lib/api-helpers.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, url, cookies }) { + const org = locals.org; + const urlAccountId = url.searchParams.get('accountId'); + + try { + // Fetch users and accounts in parallel + const [usersResponse, accountsResponse] = await Promise.all([ + apiRequest('/users/', {}, { cookies, org }), + apiRequest('/accounts/', {}, { cookies, org }) + ]); + + // Transform users + // Django UsersListView returns { active_users: { active_users: [...] }, ... } + const activeUsersList = usersResponse.active_users?.active_users || []; + const users = activeUsersList.map((user) => ({ + id: user.id, + name: user.user_details?.email || user.email, + email: user.user_details?.email || user.email + })); + + // Transform accounts + let accounts = []; + if (accountsResponse.results) { + accounts = accountsResponse.results; + } else if (accountsResponse.active_accounts?.open_accounts) { + accounts = accountsResponse.active_accounts.open_accounts; + } + + const transformedAccounts = accounts.map((acc) => ({ + id: acc.id, + name: acc.name + })); + + // If accountId is provided in URL, validate it exists + if (urlAccountId) { + const accountExists = transformedAccounts.some((account) => account.id === urlAccountId); + if (!accountExists) { + throw redirect(303, '/tasks/new'); + } + } + + return { + users, + accounts: transformedAccounts + }; + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error loading task create page:', err); + return { + users: [], + accounts: [] + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals, cookies }) => { + const { user, org } = locals; + + const formData = await request.formData(); + + const subject = formData.get('subject')?.toString(); + const status = formData.get('status')?.toString() || 'New'; + const priority = formData.get('priority')?.toString() || 'Medium'; + const dueDateStr = formData.get('dueDate')?.toString(); + const ownerId = formData.get('ownerId')?.toString(); + let accountId = formData.get('accountId')?.toString(); + const description = formData.get('description')?.toString(); + + // Validation + const errors = {}; + + if (!subject || subject.trim().length === 0) { + errors.subject = 'Subject is required'; + } + + if (!ownerId) { + errors.ownerId = 'Owner is required'; + } + + // Clean up accountId + if (accountId && (accountId === '' || accountId === 'null')) { + accountId = null; + } + + if (Object.keys(errors).length > 0) { + return fail(400, { + errors, + values: { + subject, + status, + priority, + dueDate: dueDateStr, + ownerId, + accountId, + description + } + }); + } + + try { + // Transform to Django field names + const djangoData = { + title: subject, + status: status, + priority: priority, + due_date: dueDateStr || null, + description: description || null, + assigned_to: ownerId ? [ownerId] : [], + account: accountId || null + }; + + // Create task via API + const response = await apiRequest( + '/tasks/', + { + method: 'POST', + body: djangoData + }, + { cookies, org } + ); + + const taskId = response.id || response.task_obj?.id; + + // Redirect based on where the task was created from + if (accountId) { + // If task was created from an account page, redirect back to that account + throw redirect(303, `/accounts/${accountId}`); + } else { + // Otherwise, redirect to the tasks list or detail if we have taskId + if (taskId) { + throw redirect(303, `/tasks/${taskId}`); + } else { + throw redirect(303, '/tasks/list'); + } + } + } catch (err) { + // Check if this is a redirect + if (err?.status === 303) { + throw err; + } + + console.error('Error creating task:', err); + return fail(500, { + error: 'Failed to create task: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: { + subject, + status, + priority, + dueDate: dueDateStr, + ownerId, + accountId, + description + } + }); + } + } +}; diff --git a/frontend/src/routes/(app)/tasks/new/+page.svelte b/frontend/src/routes/(app)/tasks/new/+page.svelte new file mode 100644 index 00000000..15deebdd --- /dev/null +++ b/frontend/src/routes/(app)/tasks/new/+page.svelte @@ -0,0 +1,353 @@ + + +
+
+ +
+
+
+

Create New Task

+

+ {#if selectedAccount} + Add a new task for {selectedAccount.name} + {:else} + Add a new task to keep track of your work + {/if} +

+
+ +
+
+ + +
+ {#if form?.error} +
+
+
+ + + +
+
+

{form.error}

+
+
+
+ {/if} + +
{ + isSubmitting = true; + return async ({ update }) => { + await update(); + isSubmitting = false; + }; + }} + class="p-6" + > + + {#if urlAccountId} + + {/if} + +
+ +
+

+ + Task Details +

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

+ + Assignment +

+ +
+ +
+ + +
+ + +
+ + {#if urlAccountId} + + +

+ Account pre-selected from URL +

+ {:else} + + + {/if} +
+
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+
+
+
+
+ + diff --git a/frontend/src/routes/(app)/users/+page.server.js b/frontend/src/routes/(app)/users/+page.server.js new file mode 100644 index 00000000..1ba642e8 --- /dev/null +++ b/frontend/src/routes/(app)/users/+page.server.js @@ -0,0 +1,299 @@ +/** + * Users Management Page - API Version + * + * Migrated from Prisma to Django REST API + * Allows organization admins to: + * - View all users in the organization + * - Add users to the organization by email + * - Change user roles (ADMIN/USER) + * - Remove users from the organization + * - Update organization details + * + * Django Endpoints: + * - GET /api/users/ - List organization users + * - POST /api/users/ - Create new user + * - GET /api/user/{id}/ - Get user details + * - PUT /api/user/{id}/ - Update user/profile + * - DELETE /api/user/{id}/ - Deactivate user (soft delete) + */ + +import { error, fail } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; + +const API_BASE_URL = `${env.PUBLIC_DJANGO_API_URL}/api`; + +/** + * Make authenticated API request + * @param {string} endpoint + * @param {Object} options + * @param {Object} context + * @returns {Promise} + */ +async function apiRequest(endpoint, options = {}, context) { + const { cookies, org } = context; + const accessToken = cookies.get('jwt_access') || cookies.get('access_token'); + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + org: org.id, + ...options.headers + } + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })); + throw new Error(errorData.errors || errorData.error || response.statusText); + } + + return await response.json(); +} + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + const org = locals.org; + const user = locals.user; + + try { + // Fetch users list (returns active and inactive users) + const data = await apiRequest('/users/', {}, { cookies, org }); + + // Django returns: { active_users: {...}, inactive_users: {...}, roles: [...] } + const activeUsers = data.active_users?.active_users || []; + const inactiveUsers = data.inactive_users?.inactive_users || []; + + // Check if current user is admin + // Django returns user_details with id and email + const currentUserProfile = activeUsers.find( + (p) => p.user_details?.id === user.id || p.user_details?.email === user.email + ); + const isAdmin = + currentUserProfile?.role === 'ADMIN' || currentUserProfile?.is_organization_admin; + + if (!isAdmin) { + return { + error: { + name: 'You do not have permission to access this page' + } + }; + } + + // Combine active and inactive users, transform to match Prisma format + const allUsers = [ + ...activeUsers.map((profile) => ({ + userId: profile.user_details?.id || profile.id, + organizationId: org.id, + role: profile.role, + user: { + id: profile.user_details?.id || profile.id, + email: profile.user_details?.email || 'N/A', + name: profile.user_details?.email?.split('@')[0] || 'N/A' + }, + isActive: true, + profile + })), + ...inactiveUsers.map((profile) => ({ + userId: profile.user_details?.id || profile.id, + organizationId: org.id, + role: profile.role, + user: { + id: profile.user_details?.id || profile.id, + email: profile.user_details?.email || 'N/A', + name: profile.user_details?.email?.split('@')[0] || 'N/A' + }, + isActive: false, + profile + })) + ]; + + return { + organization: { + id: org.id, + name: org.name, + domain: org.domain || '', + description: org.description || '' + }, + users: allUsers, + user: { id: user.id } + }; + } catch (err) { + console.error('Error loading users:', err); + return { + error: { + name: err.message || 'Failed to load users' + } + }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + /** + * Update organization details + */ + update: async ({ request, locals, cookies }) => { + const org = locals.org; + const user = locals.user; + + try { + // Check if user is admin (done in API, but double-check client-side) + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + + if (!name) { + return fail(400, { error: 'Name is required' }); + } + + // Update organization via Django API + await apiRequest( + `/org/${org.id}/`, + { + method: 'PUT', + body: JSON.stringify({ name }) + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error updating organization:', err); + return fail(500, { error: err.message || 'Failed to update organization' }); + } + }, + + /** + * Add user to organization by email + */ + add_user: async ({ request, locals, cookies }) => { + const org = locals.org; + + try { + const formData = await request.formData(); + const email = formData.get('email')?.toString().trim().toLowerCase(); + const role = formData.get('role')?.toString(); + + if (!email || !role) { + return fail(400, { error: 'Email and role are required' }); + } + + // Create user via Django API + // Django endpoint: POST /api/users/ + const userData = { + email, + role, + // Django requires these fields for user creation + username: email.split('@')[0], + password: Math.random().toString(36).slice(-12) // Random password + }; + + await apiRequest( + '/users/', + { + method: 'POST', + body: JSON.stringify(userData) + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error adding user:', err); + // Check for specific error messages + if ( + err.message.includes('already exists') || + err.message.includes('already in organization') + ) { + return fail(400, { error: 'User already in organization' }); + } + if (err.message.includes('not found')) { + return fail(404, { error: 'No user found with that email' }); + } + return fail(500, { error: err.message || 'Failed to add user' }); + } + }, + + /** + * Edit user role + */ + edit_role: async ({ request, locals, cookies }) => { + const org = locals.org; + const user = locals.user; + + try { + const formData = await request.formData(); + const user_id = formData.get('user_id')?.toString(); + const role = formData.get('role')?.toString(); + + if (!user_id || !role) { + return fail(400, { error: 'User and role are required' }); + } + + // Don't allow editing own role + if (user_id === user.id) { + return fail(400, { error: 'You cannot change your own role' }); + } + + // Update user role via Django API + // Django endpoint: PUT /api/user/{id}/ + await apiRequest( + `/user/${user_id}/`, + { + method: 'PUT', + body: JSON.stringify({ role }) + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error editing role:', err); + if (err.message.includes('at least one admin')) { + return fail(400, { error: 'Organization must have at least one admin' }); + } + return fail(500, { error: err.message || 'Failed to edit role' }); + } + }, + + /** + * Remove user from organization + */ + remove_user: async ({ request, locals, cookies }) => { + const org = locals.org; + const user = locals.user; + + try { + const formData = await request.formData(); + const user_id = formData.get('user_id')?.toString(); + + if (!user_id) { + return fail(400, { error: 'User is required' }); + } + + // Don't allow removing self + if (user_id === user.id) { + return fail(400, { error: 'You cannot remove yourself' }); + } + + // Remove user via Django API (soft delete - set is_active=False) + // Django endpoint: PUT /api/user/{id}/status/ + await apiRequest( + `/user/${user_id}/status/`, + { + method: 'POST', + body: JSON.stringify({ is_active: false }) + }, + { cookies, org } + ); + + return { success: true }; + } catch (err) { + console.error('Error removing user:', err); + if (err.message.includes('at least one admin')) { + return fail(400, { error: 'Organization must have at least one admin' }); + } + return fail(500, { error: err.message || 'Failed to remove user' }); + } + } +}; diff --git a/frontend/src/routes/(app)/users/+page.svelte b/frontend/src/routes/(app)/users/+page.svelte new file mode 100644 index 00000000..eb4362fb --- /dev/null +++ b/frontend/src/routes/(app)/users/+page.svelte @@ -0,0 +1,468 @@ + + +
+
+ +
+
+

Organization Settings

+

+ Manage your organization and team members +

+
+ + + Logout + +
+ + +
+
+
+
+
+ +
+
+

{org.name}

+
+ {#if org.domain} + + + {org.domain} + + {/if} + + + {users.length} member{users.length !== 1 ? 's' : ''} + +
+
+
+ {#if !editing} + + {/if} +
+ + {#if org.description && !editing} +

{org.description}

+ {/if} + + {#if editing} +
+
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+
+ {/if} +
+
+ + +
+
+
+
+
+ +
+

Team Members

+
+
+ + +
+

+ + Add New Member +

+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + {#each users as user, i} + + + + + + + {/each} + +
+ Member + + Role + + Joined + + Actions +
+
+ {#if user.avatar} + {user.name} + {:else} +
+ +
+ {/if} +
+
+ {user.name} + {#if user.isSelf} + + You + + {/if} +
+
{user.email}
+
+
+
+ {#if user.isSelf} + + {#snippet roleIcon(/** @type {string} */ role)} + {@const RoleIcon = roleIcons[role] || User} + + {/snippet} + {@render roleIcon(user.role)} + {user.role} + + {:else if user.editingRole} +
+ + + + + +
+ {:else} + + {/if} +
+ {user.joined} + + {#if user.isSelf} + — + {:else} +
{ + if (!confirm('Remove this user from the organization?')) { + e.preventDefault(); + } + }} + > + + +
+ {/if} +
+
+
+
+
+
+
diff --git a/frontend/src/routes/(no-layout)/bounce/+page.svelte b/frontend/src/routes/(no-layout)/bounce/+page.svelte new file mode 100644 index 00000000..731e6d36 --- /dev/null +++ b/frontend/src/routes/(no-layout)/bounce/+page.svelte @@ -0,0 +1,9 @@ + diff --git a/frontend/src/routes/(no-layout)/login/+page.server.js b/frontend/src/routes/(no-layout)/login/+page.server.js new file mode 100644 index 00000000..0b1f04e0 --- /dev/null +++ b/frontend/src/routes/(no-layout)/login/+page.server.js @@ -0,0 +1,188 @@ +/** + * Login Page - Secure OAuth Implementation + * + * Security features: + * - PKCE (Proof Key for Code Exchange) for authorization code protection + * - Cryptographic state parameter for CSRF protection + * - Server-side token exchange via Django backend (no client secret in frontend) + * - Secure httpOnly cookies for sensitive data + * + * Django endpoint: POST /api/auth/google/callback/ + */ + +import axios from 'axios'; +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import { env as publicEnv } from '$env/dynamic/public'; +import { generateCodeVerifier, generateCodeChallenge, generateState } from '$lib/utils/pkce.js'; + +const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; +const GOOGLE_SCOPES = ['openid', 'email', 'profile'].join(' '); + +// Cookie configuration +const COOKIE_OPTIONS = { + path: '/', + httpOnly: true, + sameSite: 'lax' +}; + +/** + * Get secure cookie options based on environment + * @param {number} maxAge - Cookie max age in seconds + * @returns {object} Cookie options with secure flag for production + */ +function getCookieOptions(maxAge) { + return { + ...COOKIE_OPTIONS, + secure: env.NODE_ENV === 'production', + maxAge + }; +} + +/** @type {import('@sveltejs/kit').ServerLoad} */ +export async function load({ url, cookies }) { + const code = url.searchParams.get('code'); + const returnedState = url.searchParams.get('state'); + const error = url.searchParams.get('error'); + const errorDescription = url.searchParams.get('error_description'); + + // Handle OAuth error returned from Google + if (error) { + console.error('Google OAuth error:', error, errorDescription); + return { + google_url: null, + error: errorDescription || `OAuth error: ${error}` + }; + } + + // Handle OAuth callback with authorization code + if (code) { + return handleOAuthCallback(code, returnedState, cookies); + } + + // Check if user is already authenticated + const jwtAccess = cookies.get('jwt_access'); + if (jwtAccess) { + throw redirect(307, '/org'); + } + + // Generate OAuth parameters and return login URL + return generateOAuthUrl(cookies); +} + +/** + * Handle the OAuth callback when Google redirects back with an authorization code + * @param {string} code - Authorization code from Google + * @param {string|null} returnedState - State parameter returned from Google + * @param {import('@sveltejs/kit').Cookies} cookies - SvelteKit cookies + */ +async function handleOAuthCallback(code, returnedState, cookies) { + // Retrieve and immediately clear OAuth cookies (one-time use) + const savedState = cookies.get('oauth_state'); + const codeVerifier = cookies.get('oauth_code_verifier'); + + // Clear OAuth cookies regardless of outcome + cookies.delete('oauth_state', { path: '/' }); + cookies.delete('oauth_code_verifier', { path: '/' }); + + // Validate state parameter (CSRF protection) + if (!savedState || savedState !== returnedState) { + console.error('OAuth state mismatch - possible CSRF attack'); + console.error('Expected:', savedState?.substring(0, 10) + '...'); + console.error('Received:', returnedState?.substring(0, 10) + '...'); + throw redirect(307, '/login?error=state_mismatch'); + } + + // Validate code verifier exists + if (!codeVerifier) { + console.error('Missing PKCE code verifier - session may have expired'); + throw redirect(307, '/login?error=session_expired'); + } + + const redirect_uri = env.GOOGLE_LOGIN_DOMAIN + '/login'; + + try { + // Exchange code for tokens via Django backend + // The backend handles the actual token exchange with Google using the client secret + const apiUrl = publicEnv.PUBLIC_DJANGO_API_URL; + console.log('Using API URL:', apiUrl); + const response = await axios.post( + `${apiUrl}/api/auth/google/callback/`, + { + code, + code_verifier: codeVerifier, + redirect_uri + }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 30000 + } + ); + + const { access_token, refresh_token, user } = response.data; + + // Store JWT tokens in secure httpOnly cookies + cookies.set('jwt_access', access_token, getCookieOptions(60 * 60 * 24)); // 1 day + cookies.set('jwt_refresh', refresh_token, getCookieOptions(60 * 60 * 24 * 365)); // 1 year + } catch (error) { + console.error('OAuth token exchange failed - Full error:', error); + console.error('Response data:', error.response?.data); + console.error('Response status:', error.response?.status); + const errorMessage = error.response?.data?.error || error.message || 'Unknown error'; + + // Provide user-friendly error messages + let userError = 'auth_failed'; + const errStr = String(errorMessage); + if (errStr.includes('code_verifier')) { + userError = 'pkce_failed'; + } else if (errStr.includes('expired')) { + userError = 'code_expired'; + } + console.error('Final error message:', errorMessage); + + throw redirect(307, `/login?error=${userError}`); + } + + // Success - redirect to organization selection + throw redirect(307, '/org'); +} + +/** + * Generate Google OAuth URL with PKCE parameters + * @param {import('@sveltejs/kit').Cookies} cookies - SvelteKit cookies + * @returns {object} Object containing the Google OAuth URL + */ +function generateOAuthUrl(cookies) { + // Generate PKCE parameters + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + + // Generate cryptographically random state for CSRF protection + const state = generateState(); + + // Store PKCE verifier and state in secure httpOnly cookies + // These expire in 10 minutes - should be plenty for OAuth flow + const oauthCookieMaxAge = 60 * 10; // 10 minutes + + cookies.set('oauth_code_verifier', codeVerifier, getCookieOptions(oauthCookieMaxAge)); + cookies.set('oauth_state', state, getCookieOptions(oauthCookieMaxAge)); + + // Build Google OAuth URL with all required parameters + const redirect_uri = env.GOOGLE_LOGIN_DOMAIN + '/login'; + + const params = new URLSearchParams({ + client_id: env.GOOGLE_CLIENT_ID, + redirect_uri, + response_type: 'code', + scope: GOOGLE_SCOPES, + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + access_type: 'offline', // Request refresh token + prompt: 'consent' // Always show consent screen (required for refresh token) + }); + + const google_login_url = `${GOOGLE_AUTH_URL}?${params.toString()}`; + + return { google_url: google_login_url }; +} diff --git a/frontend/src/routes/(no-layout)/login/+page.svelte b/frontend/src/routes/(no-layout)/login/+page.svelte new file mode 100644 index 00000000..108b3f05 --- /dev/null +++ b/frontend/src/routes/(no-layout)/login/+page.svelte @@ -0,0 +1,254 @@ + + + + Login | BottleCRM - Free Open-Source CRM for Startups + + + + +
+ +
+
+ + +
+
+ +
+
+
+ + + + +
+
+ +
+ +
+ + +
+ +
+
+ BottleCRM Logo +
+ +
+

Welcome Back

+

+ Sign in to your free BottleCRM account and start managing your customer + relationships more effectively. +

+
+
+ + +
+
+
+ + Free CRM Features +
+
    +
  • + + Unlimited contacts & users +
  • +
  • + + Self-hosted solution +
  • +
  • + + No subscription fees +
  • +
+
+
+ + + + + +
+
+ + Your data is secure and private +
+ + + +
+
+
+ + +
+

+ New to BottleCRM? + + Learn more about our free CRM + +

+
+
+
+
+
+
+
diff --git a/frontend/src/routes/(no-layout)/logout/+page.server.js b/frontend/src/routes/(no-layout)/logout/+page.server.js new file mode 100644 index 00000000..dd46cb45 --- /dev/null +++ b/frontend/src/routes/(no-layout)/logout/+page.server.js @@ -0,0 +1,35 @@ +/** + * Logout Page - API Version + * + * Migrated from Prisma to Django REST API + * Clears JWT tokens and session data + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { redirect } from '@sveltejs/kit'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals, cookies }) { + // Clear all authentication-related cookies + const cookiesToClear = ['jwt_access', 'jwt_refresh', 'org', 'oauth_state', 'oauth_code_verifier']; + + for (const cookieName of cookiesToClear) { + if (cookies.get(cookieName)) { + await cookies.delete(cookieName, { path: '/' }); + } + } + + // Clear locals + if (locals.user) { + delete locals.user; + } + if (locals.org) { + delete locals.org; + } + + // Redirect to login page + throw redirect(303, '/login'); +} diff --git a/frontend/src/routes/(no-layout)/org/+page.server.js b/frontend/src/routes/(no-layout)/org/+page.server.js new file mode 100644 index 00000000..ed5c6878 --- /dev/null +++ b/frontend/src/routes/(no-layout)/org/+page.server.js @@ -0,0 +1,128 @@ +/** + * Organization Selection Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: GET /api/auth/me/ (returns user with organizations array) + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { env as publicEnv } from '$env/dynamic/public'; +import { redirect, fail } from '@sveltejs/kit'; +import axios from 'axios'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ cookies, locals }) { + const user = locals.user; + + if (!user) { + return { orgs: [] }; + } + + try { + const jwtAccess = cookies.get('jwt_access'); + if (!jwtAccess) { + return { orgs: [] }; + } + + const apiUrl = publicEnv.PUBLIC_DJANGO_API_URL; + + // Fetch current user with organization memberships + // The /api/auth/me/ endpoint returns user data with organizations array + const response = await axios.get(`${apiUrl}/api/auth/me/`, { + headers: { + Authorization: `Bearer ${jwtAccess}` + } + }); + + // Extract organizations from the user data + // Django's MeView returns user with organizations array + let orgs = []; + + if (response.data.organizations && Array.isArray(response.data.organizations)) { + orgs = response.data.organizations.map((org) => ({ + id: org.id, + name: org.name, + logo: org.logo || null, + role: org.role || 'USER' + })); + } + + return { orgs }; + } catch (error) { + console.error('Error fetching organizations:', error); + // Return empty array so user can create a new organization + return { orgs: [] }; + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + selectOrg: async ({ request, cookies }) => { + const formData = await request.formData(); + const orgId = formData.get('org_id')?.toString(); + const orgName = formData.get('org_name')?.toString(); + + if (!orgId) { + return fail(400, { error: 'Organization ID is required' }); + } + + const jwtAccess = cookies.get('jwt_access'); + if (!jwtAccess) { + throw redirect(307, '/login'); + } + + const apiUrl = publicEnv.PUBLIC_DJANGO_API_URL; + + try { + // Call switch-org endpoint to get new tokens with org context + const response = await axios.post( + `${apiUrl}/api/auth/switch-org/`, + { org_id: orgId }, + { + headers: { + Authorization: `Bearer ${jwtAccess}`, + 'Content-Type': 'application/json' + } + } + ); + + const { access_token, refresh_token, current_org } = response.data; + + // Update cookies with new tokens that have org context embedded + cookies.set('jwt_access', access_token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 // 1 day + }); + + cookies.set('jwt_refresh', refresh_token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 365 // 1 year + }); + + // Set org cookie for reference + cookies.set('org', orgId, { + path: '/', + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 365 + }); + + // Redirect to app + throw redirect(303, '/'); + } catch (error) { + if (error.status === 303) { + throw error; // Re-throw redirect + } + console.error('Org switch failed:', error); + return fail(500, { error: 'Failed to switch organization' }); + } + } +}; diff --git a/frontend/src/routes/(no-layout)/org/+page.svelte b/frontend/src/routes/(no-layout)/org/+page.svelte new file mode 100644 index 00000000..6df63209 --- /dev/null +++ b/frontend/src/routes/(no-layout)/org/+page.svelte @@ -0,0 +1,116 @@ + + +
+
+ +
+
+

Select Organization

+

Choose an organization to continue

+
+ + + + +
+ + + {#if orgs.length > 0} +
+ {#each orgs as org} +
{ + loading = true; + return async ({ update }) => { + await update(); + loading = false; + }; + }} + class="group cursor-pointer rounded-xl border border-gray-200 bg-white shadow-sm transition-all duration-200 hover:border-blue-300 hover:shadow-md" + > + + + +
+ {/each} +
+ {:else} +
+
+ +
+

No organizations found

+

Create your first organization to get started

+ + + Create Organization + +
+ {/if} + + + {#if orgs.length > 0} + + + + {/if} +
+
diff --git a/frontend/src/routes/(no-layout)/org/new/+page.server.js b/frontend/src/routes/(no-layout)/org/new/+page.server.js new file mode 100644 index 00000000..2b19afb3 --- /dev/null +++ b/frontend/src/routes/(no-layout)/org/new/+page.server.js @@ -0,0 +1,113 @@ +/** + * Organization Create Page - API Version + * + * Migrated from Prisma to Django REST API + * Django endpoint: POST /api/org/ + * + * To activate: + * mv +page.server.js +page.server.prisma.js + * mv +page.server.api.js +page.server.js + */ + +import { env } from '$env/dynamic/private'; +import { env as publicEnv } from '$env/dynamic/public'; +import axios from 'axios'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ cookies }) { + // No data needed for load +} + +/** @satisfies {import('./$types').Actions} */ +export const actions = { + default: async ({ request, cookies, locals }) => { + // Get the user from locals + const user = locals.user; + + if (!user) { + return { + error: { + name: 'You must be logged in to create an organization' + } + }; + } + + // Get the submitted form data + const formData = await request.formData(); + const orgName = formData.get('org_name')?.toString(); + + if (!orgName || orgName.trim().length === 0) { + return { + error: { + name: 'Organization name is required' + } + }; + } + + try { + const jwtAccess = cookies.get('jwt_access'); + if (!jwtAccess) { + return { + error: { + name: 'Authentication required' + } + }; + } + + const apiUrl = publicEnv.PUBLIC_DJANGO_API_URL; + + // Create organization and profile via Django API + // Django's OrgProfileCreateView creates both org and profile + const response = await axios.post( + `${apiUrl}/api/org/`, + { + name: orgName.trim() + }, + { + headers: { + Authorization: `Bearer ${jwtAccess}`, + 'Content-Type': 'application/json' + } + } + ); + + // Response should contain the created org + const newOrg = response.data.org || response.data; + + // Set org cookie for the newly created org + await cookies.set('org', newOrg.id, { + path: '/', + httpOnly: true, + sameSite: 'strict', + secure: env.NODE_ENV === 'production' + }); + + // Return success + return { + data: { + name: orgName + } + }; + } catch (err) { + console.error('Error creating organization:', err); + + // Check if it's a duplicate name error + if (err.response?.status === 400) { + return { + error: { + name: + err.response.data?.name?.[0] || + err.response.data?.error || + 'Organization with this name may already exist' + } + }; + } + + return { + error: { + name: 'An unexpected error occurred while creating the organization.' + } + }; + } + } +}; diff --git a/frontend/src/routes/(no-layout)/org/new/+page.svelte b/frontend/src/routes/(no-layout)/org/new/+page.svelte new file mode 100644 index 00000000..dc6dda17 --- /dev/null +++ b/frontend/src/routes/(no-layout)/org/new/+page.svelte @@ -0,0 +1,86 @@ + + +
+
+ +
+
+ +
+

Create Organization

+

Set up your new organization to get started

+
+ + +
+
+ +
+ + +
+ + + {#if form?.error} +
+ + {form.error.name} +
+ {/if} + + + {#if form?.data} +
+ + + Organization "{form.data.name}" created successfully! + +
+ {/if} + + + + + + +
+
+
+
diff --git a/frontend/static/funding.json b/frontend/static/funding.json new file mode 100644 index 00000000..d7c2e9e3 --- /dev/null +++ b/frontend/static/funding.json @@ -0,0 +1,122 @@ +{ + "version": "v1.0.0", + + "entity": { + "type": "organisation", + "role": "owner", + "name": "MicroPyramid Informatics Private Limited", + "email": "hello@micropyramid.com", + "phone": "+91-9959166266", + "description": "MicroPyramid is a technology company specializing in web development, mobile applications, and SaaS solutions. We build modern, scalable software solutions for businesses of all sizes, with expertise in Python, Django, React, and other cutting-edge technologies.", + "webpageUrl": { + "url": "https://micropyramid.com", + "wellKnown": "" + } + }, + + "projects": [ + { + "guid": "startup-crm", + "name": "BottleCRM", + "description": "BottleCRM is a dynamic, SaaS CRM platform designed to streamline the entire CRM needs of startups and enterprises. Built with modern web technologies including SvelteKit, Svelte 5, and Prisma, it offers a seamless experience for users through robust role-based access control (RBAC). Each user role is equipped with tailored functionalities to enhance efficiency, engagement, and management, ensuring a streamlined and secure business process.", + "webpageUrl": { + "url": "https://bottlecrm.io", + "wellKnown": "" + }, + "repositoryUrl": { + "url": "https://github.com/MicroPyramid/Django-CRM", + "wellKnown": "" + }, + "licenses": ["spdx:MIT"], + "tags": [ + "crm", + "saas", + "business-tools", + "customer-management", + "sales", + "svelte", + "typescript", + "web-application", + "enterprise", + "startup" + ] + } + ], + + "funding": { + "channels": [ + { + "guid": "paypal", + "type": "payment-provider", + "address": "paypal@micropyramid.com", + "description": "PayPal payments for international transactions" + }, + { + "guid": "bank-transfer", + "type": "bank", + "address": "Contact hello@micropyramid.com for bank details", + "description": "Direct bank transfers for larger contributions" + }, + { + "guid": "github-sponsors", + "type": "payment-provider", + "address": "https://github.com/sponsors/micropyramid", + "description": "GitHub Sponsors for recurring monthly support" + } + ], + + "plans": [ + { + "guid": "monthly-supporter", + "status": "active", + "name": "Monthly Supporter", + "description": "Monthly support to help maintain and improve BottleCRM with regular updates, bug fixes, and new features.", + "amount": 25, + "currency": "USD", + "frequency": "monthly", + "channels": ["paypal", "github-sponsors"] + }, + { + "guid": "yearly-backer", + "status": "active", + "name": "Yearly Backer", + "description": "Annual support for sustained development and maintenance of BottleCRM platform.", + "amount": 250, + "currency": "USD", + "frequency": "yearly", + "channels": ["paypal", "bank-transfer", "github-sponsors"] + }, + { + "guid": "enterprise-sponsor", + "status": "active", + "name": "Enterprise Sponsor", + "description": "Enterprise-level sponsorship for priority support, custom features, and dedicated development resources.", + "amount": 1000, + "currency": "USD", + "frequency": "monthly", + "channels": ["bank-transfer", "paypal"] + }, + { + "guid": "one-time-donation", + "status": "active", + "name": "One-time Contribution", + "description": "One-time donation to support BottleCRM development. Any amount is appreciated.", + "amount": 0, + "currency": "USD", + "frequency": "one-time", + "channels": ["paypal", "github-sponsors"] + } + ], + + "history": [ + { + "year": 2025, + "income": 0, + "expenses": 5000, + "taxes": 0, + "currency": "USD", + "description": "Initial development phase with investment in infrastructure, development tools, and hosting." + } + ] + } +} diff --git a/frontend/static/logo.png b/frontend/static/logo.png new file mode 100644 index 00000000..1433123f Binary files /dev/null and b/frontend/static/logo.png differ diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 00000000..6af2866f --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,5 @@ +import adapter from '@sveltejs/adapter-node'; + +const config = { kit: { adapter: adapter() } }; + +export default config; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 00000000..e6059ffa --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,5 @@ +import type { Config } from 'tailwindcss'; + +export default { + darkMode: 'class' +} satisfies Config; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 00000000..2d35c4f5 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()] +}); diff --git a/installation.md b/installation.md deleted file mode 100644 index 099f4624..00000000 --- a/installation.md +++ /dev/null @@ -1,59 +0,0 @@ -# CRM Installation using docker - -- pull application image - -```sh -docker pull micropyramid/crm:0.1 -``` - -- setup environment variables, below variables are required. - -```sh -# environment type, eg: stage, live -export ENV_TYPE="" -# sentry dns value, logs backend django project errors -export SENTRY_DSN="" -# sentry dns value, logs frontend react errors -export REACT_APP_DSN="" -# swagger api base/root url, eg: http://127.0.0.1 -export SWAGGER_ROOT_URL="" -# postgresql database host address, eg: example.com or some ip address -export DBHOST="" -# postgresql database port -export DBPORT="" -# postgresql database name -export DBNAME="" -# postgresql database user name -export DBUSER="" -# postgresql database password -export DBPASSWORD="" -# s3 bucket name, required for storing media and static files. -export S3_BUCKET_NAME="" -# access key to access s3 bucket -export AWS_ACCESS_KEY_ID="" -# secret key to access s3 bucket -export AWS_SECRET_ACCESS_KEY="" -``` - -- run application - -```sh -docker run \ - -n crm \ - -p 8000:80 \ - -e ENV_TYPE="$ENV_TYPE" \ - -e SENTRY_DSN="$SENTRY_DSN" \ - -e REACT_APP_DSN="$REACT_APP_DSN" \ - -e SWAGGER_ROOT_URL="$SWAGGER_ROOT_URL" \ - -e DBHOST="$DBHOST" \ - -e DBPORT="$DBPORT" \ - -e DBNAME="$DBNAME" \ - -e DBUSER="$DBUSER" \ - -e DBPASSWORD="$DBPASSWORD" \ - -e S3_BUCKET_NAME="$S3_BUCKET_NAME" \ - -e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \ - -e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \ - micropyramid/crm:0.1 -``` - -- GOTO: http://127.0.0.1:8000 diff --git a/invoices/admin.py b/invoices/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/invoices/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/invoices/migrations/0001_initial.py b/invoices/migrations/0001_initial.py deleted file mode 100644 index a218a9db..00000000 --- a/invoices/migrations/0001_initial.py +++ /dev/null @@ -1,95 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import phonenumber_field.modelfields -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('accounts', '0001_initial'), - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('teams', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Invoice', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('invoice_title', models.CharField(max_length=50, verbose_name='Invoice Title')), - ('invoice_number', models.CharField(max_length=50, verbose_name='Invoice Number')), - ('name', models.CharField(max_length=100, verbose_name='Name')), - ('email', models.EmailField(max_length=254, verbose_name='Email')), - ('quantity', models.PositiveIntegerField(default=0)), - ('rate', models.DecimalField(decimal_places=2, default=0, max_digits=12)), - ('total_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('currency', models.CharField(blank=True, choices=[('AED', 'AED, Dirham'), ('AFN', 'AFN, Afghani'), ('ALL', 'ALL, Lek'), ('AMD', 'AMD, Dram'), ('ANG', 'ANG, Guilder'), ('AOA', 'AOA, Kwanza'), ('ARS', 'ARS, Peso'), ('AUD', 'AUD, Dollar'), ('AWG', 'AWG, Guilder'), ('AZN', 'AZN, Manat'), ('BAM', 'BAM, Marka'), ('BBD', 'BBD, Dollar'), ('BDT', 'BDT, Taka'), ('BGN', 'BGN, Lev'), ('BHD', 'BHD, Dinar'), ('BIF', 'BIF, Franc'), ('BMD', 'BMD, Dollar'), ('BND', 'BND, Dollar'), ('BOB', 'BOB, Boliviano'), ('BRL', 'BRL, Real'), ('BSD', 'BSD, Dollar'), ('BTN', 'BTN, Ngultrum'), ('BWP', 'BWP, Pula'), ('BYR', 'BYR, Ruble'), ('BZD', 'BZD, Dollar'), ('CAD', 'CAD, Dollar'), ('CDF', 'CDF, Franc'), ('CHF', 'CHF, Franc'), ('CLP', 'CLP, Peso'), ('CNY', 'CNY, Yuan Renminbi'), ('COP', 'COP, Peso'), ('CRC', 'CRC, Colon'), ('CUP', 'CUP, Peso'), ('CVE', 'CVE, Escudo'), ('CZK', 'CZK, Koruna'), ('DJF', 'DJF, Franc'), ('DKK', 'DKK, Krone'), ('DOP', 'DOP, Peso'), ('DZD', 'DZD, Dinar'), ('EGP', 'EGP, Pound'), ('ERN', 'ERN, Nakfa'), ('ETB', 'ETB, Birr'), ('EUR', 'EUR, Euro'), ('FJD', 'FJD, Dollar'), ('FKP', 'FKP, Pound'), ('GBP', 'GBP, Pound'), ('GEL', 'GEL, Lari'), ('GHS', 'GHS, Cedi'), ('GIP', 'GIP, Pound'), ('GMD', 'GMD, Dalasi'), ('GNF', 'GNF, Franc'), ('GTQ', 'GTQ, Quetzal'), ('GYD', 'GYD, Dollar'), ('HKD', 'HKD, Dollar'), ('HNL', 'HNL, Lempira'), ('HRK', 'HRK, Kuna'), ('HTG', 'HTG, Gourde'), ('HUF', 'HUF, Forint'), ('IDR', 'IDR, Rupiah'), ('ILS', 'ILS, Shekel'), ('INR', 'INR, Rupee'), ('IQD', 'IQD, Dinar'), ('IRR', 'IRR, Rial'), ('ISK', 'ISK, Krona'), ('JMD', 'JMD, Dollar'), ('JOD', 'JOD, Dinar'), ('JPY', 'JPY, Yen'), ('KES', 'KES, Shilling'), ('KGS', 'KGS, Som'), ('KHR', 'KHR, Riels'), ('KMF', 'KMF, Franc'), ('KPW', 'KPW, Won'), ('KRW', 'KRW, Won'), ('KWD', 'KWD, Dinar'), ('KYD', 'KYD, Dollar'), ('KZT', 'KZT, Tenge'), ('LAK', 'LAK, Kip'), ('LBP', 'LBP, Pound'), ('LKR', 'LKR, Rupee'), ('LRD', 'LRD, Dollar'), ('LSL', 'LSL, Loti'), ('LTL', 'LTL, Litas'), ('LVL', 'LVL, Lat'), ('LYD', 'LYD, Dinar'), ('MAD', 'MAD, Dirham'), ('MDL', 'MDL, Leu'), ('MGA', 'MGA, Ariary'), ('MKD', 'MKD, Denar'), ('MMK', 'MMK, Kyat'), ('MNT', 'MNT, Tugrik'), ('MOP', 'MOP, Pataca'), ('MRO', 'MRO, Ouguiya'), ('MUR', 'MUR, Rupee'), ('MVR', 'MVR, Rufiyaa'), ('MWK', 'MWK, Kwacha'), ('MXN', 'MXN, Peso'), ('MYR', 'MYR, Ringgit'), ('MZN', 'MZN, Metical'), ('NAD', 'NAD, Dollar'), ('NGN', 'NGN, Naira'), ('NIO', 'NIO, Cordoba'), ('NOK', 'NOK, Krone'), ('NPR', 'NPR, Rupee'), ('NZD', 'NZD, Dollar'), ('OMR', 'OMR, Rial'), ('PAB', 'PAB, Balboa'), ('PEN', 'PEN, Sol'), ('PGK', 'PGK, Kina'), ('PHP', 'PHP, Peso'), ('PKR', 'PKR, Rupee'), ('PLN', 'PLN, Zloty'), ('PYG', 'PYG, Guarani'), ('QAR', 'QAR, Rial'), ('RON', 'RON, Leu'), ('RSD', 'RSD, Dinar'), ('RUB', 'RUB, Ruble'), ('RWF', 'RWF, Franc'), ('SAR', 'SAR, Rial'), ('SBD', 'SBD, Dollar'), ('SCR', 'SCR, Rupee'), ('SDG', 'SDG, Pound'), ('SEK', 'SEK, Krona'), ('SGD', 'SGD, Dollar'), ('SHP', 'SHP, Pound'), ('SLL', 'SLL, Leone'), ('SOS', 'SOS, Shilling'), ('SRD', 'SRD, Dollar'), ('SSP', 'SSP, Pound'), ('STD', 'STD, Dobra'), ('SYP', 'SYP, Pound'), ('SZL', 'SZL, Lilangeni'), ('THB', 'THB, Baht'), ('TJS', 'TJS, Somoni'), ('TMT', 'TMT, Manat'), ('TND', 'TND, Dinar'), ('TOP', 'TOP, Paanga'), ('TRY', 'TRY, Lira'), ('TTD', 'TTD, Dollar'), ('TWD', 'TWD, Dollar'), ('TZS', 'TZS, Shilling'), ('UAH', 'UAH, Hryvnia'), ('UGX', 'UGX, Shilling'), ('USD', '$, Dollar'), ('UYU', 'UYU, Peso'), ('UZS', 'UZS, Som'), ('VEF', 'VEF, Bolivar'), ('VND', 'VND, Dong'), ('VUV', 'VUV, Vatu'), ('WST', 'WST, Tala'), ('XAF', 'XAF, Franc'), ('XCD', 'XCD, Dollar'), ('XOF', 'XOF, Franc'), ('XPF', 'XPF, Franc'), ('YER', 'YER, Rial'), ('ZAR', 'ZAR, Rand'), ('ZMK', 'ZMK, Kwacha'), ('ZWL', 'ZWL, Dollar')], max_length=3, null=True)), - ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None)), - ('amount_due', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('amount_paid', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('is_email_sent', models.BooleanField(default=False)), - ('status', models.CharField(choices=[('Draft', 'Draft'), ('Sent', 'Sent'), ('Paid', 'Paid'), ('Pending', 'Pending'), ('Cancelled', 'Cancel')], default='Draft', max_length=15)), - ('details', models.TextField(blank=True, null=True, verbose_name='Details')), - ('due_date', models.DateField(blank=True, null=True)), - ('tax', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('accounts', models.ManyToManyField(related_name='accounts_invoices', to='accounts.account')), - ('assigned_to', models.ManyToManyField(related_name='invoice_assigned_to', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_created_by', to=settings.AUTH_USER_MODEL)), - ('from_address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_from_address', to='common.address')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.org')), - ('teams', models.ManyToManyField(related_name='invoices_teams', to='teams.teams')), - ('to_address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_to_address', to='common.address')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Invoice', - 'verbose_name_plural': 'Invoices', - 'db_table': 'invoice', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='InvoiceHistory', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('invoice_title', models.CharField(max_length=50, verbose_name='Invoice Title')), - ('invoice_number', models.CharField(max_length=50, verbose_name='Invoice Number')), - ('name', models.CharField(max_length=100, verbose_name='Name')), - ('email', models.EmailField(max_length=254, verbose_name='Email')), - ('quantity', models.PositiveIntegerField(default=0)), - ('rate', models.DecimalField(decimal_places=2, default=0, max_digits=12)), - ('total_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('currency', models.CharField(blank=True, choices=[('AED', 'AED, Dirham'), ('AFN', 'AFN, Afghani'), ('ALL', 'ALL, Lek'), ('AMD', 'AMD, Dram'), ('ANG', 'ANG, Guilder'), ('AOA', 'AOA, Kwanza'), ('ARS', 'ARS, Peso'), ('AUD', 'AUD, Dollar'), ('AWG', 'AWG, Guilder'), ('AZN', 'AZN, Manat'), ('BAM', 'BAM, Marka'), ('BBD', 'BBD, Dollar'), ('BDT', 'BDT, Taka'), ('BGN', 'BGN, Lev'), ('BHD', 'BHD, Dinar'), ('BIF', 'BIF, Franc'), ('BMD', 'BMD, Dollar'), ('BND', 'BND, Dollar'), ('BOB', 'BOB, Boliviano'), ('BRL', 'BRL, Real'), ('BSD', 'BSD, Dollar'), ('BTN', 'BTN, Ngultrum'), ('BWP', 'BWP, Pula'), ('BYR', 'BYR, Ruble'), ('BZD', 'BZD, Dollar'), ('CAD', 'CAD, Dollar'), ('CDF', 'CDF, Franc'), ('CHF', 'CHF, Franc'), ('CLP', 'CLP, Peso'), ('CNY', 'CNY, Yuan Renminbi'), ('COP', 'COP, Peso'), ('CRC', 'CRC, Colon'), ('CUP', 'CUP, Peso'), ('CVE', 'CVE, Escudo'), ('CZK', 'CZK, Koruna'), ('DJF', 'DJF, Franc'), ('DKK', 'DKK, Krone'), ('DOP', 'DOP, Peso'), ('DZD', 'DZD, Dinar'), ('EGP', 'EGP, Pound'), ('ERN', 'ERN, Nakfa'), ('ETB', 'ETB, Birr'), ('EUR', 'EUR, Euro'), ('FJD', 'FJD, Dollar'), ('FKP', 'FKP, Pound'), ('GBP', 'GBP, Pound'), ('GEL', 'GEL, Lari'), ('GHS', 'GHS, Cedi'), ('GIP', 'GIP, Pound'), ('GMD', 'GMD, Dalasi'), ('GNF', 'GNF, Franc'), ('GTQ', 'GTQ, Quetzal'), ('GYD', 'GYD, Dollar'), ('HKD', 'HKD, Dollar'), ('HNL', 'HNL, Lempira'), ('HRK', 'HRK, Kuna'), ('HTG', 'HTG, Gourde'), ('HUF', 'HUF, Forint'), ('IDR', 'IDR, Rupiah'), ('ILS', 'ILS, Shekel'), ('INR', 'INR, Rupee'), ('IQD', 'IQD, Dinar'), ('IRR', 'IRR, Rial'), ('ISK', 'ISK, Krona'), ('JMD', 'JMD, Dollar'), ('JOD', 'JOD, Dinar'), ('JPY', 'JPY, Yen'), ('KES', 'KES, Shilling'), ('KGS', 'KGS, Som'), ('KHR', 'KHR, Riels'), ('KMF', 'KMF, Franc'), ('KPW', 'KPW, Won'), ('KRW', 'KRW, Won'), ('KWD', 'KWD, Dinar'), ('KYD', 'KYD, Dollar'), ('KZT', 'KZT, Tenge'), ('LAK', 'LAK, Kip'), ('LBP', 'LBP, Pound'), ('LKR', 'LKR, Rupee'), ('LRD', 'LRD, Dollar'), ('LSL', 'LSL, Loti'), ('LTL', 'LTL, Litas'), ('LVL', 'LVL, Lat'), ('LYD', 'LYD, Dinar'), ('MAD', 'MAD, Dirham'), ('MDL', 'MDL, Leu'), ('MGA', 'MGA, Ariary'), ('MKD', 'MKD, Denar'), ('MMK', 'MMK, Kyat'), ('MNT', 'MNT, Tugrik'), ('MOP', 'MOP, Pataca'), ('MRO', 'MRO, Ouguiya'), ('MUR', 'MUR, Rupee'), ('MVR', 'MVR, Rufiyaa'), ('MWK', 'MWK, Kwacha'), ('MXN', 'MXN, Peso'), ('MYR', 'MYR, Ringgit'), ('MZN', 'MZN, Metical'), ('NAD', 'NAD, Dollar'), ('NGN', 'NGN, Naira'), ('NIO', 'NIO, Cordoba'), ('NOK', 'NOK, Krone'), ('NPR', 'NPR, Rupee'), ('NZD', 'NZD, Dollar'), ('OMR', 'OMR, Rial'), ('PAB', 'PAB, Balboa'), ('PEN', 'PEN, Sol'), ('PGK', 'PGK, Kina'), ('PHP', 'PHP, Peso'), ('PKR', 'PKR, Rupee'), ('PLN', 'PLN, Zloty'), ('PYG', 'PYG, Guarani'), ('QAR', 'QAR, Rial'), ('RON', 'RON, Leu'), ('RSD', 'RSD, Dinar'), ('RUB', 'RUB, Ruble'), ('RWF', 'RWF, Franc'), ('SAR', 'SAR, Rial'), ('SBD', 'SBD, Dollar'), ('SCR', 'SCR, Rupee'), ('SDG', 'SDG, Pound'), ('SEK', 'SEK, Krona'), ('SGD', 'SGD, Dollar'), ('SHP', 'SHP, Pound'), ('SLL', 'SLL, Leone'), ('SOS', 'SOS, Shilling'), ('SRD', 'SRD, Dollar'), ('SSP', 'SSP, Pound'), ('STD', 'STD, Dobra'), ('SYP', 'SYP, Pound'), ('SZL', 'SZL, Lilangeni'), ('THB', 'THB, Baht'), ('TJS', 'TJS, Somoni'), ('TMT', 'TMT, Manat'), ('TND', 'TND, Dinar'), ('TOP', 'TOP, Paanga'), ('TRY', 'TRY, Lira'), ('TTD', 'TTD, Dollar'), ('TWD', 'TWD, Dollar'), ('TZS', 'TZS, Shilling'), ('UAH', 'UAH, Hryvnia'), ('UGX', 'UGX, Shilling'), ('USD', '$, Dollar'), ('UYU', 'UYU, Peso'), ('UZS', 'UZS, Som'), ('VEF', 'VEF, Bolivar'), ('VND', 'VND, Dong'), ('VUV', 'VUV, Vatu'), ('WST', 'WST, Tala'), ('XAF', 'XAF, Franc'), ('XCD', 'XCD, Dollar'), ('XOF', 'XOF, Franc'), ('XPF', 'XPF, Franc'), ('YER', 'YER, Rial'), ('ZAR', 'ZAR, Rand'), ('ZMK', 'ZMK, Kwacha'), ('ZWL', 'ZWL, Dollar')], max_length=3, null=True)), - ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None)), - ('amount_due', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('amount_paid', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('is_email_sent', models.BooleanField(default=False)), - ('status', models.CharField(choices=[('Draft', 'Draft'), ('Sent', 'Sent'), ('Paid', 'Paid'), ('Pending', 'Pending'), ('Cancelled', 'Cancel')], default='Draft', max_length=15)), - ('details', models.TextField(blank=True, null=True, verbose_name='Details')), - ('due_date', models.DateField(blank=True, null=True)), - ('assigned_to', models.ManyToManyField(related_name='invoice_history_assigned_to', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('from_address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_history_from_address', to='common.address')), - ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoice_history', to='invoices.invoice')), - ('to_address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_history_to_address', to='common.address')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_history_created_by', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'InvoiceHistory', - 'verbose_name_plural': 'InvoiceHistories', - 'db_table': 'invoice_history', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/invoices/swagger_params.py b/invoices/swagger_params.py deleted file mode 100644 index 7c7d9e00..00000000 --- a/invoices/swagger_params.py +++ /dev/null @@ -1,74 +0,0 @@ -from drf_yasg import openapi - -company_params_in_header = openapi.Parameter( - "org", openapi.IN_HEADER, required=True, type=openapi.TYPE_STRING -) - -invoice_list_get_params = [ - company_params_in_header, - openapi.Parameter( - "invoice_title_or_number", openapi.IN_QUERY, type=openapi.TYPE_STRING - ), - openapi.Parameter("created_by", openapi.IN_QUERY, type=openapi.TYPE_INTEGER), - openapi.Parameter("assigned_users", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("status", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("total_amount", openapi.IN_QUERY, type=openapi.TYPE_STRING), -] - -invoice_detail_post_params = [ - company_params_in_header, - openapi.Parameter( - "invoice_attachment", - openapi.IN_QUERY, - type=openapi.TYPE_FILE, - ), - openapi.Parameter("comment", openapi.IN_QUERY, type=openapi.TYPE_STRING), -] - -invoice_delete_params = [ - company_params_in_header, -] - -invoice_create_post_params = [ - company_params_in_header, - openapi.Parameter( - "invoice_title", openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING - ), - openapi.Parameter("status", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("from_address_line", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("from_street", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("from_city", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("from_state", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("from_postcode", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("from_country", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("to_address_line", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("to_street", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("to_city", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("to_state", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("to_postcode", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("to_country", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("name", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter( - "email", openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING - ), - openapi.Parameter("phone", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter( - "due_date", openapi.IN_QUERY, type=openapi.FORMAT_DATE, example="2021-02-28" - ), - openapi.Parameter( - "currency", openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING - ), - openapi.Parameter("teams", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("assigned_to", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("accounts", openapi.IN_QUERY, type=openapi.TYPE_STRING), - openapi.Parameter("quality_hours", openapi.IN_QUERY, type=openapi.TYPE_INTEGER), - openapi.Parameter("rate", openapi.IN_QUERY, type=openapi.TYPE_INTEGER), - openapi.Parameter("tax", openapi.IN_QUERY, type=openapi.TYPE_INTEGER), - openapi.Parameter("total_amount", openapi.IN_QUERY, type=openapi.TYPE_INTEGER), - openapi.Parameter("details", openapi.IN_QUERY, type=openapi.TYPE_STRING), -] - -invoice_comment_edit_params = [ - company_params_in_header, - openapi.Parameter("comment", openapi.IN_QUERY, type=openapi.TYPE_STRING), -] diff --git a/invoices/swagger_params1.py b/invoices/swagger_params1.py deleted file mode 100644 index 37ab528e..00000000 --- a/invoices/swagger_params1.py +++ /dev/null @@ -1,79 +0,0 @@ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter - -organization_params_in_header = organization_params_in_header = OpenApiParameter( - "org", OpenApiTypes.STR, OpenApiParameter.HEADER -) - -organization_params = [ - organization_params_in_header, -] - -invoice_list_get_params = [ - organization_params, - OpenApiParameter( - "invoice_title_or_number",OpenApiParameter.QUERY, OpenApiTypes.STR - ), - OpenApiParameter("created_by",OpenApiParameter.QUERY, OpenApiTypes.INT), - OpenApiParameter("assigned_users",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("status",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("total_amount",OpenApiParameter.QUERY, OpenApiTypes.STR), -] - -invoice_detail_post_params = [ - organization_params, - OpenApiParameter( - "invoice_attachment", - OpenApiParameter.QUERY, - type=openapi.TYPE_FILE, - ), - OpenApiParameter("comment",OpenApiParameter.QUERY, OpenApiTypes.STR), -] - -invoice_delete_params = [ - organization_params, -] - -invoice_create_post_params = [ - organization_params, - OpenApiParameter( - "invoice_title",OpenApiParameter.QUERY, OpenApiTypes.STR, required=True, - ), - OpenApiParameter("status",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("from_address_line",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("from_street",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("from_city",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("from_state",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("from_postcode",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("from_country",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("to_address_line",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("to_street",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("to_city",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("to_state",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("to_postcode",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("to_country",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("name",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter( - "email",OpenApiParameter.QUERY, OpenApiTypes.STR, required=True, - ), - OpenApiParameter("phone",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter( - "due_date",OpenApiParameter.QUERY, type=openapi.FORMAT_DATE, example="2021-02-28" - ), - OpenApiParameter( - "currency",OpenApiParameter.QUERY, OpenApiTypes.STR,required=True, - ), - OpenApiParameter("teams",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("assigned_to",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("accounts",OpenApiParameter.QUERY, OpenApiTypes.STR), - OpenApiParameter("quality_hours",OpenApiParameter.QUERY, OpenApiTypes.INT), - OpenApiParameter("rate",OpenApiParameter.QUERY, OpenApiTypes.INT), - OpenApiParameter("tax",OpenApiParameter.QUERY, OpenApiTypes.INT), - OpenApiParameter("total_amount",OpenApiParameter.QUERY, OpenApiTypes.INT), - OpenApiParameter("details",OpenApiParameter.QUERY, OpenApiTypes.STR), -] - -invoice_comment_edit_params = [ - organization_params, - OpenApiParameter("comment",OpenApiParameter.QUERY, OpenApiTypes.STR), -] diff --git a/invoices/templates/invoice_create_1.html b/invoices/templates/invoice_create_1.html deleted file mode 100644 index e2f54c77..00000000 --- a/invoices/templates/invoice_create_1.html +++ /dev/null @@ -1,548 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% block breadcrumb %} -{% block extralinks %} - -{% endblock %} - -{% endblock %} -{% block content %} -
-
-
-
-
-
-
-

{% if invoice_obj %}Edit{% else %}Create{% endif %} Invoice

-
-
-
-
- -
- - {{ form.invoice_title }} - {{ form.invoice_title.errors }} -
-
-
-
-
- -
- - {{ form.status }} - {{ form.status.errors }} - -
-
-
- {% if invoice_obj %} -
-
- -
- - {{ form.invoice_number.errors }} -
-
-
- {% endif %} -
-
-
- -
- - {{ from_address_form.address_line }} - {{ from_address_form.address_line.errors }} -
- {{ from_address_form.street }} - {{ from_address_form.street.errors }} -
-
- {{ from_address_form.city }} - {{ from_address_form.city.errors }} - {{ to_address_form.city.errors }} -
-
- {{ from_address_form.state }} - {{ from_address_form.state.errors }} -
-
- {{ from_address_form.postcode }} - {{ from_address_form.postcode.errors }} -
-
- {{ from_address_form.country }} - {{ from_address_form.country.errors }} -
-
-
-
-
- -
- - {{ to_address_form.address_line }} - {{ to_address_form.address_line.errors }} -
- {{ to_address_form.street }} - {{ to_address_form.street.errors }} -
-
- {{ to_address_form.city }} - {{ to_address_form.city.errors }} - {{ to_address_form.city.errors }} -
-
- {{ to_address_form.state }} - {{ to_address_form.state.errors }} -
-
- {{ to_address_form.postcode }} - {{ to_address_form.postcode.errors }} -
-
- {{ to_address_form.country }} - {{ to_address_form.country.errors }} -
-
-
-
-
-
-
-
- -
- - {{ form.name }} - {{ form.name.errors }} -
-
-
- -
- - {{ form.email }} - {{ form.email.errors }} -
-
-
- -
- {{ form.phone }} - {{ form.phone.errors }} - -
-
-
- -
-
- {{ form.due_date }} - - - -
- {{ form.due_date.errors }} -
-
-
-
-
- -
- {{ form.currency }} - {{ form.currency.errors }} -
-
- {% if request.user.is_superuser or request.user.role == 'ADMIN' %} -
- -
- - - {{ form.teams.errors }} -
-
- - -
- -
- -
-
- - - {% endif %} -
- -
- -
- {{ form.assigned_to.errors }} -
-
- -
- - {{form.accounts}} - {{ form.accounts.errors }} -
-
- -
-
-
-
- - - {{ form.quantity }} - {{ form.quantity.errors }} -
-
- - {{ form.rate }} - {{ form.rate.errors }} -
-
- - {{ form.tax }} - {{ form.tax.errors }} -
-
- - {{ form.total_amount }} - {{ form.total_amount.errors }} - -
-
-
-
-
- - {{form.details}} - {{ form.details.errors }} -
-
-
-
-
-
- - Cancel -
-
-
-
-
-
-
-
-
-
- {% endblock %} - {% block js_block %} - - -{% endblock js_block %} \ No newline at end of file diff --git a/invoices/templates/invoice_download_pdf.html b/invoices/templates/invoice_download_pdf.html deleted file mode 100644 index 9c725262..00000000 --- a/invoices/templates/invoice_download_pdf.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if invoice.footer_text %} - - {% endif %} - - -
- {{ invoice.invoice_number }} -
- -
- - {% if invoice.from_address.address_line %} - {{ invoice.from_address.address_line }}
- {% endif %} - {% if invoice.from_address.street %} - {{ invoice.from_address.street }}, - {% endif %} - {% if invoice.from_address.city %} - {{ invoice.from_address.city }}
- {% endif %} - {% if invoice.from_address.state %} - {{ invoice.from_address.state }}, - {% endif %} - {% if invoice.from_address.postcode %} - {{ invoice.from_address.postcode }}
- {% endif %} - {% if invoice.from_address.country %} - {{ invoice.from_address.country }} - {% endif %} -
- - - {% if invoice.created_on %} - -
- Date -

{{ invoice.created_on|date:'d-m-Y' }}

-
- {% endif %} - -
-
    -
  • {{ invoice.email }}
  • - {% if invoice.name %} -
  • {{ invoice.name }}
  • - {% endif %} - {% if invoice.phone %} -
  • {{ invoice.phone }}
  • - {% endif %} -
-
- - {% if invoice.to_address.address_line %} - {{ invoice.to_address.address_line }}
- {% endif %} - {% if invoice.to_address.street %} - {{ invoice.to_address.street }}, - {% endif %} - {% if invoice.to_address.city %} - {{ invoice.to_address.city }}
- {% endif %} - {% if invoice.to_address.state %} - {{ invoice.to_address.state }}, - {% endif %} - {% if invoice.to_address.postcode %} - {{ invoice.to_address.postcode }}
- {% endif %} - {% if invoice.to_address.country %} - {{ invoice.to_address.country }} - {% endif %} -
- {% if invoice.due_date %} -
- Due Date -

{{ invoice.due_date|date:'d-m-Y' }}

-
- {% endif %} -
-   -
-
- {% if invoice.invoice_title %} -
-

{{ invoice.invoice_title|upper }}

-
- {% endif %} -
-
-
- {% if invoice.details %} -
-

{{ invoice.details }}

-
- {% endif %} -
-
- - - - - - - - {% if invoice.quantity %} - - - {% endif %} - - - {% if invoice.rate %} - - - {% endif %} - - - {% if invoice.tax %} - - - {% endif %} - - - - - - - -
- Invoice Quantity - {{ invoice.quantity }}
- Invoice Rate - {{ invoice.rate }}
- Tax - {{ invoice.tax }}
- Invoice Total - {{ invoice.formatted_total_amount }}
-
-
- {{ invoice.footer_text|safe }} -
-
- diff --git a/invoices/templates/invoices_create.html b/invoices/templates/invoices_create.html deleted file mode 100644 index 3d2d3e91..00000000 --- a/invoices/templates/invoices_create.html +++ /dev/null @@ -1,267 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% block breadcrumb %} - -{% endblock %} -{% block content %} -
-
-
-
-
-
- {% if invoice_obj %}EDIT{% else %}CREATE{% endif %} INVOICE -
-
-
-
-
- - {{ form.invoice_title }} - {{ form.invoice_title.errors }} -
-
-
-
- - {{ form.invoice_number }} - {{ form.invoice_number.errors }} -
-
-
-
- - {{ form.name }} - {{ form.name.errors }} -
-
-
-
- - {{ form.email }} - {{ form.email.errors }} -
-
-
-
- - {{ form.currency }} - {{ form.currency.errors }} -
-
-
-
- - {{ form.phone }} - {{ form.phone.errors }} -
-
-
-
-
-
-
- - {{ form.quantity }} - {{ form.quantity.errors }} -
-
-
-
- - {{ form.rate }} - {{ form.rate.errors }} -
-
-
-
- - {{ form.total_amount }} - {{ form.total_amount.errors }} -
-
-
-
-
- - {{ form.status }} - {{ form.status.errors }} -
-
-
-
- - {{form.assigned_to}} - {{ form.assigned_to.errors }} -
-
-
-
- -
- {{ form.due_date }} - - - -
- {{ form.due_date.errors }} -
-
-
-
-
-
-
-
- - {{form.details}} - {{ form.details.errors }} -
-
-
-
- - {{ from_address_form.address_line }} - {{ from_address_form.address_line.errors }} -
- {{ from_address_form.street }} - {{ from_address_form.street.errors }} -
-
- {{ from_address_form.city }} - {{ from_address_form.city.errors }} - {{ to_address_form.city.errors }} -
-
- {{ from_address_form.state }} - {{ from_address_form.state.errors }} -
-
- {{ from_address_form.postcode }} - {{ from_address_form.postcode.errors }} -
-
- {{ from_address_form.country }} - {{ from_address_form.country.errors }} -
-
-
-
-
-
- - {{ to_address_form.address_line }} - {{ to_address_form.address_line.errors }} -
- {{ to_address_form.street }} - {{ to_address_form.street.errors }} -
-
- {{ to_address_form.city }} - {{ to_address_form.city.errors }} - {{ to_address_form.city.errors }} -
-
- {{ to_address_form.state }} - {{ to_address_form.state.errors }} -
-
- {{ to_address_form.postcode }} - {{ to_address_form.postcode.errors }} -
-
- {{ to_address_form.country }} - {{ to_address_form.country.errors }} -
-
-
-
-
-
-
-
-
- - Cancel -
-
-
-
-
-
- -
- -
-{% endblock %} -{% block js_block %} - -{% endblock js_block %} \ No newline at end of file diff --git a/invoices/templates/invoices_detail.html b/invoices/templates/invoices_detail.html deleted file mode 100644 index d62ad7f1..00000000 --- a/invoices/templates/invoices_detail.html +++ /dev/null @@ -1,524 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load thumbnail %} -{% block breadcrumb %} -{% block extralinks %} - - -{% endblock extralinks %} - - -{% endblock breadcrumb %} -{% block content %} - -
-
-
-
-
-
-
- Overview - - - -
-
-
-
- {% if invoice.invoice_title %} -
-
- - -
-
- {% endif %} - {% if invoice.invoice_number %} -
-
- - -
-
- {% endif %} - {% if invoice.name %} -
-
- - -
-
- {% endif %} - {% if invoice.email %} -
-
- - -
-
- {% endif %} - {% if invoice.phone %} -
-
- - -
-
- {% endif %} -
-
- {% if invoice.quantity %} -
-
- - -
-
- {% endif %} - {% if invoice.rate %} -
-
- - -
-
- {% endif %} - {% if invoice.total_amount %} -
-
- - -
-
- {% endif %} - {% if invoice.currency %} -
-
- - -
-
- {% endif %} - {% if invoice.status %} -
-
- - -
-
- {% endif %} -
-
- - {% if invoice.is_email_sent %} - - {% else %} - - {% endif %} -
-
-
-
- - {% if invoice.due_date %} -
-
- - -
-
- {% endif %} - - {% if invoice.details %} -
-
- - -
-
- {% endif %} - - {% if invoice.assigned_to.all %} -
-
- - {% for user in invoice.assigned_to.all %} - - {% endfor %} -
-
- {% endif %} -
-
- {% if invoice.from_address.address_line or invoice.from_address.street or invoice.from_address.city or invoice.from_address.state or invoice.from_address.postcode or invoice.from_address.country %} - -
- {{invoice.from_address.get_complete_address}} -
- {% endif %} -
-
-
-
- {% if invoice.to_address.address_line or invoice.to_address.street or invoice.to_address.city or invoice.to_address.state or invoice.to_address.postcode or invoice.to_address.country %} - -
- {{invoice.to_address.get_complete_address}} -
- {% endif %} -
-
-
-
-
- Created by {{ invoice.created_by }} created on - {{ invoice.created_on }} -
-
-
- -
- -
-
-
Attachments
-
-
-
-
-
- -
- -
-
-
- -
-
- -
-
    - {% for attachment in attachments %} -
  • - {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == attachment.created_by %} -
    -
    - -
    -
    - {% endif %} -
    - {% if attachment.attachment %} - {% if 'image' in attachment.file_type %} - {% thumbnail attachment.attachment "80x80" crop="center" as im %} - - {% endthumbnail %} - {% endif %} - {% endif %} -
      {{attachment.file_name}} Download
    -
    -
    -
    Uploaded by :{{ attachment.created_by }}
    -
    {{ attachment.created_on|date:'m/d/Y,  h:i A' }}
    -
    -
  • - {% endfor %} -
-
-
-
- - -
-
-
Comments
-
-
-
-
-
- -
- -
-
-
- -
-
- -
-
    - {% for comment in comments %} -
  • - {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == comment.commented_by %} -
    - - -
    - {% endif %} -
    -
    {{ comment.comment }}
    -
    -
    -
    {{ comment.commented_by }}
    -
    {{ comment.commented_on|date:'m/d/Y,  h:i:s A' }}
    -
    -
  • - {% endfor %} -
-
-
- -
- -
-
-
-
-
-
-
-
- -
- Processing File... -
-
-
- -
-
- - -
- - - - -{% endblock content %} -{% block js_block %} - - - -{% endblock js_block %} \ No newline at end of file diff --git a/invoices/templates/invoices_detail_1.html b/invoices/templates/invoices_detail_1.html deleted file mode 100644 index 4387aea7..00000000 --- a/invoices/templates/invoices_detail_1.html +++ /dev/null @@ -1,636 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load paginate %} -{% load thumbnail %} -{% block breadcrumb %} -{% block extralinks %} - - -{% endblock extralinks %} - - -{% endblock breadcrumb %} -{% block content %} - -
-
-
-
-
-
-
- -
-
- Overview - - - -
-
-
-
-
-
- {% if invoice.invoice_title %} -
- -
- {{ invoice.invoice_title }} -
-
- {% endif %} -
-
- {% if invoice.status %} -
- -
- {{ invoice.status }} -
-
- {% endif %} - -
- {% if invoice.invoice_number %} -
-
- -
- {{ invoice.invoice_number }} -
-
-
- {% endif %} - - - - -
-
- {% if invoice.from_address.address_line or invoice.from_address.street or invoice.from_address.city or invoice.from_address.state or invoice.from_address.postcode or invoice.from_address.country %} -
- -
- -
- {{invoice.from_address.get_complete_address}} -
-
-
-
- {% endif %} - {% if invoice.to_address.address_line or invoice.to_address.street or invoice.to_address.city or invoice.to_address.state or invoice.to_address.postcode or invoice.to_address.country %} -
- -
- -
- {{invoice.to_address.get_complete_address}} -
-
-
-
- {% endif %} -
-
-
- {% if invoice.name %} -
- -
- {{ invoice.name }} -
-
- {% endif %} -
- {% if invoice.email %} - -
- {{ invoice.email }} -
- {% endif %} -
-
- {% if invoice.email %} - -
- {{ invoice.phone }} -
- {% endif %} -
-
-
-
- {% if invoice.currency %} - -
- {{ invoice.currency }} -
- {% endif %} -
-
- {% if invoice.assigned_to.all %} - -
- {% for user in invoice.assigned_to.all %} - - {% endfor %} -
- {% endif %} -
-
- {% if invoice.due_date %} - -
- {{ invoice.due_date }} -
- {% endif %} -
-
-
-
- {% if invoice.quantity %} -
- - {{ invoice.quantity }} -
- {% endif %} - {% if invoice.rate %} -
- - {{ invoice.rate }} -
- {% endif %} - {% if invoice.tax %} -
- - {{ invoice.tax }} -
- {% endif %} - {% if invoice.total_amount %} -
- - {{ invoice.total_amount }} -
- {% endif %} -
-
-
-
- {% if invoice.details %} - -
- {{ invoice.details }} - {% endif %} -
-
-
-
- Created by {{ invoice.created_by }} on {{ invoice.created_on_arrow }} -
-
-
-
-
-
- -
-
-
Attachments
-
-
-
-
-
- -
- -
-
-
- -
-
- -
-
    - {% for attachment in attachments %} -
  • - {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == attachment.created_by %} -
    -
    - -
    -
    - {% endif %} -
    - {% if attachment.attachment %} - {% if 'image' in attachment.file_type %} - {% thumbnail attachment.attachment "80x80" crop="center" as im %} - - {% endthumbnail %} - {% endif %} - {% endif %} -
      {{attachment.file_name}} Download
    -
    -
    -
    Uploaded by :{{ attachment.created_by }}
    -
    {{ attachment.created_on_arrow }}
    -
    -
  • - {% endfor %} -
-
-
-
- -
- -
-
-
Comments
-
-
-
-
-
- -
- -
-
-
- -
-
- -
-
    - {% for comment in comments %} -
  • - {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == comment.commented_by %} -
    - - -
    - {% endif %} -
    -
    {{ comment.comment }}
    -
    -
    -
    {{ comment.commented_by }}
    -
    {{ comment.commented_on_arrow }}
    -
    -
  • - {% endfor %} -
-
-
- -
- -
-
-
-
-
- Invoice History -
-
- - {% if invoice_history|length > 0 %} - - - - - - - - - - - - {% endif %} - - {% if per_page %} - {% paginate per_page invoice_history %} - {% else%} - {% paginate 10 invoice_history %} - {% endif %} - {% for invoice in invoice_history %} - - - - - - - - - - {% endfor %} - -
S.NoDateInvoice NumberNameStatus Description Amount
{{ forloop.counter }} - {{ invoice.created_on_arrow }}{{ invoice.invoice_number }} - {% if invoice.updated_by %} - {% if invoice.updated_by.profile_pic %} - {% thumbnail invoice.updated_by.profile_pic "40x40" crop="center" as im %} - - {% endthumbnail %} - {% else %} - Micro profile pic - {% endif %} - {% else %} - Micro profile pic - {% endif %} - {{ invoice.status }}{{ invoice.details }}{{ invoice.total_amount }}
-
- {% show_pages %} -
-
- {% ifequal invoice_history|length 0 %} -
No Invoice History Records Found
- - {% endifequal %} -
-
-
-
-
-
-
-
-
-
-
-
- Processing File... -
-
-
-
-
-{% endblock content %} -{% block js_block %} - - - -{% endblock js_block %} \ No newline at end of file diff --git a/invoices/templates/invoices_list.html b/invoices/templates/invoices_list.html deleted file mode 100644 index 8d76d144..00000000 --- a/invoices/templates/invoices_list.html +++ /dev/null @@ -1,605 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% load paginate %} -{% load thumbnail %} -{% block extralinks %} - -{% endblock %} -{% block content %} - -
- - - -
-
-
-
-
-
-
Filters
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- -
-
- - Clear -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Invoices - {% if show_pageitems %} - {% show_pageitems %}{% else %} - {{ invoices|length }}{% endif %} - - - -
-
- - - {% if invoices|length > 0 %} - - - - - - - - - - - - {% endif %} - - - {% if per_page %} - {% paginate per_page invoices %} - {% else %} - {% paginate 10 invoices %} - {% endif %} - {% for invoice in invoices %} - {% if invoice.due_date < today %} - - - - - - - - - - - - {% else %} - - - - - - - - - - - - {% endif %} - {% endfor %} - -
IDInvoice TitleInvoice NumberEmailStatusAssigned ToCreated byCreated OnActions
{{ forloop.counter }} - {{ invoice.invoice_title }} {{ invoice.invoice_number }} {{ invoice.email }} {{ invoice.status }} - {% with users=invoice.get_team_and_assigned_users %} - {% for user in users %} - {% if user.profile_pic %} - {% thumbnail user.profile_pic "40x40" crop="center" as im %} - - - - {% endthumbnail %} - {% else %} - - - - {% endif %} - {% empty %} - None - {% endfor %} - {% endwith %} - - {% if invoice.created_by %} - {% if invoice.created_by.profile_pic %} - {% thumbnail invoice.created_by.profile_pic "40x40" crop="center" as im %} - - {% endthumbnail %} - {% else %} - Micro profile pic - {% endif %} - {% else %} - None - {% endif %} - {{ invoice.created_on_arrow }} - - {% if request.user == invoice.created_by or request.user.role == "ADMIN" or request.user.is_superuser %} - - - {% if invoice.is_draft %} - - {% endif %} - {% if invoice.is_sent == True %} - - - - {% endif %} - {% if invoice.is_resent %} - - - {% endif %} - {% if invoice.is_paid_or_cancelled == True %} - - {% endif %} - {% endif %} -
{{ forloop.counter }} - {{ invoice.invoice_title }} {{ invoice.invoice_number }} {{ invoice.email }} {{ invoice.status }} - {% with users=invoice.get_team_and_assigned_users %} - {% for user in users %} - {% if user.profile_pic %} - {% thumbnail user.profile_pic "40x40" crop="center" as im %} - - - - {% endthumbnail %} - {% else %} - - - - {% endif %} - {% empty %} - None - {% endfor %} - {% endwith %} - - {% if invoice.created_by %} - {% if invoice.created_by.profile_pic %} - {% thumbnail invoice.created_by.profile_pic "40x40" crop="center" as im %} - - {% endthumbnail %} - {% else %} - Micro profile pic - {% endif %} - {% else %} - None - {% endif %} - {{ invoice.created_on_arrow }} - - {% if request.user == invoice.created_by or request.user.role == "ADMIN" or request.user.is_superuser %} - - - {% if invoice.is_draft %} - - {% endif %} - {% if invoice.is_sent == True %} - - - - {% endif %} - {% if invoice.is_resent %} - - - {% endif %} - {% if invoice.is_paid_or_cancelled == True %} - - {% endif %} - {% endif %} -
-
- {%ifequal invoices|length 0%} -
No Invoices Found
- {%endifequal%} -
- {% show_pages %} -
-
-
-
- - - -
-
-
-
-
- - -{% for invoice in invoices %} - - - - - - -{% endfor %} - - -{% endblock %} -{% block js_block %} - -{% endblock js_block %} diff --git a/invoices/views.py b/invoices/views.py deleted file mode 100644 index d5ab0294..00000000 --- a/invoices/views.py +++ /dev/null @@ -1,678 +0,0 @@ -# import io -# import os - -# import pdfkit -# from datetime import datetime -# from django.contrib.auth.decorators import login_required -# from django.contrib.auth.mixins import LoginRequiredMixin -# from django.contrib.sites.shortcuts import get_current_site -# from django.core.exceptions import PermissionDenied -# from django.db.models import Q -# from django.http.response import HttpResponse, JsonResponse -# from django.shortcuts import get_object_or_404, redirect, render, reverse -# from django.template.loader import render_to_string -# from django.views.generic import ( -# CreateView, -# DeleteView, -# DetailView, -# FormView, -# TemplateView, -# UpdateView, -# View, -# ) - -# from common.models import Address, Attachments, Comment, User -# from common.tasks import send_email_user_mentions -# from invoices.forms import ( -# InvoiceAddressForm, -# InvoiceAttachmentForm, -# InvoiceCommentForm, -# InvoiceForm, -# ) -# from invoices.models import Invoice -# from invoices.tasks import ( -# send_email, -# send_invoice_email, -# send_invoice_email_cancel, -# create_invoice_history, -# ) -# from common.access_decorators_mixins import ( -# sales_access_required, -# marketing_access_required, -# SalesAccessRequiredMixin, -# MarketingAccessRequiredMixin, -# ) -# from teams.models import Teams - - -# @login_required -# def get_teams_and_users(request): -# data = {} -# teams = Teams.objects.all() -# teams_data = [ -# {"team": team.id, "users": [user.id for user in team.users.all()]} -# for team in teams -# ] -# users = User.objects.all().values_list("id", flat=True) -# data["teams"] = teams_data -# data["users"] = list(users) -# return JsonResponse(data) - - -# @login_required -# @sales_access_required -# def invoices_list(request): - -# if request.user.role == "ADMIN" or request.user.is_superuser: -# users = User.objects.all() -# # elif request.user.google.all(): -# # # users = User.objects.none() -# # # users = User.objects.filter(id=request.user.id) -# # users = User.objects.filter(Q(role='ADMIN') | Q(id=request.user.id)) -# elif request.user.role == "USER": -# users = User.objects.filter(Q(role="ADMIN") | Q(id=request.user.id)) -# status = ( -# ("Draft", "Draft"), -# ("Sent", "Sent"), -# ("Paid", "Paid"), -# ("Pending", "Pending"), -# ("Cancel", "Cancel"), -# ) - -# if request.method == "GET": -# context = {} -# if request.user.role == "ADMIN" or request.user.is_superuser: -# invoices = Invoice.objects.all().distinct().order_by("-created_on") -# else: -# invoices = ( -# Invoice.objects.filter( -# Q(created_by=request.user) | Q(assigned_to=request.user) -# ) -# .distinct() -# .order_by("-created_on") -# ) -# context["invoices"] = invoices.filter(company=request.company).order_by( -# "-created_on" -# ) -# context["status"] = status -# context["users"] = users.filter(company=request.company) -# user_ids = list(invoices.values_list("created_by", flat=True)) -# user_ids.append(request.user.id) -# context["created_by_users"] = users.filter(is_active=True, id__in=user_ids) -# today = datetime.today().date() -# context["today"] = today -# return render(request, "invoices_list.html", context) - -# if request.method == "POST": -# context = {} -# context["status"] = status -# context["users"] = users.filter(company=request.company) -# invoices = Invoice.objects.filter(company=request.company).order_by( -# "-created_on" -# ) -# if request.user.role == "ADMIN" or request.user.is_superuser: -# invoices = invoices -# else: -# invoices = invoices.filter( -# Q(created_by=request.user) | Q(assigned_to=request.user) -# ).distinct() - -# if request.POST.get("invoice_title_number", None): -# invoices = invoices.filter( -# Q(invoice_title__icontains=request.POST.get("invoice_title_number")) -# | Q(invoice_number__icontains=request.POST.get("invoice_title_number")) -# ) - -# if request.POST.get("created_by", None): -# invoices = invoices.filter(created_by__id=request.POST.get("created_by")) - -# if request.POST.getlist("assigned_to", None): -# invoices = invoices.filter( -# assigned_to__in=request.POST.getlist("assigned_to") -# ) -# context["assigned_to"] = request.POST.getlist("assigned_to") - -# if request.POST.get("status", None): -# invoices = invoices.filter(status=request.POST.get("status")) - -# if request.POST.get("total_amount", None): -# invoices = invoices.filter(total_amount=request.POST.get("total_amount")) - -# user_ids = list(invoices.values_list("created_by", flat=True)) -# user_ids.append(request.user.id) -# context["created_by_users"] = users.filter(is_active=True, id__in=user_ids) -# context["invoices"] = invoices.distinct().order_by("-created_on") -# today = datetime.today().date() -# context["today"] = today -# return render(request, "invoices_list.html", context) - - -# @login_required -# @sales_access_required -# def invoices_create(request): -# if request.method == "GET": -# context = {} -# context["form"] = InvoiceForm(request_user=request.user, request_obj=request) -# context["users"] = User.objects.filter(company=request.company) -# # "prefix" use case in form, both from address and to address use the -# # same model and same model form, so the name attribute in the form will -# # be same for the both forms and the address will be same for both -# # shipping and to address, so use prefix parameter to distinguish the -# # forms, don't remove this comment -# context["from_address_form"] = InvoiceAddressForm(prefix="from") -# context["to_address_form"] = InvoiceAddressForm(prefix="to") -# if request.user.role == "ADMIN" or request.user.is_superuser: -# context["teams"] = Teams.objects.filter(company=request.company) - -# return render(request, "invoice_create_1.html", context) - -# if request.method == "POST": -# form = InvoiceForm(request.POST, request_user=request.user, request_obj=request) -# from_address_form = InvoiceAddressForm(request.POST, prefix="from") -# to_address_form = InvoiceAddressForm(request.POST, prefix="to") -# if ( -# form.is_valid() -# and from_address_form.is_valid() -# and to_address_form.is_valid() -# ): -# from_address_obj = from_address_form.save() -# to_address_obj = to_address_form.save() -# invoice_obj = form.save(commit=False) -# invoice_obj.created_by = request.user -# invoice_obj.company = request.company -# invoice_obj.from_address = from_address_obj -# invoice_obj.to_address = to_address_obj -# invoice_obj.save() -# form.save_m2m() - -# if request.POST.getlist("teams", []): -# user_ids = Teams.objects.filter( -# id__in=request.POST.getlist("teams") -# ).values_list("users", flat=True) -# assinged_to_users_ids = invoice_obj.assigned_to.all().values_list( -# "id", flat=True -# ) -# for user_id in user_ids: -# if user_id not in assinged_to_users_ids: -# invoice_obj.assigned_to.add(user_id) - -# if request.POST.getlist("teams", []): -# invoice_obj.teams.add(*request.POST.getlist("teams")) - -# create_invoice_history(invoice_obj.id, request.user.id, []) -# kwargs = {"domain": request.get_host(), "protocol": request.scheme} -# assigned_to_list = list( -# invoice_obj.assigned_to.all().values_list("id", flat=True) -# ) -# send_email.delay(invoice_obj.id, assigned_to_list, **kwargs) -# if request.POST.get("from_account"): -# return JsonResponse( -# { -# "error": False, -# "success_url": reverse( -# "accounts:view_account", -# args=(request.POST.get("from_account"),), -# ), -# } -# ) -# return JsonResponse( -# {"error": False, "success_url": reverse("invoices:invoices_list")} -# ) -# else: -# return JsonResponse( -# { -# "error": True, -# "errors": form.errors, -# "from_address_errors": from_address_form.errors, -# "to_address_errors": to_address_form.errors, -# } -# ) - - -# @login_required -# @sales_access_required -# def invoice_details(request, invoice_id): -# invoice = get_object_or_404( -# Invoice.objects.select_related("from_address", "to_address"), pk=invoice_id -# ) - -# if invoice.company != request.company: -# raise PermissionDenied - -# user_assigned_account = False -# user_assigned_accounts = set( -# request.user.account_assigned_users.values_list("id", flat=True) -# ) -# invoice_accounts = set(invoice.accounts.values_list("id", flat=True)) -# if user_assigned_accounts.intersection(invoice_accounts): -# user_assigned_account = True - -# if not ( -# (request.user.role == "ADMIN") -# or (request.user.is_superuser) -# or (invoice.created_by == request.user) -# or (request.user in invoice.assigned_to.all()) -# or user_assigned_account -# ): -# raise PermissionDenied - -# if request.method == "GET": -# context = {} -# context["invoice"] = invoice -# context["attachments"] = invoice.invoice_attachment.all() -# context["comments"] = invoice.invoice_comments.all() -# context["invoice_history"] = invoice.invoice_history.all() -# if request.user.is_superuser or request.user.role == "ADMIN": -# context["users_mention"] = list( -# User.objects.filter(is_active=True, company=request.company).values( -# "username" -# ) -# ) -# elif request.user != invoice.created_by: -# context["users_mention"] = [{"username": invoice.created_by.username}] -# else: -# context["users_mention"] = list( -# invoice.assigned_to.all().values("username") -# ) -# return render(request, "invoices_detail_1.html", context) - - -# @login_required -# @sales_access_required -# def invoice_edit(request, invoice_id): -# invoice_obj = get_object_or_404(Invoice, pk=invoice_id) - -# if invoice_obj.company != request.company: -# raise PermissionDenied - -# if not ( -# request.user.role == "ADMIN" -# or request.user.is_superuser -# or invoice_obj.created_by == request.user -# or request.user in invoice_obj.assigned_to.all() -# ): -# raise PermissionDenied - -# if request.method == "GET": -# context = {} -# context["invoice_obj"] = invoice_obj -# context["teams"] = Teams.objects.filter(company=request.company) -# context["users"] = User.objects.filter(company=request.company) -# context["form"] = InvoiceForm( -# instance=invoice_obj, request_user=request.user, request_obj=request -# ) -# context["from_address_form"] = InvoiceAddressForm( -# prefix="from", instance=invoice_obj.from_address -# ) -# context["to_address_form"] = InvoiceAddressForm( -# prefix="to", instance=invoice_obj.to_address -# ) -# return render(request, "invoice_create_1.html", context) - -# if request.method == "POST": -# form = InvoiceForm( -# request.POST, -# instance=invoice_obj, -# request_user=request.user, -# request_obj=request, -# ) -# from_address_form = InvoiceAddressForm( -# request.POST, instance=invoice_obj.from_address, prefix="from" -# ) -# to_address_form = InvoiceAddressForm( -# request.POST, instance=invoice_obj.to_address, prefix="to" -# ) -# previous_assigned_to_users = list( -# invoice_obj.assigned_to.all().values_list("id", flat=True) -# ) -# if ( -# form.is_valid() -# and from_address_form.is_valid() -# and to_address_form.is_valid() -# ): -# form_changed_data = form.changed_data -# form_changed_data.remove("from_address") -# form_changed_data.remove("to_address") -# form_changed_data = form_changed_data + [ -# "from_" + field for field in from_address_form.changed_data -# ] -# form_changed_data = form_changed_data + [ -# "to_" + field for field in to_address_form.changed_data -# ] -# # if form_changed_data: -# # create_invoice_history(invoice_obj.id, request.user.id, form_changed_data) -# from_address_obj = from_address_form.save() -# to_address_obj = to_address_form.save() -# invoice_obj = form.save(commit=False) -# invoice_obj.created_by = request.user -# invoice_obj.from_address = from_address_obj -# invoice_obj.to_address = to_address_obj -# invoice_obj.save() -# form.save_m2m() -# if form_changed_data: -# create_invoice_history( -# invoice_obj.id, request.user.id, form_changed_data -# ) - -# if request.POST.getlist("teams", []): -# user_ids = Teams.objects.filter( -# id__in=request.POST.getlist("teams") -# ).values_list("users", flat=True) -# assinged_to_users_ids = invoice_obj.assigned_to.all().values_list( -# "id", flat=True -# ) -# for user_id in user_ids: -# if user_id not in assinged_to_users_ids: -# invoice_obj.assigned_to.add(user_id) - -# if request.POST.getlist("teams", []): -# invoice_obj.teams.clear() -# invoice_obj.teams.add(*request.POST.getlist("teams")) -# else: -# invoice_obj.teams.clear() - -# kwargs = {"domain": request.get_host(), "protocol": request.scheme} - -# assigned_to_list = list( -# invoice_obj.assigned_to.all().values_list("id", flat=True) -# ) -# recipients = list(set(assigned_to_list) - set(previous_assigned_to_users)) -# # send_email_to_assigned_user.delay(recipients, account_object.id, domain=current_site.domain, -# # protocol=self.request.scheme) - -# send_email.delay(invoice_obj.id, recipients, **kwargs) -# if request.POST.get("from_account"): -# return JsonResponse( -# { -# "error": False, -# "success_url": reverse( -# "accounts:view_account", -# args=(request.POST.get("from_account"),), -# ), -# } -# ) -# return JsonResponse( -# {"error": False, "success_url": reverse("invoices:invoices_list")} -# ) -# else: -# return JsonResponse( -# { -# "error": True, -# "errors": form.errors, -# "from_address_errors": from_address_form.errors, -# "to_address_errors": to_address_form.errors, -# } -# ) - - -# @login_required -# @sales_access_required -# def invoice_delete(request, invoice_id): -# invoice = get_object_or_404(Invoice, pk=invoice_id) - -# if invoice.company != request.company: -# raise PermissionDenied - -# if not ( -# request.user.role == "ADMIN" -# or request.user.is_superuser -# or invoice.created_by == request.user -# ): -# raise PermissionDenied - -# if request.method == "GET": -# invoice.delete() -# if request.GET.get("view_account"): -# return redirect( -# reverse( -# "accounts:view_account", args=(request.GET.get("view_account"),) -# ) -# ) -# return redirect("invoices:invoices_list") - - -# @login_required -# @sales_access_required -# def invoice_send_mail(request, invoice_id): -# invoice = get_object_or_404(Invoice, pk=invoice_id) -# if not ( -# request.user.role == "ADMIN" -# or request.user.is_superuser -# or invoice.created_by == request.user -# ): -# raise PermissionDenied - -# if request.method == "GET": -# Invoice.objects.filter(id=invoice_id).update(is_email_sent=True, status="Sent") -# kwargs = {"domain": request.get_host(), "protocol": request.scheme} -# send_invoice_email.delay(invoice_id, **kwargs) -# return redirect("invoices:invoices_list") - - -# @login_required -# @sales_access_required -# def invoice_change_status_paid(request, invoice_id): -# invoice = get_object_or_404(Invoice, pk=invoice_id) -# if not ( -# request.user.role == "ADMIN" -# or request.user.is_superuser -# or invoice.created_by == request.user -# ): -# raise PermissionDenied - -# if request.method == "GET": -# Invoice.objects.filter(id=invoice_id).update(status="Paid") -# kwargs = {"domain": request.get_host(), "protocol": request.scheme} -# # send_invoice_email.delay(invoice_id, **kwargs) -# return redirect("invoices:invoices_list") - - -# @login_required -# @sales_access_required -# def invoice_change_status_cancelled(request, invoice_id): -# invoice = get_object_or_404(Invoice, pk=invoice_id) -# if not ( -# request.user.role == "ADMIN" -# or request.user.is_superuser -# or invoice.created_by == request.user -# ): -# raise PermissionDenied - -# if request.method == "GET": -# Invoice.objects.filter(id=invoice_id).update(status="Cancelled") -# kwargs = {"domain": request.get_host(), "protocol": request.scheme} -# send_invoice_email_cancel.delay(invoice_id, **kwargs) -# return redirect("invoices:invoices_list") - - -# @login_required -# @sales_access_required -# def invoice_download(request, invoice_id): -# invoice = get_object_or_404( -# Invoice.objects.select_related("from_address", "to_address"), pk=invoice_id -# ) -# if not ( -# request.user.role == "ADMIN" -# or request.user.is_superuser -# or invoice.created_by == request.user -# or request.user in invoice.assigned_to.all() -# ): -# raise PermissionDenied - -# if request.method == "GET": -# context = {} -# context["invoice"] = invoice -# html = render_to_string("invoice_download_pdf.html", context=context) -# pdfkit.from_string(html, "out.pdf", options={"encoding": "UTF-8"}) -# pdf = open("out.pdf", "rb") -# response = HttpResponse(pdf.read(), content_type="application/pdf") -# response["Content-Disposition"] = "attachment; filename=Invoice.pdf" -# pdf.close() -# os.remove("out.pdf") -# return response - - -# class AddCommentView(LoginRequiredMixin, CreateView): -# model = Comment -# form_class = InvoiceCommentForm -# http_method_names = ["post"] - -# def post(self, request, *args, **kwargs): -# self.object = None -# self.invoice = get_object_or_404(Invoice, id=request.POST.get("invoice_id")) -# if ( -# request.user == self.invoice.created_by -# or request.user.is_superuser -# or request.user.role == "ADMIN" -# ): -# form = self.get_form() -# if form.is_valid(): -# return self.form_valid(form) -# return self.form_invalid(form) - -# data = {"error": "You don't have permission to comment for this account."} -# return JsonResponse(data) - -# def form_valid(self, form): -# comment = form.save(commit=False) -# comment.commented_by = self.request.user -# comment.invoice = self.invoice -# comment.save() -# comment_id = comment.id -# current_site = get_current_site(self.request) -# send_email_user_mentions.delay( -# comment_id, -# "invoices", -# domain=current_site.domain, -# protocol=self.request.scheme, -# ) -# return JsonResponse( -# { -# "comment_id": comment.id, -# "comment": comment.comment, -# "commented_on": comment.commented_on, -# "commented_on_arrow": comment.commented_on_arrow, -# "commented_by": comment.commented_by.email, -# } -# ) - -# def form_invalid(self, form): -# return JsonResponse({"error": form["comment"].errors}) - - -# class UpdateCommentView(LoginRequiredMixin, View): -# http_method_names = ["post"] - -# def post(self, request, *args, **kwargs): -# self.comment_obj = get_object_or_404(Comment, id=request.POST.get("commentid")) -# if request.user == self.comment_obj.commented_by: -# form = InvoiceCommentForm(request.POST, instance=self.comment_obj) -# if form.is_valid(): -# return self.form_valid(form) - -# return self.form_invalid(form) - -# data = {"error": "You don't have permission to edit this comment."} -# return JsonResponse(data) - -# def form_valid(self, form): -# self.comment_obj.comment = form.cleaned_data.get("comment") -# self.comment_obj.save(update_fields=["comment"]) -# comment_id = self.comment_obj.id -# current_site = get_current_site(self.request) -# send_email_user_mentions.delay( -# comment_id, -# "invoices", -# domain=current_site.domain, -# protocol=self.request.scheme, -# ) -# return JsonResponse( -# { -# "comment_id": self.comment_obj.id, -# "comment": self.comment_obj.comment, -# } -# ) - -# def form_invalid(self, form): -# return JsonResponse({"error": form["comment"].errors}) - - -# class DeleteCommentView(LoginRequiredMixin, View): -# def post(self, request, *args, **kwargs): -# self.object = get_object_or_404(Comment, id=request.POST.get("comment_id")) -# if request.user == self.object.commented_by: -# self.object.delete() -# data = {"cid": request.POST.get("comment_id")} -# return JsonResponse(data) - -# data = {"error": "You don't have permission to delete this comment."} -# return JsonResponse(data) - - -# class AddAttachmentView(LoginRequiredMixin, CreateView): -# model = Attachments -# form_class = InvoiceAttachmentForm -# http_method_names = ["post"] - -# def post(self, request, *args, **kwargs): -# self.object = None -# self.invoice = get_object_or_404(Invoice, id=request.POST.get("invoice_id")) -# if ( -# request.user == self.invoice.created_by -# or request.user.is_superuser -# or request.user.role == "ADMIN" -# ): -# form = self.get_form() -# if form.is_valid(): -# return self.form_valid(form) - -# return self.form_invalid(form) - -# data = { -# "error": "You don't have permission to add attachment \ -# for this account." -# } -# return JsonResponse(data) - -# def form_valid(self, form): -# attachment = form.save(commit=False) -# attachment.created_by = self.request.user -# attachment.file_name = attachment.attachment.name -# attachment.invoice = self.invoice -# attachment.save() -# return JsonResponse( -# { -# "attachment_id": attachment.id, -# "attachment": attachment.file_name, -# "attachment_url": attachment.attachment.url, -# "download_url": reverse( -# "common:download_attachment", kwargs={"pk": attachment.id} -# ), -# "attachment_display": attachment.get_file_type_display(), -# "created_on": attachment.created_on, -# "created_on_arrow": attachment.created_on_arrow, -# "created_by": attachment.created_by.email, -# "file_type": attachment.file_type(), -# } -# ) - -# def form_invalid(self, form): -# return JsonResponse({"error": form["attachment"].errors}) - - -# class DeleteAttachmentsView(LoginRequiredMixin, View): -# def post(self, request, *args, **kwargs): -# self.object = get_object_or_404( -# Attachments, id=request.POST.get("attachment_id") -# ) -# if ( -# request.user == self.object.created_by -# or request.user.is_superuser -# or request.user.role == "ADMIN" -# ): -# self.object.delete() -# data = {"acd": request.POST.get("attachment_id")} -# return JsonResponse(data) - -# data = {"error": "You don't have permission to delete this attachment."} -# return JsonResponse(data) diff --git a/leads/migrations/0001_initial.py b/leads/migrations/0001_initial.py deleted file mode 100644 index 42594282..00000000 --- a/leads/migrations/0001_initial.py +++ /dev/null @@ -1,88 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import phonenumber_field.modelfields -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('teams', '0001_initial'), - ('accounts', '0001_initial'), - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contacts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Company', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(blank=True, max_length=100, null=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.org')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Company', - 'verbose_name_plural': 'Companies', - 'db_table': 'company', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='Lead', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('title', models.CharField(max_length=64, verbose_name='Title')), - ('first_name', models.CharField(max_length=255, null=True, verbose_name='First name')), - ('last_name', models.CharField(max_length=255, null=True, verbose_name='Last name')), - ('email', models.EmailField(blank=True, max_length=254, null=True)), - ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None)), - ('status', models.CharField(blank=True, choices=[('assigned', 'Assigned'), ('in process', 'In Process'), ('converted', 'Converted'), ('recycled', 'Recycled'), ('closed', 'Closed')], max_length=255, null=True, verbose_name='Status of Lead')), - ('source', models.CharField(blank=True, choices=[('call', 'Call'), ('email', 'Email'), ('existing customer', 'Existing Customer'), ('partner', 'Partner'), ('public relations', 'Public Relations'), ('compaign', 'Campaign'), ('other', 'Other')], max_length=255, null=True, verbose_name='Source of Lead')), - ('address_line', models.CharField(blank=True, max_length=255, null=True, verbose_name='Address')), - ('street', models.CharField(blank=True, max_length=55, null=True, verbose_name='Street')), - ('city', models.CharField(blank=True, max_length=255, null=True, verbose_name='City')), - ('state', models.CharField(blank=True, max_length=255, null=True, verbose_name='State')), - ('postcode', models.CharField(blank=True, max_length=64, null=True, verbose_name='Post/Zip-code')), - ('country', models.CharField(blank=True, choices=[('GB', 'United Kingdom'), ('AF', 'Afghanistan'), ('AX', 'Aland Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('CV', 'Cape Verde'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo, The Democratic Republic of the'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "Cote d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands (Malvinas)'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See (Vatican City State)'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran, Islamic Republic of'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', "Lao People's Democratic Republic"), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libyan Arab Jamahiriya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia, The Former Yugoslav Republic of'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia, Federated States of'), ('MD', 'Moldova'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('AN', 'Netherlands Antilles'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestinian Territory, Occupied'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('BL', 'Saint Barthelemy'), ('SH', 'Saint Helena'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syrian Arab Republic'), ('TW', 'Taiwan, Province of China'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania, United Republic of'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('US', 'United States'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Viet Nam'), ('VG', 'Virgin Islands, British'), ('VI', 'Virgin Islands, U.S.'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], max_length=3, null=True)), - ('website', models.CharField(blank=True, max_length=255, null=True, verbose_name='Website')), - ('description', models.TextField(blank=True, null=True)), - ('account_name', models.CharField(blank=True, max_length=255, null=True)), - ('opportunity_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Opportunity Amount')), - ('is_active', models.BooleanField(default=False)), - ('enquiry_type', models.CharField(blank=True, max_length=255, null=True)), - ('created_from_site', models.BooleanField(default=False)), - ('skype_ID', models.CharField(blank=True, max_length=100, null=True)), - ('industry', models.CharField(blank=True, choices=[('ADVERTISING', 'ADVERTISING'), ('AGRICULTURE', 'AGRICULTURE'), ('APPAREL & ACCESSORIES', 'APPAREL & ACCESSORIES'), ('AUTOMOTIVE', 'AUTOMOTIVE'), ('BANKING', 'BANKING'), ('BIOTECHNOLOGY', 'BIOTECHNOLOGY'), ('BUILDING MATERIALS & EQUIPMENT', 'BUILDING MATERIALS & EQUIPMENT'), ('CHEMICAL', 'CHEMICAL'), ('COMPUTER', 'COMPUTER'), ('EDUCATION', 'EDUCATION'), ('ELECTRONICS', 'ELECTRONICS'), ('ENERGY', 'ENERGY'), ('ENTERTAINMENT & LEISURE', 'ENTERTAINMENT & LEISURE'), ('FINANCE', 'FINANCE'), ('FOOD & BEVERAGE', 'FOOD & BEVERAGE'), ('GROCERY', 'GROCERY'), ('HEALTHCARE', 'HEALTHCARE'), ('INSURANCE', 'INSURANCE'), ('LEGAL', 'LEGAL'), ('MANUFACTURING', 'MANUFACTURING'), ('PUBLISHING', 'PUBLISHING'), ('REAL ESTATE', 'REAL ESTATE'), ('SERVICE', 'SERVICE'), ('SOFTWARE', 'SOFTWARE'), ('SPORTS', 'SPORTS'), ('TECHNOLOGY', 'TECHNOLOGY'), ('TELECOMMUNICATIONS', 'TELECOMMUNICATIONS'), ('TELEVISION', 'TELEVISION'), ('TRANSPORTATION', 'TRANSPORTATION'), ('VENTURE CAPITAL', 'VENTURE CAPITAL')], max_length=255, null=True, verbose_name='Industry Type')), - ('organization', models.CharField(max_length=255, null=True, verbose_name='Organization')), - ('probability', models.IntegerField(blank=True, default=0, null=True)), - ('close_date', models.DateField(default=None, null=True)), - ('assigned_to', models.ManyToManyField(related_name='lead_assigned_users', to='common.profile')), - ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lead_company', to='leads.company')), - ('contacts', models.ManyToManyField(related_name='lead_contacts', to='contacts.contact')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lead_created_by', to='common.profile')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lead_org', to='common.org')), - ('tags', models.ManyToManyField(blank=True, to='accounts.tags')), - ('teams', models.ManyToManyField(related_name='lead_teams', to='teams.teams')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Lead', - 'verbose_name_plural': 'Leads', - 'db_table': 'lead', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/leads/migrations/0002_alter_lead_created_by.py b/leads/migrations/0002_alter_lead_created_by.py deleted file mode 100644 index 97a18363..00000000 --- a/leads/migrations/0002_alter_lead_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-29 07:31 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('leads', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='lead', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/opportunity/migrations/0001_initial.py b/opportunity/migrations/0001_initial.py deleted file mode 100644 index 9cc9679b..00000000 --- a/opportunity/migrations/0001_initial.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('teams', '0001_initial'), - ('accounts', '0001_initial'), - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contacts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Opportunity', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=64, verbose_name='Name')), - ('stage', models.CharField(choices=[('QUALIFICATION', 'QUALIFICATION'), ('NEEDS ANALYSIS', 'NEEDS ANALYSIS'), ('VALUE PROPOSITION', 'VALUE PROPOSITION'), ('ID.DECISION MAKERS', 'ID.DECISION MAKERS'), ('PERCEPTION ANALYSIS', 'PERCEPTION ANALYSIS'), ('PROPOSAL/PRICE QUOTE', 'PROPOSAL/PRICE QUOTE'), ('NEGOTIATION/REVIEW', 'NEGOTIATION/REVIEW'), ('CLOSED WON', 'CLOSED WON'), ('CLOSED LOST', 'CLOSED LOST')], max_length=64, verbose_name='Stage')), - ('currency', models.CharField(blank=True, choices=[('AED', 'AED, Dirham'), ('AFN', 'AFN, Afghani'), ('ALL', 'ALL, Lek'), ('AMD', 'AMD, Dram'), ('ANG', 'ANG, Guilder'), ('AOA', 'AOA, Kwanza'), ('ARS', 'ARS, Peso'), ('AUD', 'AUD, Dollar'), ('AWG', 'AWG, Guilder'), ('AZN', 'AZN, Manat'), ('BAM', 'BAM, Marka'), ('BBD', 'BBD, Dollar'), ('BDT', 'BDT, Taka'), ('BGN', 'BGN, Lev'), ('BHD', 'BHD, Dinar'), ('BIF', 'BIF, Franc'), ('BMD', 'BMD, Dollar'), ('BND', 'BND, Dollar'), ('BOB', 'BOB, Boliviano'), ('BRL', 'BRL, Real'), ('BSD', 'BSD, Dollar'), ('BTN', 'BTN, Ngultrum'), ('BWP', 'BWP, Pula'), ('BYR', 'BYR, Ruble'), ('BZD', 'BZD, Dollar'), ('CAD', 'CAD, Dollar'), ('CDF', 'CDF, Franc'), ('CHF', 'CHF, Franc'), ('CLP', 'CLP, Peso'), ('CNY', 'CNY, Yuan Renminbi'), ('COP', 'COP, Peso'), ('CRC', 'CRC, Colon'), ('CUP', 'CUP, Peso'), ('CVE', 'CVE, Escudo'), ('CZK', 'CZK, Koruna'), ('DJF', 'DJF, Franc'), ('DKK', 'DKK, Krone'), ('DOP', 'DOP, Peso'), ('DZD', 'DZD, Dinar'), ('EGP', 'EGP, Pound'), ('ERN', 'ERN, Nakfa'), ('ETB', 'ETB, Birr'), ('EUR', 'EUR, Euro'), ('FJD', 'FJD, Dollar'), ('FKP', 'FKP, Pound'), ('GBP', 'GBP, Pound'), ('GEL', 'GEL, Lari'), ('GHS', 'GHS, Cedi'), ('GIP', 'GIP, Pound'), ('GMD', 'GMD, Dalasi'), ('GNF', 'GNF, Franc'), ('GTQ', 'GTQ, Quetzal'), ('GYD', 'GYD, Dollar'), ('HKD', 'HKD, Dollar'), ('HNL', 'HNL, Lempira'), ('HRK', 'HRK, Kuna'), ('HTG', 'HTG, Gourde'), ('HUF', 'HUF, Forint'), ('IDR', 'IDR, Rupiah'), ('ILS', 'ILS, Shekel'), ('INR', 'INR, Rupee'), ('IQD', 'IQD, Dinar'), ('IRR', 'IRR, Rial'), ('ISK', 'ISK, Krona'), ('JMD', 'JMD, Dollar'), ('JOD', 'JOD, Dinar'), ('JPY', 'JPY, Yen'), ('KES', 'KES, Shilling'), ('KGS', 'KGS, Som'), ('KHR', 'KHR, Riels'), ('KMF', 'KMF, Franc'), ('KPW', 'KPW, Won'), ('KRW', 'KRW, Won'), ('KWD', 'KWD, Dinar'), ('KYD', 'KYD, Dollar'), ('KZT', 'KZT, Tenge'), ('LAK', 'LAK, Kip'), ('LBP', 'LBP, Pound'), ('LKR', 'LKR, Rupee'), ('LRD', 'LRD, Dollar'), ('LSL', 'LSL, Loti'), ('LTL', 'LTL, Litas'), ('LVL', 'LVL, Lat'), ('LYD', 'LYD, Dinar'), ('MAD', 'MAD, Dirham'), ('MDL', 'MDL, Leu'), ('MGA', 'MGA, Ariary'), ('MKD', 'MKD, Denar'), ('MMK', 'MMK, Kyat'), ('MNT', 'MNT, Tugrik'), ('MOP', 'MOP, Pataca'), ('MRO', 'MRO, Ouguiya'), ('MUR', 'MUR, Rupee'), ('MVR', 'MVR, Rufiyaa'), ('MWK', 'MWK, Kwacha'), ('MXN', 'MXN, Peso'), ('MYR', 'MYR, Ringgit'), ('MZN', 'MZN, Metical'), ('NAD', 'NAD, Dollar'), ('NGN', 'NGN, Naira'), ('NIO', 'NIO, Cordoba'), ('NOK', 'NOK, Krone'), ('NPR', 'NPR, Rupee'), ('NZD', 'NZD, Dollar'), ('OMR', 'OMR, Rial'), ('PAB', 'PAB, Balboa'), ('PEN', 'PEN, Sol'), ('PGK', 'PGK, Kina'), ('PHP', 'PHP, Peso'), ('PKR', 'PKR, Rupee'), ('PLN', 'PLN, Zloty'), ('PYG', 'PYG, Guarani'), ('QAR', 'QAR, Rial'), ('RON', 'RON, Leu'), ('RSD', 'RSD, Dinar'), ('RUB', 'RUB, Ruble'), ('RWF', 'RWF, Franc'), ('SAR', 'SAR, Rial'), ('SBD', 'SBD, Dollar'), ('SCR', 'SCR, Rupee'), ('SDG', 'SDG, Pound'), ('SEK', 'SEK, Krona'), ('SGD', 'SGD, Dollar'), ('SHP', 'SHP, Pound'), ('SLL', 'SLL, Leone'), ('SOS', 'SOS, Shilling'), ('SRD', 'SRD, Dollar'), ('SSP', 'SSP, Pound'), ('STD', 'STD, Dobra'), ('SYP', 'SYP, Pound'), ('SZL', 'SZL, Lilangeni'), ('THB', 'THB, Baht'), ('TJS', 'TJS, Somoni'), ('TMT', 'TMT, Manat'), ('TND', 'TND, Dinar'), ('TOP', 'TOP, Paanga'), ('TRY', 'TRY, Lira'), ('TTD', 'TTD, Dollar'), ('TWD', 'TWD, Dollar'), ('TZS', 'TZS, Shilling'), ('UAH', 'UAH, Hryvnia'), ('UGX', 'UGX, Shilling'), ('USD', '$, Dollar'), ('UYU', 'UYU, Peso'), ('UZS', 'UZS, Som'), ('VEF', 'VEF, Bolivar'), ('VND', 'VND, Dong'), ('VUV', 'VUV, Vatu'), ('WST', 'WST, Tala'), ('XAF', 'XAF, Franc'), ('XCD', 'XCD, Dollar'), ('XOF', 'XOF, Franc'), ('XPF', 'XPF, Franc'), ('YER', 'YER, Rial'), ('ZAR', 'ZAR, Rand'), ('ZMK', 'ZMK, Kwacha'), ('ZWL', 'ZWL, Dollar')], max_length=3, null=True)), - ('amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Opportunity Amount')), - ('lead_source', models.CharField(blank=True, choices=[('NONE', 'NONE'), ('CALL', 'CALL'), ('EMAIL', ' EMAIL'), ('EXISTING CUSTOMER', 'EXISTING CUSTOMER'), ('PARTNER', 'PARTNER'), ('PUBLIC RELATIONS', 'PUBLIC RELATIONS'), ('CAMPAIGN', 'CAMPAIGN'), ('WEBSITE', 'WEBSITE'), ('OTHER', 'OTHER')], max_length=255, null=True, verbose_name='Source of Lead')), - ('probability', models.IntegerField(blank=True, default=0, null=True)), - ('closed_on', models.DateField(blank=True, null=True)), - ('description', models.TextField(blank=True, null=True)), - ('is_active', models.BooleanField(default=False)), - ('account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='accounts.account')), - ('assigned_to', models.ManyToManyField(related_name='opportunity_assigned_to', to='common.profile')), - ('closed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oppurtunity_closed_by', to='common.profile')), - ('contacts', models.ManyToManyField(to='contacts.contact')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity_created_by', to='common.profile')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oppurtunity_org', to='common.org')), - ('tags', models.ManyToManyField(blank=True, to='accounts.tags')), - ('teams', models.ManyToManyField(related_name='oppurtunity_teams', to='teams.teams')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Opportunity', - 'verbose_name_plural': 'Opportunities', - 'db_table': 'opportunity', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/opportunity/migrations/0002_alter_opportunity_created_by.py b/opportunity/migrations/0002_alter_opportunity_created_by.py deleted file mode 100644 index f4b2ec97..00000000 --- a/opportunity/migrations/0002_alter_opportunity_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-31 12:35 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('opportunity', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='opportunity', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/opportunity/models.py b/opportunity/models.py deleted file mode 100644 index be39c647..00000000 --- a/opportunity/models.py +++ /dev/null @@ -1,91 +0,0 @@ -import arrow -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import pgettext_lazy - -from accounts.models import Account, Tags -from common.models import Org, Profile -from common.base import BaseModel -from common.utils import CURRENCY_CODES, SOURCES, STAGES -from contacts.models import Contact -from teams.models import Teams - - -class Opportunity(BaseModel): - name = models.CharField(pgettext_lazy("Name of Opportunity", "Name"), max_length=64) - account = models.ForeignKey( - Account, - related_name="opportunities", - on_delete=models.CASCADE, - blank=True, - null=True, - ) - stage = models.CharField( - pgettext_lazy("Stage of Opportunity", "Stage"), max_length=64, choices=STAGES - ) - currency = models.CharField( - max_length=3, choices=CURRENCY_CODES, blank=True, null=True - ) - amount = models.DecimalField( - _("Opportunity Amount"), decimal_places=2, max_digits=12, blank=True, null=True - ) - lead_source = models.CharField( - _("Source of Lead"), max_length=255, choices=SOURCES, blank=True, null=True - ) - probability = models.IntegerField(default=0, blank=True, null=True) - contacts = models.ManyToManyField(Contact) - closed_by = models.ForeignKey( - Profile, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="oppurtunity_closed_by", - ) - # closed_on = models.DateTimeField(blank=True, null=True) - closed_on = models.DateField(blank=True, null=True) - description = models.TextField(blank=True, null=True) - assigned_to = models.ManyToManyField( - Profile, related_name="opportunity_assigned_to" - ) - is_active = models.BooleanField(default=False) - tags = models.ManyToManyField(Tags, blank=True) - teams = models.ManyToManyField(Teams, related_name="oppurtunity_teams") - org = models.ForeignKey( - Org, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="oppurtunity_org", - ) - - class Meta: - verbose_name = "Opportunity" - verbose_name_plural = "Opportunities" - db_table = "opportunity" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.name}" - - @property - def created_on_arrow(self): - return arrow.get(self.created_at).humanize() - - @property - def get_team_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return Profile.objects.filter(id__in=team_user_ids) - - @property - def get_team_and_assigned_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = team_user_ids + assigned_user_ids - return Profile.objects.filter(id__in=user_ids) - - @property - def get_assigned_users_not_in_teams(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = set(assigned_user_ids) - set(team_user_ids) - return Profile.objects.filter(id__in=list(user_ids)) diff --git a/planner/__init__.py b/planner/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/planner/admin.py b/planner/admin.py deleted file mode 100644 index c79ff80c..00000000 --- a/planner/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin - -from .models import Reminder - -# Register your models here. - -# admin.site.register(Event) -admin.site.register(Reminder) diff --git a/planner/apps.py b/planner/apps.py deleted file mode 100644 index a52d5f9f..00000000 --- a/planner/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class PlannerConfig(AppConfig): - name = "planner" diff --git a/planner/forms.py b/planner/forms.py deleted file mode 100644 index b2d481bb..00000000 --- a/planner/forms.py +++ /dev/null @@ -1,97 +0,0 @@ -from django import forms - -from planner.models import Reminder - -# class EventForm(forms.ModelForm): -# class Meta: -# model = Event -# fields = ['name', 'status', 'start_date', 'description', 'assigned_to', 'attendees_user', -# 'attendees_contacts', 'attendees_leads'] - -# start_date = forms.DateTimeField(input_formats=['%m/%d/%Y %H:%M:%S'], error_messages={ -# 'invalid': ("Invalid Startdate")}, required=True) -# parent_id = forms.IntegerField(required=False) -# parent_typeChoices = ( -# ('Account', 'Account'), ('Lead', 'Lead'), ('Opportunity', 'Opportunity'), -# ('Case', 'Case')) -# parent_type = forms.ChoiceField(choices=parent_typeChoices, error_messages={ -# 'invalid': ("Invalid parent_type selected.")}, required=False) - - -# class TaskEventForm(EventForm): -# class Meta(EventForm.Meta): -# model = Event -# fields = ['priority', 'close_date', 'event_type'] + EventForm.Meta.fields - -# parent_typeChoices = ( -# ('Account', 'Account'), ('Contact', 'Contact'), ('Lead', 'Lead'), ('Opportunity', 'Opportunity'), -# ('Case', 'Case')) -# parent_type = forms.ChoiceField(choices=parent_typeChoices, error_messages={ -# 'invalid': ("Invalid parent_type selected.")}, required=False) -# priority = forms.ChoiceField(choices=(('Low', 'Low'), ('Normal', 'Normal'), ('High', 'High'), ('Urgent', 'Urgent')), -# error_messages={ -# 'invalid': ("Invalid Priority selected.")}, required=True) # only tasks -# close_date = forms.DateTimeField(input_formats=['%m/%d/%Y %H:%M:%S'], required=True) - - -# class CallEventForm(EventForm): -# class Meta(EventForm.Meta): -# model = Event -# fields = ['direction', 'duration', 'event_type'] + EventForm.Meta.fields - -# direction = forms.ChoiceField(choices=(('Outbound', 'Outbound'), -# ('Inbound', 'Inbound')), error_messages={ -# 'invalid': ("Invalid Direction selected.")}, required=True) # only calls -# duration = forms.ChoiceField(choices=(("300", "5m"), -# ("600", "10m"), -# ("900", "15m"), -# ("1800", "30m"), -# ("2700", "45m"), -# ("3600", "1h"), -# ("7600", "2h")), error_messages={ -# 'invalid': ("Invalid Duration selected.")}, required=True) # not for tasks - - -class ReminderForm(forms.ModelForm): - class Meta: - model = Reminder - fields = "__all__" - - reminder_type = forms.ChoiceField( - choices=(("Email", "Email"), ("Popup", "Popup")), - widget=forms.Select(attrs={"class": "form-control input-sm"}), - ) - reminder_time = forms.ChoiceField( - choices=( - ("0", "on time"), - ("60", "1m before"), - ("120", "2m before"), - ("300", "5m before"), - ("600", "10m before"), - ("900", "15m before"), - ("1800", "30m before"), - ("3600", "1h before"), - ("7200", "2h before"), - ("10800", "3h before"), - ("18000", "5h before"), - ("86400", "1d before"), - ), - widget=forms.Select(attrs={"class": "form-control input-sm"}), - ) - - -# class MeetingEventForm(EventForm): -# class Meta(EventForm.Meta): -# model = Event -# fields = ['duration', 'close_date', 'event_type', ] + EventForm.Meta.fields - -# duration = forms.ChoiceField(choices=( -# ("900", "15m"), -# ("1800", "30m"), -# ("3600", "1h"), -# ("7200", "2h"), -# ("10800", "3h"), -# ("86400", "1d") -# ), error_messages={ -# 'invalid': ("Invalid Duration selected.")}, required=True) # not for tasks -# close_date = forms.DateTimeField(input_formats=['%m/%d/%Y %H:%M:%S'], required=True) diff --git a/planner/migrations/0001_initial.py b/planner/migrations/0001_initial.py deleted file mode 100644 index a50ab301..00000000 --- a/planner/migrations/0001_initial.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('leads', '0001_initial'), - ('contacts', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Reminder', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('reminder_type', models.CharField(blank=True, max_length=5, null=True)), - ('reminder_time', models.IntegerField(blank=True, null=True, verbose_name='Reminder')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Reminder', - 'verbose_name_plural': 'Reminders', - 'db_table': 'reminder', - 'ordering': ('-created_at',), - }, - ), - migrations.CreateModel( - name='PlannerEvent', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=64, verbose_name='Event')), - ('event_type', models.CharField(max_length=7, verbose_name='Type of the event')), - ('object_id', models.PositiveIntegerField(blank=True, null=True)), - ('status', models.CharField(blank=True, choices=[('Planned', 'Planned'), ('Held', 'Held'), ('Not Held', 'Not Held'), ('Not Started', 'Not Started'), ('Started', 'Started'), ('Completed', 'Completed'), ('Canceled', 'Canceled'), ('Deferred', 'Deferred')], max_length=64, verbose_name='Status')), - ('direction', models.CharField(blank=True, max_length=20)), - ('start_date', models.DateField(default=None)), - ('close_date', models.DateField(default=None, null=True)), - ('duration', models.IntegerField(default=None, null=True, verbose_name='Durations')), - ('priority', models.CharField(blank=True, max_length=10)), - ('updated_on', models.DateTimeField(auto_now=True)), - ('created_on', models.DateTimeField(auto_now_add=True, verbose_name='Created on')), - ('description', models.TextField(blank=True, null=True)), - ('is_active', models.BooleanField(default=False)), - ('assigned_to', models.ManyToManyField(blank=True, related_name='event_assigned_users', to=settings.AUTH_USER_MODEL)), - ('attendees_contacts', models.ManyToManyField(blank=True, related_name='attendees_contact', to='contacts.contact')), - ('attendees_leads', models.ManyToManyField(blank=True, related_name='attendees_lead', to='leads.lead')), - ('attendees_user', models.ManyToManyField(blank=True, related_name='attendees_user', to=settings.AUTH_USER_MODEL)), - ('content_type', models.ForeignKey(blank=True, choices=[(10, 'Account'), (13, 'Lead'), (14, 'Opportunity'), (11, 'Case')], limit_choices_to=models.Q(models.Q(('app_label', 'account'), ('id', 10), ('model', 'Account')), models.Q(('app_label', 'leads'), ('id', 13), ('model', 'Lead')), models.Q(('app_label', 'opportunity'), ('id', 14), ('model', 'Opportunity')), models.Q(('app_label', 'cases'), ('id', 11), ('model', 'Case')), _connector='OR'), null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='event_created_by', to=settings.AUTH_USER_MODEL)), - ('reminders', models.ManyToManyField(blank=True, to='planner.reminder')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_user', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'PlannerEvent', - 'verbose_name_plural': 'PlannerEvents', - 'db_table': 'planner_event', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/planner/migrations/__init__.py b/planner/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/planner/models.py b/planner/models.py deleted file mode 100644 index 2493f802..00000000 --- a/planner/models.py +++ /dev/null @@ -1,106 +0,0 @@ -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import pgettext_lazy - -from common.models import User -from common.base import BaseModel -from common.utils import EVENT_PARENT_TYPE, EVENT_STATUS -from contacts.models import Contact -from leads.models import Lead - - -class Reminder(BaseModel): - reminder_type = models.CharField(max_length=5, blank=True, null=True) - reminder_time = models.IntegerField( - pgettext_lazy("time of the reminder to event in Seconds", "Reminder"), - blank=True, - null=True, - ) - - class Meta: - verbose_name = "Reminder" - verbose_name_plural = "Reminders" - db_table = "reminder" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.reminder_type}" - - -class PlannerEvent(BaseModel): - limit = ( - models.Q(app_label="account", model="Account", id=10) - | models.Q(app_label="leads", model="Lead", id=13) - | models.Q(app_label="opportunity", model="Opportunity", id=14) - | models.Q(app_label="cases", model="Case", id=11) - ) - name = models.CharField(pgettext_lazy("Name of the Event", "Event"), max_length=64) - event_type = models.CharField( - _("Type of the event"), max_length=7 - ) # call meeting task - content_type = models.ForeignKey( - ContentType, - on_delete=models.CASCADE, - blank=True, - null=True, - limit_choices_to=limit, - choices=EVENT_PARENT_TYPE, - ) - object_id = models.PositiveIntegerField(blank=True, null=True) - parent = GenericForeignKey("content_type", "object_id") - status = models.CharField( - pgettext_lazy("status of the Event", "Status"), - choices=EVENT_STATUS, - max_length=64, - blank=True, - ) - direction = models.CharField(max_length=20, blank=True) # only for calls - # start_date = models.DateTimeField(default=None) - # close_date = models.DateTimeField(default=None, null=True) - start_date = models.DateField(default=None) - close_date = models.DateField(default=None, null=True) - - duration = models.IntegerField( - pgettext_lazy("Duration of the Event in Seconds", "Durations"), - default=None, - null=True, - ) # not for tasks - reminders = models.ManyToManyField(Reminder, blank=True) - priority = models.CharField(max_length=10, blank=True) # only for task - updated_on = models.DateTimeField(auto_now=True) - updated_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="updated_user", - ) - attendees_user = models.ManyToManyField( - User, blank=True, related_name="attendees_user" - ) - attendees_contacts = models.ManyToManyField( - Contact, blank=True, related_name="attendees_contact" - ) - attendees_leads = models.ManyToManyField( - Lead, blank=True, related_name="attendees_lead" - ) - created_on = models.DateTimeField(_("Created on"), auto_now_add=True) - created_by = models.ForeignKey( - User, related_name="event_created_by", on_delete=models.SET_NULL, null=True - ) - assigned_to = models.ManyToManyField( - User, blank=True, related_name="event_assigned_users", - ) - description = models.TextField(blank=True, null=True) - is_active = models.BooleanField(default=False) - - class Meta: - verbose_name = "PlannerEvent" - verbose_name_plural = "PlannerEvents" - db_table = "planner_event" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.name}" diff --git a/planner/serializer.py b/planner/serializer.py deleted file mode 100644 index e69de29b..00000000 diff --git a/planner/templates/calls.html b/planner/templates/calls.html deleted file mode 100644 index 029eca67..00000000 --- a/planner/templates/calls.html +++ /dev/null @@ -1,570 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% block content %} - -
-
CALLS
-
-
-
-
-
- -
- -
-
- -
-
- - - - - - - -    - - - - - -
-
-
- - - - -    - - - - -
-
- -
-
-
- - - - -    - - - - - -
-
-
- - - -   - - - -
- -
- - - -   - - - -
- -
- - - -   - - - -
- -
- - - - -    - - - - -
-
-
-
- - - - - - - - - -
-
- -
-
- - - - -
-
-
-
-
- {# #} - - -
-
-
-
-
- -
- - -
-
-
-
- filters -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - - -
-
-
-
- - - -
-
-
-
-
-
-
- - -
-
-
-
- {% include 'planner/calls_list.html' %} -
- -
- -
- - - -
- - -
-
- - - - - -{% include 'planner/dialog_models.html' %} - - -{% endblock %} \ No newline at end of file diff --git a/planner/templates/calls_list.html b/planner/templates/calls_list.html deleted file mode 100644 index 081a797a..00000000 --- a/planner/templates/calls_list.html +++ /dev/null @@ -1,61 +0,0 @@ -{% load static %} - - - - - - - - - - - {% for call in calls %} - - - - - - - - - {% endfor %} - -
NameParentStatusDate StartActions
{{ call.name }}{{ call.parent }}{{ call.status }}{{ call.start_date }} - -
-{% if calls|length == 0 %} -
No Calls found
-{% endif %} -
-
    -
  • - {% if calls.has_previous %} - Previous - {% endif %} -
  • -
  • - {% for page in calls.paginator.page_range %} -
  • {{ page }} -
  • - {% endfor %} - -
  • - {% if calls.has_next %} - Next - {% endif %} -
  • -
-
- \ No newline at end of file diff --git a/planner/templates/contacts.html b/planner/templates/contacts.html deleted file mode 100644 index 4a0b0ebe..00000000 --- a/planner/templates/contacts.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - {% ifequal contacts|length 0 %} - - - - {% endifequal %} - {% for contact in contacts %} - - - - - - {% endfor %} - -
- - - Name - - - - - Email - -
- NO CONTACTS FOUND!! -
- - - {{ contact.name }} - - {{ contact.email }} -
-
-
    -
  • - {% if contacts.has_previous %} - Previous - {% endif %} -
  • -
  • - {% for page in contacts.paginator.page_range %} -
  • {{ page }} -
  • - {% endfor %} - -
  • - {% if contacts.has_next %} - Next - {% endif %} -
  • -
-
- \ No newline at end of file diff --git a/planner/templates/dialog_models.html b/planner/templates/dialog_models.html deleted file mode 100644 index 35cfa7a6..00000000 --- a/planner/templates/dialog_models.html +++ /dev/null @@ -1,101 +0,0 @@ -{% load static %} - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/planner/templates/events_base.html b/planner/templates/events_base.html deleted file mode 100644 index d2a70256..00000000 --- a/planner/templates/events_base.html +++ /dev/null @@ -1,158 +0,0 @@ - -
-
Contacts
-
-
-
-
-
- - -
- -
- -
-
-
-
-
- - - -
-
-
-
-
- {% block addnew %}{% endblock %} -
-
- - -
-
-
-
- filters -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - - -
-
-
-
- - - -
-
-
-
-
-
-
- - -
-
-
-
- - {% block eventcount %}{% endblock %} -
-
- {% block eventslist %}{% endblock %} -
-
-
-
-
- - - \ No newline at end of file diff --git a/planner/templates/leads.html b/planner/templates/leads.html deleted file mode 100644 index fbbb99c6..00000000 --- a/planner/templates/leads.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - {% ifequal leads|length 0 %} - - - - {% endifequal %} - {% for lead in leads %} - - - - - - {% endfor %} - -
- - - Name - - - - - Email - -
- NO LEADS FOUND!! -
- - - {{ lead.first_name }} - - {{ lead.email }} -
-
-
    -
  • - {% if leads.has_previous %} - Previous - {% endif %} -
  • -
  • - {% for page in leads.paginator.page_range %} -
  • {{ page }} -
  • - {% endfor %} - -
  • - {% if leads.has_next %} - Next - {% endif %} -
  • -
-
- \ No newline at end of file diff --git a/planner/templates/meetings.html b/planner/templates/meetings.html deleted file mode 100644 index 1963c461..00000000 --- a/planner/templates/meetings.html +++ /dev/null @@ -1,519 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% block content %} - -
-
MEETINGS
-
-
-
-
-
- -
- -
-
- -
-
- - - - - - - -    - - - - - -
-
-
- - - - -    - - - - -
-
- -
-
-
- - - - -    - - - - -
- -
-
-
- - - -   - - - -
- -
- - - -   - - - -
- -
- - - -   - - - -
- -
- - - - -    - - - - -
-
-
-
- - - - - - - - - -
-
- -
-
- - - - -
-
-
-
-
- {# #} - - -
-
-
-
-
- -
- -
-
-
-
- {% include 'planner/meetings_list.html' %} -
- -
- -
-
- - - - - - -
-
- - - - - -{% include 'planner/dialog_models.html' %} - - -{% endblock %} \ No newline at end of file diff --git a/planner/templates/meetings_list.html b/planner/templates/meetings_list.html deleted file mode 100644 index f9261d49..00000000 --- a/planner/templates/meetings_list.html +++ /dev/null @@ -1,81 +0,0 @@ -{% load static %} - - - - - - - - - - - - - {% for meeting in meetings %} - - - - - - - - {% endfor %} - -
NameParentStatusDate StartActions
{{ meeting.name }}{{ meeting.parent }}{{ meeting.status }}{{ meeting.start_date }} - -
-{% if meetings|length == 0 %} -
No Meetings found
-{% endif %} -
-
    -
  • - {% if meetings.has_previous %} - Previous - {% endif %} -
  • -
  • - {% for page in meetings.paginator.page_range %} -
  • {{ page }} -
  • - {% endfor %} - -
  • - {% if meetings.has_next %} - Next - {% endif %} -
  • -
-
- - \ No newline at end of file diff --git a/planner/templates/reminder.html b/planner/templates/reminder.html deleted file mode 100644 index 7fd0a1de..00000000 --- a/planner/templates/reminder.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load static %} -
- {{ reminder_form_set.management_form }} - {% for form in reminder_form_set %} -
- {{ form.reminder_type }} - {{ form.reminder_time }} - {{ form.DELETE }} - {{ form.id }} -
- {% endfor %} -
- \ No newline at end of file diff --git a/planner/templates/tasks.html b/planner/templates/tasks.html deleted file mode 100644 index ff555e96..00000000 --- a/planner/templates/tasks.html +++ /dev/null @@ -1,405 +0,0 @@ -{% extends 'sales/base.html' %} -{% load static %} -{% block content %} - -
-
TASKS
-
-
-
-
-
- -
- -
-
- -
-
- - - - - - - -    - - - - - -
-
-
- - - - -    - - - - -
-
- -
-
- - - -   - - - -
- -
- - - -   - - - -
- -
- - - -   - - - -
- -
- - - - - - - - - -
-
-
-
-
- - - - -
-
-
-
-
- {# #} - - -
-
-
-
-
- -
- - -
-
-
-
- filters -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - - -
-
-
-
- - - -
-
-
-
-
-
-
- - -
-
-
-
- {% include 'planner/tasks_list.html' %} -
- -
- -
- -
-
-
- - - - - - - -{% include 'planner/dialog_models.html' %} - - -{% endblock %} \ No newline at end of file diff --git a/planner/templates/tasks_list.html b/planner/templates/tasks_list.html deleted file mode 100644 index 1fdae542..00000000 --- a/planner/templates/tasks_list.html +++ /dev/null @@ -1,66 +0,0 @@ -{% load static %} - - - - - - - - - - - - - - {% for task in tasks %} - - - - - - - - - - {% endfor %} - -
NameStatusPriorityDate DueActions
{{ task.name }}{{ task.status }}{{ task.priority }}{{ task.start_date }} - -
-{% if tasks|length == 0 %} -
No Tasks found
-{% endif %} -
-
    -
  • - {% if tasks.has_previous %} - Previous - {% endif %} -
  • -
  • - {% for page in tasks.paginator.page_range %} -
  • {{ page }} -
  • - {% endfor %} - -
  • - {% if tasks.has_next %} - Next - {% endif %} -
  • -
-
- \ No newline at end of file diff --git a/planner/templates/users.html b/planner/templates/users.html deleted file mode 100644 index c6529f16..00000000 --- a/planner/templates/users.html +++ /dev/null @@ -1,64 +0,0 @@ -{% load static %} - - - - - - - - - - {% ifequal users|length 0 %} - - - - {% endifequal %} - {% for user in users %} - - - - - - {% endfor %} - -
- - - Name - - - - - Email - -
- NO USERS FOUND!! -
- - - {{ user.email }} - - {{ user.email }} -
-
-
    -
  • - {% if users.has_previous %} - Previous - {% endif %} -
  • -
  • - {% for page in users.paginator.page_range %} -
  • {{ page }} -
  • - {% endfor %} - -
  • - {% if users.has_next %} - Next - {% endif %} -
  • -
-
- \ No newline at end of file diff --git a/planner/tests.py b/planner/tests.py deleted file mode 100644 index 51ae38d6..00000000 --- a/planner/tests.py +++ /dev/null @@ -1,282 +0,0 @@ -# import datetime -# from django.utils import timezone -# from django.test import TestCase -# from planner.models import Event -# from django.contrib.auth.models import User - - -# class EventObjects(object): -# def setUp(self): -# self.user = User.objects.create_superuser( -# 'user@micropyramid.com', 'username', 'password') - -# user_login = self.client.login(username='user@micropyramid.com', password='password') - -# self.meeting = Event.objects.create(event_type='Meeting', name='llll', status='Not Held', -# description='addsdasda', start_date=timezone.now(), -# close_date=timezone.now() + datetime.timedelta(days=5), created_user=self.user) -# self.task = Event.objects.create(event_type='Task', name='dddl', status='Not Held', -# description='addsdasda', start_date=timezone.now(), -# close_date=timezone.now() + datetime.timedelta(days=1), created_user=self.user) -# self.call = Event.objects.create(event_type='Call', name='asdfjkas dfasdfa 555', -# status='Held', -# description='addsdasda', start_date=timezone.now(), -# close_date=timezone.now() + datetime.timedelta(days=2), created_user=self.user) - - -# class EventCreateTestCase(EventObjects, TestCase): -# ''' -# Test for creating Events Through Models -# ''' - -# def test_create_meetings(self): -# self.assertEqual(self.meeting.id, 1) - -# def test_create_task(self): -# self.assertEqual(self.task.id, 2) - -# def test_create_call(self): -# self.assertEqual(self.call.id, 3) - - -# class EventListViewsTestCase(EventObjects, TestCase): -# '''Tests for events lists''' - -# def test_load_meetings(self): -# self.meetings = Event.objects.filter(event_type='Meeting').order_by('-id') -# response = self.client.get('/planner/meetings/list/') -# self.assertEqual(response.status_code, 200) -# self.assertTrue(response.context['meetings']) -# self.assertEqual(response.context['meetings'][0].id, self.meeting.id) -# self.assertTrue(response.context['reminder_form_set']) - -# def test_load_tasks(self): -# self.tasks = Event.objects.filter(event_type='Task').order_by('-id') -# response = self.client.get('/planner/tasks/list/') -# self.assertEqual(response.status_code, 200) -# self.assertEqual(response.context['tasks'][0].id, self.task.id) -# self.assertTrue(response.context['tasks']) - -# def test_load_calls(self): -# self.calls = Event.objects.filter(event_type='Call').order_by('-id') -# response = self.client.get('/planner/calls/list/') -# self.assertEqual(response.status_code, 200) -# self.assertTrue(response.context['calls']) -# self.assertEqual(response.context['calls'][0].id, self.call.id) -# self.assertTrue(response.context['reminder_form_set']) - - -# class EventCreateUsingURLsTestCase(EventObjects, TestCase): -# ''' -# Tests for testing Views using URLs and POST data -# ''' - -# def test_create_meeting(self): -# response = self.client.post('/planner/meeting/create/', {'name': 'dfasdf', -# 'parent_type': 'Case', -# 'parent_id': '', -# 'status': 'Held', -# 'event_type': 'Meeting', -# 'duration': '7200', -# 'start_date': str(timezone.now().strftime( -# '%m/%d/%Y %H:%M:%S')), -# 'close_date': str( -# (timezone.now() + datetime.timedelta( -# days=6)).strftime('%m/%d/%Y %H:%M:%S')), -# 'form-TOTAL_FORMS': '1', -# 'form-INITIAL_FORMS': '0', -# 'form-MAX_NUM_FORMS': '10', -# 'form-0-reminder_type': 'Email', -# 'form-0-reminder_time': '120', -# 'form-0-DELETE': '', -# 'form-0-id': '' -# }) - -# self.assertEqual(response.status_code, 200) -# self.assertEqual('Meeting Created', response.json()['success']) - -# def test_create_meeting_with_empty_data(self): -# response = self.client.post('/planner/meeting/create/', {'name': '', -# 'parent_type': '', -# 'status': '', -# 'event_type': '', -# 'parent_id': '', -# 'duration': '', -# 'start_date': '', -# 'close_date': '', -# 'form-TOTAL_FORMS': '', -# 'form-INITIAL_FORMS': '', -# 'form-MAX_NUM_FORMS': '', -# 'form-0-reminder_type': '', -# 'form-0-reminder_time': '', -# 'form-0-DELETE': '', -# 'form-0-id': '' -# }) - -# self.assertEqual(response.status_code, 200) -# self.assertIn('name', response.json().keys()) -# self.assertIn('duration', response.json().keys()) -# self.assertEqual(['This field is required.'], response.json()['name']) -# self.assertEqual(['This field is required.'], response.json()['event_type']) -# self.assertEqual(['This field is required.'], response.json()['start_date']) -# self.assertEqual(['This field is required.'], response.json()['close_date']) - -# def test_create_meeting_with_invalid_data(self): -# response = self.client.post('/planner/meeting/create/', {'name': '', -# 'parent_type': 'qqqweq', -# 'parent_id': '22', -# 'status': 'qweqw#e', -# 'event_type': 'eq##we', -# 'duration': 'qw66e', -# 'start_date': '654sad654', -# 'close_date': '65sda', -# 'form-TOTAL_FORMS': '', -# 'form-INITIAL_FORMS': '', -# 'form-MAX_NUM_FORMS': '', -# 'form-0-reminder_type': '', -# 'form-0-reminder_time': '', -# 'form-0-DELETE': '', -# 'form-0-id': '' -# }) - -# self.assertEqual(response.status_code, 200) -# self.assertEqual(['This field is required.'], response.json()['name']) -# self.assertEqual(['Enter a valid date/time.'], response.json()['close_date']) -# self.assertEqual(['Invalid Startdate'], response.json()['start_date']) -# self.assertEqual(['Select a valid choice. qw66e is not one of the available choices.'], -# response.json()['duration']) -# self.assertEqual(['Select a valid choice. qweqw#e is not one of the available choices.'], -# response.json()['status']) -# self.assertEqual(['Select a valid choice. qqqweq is not one of the available choices.'], -# response.json()['parent_type']) - -# def test_create_task(self): -# data = {'name': 'dfasdf', -# 'parent_type': 'Account', -# 'status': 'Held', -# 'event_type': 'Task', -# 'duration': '1800', -# 'priority': 'High', -# 'start_date': str(timezone.now().strftime('%m/%d/%Y %H:%M:%S')), -# 'close_date': str((timezone.now() + datetime.timedelta( -# days=6)).strftime('%m/%d/%Y %H:%M:%S')), -# } -# response = self.client.post('/planner/task/create/', data) -# self.assertEqual(response.status_code, 200) -# self.assertEqual('Task Created', response.json()['success']) - -# def test_create_call(self): -# response = self.client.post('/planner/call/create/', {'name': 'dfasdf', -# 'parent_type': 'Account', -# 'event_type': 'Call', -# 'duration': '3600', -# 'direction': 'Outbound', -# 'start_date': str(timezone.now().strftime( -# '%m/%d/%Y %H:%M:%S')), -# 'form-TOTAL_FORMS': '1', -# 'form-INITIAL_FORMS': '0', -# 'form-MAX_NUM_FORMS': '10', -# 'form-0-reminder_type': 'Email', -# 'form-0-reminder_time': '120', -# 'form-0-DELETE': '', -# 'form-0-id': '' -# }) - -# self.assertEqual(response.status_code, 200) -# self.assertEqual('Call Created', response.json()['success']) - - -# class EventsGetViewsTestCase(EventObjects, TestCase): -# ''' -# Tests for getting a meeting with ID -# ''' - -# def test_get_meeting_validID(self): -# response = self.client.post('/planner/get/meeting/', { -# 'meetingID': self.meeting.id -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual('Meeting', response.json()['meeting']['event_type']) - -# def test_get_meeting_invalidID(self): -# response = self.client.post('/planner/get/meeting/', { -# 'meetingID': 6 -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'Event': 'DoesNotExist'}, response.json()) - -# def test_get_task_validID(self): -# response = self.client.post('/planner/get/task/', { -# 'taskID': self.task.id -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual('Task', response.json()['task']['event_type']) - -# def test_get_task_invalidID(self): -# response = self.client.post('/planner/get/task/', { -# 'taskID': 6 -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'Event': 'DoesNotExist'}, response.json()) - -# def test_get_call_validID(self): -# response = self.client.post('/planner/get/call/', { -# 'callID': self.call.id -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual('Call', response.json()['call']['event_type']) - -# def test_get_call_invalidID(self): -# response = self.client.post('/planner/get/call/', { -# 'callID': 67 -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'Event': 'DoesNotExist'}, response.json()) - - -# class EventsDeleteViewsTestCase(EventObjects, TestCase): -# ''' -# Tests for getting a meeting with ID -# ''' - -# def test_delete_meeting_valid_ID(self): -# response = self.client.post('/planner/meeting/delete/', { -# 'meetingID': self.meeting.id -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'success': 'Meeting Deleted'}, response.json()) - -# def test_delete_meeting_InvalidID(self): -# response = self.client.post('/planner/meeting/delete/', { -# 'meetingID': '5' -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'Event': 'DoesNotExist'}, response.json()) - -# def test_delete_task_valid_ID(self): -# response = self.client.post('/planner/task/delete/', { -# 'taskID': self.task.id -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'success': 'Task Deleted'}, response.json()) - -# def test_delete_task_InvalidID(self): -# response = self.client.post('/planner/task/delete/', { -# 'taskID': '5' -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'Event': 'DoesNotExist'}, response.json()) - -# def test_delete_call_valid_ID(self): -# response = self.client.post('/planner/call/delete/', { -# 'callID': self.call.id -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'success': 'Call Deleted'}, response.json()) - -# def test_call_task_InvalidID(self): -# response = self.client.post('/planner/call/delete/', { -# 'callID': '55' -# }) -# self.assertEqual(response.status_code, 200) -# self.assertEqual({'Event': 'DoesNotExist'}, response.json()) diff --git a/planner/urls.py b/planner/urls.py deleted file mode 100644 index d5403d9b..00000000 --- a/planner/urls.py +++ /dev/null @@ -1,38 +0,0 @@ -# from django.conf.urls import url -# from . import views - -# app_name = 'planner' - - -# urlpatterns = [ - -# url(r'^meetings/list/$', views.load_meetings, name='meetings_list'), -# url(r'^tasks/list/$', views.load_tasks, name='tasks_list'), -# url(r'^calls/list/$', views.load_calls, name='calls_list'), - -# url(r'^meeting/create/$', views.create_meeting, name='create_meeting'), -# url(r'^task/create/$', views.create_task, name='create_task'), -# url(r'^call/create/$', views.create_call, name='create_call'), - -# url(r'^meeting/delete/$', views.delete_meeting, name='delete_meeting'), -# url(r'^task/delete/$', views.delete_task, name='delete_task'), -# url(r'^call/delete/$', views.delete_call, name='delete_call'), - -# url(r'^get/meeting/$', views.get_meeting, name='get_meeting'), -# url(r'^get/task/$', views.get_task, name='get_task'), -# url(r'^get/call/$', views.get_call, name='get_call'), - -# url(r'^meeting/update/$', views.update_meeting, name='update_meeting'), -# url(r'^task/update/$', views.update_task, name='update_task'), -# url(r'^call/update/$', views.update_call, name='update_call'), - -# url(r'^get/reminders/$', views.get_reminder_form, name='get_reminder_form'), -# url(r'^get/users/$', views.get_users, name='get_users'), -# url(r'^get/contacts/$', views.get_contacts, name='get_contacts'), -# url(r'^get/leads/$', views.get_leads, name='get_leads'), - -# url(r'^event/set/status/$', views.update_event_status, name='update_event_status'), - -# url(r'^search/events/$', views.search_meetings, name='search_meetings'), - -# ] diff --git a/planner/views.py b/planner/views.py deleted file mode 100644 index b7d5b8d9..00000000 --- a/planner/views.py +++ /dev/null @@ -1,704 +0,0 @@ -# from django.shortcuts import render -# from planner.forms import ReminderForm, MeetingEventForm, CallEventForm, TaskEventForm -# from django.http import HttpResponse, JsonResponse -# from django.contrib.auth.decorators import login_required -# from django.forms import modelformset_factory -# from planner.models import Reminder, Event -# from planner import models -# from django.contrib.auth.models import User -# from accounts.models import Account -# from cases.models import Case -# from opportunity.models import Opportunity -# from contacts.models import Contact -# from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -# from django.db.models import Q -# from leads.models import Lead -# from django.contrib.contenttypes.models import ContentType -# import datetime - - -# @login_required -# def load_meetings(request): -# if request.method == 'GET': -# RemindersFormSet = modelformset_factory(Reminder, form=ReminderForm, can_delete=True) -# data = { -# 'form-TOTAL_FORMS': '1', -# 'form-INITIAL_FORMS': '0', -# 'form-MAX_NUM_FORMS': '10', -# } -# reminder_form_set = RemindersFormSet(data) -# meetings = Event.objects.filter(Q(created_user=request.user) | Q(updated_user=request.user), -# event_type='Meeting').order_by('-id') -# paginator = Paginator(meetings, 5) -# try: -# meetings = paginator.page(request.GET.get('page')) -# return render(request, 'meetings_list.html', -# {'meetings': meetings}) -# except PageNotAnInteger: -# meetings = paginator.page(1) -# except EmptyPage: -# meetings = paginator.page(paginator.num_pages) -# return render(request, 'meetings.html', -# {'meetings': meetings, 'reminder_form_set': reminder_form_set}) -# else: -# return HttpResponse('Invalid Method or Not Authanticated in load_meetings') - - -# @login_required -# def load_tasks(request): -# if request.method == 'GET': -# tasks = Event.objects.filter(Q(created_user=request.user) | Q(updated_user=request.user), -# event_type='Task').order_by('-id') -# paginator = Paginator(tasks, 3) -# try: -# tasks = paginator.page(request.GET.get('page')) -# return render(request, 'tasks_list.html', -# {'tasks': tasks}) -# except PageNotAnInteger: -# tasks = paginator.page(1) -# except EmptyPage: -# tasks = paginator.page(paginator.num_pages) -# return render(request, 'tasks.html', {'tasks': tasks}) -# else: -# return HttpResponse('Invalid Method or Not Authanticated in load_tasks') - - -# @login_required -# def load_calls(request): -# if request.method == 'GET': -# RemindersFormSet = modelformset_factory(Reminder, form=ReminderForm, can_delete=True) -# data = { -# 'form-TOTAL_FORMS': '1', -# 'form-INITIAL_FORMS': '0', -# 'form-MAX_NUM_FORMS': '10', -# } -# reminder_form_set = RemindersFormSet(data) -# calls = Event.objects.filter(Q(created_user=request.user) | Q(updated_user=request.user), -# event_type='Call').order_by('-id') -# paginator = Paginator(calls, 3) -# try: -# calls = paginator.page(request.GET.get('page')) -# return render(request, 'calls_list.html', -# {'calls': calls}) -# except PageNotAnInteger: -# calls = paginator.page(1) -# except EmptyPage: -# calls = paginator.page(paginator.num_pages) -# return render(request, 'calls.html', {'calls': calls, 'reminder_form_set': reminder_form_set}) -# else: -# return HttpResponse('Invalid Method or Not Authanticated in load_calls') - - -# @login_required -# def create_meeting(request): -# if request.method == 'POST': -# eventForm = MeetingEventForm(request.POST) -# ReminderFormSet = modelformset_factory(Reminder, ReminderForm, can_delete=True) -# reminder_form_set = ReminderFormSet(request.POST) -# if eventForm.is_valid() and reminder_form_set.is_valid(): -# parent_name = None -# if eventForm.cleaned_data.get('parent_type') and eventForm.cleaned_data.get('parent_id'): -# meeting_object = add_parent(eventForm.cleaned_data.get('parent_type'), -# eventForm.cleaned_data.get('parent_id'), eventForm, request.user) -# meeting = meeting_object[0] -# parent_name = meeting_object[1] -# else: -# meeting = eventForm.save(commit=False) -# meeting.created_user = request.user -# meeting.save() -# eventForm.save_m2m() -# reminders = reminder_form_set.save() -# meeting.reminders.add(*reminders) -# return JsonResponse({'success': 'Meeting Created', -# 'id': meeting.id, -# 'name': meeting.name, -# 'parent': parent_name, -# 'status': meeting.status, -# 'startdate': meeting.start_date -# }) -# else: -# return JsonResponse(dict(eventForm.errors)) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def create_task(request): -# if request.method == 'POST': -# taskForm = TaskEventForm(request.POST) -# if taskForm.is_valid(): -# if taskForm.cleaned_data.get('parent_type') and taskForm.cleaned_data.get('parent_id'): -# task_object = add_parent(taskForm.cleaned_data.get('parent_type'), -# taskForm.cleaned_data.get('parent_id'), taskForm, request.user) -# task = task_object[0] -# else: -# task = taskForm.save(commit=False) -# task.created_user = request.user -# task.save() -# return JsonResponse({'success': 'Task Created', -# 'id': task.id, -# 'name': task.name, -# 'status': task.status, -# 'priority': task.priority, -# 'startdate': task.start_date}) -# else: -# return JsonResponse(dict(taskForm.errors)) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def create_call(request): -# if request.method == 'POST': -# callsForm = CallEventForm(request.POST) -# ReminderFormSet = modelformset_factory(Reminder, ReminderForm, can_delete=True) -# reminder_form_set = ReminderFormSet(request.POST) -# if callsForm.is_valid() and reminder_form_set.is_valid(): -# parent_name = None -# if callsForm.cleaned_data.get('parent_type') and callsForm.cleaned_data.get('parent_id'): -# call_object = add_parent(callsForm.cleaned_data.get('parent_type'), -# callsForm.cleaned_data.get('parent_id'), callsForm, request.user) -# call = call_object[0] -# parent_name = call_object[1] -# else: -# call = callsForm.save(commit=False) -# call.created_user = request.user -# call.save() -# callsForm.save_m2m() -# reminders = reminder_form_set.save() -# call.reminders.add(*reminders) -# return JsonResponse({'success': 'Call Created', -# 'id': call.id, -# 'name': call.name, -# 'parent': parent_name, -# 'status': call.status, -# 'start_date': call.start_date}) -# else: -# return JsonResponse(dict(callsForm.errors)) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def delete_meeting(request): -# if request.method == 'POST': -# try: -# Event.objects.get(id=int(request.POST.get('meetingID')), created_user=request.user).delete() -# return JsonResponse({'success': 'Meeting Deleted'}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def delete_task(request): -# if request.method == 'POST': -# try: -# Event.objects.get(id=int(request.POST.get('taskID')), created_user=request.user).delete() -# return JsonResponse({'success': 'Task Deleted'}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def delete_call(request): -# if request.method == 'POST': -# try: -# Event.objects.get(id=int(request.POST.get('callID')), created_user=request.user).delete() -# return JsonResponse({'success': 'Call Deleted'}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def get_meeting(request): -# if request.method == 'POST': -# try: -# meeting = Event.objects.get(id=int(request.POST.get('meetingID')), created_user=request.user) -# ReminderFormSet = modelformset_factory(Reminder, ReminderForm, can_delete=True, extra=0) -# reminder_form_set = ReminderFormSet(queryset=meeting.reminders.all()) -# remindersHtML = render(request, 'reminder.html', {'reminder_form_set': reminder_form_set}) -# data = {} -# if meeting.parent: -# data['parent_type'] = meeting.parent.__class__.__name__ -# data['parent_name'] = meeting.parent.__str__() -# data['parent_id'] = meeting.object_id -# else: -# data['parent_name'] = '' -# data['parent_id'] = '' -# if meeting.assigned_users: data['assigned_users'] = { -# meeting.assigned_users.id: meeting.assigned_users.username} -# if meeting.attendees_user: data['attendees_user'] = {key.id: key.username for key in -# meeting.attendees_user.all()} -# if meeting.attendees_contacts: data['attendees_contacts'] = {key.id: key.name for key in -# meeting.attendees_contacts.all()} -# if meeting.attendees_leads: data['attendees_leads'] = {key.id: key.first_name for key in -# meeting.attendees_leads.all()} -# del meeting._state -# del meeting._parent_cache -# del meeting._assigned_users_cache -# meeting.__dict__['close_date'] = meeting.__dict__['close_date'].strftime('%m/%d/%Y %H:%M:%S') -# meeting.__dict__['start_date'] = meeting.__dict__['start_date'].strftime('%m/%d/%Y %H:%M:%S') -# data['meeting'] = meeting.__dict__ -# data['remindersHTML'] = str(remindersHtML.content) -# if meeting: -# return JsonResponse(data) -# else: -# return JsonResponse({'failed': 'ERROR'}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def get_task(request): -# if request.method == 'POST': -# try: -# task = Event.objects.get(id=int(request.POST.get('taskID')), created_user=request.user) -# data = {} -# if task.parent: -# data['parent_type'] = task.parent.__class__.__name__ -# data['parent_name'] = task.parent.__str__() -# data['parent_id'] = task.object_id -# else: -# data['parent_name'] = '' -# data['parent_id'] = '' -# if task.assigned_users: data['assigned_users'] = { -# task.assigned_users.id: task.assigned_users.username} -# del task._state -# del task._parent_cache -# del task._assigned_users_cache -# task.__dict__['close_date'] = task.__dict__['close_date'].strftime('%m/%d/%Y %H:%M:%S') -# task.__dict__['start_date'] = task.__dict__['start_date'].strftime('%m/%d/%Y %H:%M:%S') -# data['task'] = task.__dict__ -# if task: -# return JsonResponse(data) -# else: -# return JsonResponse({'failed': 'ERROR'}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def get_call(request): -# if request.method == 'POST': -# try: -# call = Event.objects.get(id=int(request.POST.get('callID')), created_user=request.user) -# ReminderFormSet = modelformset_factory(Reminder, ReminderForm, can_delete=True, extra=0) -# reminder_form_set = ReminderFormSet(queryset=call.reminders.all()) -# remindersHtML = render(request, 'planner/reminder.html', {'reminder_form_set': reminder_form_set}) -# data = {} -# if call.parent: -# data['parent_type'] = call.parent.__class__.__name__ -# data['parent_name'] = call.parent.__str__() -# data['parent_id'] = call.object_id -# else: -# data['parent_name'] = '' -# data['parent_id'] = '' -# if call.assigned_users: data['assigned_users'] = { -# call.assigned_users.id: call.assigned_users.username} -# if call.attendees_user: data['attendees_user'] = {key.id: key.username for key in -# call.attendees_user.all()} -# if call.attendees_contacts: data['attendees_contacts'] = {key.id: key.name for key in -# call.attendees_contacts.all()} -# if call.attendees_leads: data['attendees_leads'] = {key.id: key.first_name for key in -# call.attendees_leads.all()} -# del call._state -# del call._parent_cache -# del call._assigned_users_cache -# call.__dict__['start_date'] = call.__dict__['start_date'].strftime('%m/%d/%Y %H:%M:%S') -# data['call'] = call.__dict__ -# data['remindersHTML'] = str(remindersHtML.content) -# if call: -# return JsonResponse(data) -# else: -# return JsonResponse({'failed': 'ERROR'}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def update_meeting(request): -# if request.method == 'POST': -# try: -# meeting = Event.objects.get(Q(created_user=request.user) | Q(updated_user=request.user), -# id=int(request.POST.get('meetingID'))) -# eventForm = MeetingEventForm(request.POST, instance=meeting) -# ReminderFormSet = modelformset_factory(Reminder, ReminderForm, can_delete=True) -# reminder_form_set = ReminderFormSet(request.POST) -# if eventForm.is_valid() and reminder_form_set.is_valid(): -# meeting = None -# parent_name = None -# if eventForm.cleaned_data.get('parent_type') and eventForm.cleaned_data.get('parent_id'): -# meeting_object = add_parent(eventForm.cleaned_data.get('parent_type'), -# eventForm.cleaned_data.get('parent_id'), eventForm, None) -# meeting = meeting_object[0] -# parent_name = meeting_object[1] -# else: -# meeting = eventForm.save(commit=False) -# meeting.updated_user = request.user -# meeting.save() -# eventForm.save_m2m() -# reminders = reminder_form_set.save() -# meeting.reminders.add(*reminders) -# return JsonResponse({'success': 'Meeting Updated', -# 'id': meeting.id, -# 'name': meeting.name, -# 'parent': parent_name or None, -# 'status': meeting.status, -# 'startdate': meeting.start_date -# }) -# else: -# if reminder_form_set.errors: -# return JsonResponse({'reminder': reminder_form_set.errors[-1]['__all__'][0]}) -# elif eventForm.errors: -# return JsonResponse(dict(eventForm.errors)) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def update_task(request): -# if request.method == 'POST': -# try: -# task = Event.objects.get(Q(created_user=request.user) | Q(updated_user=request.user), -# id=int(request.POST.get('taskID'))) -# eventForm = TaskEventForm(request.POST, instance=task) -# if eventForm.is_valid(): -# task = None -# if eventForm.cleaned_data.get('parent_type') and eventForm.cleaned_data.get('parent_id'): -# task_object = add_parent(eventForm.cleaned_data.get('parent_type'), -# eventForm.cleaned_data.get('parent_id'), eventForm, None) -# task = task_object[0] -# else: -# task = eventForm.save(commit=False) -# task.updated_user = request.user -# task.save() -# return JsonResponse({'success': 'Task Updated', -# 'name': task.name, -# 'id': task.id, -# 'status': task.status, -# 'priority': task.priority, -# 'startdate': task.start_date}) -# else: -# return JsonResponse(dict(eventForm.errors)) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def update_call(request): -# if request.method == 'POST': -# try: -# call = Event.objects.get(Q(created_user=request.user) | Q(updated_user=request.user), -# id=int(request.POST.get('callID'))) -# callsForm = CallEventForm(request.POST, instance=call) -# ReminderFormSet = modelformset_factory(Reminder, ReminderForm, can_delete=True) -# reminder_form_set = ReminderFormSet(request.POST) -# if callsForm.is_valid() and reminder_form_set.is_valid(): -# parent_name = None -# if callsForm.cleaned_data.get('parent_type') and callsForm.cleaned_data.get('parent_id'): -# call_object = add_parent(callsForm.cleaned_data.get('parent_type'), -# callsForm.cleaned_data.get('parent_id'), callsForm, None) -# call = call_object[0] -# parent_name = call_object[1] -# else: -# call = callsForm.save(commit=False) -# call.updated_user = request.user -# call.save() -# callsForm.save_m2m() -# reminders = reminder_form_set.save() -# call.reminders.add(*reminders) -# return JsonResponse({'success': 'Call Updated', -# 'id': call.id, -# 'name': call.name, -# 'parent': parent_name or None, -# 'status': call.status, -# 'startdate': call.start_date -# }) -# else: -# return JsonResponse(dict(callsForm.errors)) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def update_event_status(request): -# if request.method == 'POST': -# try: -# event = Event.objects.get(Q(created_user=request.user) | Q(updated_user=request.user), -# Q(assigned_users=request.user), Q(attendees_user=request.user), -# id=int(request.POST.get('id'))) -# if request.POST.get('status') == 'Held': -# event.status = request.POST.get('status') -# event.updated_user = request.user -# event.save() -# return JsonResponse({'success': 'Event Updated', -# 'status': event.status, -# 'id': event.id, -# }) -# elif request.POST.get('status') == 'Not Held': -# event.status = request.POST.get('status') -# event.save() -# return JsonResponse({'success': 'Event Updated', -# 'id': event.id, -# 'status': event.status, -# }) -# else: -# return JsonResponse({'INVALID': 'DATA'}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def search_meetings(request): -# if request.method == 'GET': -# try: -# event = Event.objects.filter(Q(created_user=request.user) | Q(updated_user=request.user) | -# Q(assigned_users=request.user) | Q(attendees_user=request.user)).distinct() -# if request.GET.get('event_type'): -# event = event.filter(event_type=request.GET.get('event_type')) -# if request.GET.get('event_name'): -# event = event.filter(name=request.GET.get('event_name')) or event -# if request.GET.get('parent_type') and request.GET.get('parent_id'): -# parent = get_parent(request.GET.get('parent_type'), request.GET.get('parent_id')) -# if parent is not None: -# parent_content_type = ContentType.objects.get_for_model(parent) -# event = event.filter(content_type__pk=parent_content_type.id, object_id=parent.id) or event - -# if request.GET.get('start_date'): -# start_date = datetime.datetime.strptime(request.GET.get('start_date'), '%m/%d/%Y %H:%M:%S') -# event = event.filter(start_date=start_date) or event -# if request.GET.get('created_at'): -# created_at = datetime.datetime.strptime(request.GET.get('created_at'), '%m/%d/%Y %H:%M:%S') -# event = event.filter(created_at=created_at) or event -# if request.GET.get('updated_at'): -# updated_at = datetime.datetime.strptime(request.GET.get('updated_at'), '%m/%d/%Y %H:%M:%S') -# event = event.filter(updated_at=updated_at) or event - -# if request.GET.get('status'): -# event = event.filter(status=request.GET.get('status')) or event - -# if request.GET.getlist('attendees_contacts'): -# event = event.filter(attendees_contacts__in=request.GET.getlist('attendees_contacts')) or event -# if request.GET.getlist('attendees_user'): -# event = event.filter(attendees_user__in=request.GET.getlist('attendees_user')) or event -# if request.GET.getlist('attendees_leads'): -# event = event.filter(attendees_leads__in=request.GET.getlist('attendees_leads')) or event - -# if request.GET.get('assigned_users'): -# event = event.filter(assigned_users=request.GET.get('assigned_users')) or event -# if request.GET.get('created_user'): -# event = event.filter(created_user=request.GET.get('created_user')) or event -# if request.GET.get('updated_user'): -# event = event.filter(updated_user=request.GET.get('updated_user')) or event - -# paginator = Paginator(event, 5) -# if request.GET.get('event_type') == 'Meeting': -# try: -# event = paginator.page(request.GET.get('page')) -# return render(request, 'meetings_list.html', -# {'meetings': event}) -# except PageNotAnInteger: -# event = paginator.page(1) -# except EmptyPage: -# event = paginator.page(paginator.num_pages) -# return render(request, 'meetings_list.html', -# {'meetings': event}) -# elif request.GET.get('event_type') == 'Call': -# try: -# event = paginator.page(request.GET.get('page')) -# return render(request, 'calls_list.html', -# {'calls': event}) -# except PageNotAnInteger: -# event = paginator.page(1) -# except EmptyPage: -# event = paginator.page(paginator.num_pages) -# return render(request, 'calls_list.html', -# {'calls': event}) -# elif request.GET.get('event_type') == 'Task': -# try: -# event = paginator.page(request.GET.get('page')) -# return render(request, 'tasks_list.html', -# {'tasks': event}) -# except PageNotAnInteger: -# event = paginator.page(1) -# except EmptyPage: -# event = paginator.page(paginator.num_pages) -# return render(request, 'tasks_list.html', -# {'tasks': event}) -# except models.Event.DoesNotExist: -# return JsonResponse({'Event': 'DoesNotExist'}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# @login_required -# def get_reminder_form(request): -# if request.method == 'GET': -# RemindersFormSet = modelformset_factory(Reminder, form=ReminderForm, can_delete=True) -# data = { -# 'form-TOTAL_FORMS': '1', -# 'form-INITIAL_FORMS': '0', -# 'form-MAX_NUM_FORMS': '10', -# } -# reminder_form_set = RemindersFormSet(data) -# return render(request, 'reminder.html', {'reminder_form_set': reminder_form_set}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# def add_parent(parent_type, parent_id, form, request_user): -# if parent_type == 'Account': -# account = Account.objects.get(id=int(parent_id)) -# event = form.save(commit=False) -# event.parent = account -# if request_user is not None: -# event.created_user = request_user -# else: -# event.updated_user = request_user -# event.save() -# return (event, event.parent.name) -# elif parent_type == 'Case': -# case = Case.objects.get(id=int(parent_id)) -# event = form.save(commit=False) -# event.parent = case -# if request_user is not None: -# event.created_user = request_user -# else: -# event.updated_user = request_user -# event.save() -# return (event, event.parent.name) -# elif parent_type == 'Lead': -# lead = Lead.objects.get(id=int(parent_id)) -# event = form.save(commit=False) -# event.parent = lead -# if request_user is not None: -# event.created_user = request_user -# else: -# event.updated_user = request_user -# event.save() -# return (event, event.parent.first_name) - -# elif parent_type == 'Opportunity': -# opportunity = Opportunity.objects.get(id=int(parent_id)) -# event = form.save(commit=False) -# event.parent = opportunity -# if request_user is not None: -# event.created_user = request_user -# else: -# event.updated_user = request_user -# event.save() -# return (event, event.parent.name) -# elif parent_type == 'Contact': -# contact = Contact.objects.get(id=int(parent_id)) -# event = form.save(commit=False) -# event.parent = contact -# if request_user is not None: -# event.created_user = request_user -# else: -# event.updated_user = request_user -# event.save() -# return (event, event.parent.name) - - -# def get_parent(parent_type, parent_id): -# if parent_type == 'Account': -# try: -# account = Account.objects.get(id=parent_id) -# return account -# except Account.DoesNotExist: -# return None -# elif parent_type == 'Lead': -# try: -# lead = Lead.objects.get(id=parent_id) -# return lead -# except Lead.DoesNotExist: -# return None -# elif parent_type == 'Opportunity': -# try: -# opportunity = Opportunity.objects.get(id=parent_id) -# return opportunity -# except Opportunity.DoesNotExist: -# return None -# elif parent_type == 'Case': -# try: -# case = Case.objects.get(id=parent_id) -# return case -# except Case.DoesNotExist: -# return None -# elif parent_type == 'Contact': -# try: -# contact = Contact.objects.get(id=parent_id) -# return contact -# except Contact.DoesNotExist: -# return None - - -# def get_users(request): -# if request.method == 'GET': -# users = User.objects.all() -# paginator = Paginator(users, 5) -# try: -# users = paginator.page(request.GET.get('page')) -# return render(request, 'users.html', -# {'users': users}) -# except PageNotAnInteger: -# users = paginator.page(1) -# except EmptyPage: -# users = paginator.page(paginator.num_pages) -# return render(request, 'users.html', {'users': users}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# def get_contacts(request): -# if request.method == 'GET': -# contacts = Contact.objects.all() -# paginator = Paginator(contacts, 5) -# try: -# contacts = paginator.page(request.GET.get('page')) -# return render(request, 'contacts.html', -# {'contacts': contacts}) -# except PageNotAnInteger: -# contacts = paginator.page(1) -# except EmptyPage: -# contacts = paginator.page(paginator.num_pages) -# return render(request, 'contacts.html', {'contacts': contacts}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) - - -# def get_leads(request): -# if request.method == 'GET': -# leads = Lead.objects.all() -# paginator = Paginator(leads, 5) -# try: -# leads = paginator.page(request.GET.get('page')) -# return render(request, 'leads.html', -# {'leads': leads}) -# except PageNotAnInteger: -# leads = paginator.page(1) -# except EmptyPage: -# leads = paginator.page(paginator.num_pages) -# return render(request, 'leads.html', {'leads': leads}) -# else: -# return JsonResponse({'METHOD': 'INVALID'}) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 129686d6..00000000 --- a/requirements.txt +++ /dev/null @@ -1,22 +0,0 @@ -django==4.2.1 -celery==5.2.7 -python-dotenv==1.0.0 -django-cors-headers==4.0.0 -djangorestframework==3.14.0 -djangorestframework-simplejwt==5.2.2 -drf-spectacular==0.26.2 -django-ses==3.5.0 -psycopg2-binary==2.9.6 -whitenoise==6.4.0 -sentry-sdk==1.24.0 -wagtail==5.0.1 -django_extensions==3.2.1 -django_storages==1.13.2 - -drf-rw-serializers==1.0.5 -django-crum==0.7.9 -Redis==4.6.0 -# remove it -django-phonenumber-field==7.1.0 -arrow==1.2.3 -phonenumbers==8.13.13 diff --git a/scripts/gunicorn.sh b/scripts/gunicorn.sh deleted file mode 100755 index d93d6709..00000000 --- a/scripts/gunicorn.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -APP_NAME=bottlecrm-api - -cd /home/ubuntu/$APP_NAME/$APP_NAME -. ../venv/bin/activate - -# debug -whoami; pwd; ls -al; env - -exec gunicorn crm.wsgi:application \ - --name $APP_NAME \ - --workers 2 \ - --threads 4 \ - --worker-class gthread \ - --worker-tmp-dir /dev/shm \ - --bind 0.0.0.0:8000 \ - --log-level debug diff --git a/scripts/migrate.sh b/scripts/migrate.sh deleted file mode 100755 index ea8cbd39..00000000 --- a/scripts/migrate.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -APP_NAME=bottlecrm-api - -cd /home/ubuntu/$APP_NAME/$APP_NAME -. ../venv/bin/activate - -# debug -whoami; pwd; ls -al; env - -exec python manage.py migrate diff --git a/static/assets/css/animate.min.css b/static/assets/css/animate.min.css deleted file mode 100644 index ab93d29b..00000000 --- a/static/assets/css/animate.min.css +++ /dev/null @@ -1,7 +0,0 @@ -@charset "UTF-8";/*! - * animate.css - https://animate.style/ - * Version - 4.1.1 - * Licensed under the MIT license - http://opensource.org/licenses/MIT - * - * Copyright (c) 2020 Animate.css - */:root{--animate-duration:1s;--animate-delay:1s;--animate-repeat:1}.animate__animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-duration:var(--animate-duration);animation-duration:var(--animate-duration);-webkit-animation-fill-mode:both;animation-fill-mode:both}.animate__animated.animate__infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animate__animated.animate__repeat-1{-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-animation-iteration-count:var(--animate-repeat);animation-iteration-count:var(--animate-repeat)}.animate__animated.animate__repeat-2{-webkit-animation-iteration-count:2;animation-iteration-count:2;-webkit-animation-iteration-count:calc(var(--animate-repeat)*2);animation-iteration-count:calc(var(--animate-repeat)*2)}.animate__animated.animate__repeat-3{-webkit-animation-iteration-count:3;animation-iteration-count:3;-webkit-animation-iteration-count:calc(var(--animate-repeat)*3);animation-iteration-count:calc(var(--animate-repeat)*3)}.animate__animated.animate__delay-1s{-webkit-animation-delay:1s;animation-delay:1s;-webkit-animation-delay:var(--animate-delay);animation-delay:var(--animate-delay)}.animate__animated.animate__delay-2s{-webkit-animation-delay:2s;animation-delay:2s;-webkit-animation-delay:calc(var(--animate-delay)*2);animation-delay:calc(var(--animate-delay)*2)}.animate__animated.animate__delay-3s{-webkit-animation-delay:3s;animation-delay:3s;-webkit-animation-delay:calc(var(--animate-delay)*3);animation-delay:calc(var(--animate-delay)*3)}.animate__animated.animate__delay-4s{-webkit-animation-delay:4s;animation-delay:4s;-webkit-animation-delay:calc(var(--animate-delay)*4);animation-delay:calc(var(--animate-delay)*4)}.animate__animated.animate__delay-5s{-webkit-animation-delay:5s;animation-delay:5s;-webkit-animation-delay:calc(var(--animate-delay)*5);animation-delay:calc(var(--animate-delay)*5)}.animate__animated.animate__faster{-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-duration:calc(var(--animate-duration)/2);animation-duration:calc(var(--animate-duration)/2)}.animate__animated.animate__fast{-webkit-animation-duration:.8s;animation-duration:.8s;-webkit-animation-duration:calc(var(--animate-duration)*0.8);animation-duration:calc(var(--animate-duration)*0.8)}.animate__animated.animate__slow{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2)}.animate__animated.animate__slower{-webkit-animation-duration:3s;animation-duration:3s;-webkit-animation-duration:calc(var(--animate-duration)*3);animation-duration:calc(var(--animate-duration)*3)}@media (prefers-reduced-motion:reduce),print{.animate__animated{-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-transition-duration:1ms!important;transition-duration:1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important}.animate__animated[class*=Out]{opacity:0}}@-webkit-keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}@keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}.animate__bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.animate__flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__pulse{-webkit-animation-name:pulse;animation-name:pulse;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.animate__shakeX{-webkit-animation-name:shakeX;animation-name:shakeX}@-webkit-keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}@keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}.animate__shakeY{-webkit-animation-name:shakeY;animation-name:shakeY}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.animate__headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.animate__swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.animate__jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}.animate__heartBeat{-webkit-animation-name:heartBeat;animation-name:heartBeat;-webkit-animation-duration:1.3s;animation-duration:1.3s;-webkit-animation-duration:calc(var(--animate-duration)*1.3);animation-duration:calc(var(--animate-duration)*1.3);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInDown{-webkit-animation-name:backInDown;animation-name:backInDown}@-webkit-keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInLeft{-webkit-animation-name:backInLeft;animation-name:backInLeft}@-webkit-keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInRight{-webkit-animation-name:backInRight;animation-name:backInRight}@-webkit-keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInUp{-webkit-animation-name:backInUp;animation-name:backInUp}@-webkit-keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}@keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}.animate__backOutDown{-webkit-animation-name:backOutDown;animation-name:backOutDown}@-webkit-keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}@keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}.animate__backOutLeft{-webkit-animation-name:backOutLeft;animation-name:backOutLeft}@-webkit-keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}@keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}.animate__backOutRight{-webkit-animation-name:backOutRight;animation-name:backOutRight}@-webkit-keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}@keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}.animate__backOutUp{-webkit-animation-name:backOutUp;animation-name:backOutUp}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__bounceIn{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.animate__bounceOut{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}.animate__bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}.animate__bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}.animate__bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}.animate__bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate__fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopLeft{-webkit-animation-name:fadeInTopLeft;animation-name:fadeInTopLeft}@-webkit-keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopRight{-webkit-animation-name:fadeInTopRight;animation-name:fadeInTopRight}@-webkit-keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomLeft{-webkit-animation-name:fadeInBottomLeft;animation-name:fadeInBottomLeft}@-webkit-keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomRight{-webkit-animation-name:fadeInBottomRight;animation-name:fadeInBottomRight}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.animate__fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.animate__fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.animate__fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.animate__fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.animate__fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}@keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}.animate__fadeOutTopLeft{-webkit-animation-name:fadeOutTopLeft;animation-name:fadeOutTopLeft}@-webkit-keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}@keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}.animate__fadeOutTopRight{-webkit-animation-name:fadeOutTopRight;animation-name:fadeOutTopRight}@-webkit-keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}@keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}.animate__fadeOutBottomRight{-webkit-animation-name:fadeOutBottomRight;animation-name:fadeOutBottomRight}@-webkit-keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}@keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}.animate__fadeOutBottomLeft{-webkit-animation-name:fadeOutBottomLeft;animation-name:fadeOutBottomLeft}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animate__animated.animate__flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.animate__flipOutX{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.animate__flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInRight{-webkit-animation-name:lightSpeedInRight;animation-name:lightSpeedInRight;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInLeft{-webkit-animation-name:lightSpeedInLeft;animation-name:lightSpeedInLeft;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.animate__lightSpeedOutRight{-webkit-animation-name:lightSpeedOutRight;animation-name:lightSpeedOutRight;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}@keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}.animate__lightSpeedOutLeft{-webkit-animation-name:lightSpeedOutLeft;animation-name:lightSpeedOutLeft;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.animate__rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.animate__rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.animate__rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.animate__hinge{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2);-webkit-animation-name:hinge;animation-name:hinge;-webkit-transform-origin:top left;transform-origin:top left}@-webkit-keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.animate__jackInTheBox{-webkit-animation-name:jackInTheBox;animation-name:jackInTheBox}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.animate__rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.animate__zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}.animate__zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}.animate__zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft;-webkit-transform-origin:left center;transform-origin:left center}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}.animate__zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight;-webkit-transform-origin:right center;transform-origin:right center}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file diff --git a/static/assets/css/bootstrap.min.css b/static/assets/css/bootstrap.min.css deleted file mode 100644 index 526a50e9..00000000 --- a/static/assets/css/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.0.0-beta1 (https://getbootstrap.com/) - * Copyright 2011-2020 The Bootstrap Authors - * Copyright 2011-2020 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus{outline:dotted 1px;outline:-webkit-focus-ring-color auto 5px}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x)/ -2);margin-left:calc(var(--bs-gutter-x)/ -2)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x)/ 2);padding-left:calc(var(--bs-gutter-x)/ 2);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.3333333333%}.col-2{flex:0 0 auto;width:16.6666666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.3333333333%}.col-5{flex:0 0 auto;width:41.6666666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.3333333333%}.col-8{flex:0 0 auto;width:66.6666666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.3333333333%}.col-11{flex:0 0 auto;width:91.6666666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.3333333333%}.col-sm-2{flex:0 0 auto;width:16.6666666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.3333333333%}.col-sm-5{flex:0 0 auto;width:41.6666666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.3333333333%}.col-sm-8{flex:0 0 auto;width:66.6666666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.3333333333%}.col-sm-11{flex:0 0 auto;width:91.6666666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.3333333333%}.col-md-2{flex:0 0 auto;width:16.6666666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.3333333333%}.col-md-5{flex:0 0 auto;width:41.6666666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.3333333333%}.col-md-8{flex:0 0 auto;width:66.6666666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.3333333333%}.col-md-11{flex:0 0 auto;width:91.6666666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.3333333333%}.col-lg-2{flex:0 0 auto;width:16.6666666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.3333333333%}.col-lg-5{flex:0 0 auto;width:41.6666666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.3333333333%}.col-lg-8{flex:0 0 auto;width:66.6666666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.3333333333%}.col-lg-11{flex:0 0 auto;width:91.6666666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.3333333333%}.col-xl-2{flex:0 0 auto;width:16.6666666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.3333333333%}.col-xl-5{flex:0 0 auto;width:41.6666666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.3333333333%}.col-xl-8{flex:0 0 auto;width:66.6666666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.3333333333%}.col-xl-11{flex:0 0 auto;width:91.6666666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.3333333333%}.col-xxl-2{flex:0 0 auto;width:16.6666666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.3333333333%}.col-xxl-5{flex:0 0 auto;width:41.6666666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.3333333333%}.col-xxl-8{flex:0 0 auto;width:66.6666666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.3333333333%}.col-xxl-11{flex:0 0 auto;width:91.6666666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.3333333333%}.offset-xxl-2{margin-left:16.6666666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.3333333333%}.offset-xxl-5{margin-left:41.6666666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.3333333333%}.offset-xxl-8{margin-left:66.6666666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.3333333333%}.offset-xxl-11{margin-left:91.6666666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);background-image:linear-gradient(var(--bs-table-accent-bg),var(--bs-table-accent-bg));border-bottom-width:1px}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;vertical-align:middle;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{color:#6c757d;background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;transition:background-color .15s ease-in-out,background-position .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-check-input{transition:none}}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);padding:1rem .75rem}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control::-webkit-input-placeholder{color:transparent}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:1.75rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754;padding-right:calc(.75em + 2.3125rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 1.75rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 1.75rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[style]{right:auto!important}.dropdown-menu-start{--bs-position:start;right:auto;left:0}.dropdown-menu-end{--bs-position:end;right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start;right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end;right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start;right:auto;left:0}.dropdown-menu-md-end{--bs-position:end;right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start;right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end;right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start;right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end;right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start;right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end;right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{align-items:center;width:100%}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-collapse{display:flex!important}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-collapse{display:flex!important}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-collapse{display:flex!important}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-collapse{display:flex!important}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-collapse{display:flex!important}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-collapse{display:flex!important}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;background-color:transparent;border:1px solid rgba(0,0,0,.125);border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button.collapsed{border-bottom-width:0}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item:first-of-type .accordion-button{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-width:1px;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-collapse{border-bottom-width:1px;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-collapse{border:solid rgba(0,0,0,.125);border-width:0 1px}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-button{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item:first-of-type .accordion-button{border-top-width:0;border-top-left-radius:0;border-top-right-radius:0}.accordion-flush .accordion-item:last-of-type .accordion-button.collapsed{border-bottom-width:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top],.bs-popover-top{margin-bottom:.5rem!important}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right],.bs-popover-end{margin-left:.5rem!important}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom],.bs-popover-bottom{margin-top:.5rem!important}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left],.bs-popover-start{margin-right:.5rem!important}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid #d8d8d8;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators li{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--aspect-ratio:100%}.ratio-4x3{--aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-0{border-width:0!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}.fs-sm-1{font-size:2.5rem!important}.fs-sm-2{font-size:2rem!important}.fs-sm-3{font-size:1.75rem!important}.fs-sm-4{font-size:1.5rem!important}.fs-md-1{font-size:2.5rem!important}.fs-md-2{font-size:2rem!important}.fs-md-3{font-size:1.75rem!important}.fs-md-4{font-size:1.5rem!important}.fs-lg-1{font-size:2.5rem!important}.fs-lg-2{font-size:2rem!important}.fs-lg-3{font-size:1.75rem!important}.fs-lg-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/static/assets/css/bootstrap.min.css.map b/static/assets/css/bootstrap.min.css.map deleted file mode 100644 index e69de29b..00000000 diff --git a/static/assets/css/flaticon.css b/static/assets/css/flaticon.css deleted file mode 100644 index af40b678..00000000 --- a/static/assets/css/flaticon.css +++ /dev/null @@ -1,179 +0,0 @@ -/* -Flaticon icon font: Flaticon -*/ -@font-face { - font-family: "Flaticon"; - src: url("../../assets/webfonts/Flaticon.eot"); - src: url("../../assets/webfonts/Flaticon.eot?#iefix") format("embedded-opentype"), - url("../../assets/webfonts/Flaticon.woff2") format("woff2"), - url("../../assets/webfonts/Flaticon.woff") format("woff"), - url("../../assets/webfonts/Flaticon.ttf") format("truetype"), - url("../../assets/webfonts/Flaticon.svg#Flaticon") format("svg"); - font-weight: normal; - font-style: normal; -} -@media screen and (-webkit-min-device-pixel-ratio:0) { - @font-face { - font-family: "Flaticon"; - src: url("../../assets/webfonts/Flaticon.svg#Flaticon") format("svg"); - } -} -[class^="flaticon-"]:before, [class*=" flaticon-"]:before, -[class^="flaticon-"]:after, [class*=" flaticon-"]:after { - font-family: Flaticon; - font-style: normal; -} - -.flaticon-video-conference:before { content: "\f100"; } -.flaticon-bank:before { content: "\f101"; } -.flaticon-manufacturing:before { content: "\f102"; } -.flaticon-oil:before { content: "\f103"; } -.flaticon-oil-1:before { content: "\f104"; } -.flaticon-suitcase:before { content: "\f105"; } -.flaticon-refresh:before { content: "\f106"; } -.flaticon-checking:before { content: "\f107"; } -.flaticon-hacker:before { content: "\f108"; } -.flaticon-antivirus:before { content: "\f109"; } -.flaticon-internet:before { content: "\f10a"; } -.flaticon-edit:before { content: "\f10b"; } -.flaticon-promotion:before { content: "\f10c"; } -.flaticon-email:before { content: "\f10d"; } -.flaticon-education:before { content: "\f10e"; } -.flaticon-project-management:before { content: "\f10f"; } -.flaticon-social-media:before { content: "\f110"; } -.flaticon-sketch:before { content: "\f111"; } -.flaticon-enterprise:before { content: "\f112"; } -.flaticon-verified:before { content: "\f113"; } -.flaticon-www:before { content: "\f114"; } -.flaticon-data:before { content: "\f115"; } -.flaticon-book:before { content: "\f116"; } -.flaticon-heart:before { content: "\f117"; } -.flaticon-growth:before { content: "\f118"; } -.flaticon-fast-time:before { content: "\f119"; } -.flaticon-domain:before { content: "\f11a"; } -.flaticon-brain:before { content: "\f11b"; } -.flaticon-innovation:before { content: "\f11c"; } -.flaticon-chip:before { content: "\f11d"; } -.flaticon-tools:before { content: "\f11e"; } -.flaticon-startup:before { content: "\f11f"; } -.flaticon-smartphone:before { content: "\f120"; } -.flaticon-process:before { content: "\f121"; } -.flaticon-calendar:before { content: "\f122"; } -.flaticon-shopping-cart:before { content: "\f123"; } -.flaticon-calendar-1:before { content: "\f124"; } -.flaticon-coin:before { content: "\f125"; } -.flaticon-idea:before { content: "\f126"; } -.flaticon-thinking:before { content: "\f127"; } -.flaticon-paper-plane:before { content: "\f128"; } -.flaticon-debit-card:before { content: "\f129"; } -.flaticon-checklist:before { content: "\f12a"; } -.flaticon-university:before { content: "\f12b"; } -.flaticon-database:before { content: "\f12c"; } -.flaticon-promotion-1:before { content: "\f12d"; } -.flaticon-wifi:before { content: "\f12e"; } -.flaticon-soap:before { content: "\f12f"; } -.flaticon-diamond:before { content: "\f130"; } -.flaticon-planet-earth:before { content: "\f131"; } -.flaticon-folder:before { content: "\f132"; } -.flaticon-online-shop:before { content: "\f133"; } -.flaticon-quality:before { content: "\f134"; } -.flaticon-darts:before { content: "\f135"; } -.flaticon-garbage:before { content: "\f136"; } -.flaticon-idea-1:before { content: "\f137"; } -.flaticon-rating:before { content: "\f138"; } -.flaticon-key:before { content: "\f139"; } -.flaticon-discuss-issue:before { content: "\f13a"; } -.flaticon-megaphone:before { content: "\f13b"; } -.flaticon-target:before { content: "\f13c"; } -.flaticon-megaphone-1:before { content: "\f13d"; } -.flaticon-push-pin:before { content: "\f13e"; } -.flaticon-cloud-computing:before { content: "\f13f"; } -.flaticon-megaphone-2:before { content: "\f140"; } -.flaticon-around:before { content: "\f141"; } -.flaticon-rating-1:before { content: "\f142"; } -.flaticon-cake:before { content: "\f143"; } -.flaticon-research:before { content: "\f144"; } -.flaticon-locked:before { content: "\f145"; } -.flaticon-fingerprint:before { content: "\f146"; } -.flaticon-mental-health:before { content: "\f147"; } -.flaticon-folder-1:before { content: "\f148"; } -.flaticon-customer-service:before { content: "\f149"; } -.flaticon-analysis-1:before { content: "\f14a"; } -.flaticon-compose:before { content: "\f14b"; } -.flaticon-graduation-cap:before { content: "\f14c"; } -.flaticon-credit-card:before { content: "\f14d"; } -.flaticon-online-course:before { content: "\f14e"; } -.flaticon-stats:before { content: "\f14f"; } -.flaticon-business-and-trade:before { content: "\f150"; } -.flaticon-business-plan:before { content: "\f151"; } -.flaticon-money:before { content: "\f152"; } -.flaticon-goal:before { content: "\f153"; } -.flaticon-employee:before { content: "\f154"; } -.flaticon-business:before { content: "\f155"; } -.flaticon-money-1:before { content: "\f156"; } -.flaticon-business-1:before { content: "\f157"; } -.flaticon-launch:before { content: "\f158"; } -.flaticon-strategy:before { content: "\f159"; } -.flaticon-contract:before { content: "\f15a"; } -.flaticon-stock-market:before { content: "\f15b"; } -.flaticon-profits:before { content: "\f15c"; } -.flaticon-development:before { content: "\f15d"; } -.flaticon-puzzle-pieces:before { content: "\f15e"; } -.flaticon-benefit:before { content: "\f15f"; } -.flaticon-handcuff:before { content: "\f160"; } -.flaticon-piggy-bank:before { content: "\f161"; } -.flaticon-chat:before { content: "\f162"; } -.flaticon-planning:before { content: "\f163"; } -.flaticon-development-1:before { content: "\f164"; } -.flaticon-solution:before { content: "\f165"; } -.flaticon-contract-1:before { content: "\f166"; } -.flaticon-consultant-services:before { content: "\f167"; } -.flaticon-file:before { content: "\f168"; } -.flaticon-rating-2:before { content: "\f169"; } -.flaticon-group:before { content: "\f16a"; } -.flaticon-trophy:before { content: "\f16b"; } -.flaticon-digital-marketing:before { content: "\f16c"; } -.flaticon-statistics:before { content: "\f16d"; } -.flaticon-consulting:before { content: "\f16e"; } -.flaticon-graphic-design:before { content: "\f16f"; } -.flaticon-development-2:before { content: "\f170"; } -.flaticon-play-button:before { content: "\f171"; } -.flaticon-log-in:before { content: "\f172"; } -.flaticon-automation:before { content: "\f173"; } -.flaticon-customer-support:before { content: "\f174"; } -.flaticon-cloud-storage:before { content: "\f175"; } -.flaticon-mobile-app:before { content: "\f176"; } -.flaticon-ux:before { content: "\f177"; } -.flaticon-seo:before { content: "\f178"; } -.flaticon-application:before { content: "\f179"; } -.flaticon-bullhorn:before { content: "\f17a"; } -.flaticon-web-development:before { content: "\f17b"; } -.flaticon-development-3:before { content: "\f17c"; } -.flaticon-digital-marketing-1:before { content: "\f17d"; } -.flaticon-analytics:before { content: "\f17e"; } -.flaticon-solution-1:before { content: "\f17f"; } -.flaticon-consulting-1:before { content: "\f180"; } -.flaticon-analysis-2:before { content: "\f181"; } -.flaticon-project-management-1:before { content: "\f182"; } -.flaticon-analytics-1:before { content: "\f183"; } -.flaticon-innovation-1:before { content: "\f184"; } -.flaticon-growth-1:before { content: "\f185"; } -.flaticon-consultation:before { content: "\f186"; } -.flaticon-product-design:before { content: "\f187"; } -.flaticon-web-design:before { content: "\f188"; } -.flaticon-logo:before { content: "\f189"; } -.flaticon-print:before { content: "\f18a"; } -.flaticon-graphic-design-1:before { content: "\f18b"; } -.flaticon-motion-graphic:before { content: "\f18c"; } -.flaticon-engineering:before { content: "\f18d"; } -.flaticon-secure-data:before { content: "\f18e"; } -.flaticon-license:before { content: "\f18f"; } -.flaticon-planning-1:before { content: "\f190"; } -.flaticon-analytics-2:before { content: "\f191"; } -.flaticon-scalable:before { content: "\f192"; } -.flaticon-system-update:before { content: "\f193"; } -.flaticon-left-arrow:before { content: "\f194"; } -.flaticon-right-arrow:before { content: "\f195"; } -.flaticon-quotation:before { content: "\f196"; } -.flaticon-apple:before { content: "\f197"; } -.flaticon-play-store:before { content: "\f198"; } \ No newline at end of file diff --git a/static/assets/css/fontawesome.min.css b/static/assets/css/fontawesome.min.css deleted file mode 100644 index 656a5074..00000000 --- a/static/assets/css/fontawesome.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\e059"}.fa-bacterium:before{content:"\e05a"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\e05b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudflare:before{content:"\e07d"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\e052"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-deezer:before{content:"\e077"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\e078"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\e005"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\e007"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\e079"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guilded:before{content:"\e07e"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\e05d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\e05e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\e05f"}.fa-handshake-slash:before{content:"\e060"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\e061"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-head-side-mask:before{content:"\e063"}.fa-head-side-virus:before{content:"\e064"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hive:before{content:"\e07f"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\e065"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\e013"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-innosoft:before{content:"\e080"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\e055"}.fa-instalod:before{content:"\e081"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\e066"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\e067"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\e01a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\e056"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-octopus-deploy:before{content:"\e082"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\e068"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-perbyte:before{content:"\e083"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\e01e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\e069"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\e06a"}.fa-pump-soap:before{content:"\e06b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-rust:before{content:"\e07a"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\e06c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\e057"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sink:before{content:"\e06d"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\e06e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\e06f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\e070"}.fa-store-slash:before{content:"\e071"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-tiktok:before{content:"\e07b"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\e041"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-uncharted:before{content:"\e084"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\e049"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-unsplash:before{content:"\e07c"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-users-slash:before{content:"\e073"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-vest:before{content:"\e085"}.fa-vest-patches:before{content:"\e086"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\e074"}.fa-virus-slash:before{content:"\e075"}.fa-viruses:before{content:"\e076"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-watchman-monitoring:before{content:"\e087"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wodu:before{content:"\e088"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.fab,.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/static/assets/css/magnific-popup.min.css b/static/assets/css/magnific-popup.min.css deleted file mode 100644 index c00c6ff9..00000000 --- a/static/assets/css/magnific-popup.min.css +++ /dev/null @@ -1 +0,0 @@ -.mfp-bg{top:0;left:0;width:100%;height:100%;z-index:1042;overflow:hidden;position:fixed;background:#0b0b0b;opacity:.8}.mfp-wrap{top:0;left:0;width:100%;height:100%;z-index:1043;position:fixed;outline:none !important;-webkit-backface-visibility:hidden}.mfp-container{text-align:center;position:absolute;width:100%;height:100%;left:0;top:0;padding:0 8px;box-sizing:border-box}.mfp-container:before{content:'';display:inline-block;height:100%;vertical-align:middle}.mfp-align-top .mfp-container:before{display:none}.mfp-content{position:relative;display:inline-block;vertical-align:middle;margin:0 auto;text-align:left;z-index:1045}.mfp-inline-holder .mfp-content,.mfp-ajax-holder .mfp-content{width:100%;cursor:auto}.mfp-ajax-cur{cursor:progress}.mfp-zoom-out-cur,.mfp-zoom-out-cur .mfp-image-holder .mfp-close{cursor:-moz-zoom-out;cursor:-webkit-zoom-out;cursor:zoom-out}.mfp-zoom{cursor:pointer;cursor:-webkit-zoom-in;cursor:-moz-zoom-in;cursor:zoom-in}.mfp-auto-cursor .mfp-content{cursor:auto}.mfp-close,.mfp-arrow,.mfp-preloader,.mfp-counter{-webkit-user-select:none;-moz-user-select:none;user-select:none}.mfp-loading.mfp-figure{display:none}.mfp-hide{display:none !important}.mfp-preloader{color:#CCC;position:absolute;top:50%;width:auto;text-align:center;margin-top:-0.8em;left:8px;right:8px;z-index:1044}.mfp-preloader a{color:#CCC}.mfp-preloader a:hover{color:#FFF}.mfp-s-ready .mfp-preloader{display:none}.mfp-s-error .mfp-content{display:none}button.mfp-close,button.mfp-arrow{overflow:visible;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;display:block;outline:0;padding:0;z-index:1046;box-shadow:none;touch-action:manipulation}button::-moz-focus-inner{padding:0;border:0}.mfp-close{width:44px;height:44px;line-height:44px;position:absolute;right:0;top:0;text-decoration:none;text-align:center;opacity:.65;padding:0 0 18px 10px;color:#FFF;font-style:normal;font-size:28px;font-family:Arial,Baskerville,monospace}.mfp-close:hover,.mfp-close:focus{opacity:1}.mfp-close:active{top:1px}.mfp-close-btn-in .mfp-close{color:#333}.mfp-image-holder .mfp-close,.mfp-iframe-holder .mfp-close{color:#FFF;right:-6px;text-align:right;padding-right:6px;width:100%}.mfp-counter{position:absolute;top:0;right:0;color:#CCC;font-size:12px;line-height:18px;white-space:nowrap}.mfp-arrow{position:absolute;opacity:.65;margin:0;top:50%;margin-top:-55px;padding:0;width:90px;height:110px;-webkit-tap-highlight-color:transparent}.mfp-arrow:active{margin-top:-54px}.mfp-arrow:hover,.mfp-arrow:focus{opacity:1}.mfp-arrow:before,.mfp-arrow:after{content:'';display:block;width:0;height:0;position:absolute;left:0;top:0;margin-top:35px;margin-left:35px;border:medium inset transparent}.mfp-arrow:after{border-top-width:13px;border-bottom-width:13px;top:8px}.mfp-arrow:before{border-top-width:21px;border-bottom-width:21px;opacity:.7}.mfp-arrow-left{left:0}.mfp-arrow-left:after{border-right:17px solid #FFF;margin-left:31px}.mfp-arrow-left:before{margin-left:25px;border-right:27px solid #3f3f3f}.mfp-arrow-right{right:0}.mfp-arrow-right:after{border-left:17px solid #FFF;margin-left:39px}.mfp-arrow-right:before{border-left:27px solid #3f3f3f}.mfp-iframe-holder{padding-top:40px;padding-bottom:40px}.mfp-iframe-holder .mfp-content{line-height:0;width:100%;max-width:900px}.mfp-iframe-holder .mfp-close{top:-40px}.mfp-iframe-scaler{width:100%;height:0;overflow:hidden;padding-top:56.25%}.mfp-iframe-scaler iframe{position:absolute;display:block;top:0;left:0;width:100%;height:100%;box-shadow:0 0 8px rgba(0,0,0,0.6);background:#000}img.mfp-img{width:auto;max-width:100%;height:auto;display:block;line-height:0;box-sizing:border-box;padding:40px 0 40px;margin:0 auto}.mfp-figure{line-height:0}.mfp-figure:after{content:'';position:absolute;left:0;top:40px;bottom:40px;display:block;right:0;width:auto;height:auto;z-index:-1;box-shadow:0 0 8px rgba(0,0,0,0.6);background:#444}.mfp-figure small{color:#bdbdbd;display:block;font-size:12px;line-height:14px}.mfp-figure figure{margin:0}.mfp-bottom-bar{margin-top:-36px;position:absolute;top:100%;left:0;width:100%;cursor:auto}.mfp-title{text-align:left;line-height:18px;color:#f3f3f3;word-wrap:break-word;padding-right:36px}.mfp-image-holder .mfp-content{max-width:100%}.mfp-gallery .mfp-image-holder .mfp-figure{cursor:pointer}@media screen and (max-width:800px) and (orientation:landscape),screen and (max-height:300px){.mfp-img-mobile .mfp-image-holder{padding-left:0;padding-right:0}.mfp-img-mobile img.mfp-img{padding:0}.mfp-img-mobile .mfp-figure:after{top:0;bottom:0}.mfp-img-mobile .mfp-figure small{display:inline;margin-left:5px}.mfp-img-mobile .mfp-bottom-bar{background:rgba(0,0,0,0.6);bottom:0;margin:0;top:auto;padding:3px 5px;position:fixed;box-sizing:border-box}.mfp-img-mobile .mfp-bottom-bar:empty{padding:0}.mfp-img-mobile .mfp-counter{right:5px;top:3px}.mfp-img-mobile .mfp-close{top:0;right:0;width:35px;height:35px;line-height:35px;background:rgba(0,0,0,0.6);position:fixed;text-align:center;padding:0}}@media all and (max-width:900px){.mfp-arrow{-webkit-transform:scale(0.75);transform:scale(0.75)}.mfp-arrow-left{-webkit-transform-origin:0 0;transform-origin:0 0}.mfp-arrow-right{-webkit-transform-origin:100%;transform-origin:100%}.mfp-container{padding-left:6px;padding-right:6px}} \ No newline at end of file diff --git a/static/assets/css/meanmenu.min.css b/static/assets/css/meanmenu.min.css deleted file mode 100644 index 3a73cd9a..00000000 --- a/static/assets/css/meanmenu.min.css +++ /dev/null @@ -1,8 +0,0 @@ -/*! ####################################################################### - - MeanMenu 2.0.7 - -------- - - To be used with jquery.meanmenu.js by Chris Wharton (http://www.meanthemes.com/plugins/meanmenu/) - -####################################################################### */a.meanmenu-reveal{display:none}.mean-container .mean-bar{float:left;width:100%;position:relative;background:#0c1923;padding:4px 0;min-height:42px;z-index:999999}.mean-container a.meanmenu-reveal{width:22px;height:22px;padding:13px 13px 11px;position:absolute;top:0;right:0;cursor:pointer;color:#fff;text-decoration:none;font-size:16px;text-indent:-9999em;line-height:22px;font-size:1px;display:block;font-family:Arial,Helvetica,sans-serif;font-weight:700}.mean-container a.meanmenu-reveal span{display:block;background:#fff;height:3px;margin-top:3px}.mean-container .mean-nav{float:left;width:100%;background:#0c1923;margin-top:44px}.mean-container .mean-nav ul{padding:0;margin:0;width:100%;list-style-type:none}.mean-container .mean-nav ul li{position:relative;float:left;width:100%}.mean-container .mean-nav ul li a{display:block;float:left;width:90%;padding:1em 5%;margin:0;text-align:left;color:#fff;border-top:1px solid #383838;border-top:1px solid rgba(255,255,255,.5);text-decoration:none;text-transform:uppercase}.mean-container .mean-nav ul li li a{width:80%;padding:1em 10%;border-top:1px solid #f1f1f1;border-top:1px solid rgba(255,255,255,.25);opacity:.75;filter:alpha(opacity=75);text-shadow:none!important;visibility:visible}.mean-container .mean-nav ul li.mean-last a{border-bottom:0;margin-bottom:0}.mean-container .mean-nav ul li li li a{width:70%;padding:1em 15%}.mean-container .mean-nav ul li li li li a{width:60%;padding:1em 20%}.mean-container .mean-nav ul li li li li li a{width:50%;padding:1em 25%}.mean-container .mean-nav ul li a:hover{background:#252525;background:rgba(255,255,255,.1)}.mean-container .mean-nav ul li a.mean-expand{margin-top:1px;width:26px;height:32px;padding:12px!important;text-align:center;position:absolute;right:0;top:0;z-index:2;font-weight:700;background:rgba(255,255,255,.1);border:0!important;border-left:1px solid rgba(255,255,255,.4)!important;border-bottom:1px solid rgba(255,255,255,.2)!important}.mean-container .mean-nav ul li a.mean-expand:hover{background:rgba(0,0,0,.9)}.mean-container .mean-push{float:left;width:100%;padding:0;margin:0;clear:both}.mean-nav .wrapper{width:100%;padding:0;margin:0}.mean-container .mean-bar,.mean-container .mean-bar *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.mean-remove{display:none!important} \ No newline at end of file diff --git a/static/assets/css/odometer.min.css b/static/assets/css/odometer.min.css deleted file mode 100644 index ac13c6f6..00000000 --- a/static/assets/css/odometer.min.css +++ /dev/null @@ -1,2 +0,0 @@ -/*! odometer 0.4.8 */ -.odometer.odometer-auto-theme,.odometer.odometer-theme-default{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;position:relative}.odometer.odometer-auto-theme .odometer-digit,.odometer.odometer-theme-default .odometer-digit{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;position:relative}.odometer.odometer-auto-theme .odometer-digit .odometer-digit-spacer,.odometer.odometer-theme-default .odometer-digit .odometer-digit-spacer{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;visibility:hidden}.odometer.odometer-auto-theme .odometer-digit .odometer-digit-inner,.odometer.odometer-theme-default .odometer-digit .odometer-digit-inner{text-align:left;display:block;position:absolute;top:0;left:0;right:0;bottom:0;overflow:hidden}.odometer.odometer-auto-theme .odometer-digit .odometer-ribbon,.odometer.odometer-theme-default .odometer-digit .odometer-ribbon{display:block}.odometer.odometer-auto-theme .odometer-digit .odometer-ribbon-inner,.odometer.odometer-theme-default .odometer-digit .odometer-ribbon-inner{display:block;-webkit-backface-visibility:hidden}.odometer.odometer-auto-theme .odometer-digit .odometer-value,.odometer.odometer-theme-default .odometer-digit .odometer-value{display:block;-webkit-transform:translateZ(0)}.odometer.odometer-auto-theme .odometer-digit .odometer-value.odometer-last-value,.odometer.odometer-theme-default .odometer-digit .odometer-value.odometer-last-value{position:absolute}.odometer.odometer-auto-theme.odometer-animating-up .odometer-ribbon-inner,.odometer.odometer-theme-default.odometer-animating-up .odometer-ribbon-inner{-webkit-transition:-webkit-transform 2s;-moz-transition:-moz-transform 2s;-ms-transition:-ms-transform 2s;-o-transition:-o-transform 2s;transition:transform 2s}.odometer.odometer-auto-theme.odometer-animating-up.odometer-animating .odometer-ribbon-inner,.odometer.odometer-theme-default.odometer-animating-up.odometer-animating .odometer-ribbon-inner{-webkit-transform:translateY(-100%);-moz-transform:translateY(-100%);-ms-transform:translateY(-100%);-o-transform:translateY(-100%);transform:translateY(-100%)}.odometer.odometer-auto-theme.odometer-animating-down .odometer-ribbon-inner,.odometer.odometer-theme-default.odometer-animating-down .odometer-ribbon-inner{-webkit-transform:translateY(-100%);-moz-transform:translateY(-100%);-ms-transform:translateY(-100%);-o-transform:translateY(-100%);transform:translateY(-100%)}.odometer.odometer-auto-theme.odometer-animating-down.odometer-animating .odometer-ribbon-inner,.odometer.odometer-theme-default.odometer-animating-down.odometer-animating .odometer-ribbon-inner{-webkit-transition:-webkit-transform 2s;-moz-transition:-moz-transform 2s;-ms-transition:-ms-transform 2s;-o-transition:-o-transform 2s;transition:transform 2s;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}.odometer.odometer-auto-theme .odometer-value,.odometer.odometer-theme-default .odometer-value{text-align:center} \ No newline at end of file diff --git a/static/assets/css/owl.carousel.min.css b/static/assets/css/owl.carousel.min.css deleted file mode 100644 index 1b3ecda6..00000000 --- a/static/assets/css/owl.carousel.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Owl Carousel v2.3.4 - * Copyright 2013-2018 David Deutsch - * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE - */ - .owl-carousel,.owl-carousel .owl-item{-webkit-tap-highlight-color:transparent;position:relative}.owl-carousel{display:none;width:100%;z-index:1}.owl-carousel .owl-stage{position:relative;-ms-touch-action:pan-Y;touch-action:manipulation;-moz-backface-visibility:hidden}.owl-carousel .owl-stage:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.owl-carousel .owl-stage-outer{position:relative;overflow:hidden;-webkit-transform:translate3d(0,0,0)}.owl-carousel .owl-item,.owl-carousel .owl-wrapper{-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}.owl-carousel .owl-item{min-height:1px;float:left;-webkit-backface-visibility:hidden;-webkit-touch-callout:none}.owl-carousel .owl-item img{display:block;width:100%}.owl-carousel .owl-dots.disabled,.owl-carousel .owl-nav.disabled{display:none}.no-js .owl-carousel,.owl-carousel.owl-loaded{display:block}.owl-carousel .owl-dot,.owl-carousel .owl-nav .owl-next,.owl-carousel .owl-nav .owl-prev{cursor:pointer;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel .owl-nav button.owl-next,.owl-carousel .owl-nav button.owl-prev,.owl-carousel button.owl-dot{background:0 0;color:inherit;border:none;padding:0!important;font:inherit}.owl-carousel.owl-loading{opacity:0;display:block}.owl-carousel.owl-hidden{opacity:0}.owl-carousel.owl-refresh .owl-item{visibility:hidden}.owl-carousel.owl-drag .owl-item{-ms-touch-action:pan-y;touch-action:pan-y;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-grab{cursor:move;cursor:grab}.owl-carousel.owl-rtl{direction:rtl}.owl-carousel.owl-rtl .owl-item{float:right}.owl-carousel .animated{animation-duration:1s;animation-fill-mode:both}.owl-carousel .owl-animated-in{z-index:0}.owl-carousel .owl-animated-out{z-index:1}.owl-carousel .fadeOut{animation-name:fadeOut}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.owl-height{transition:height .5s ease-in-out}.owl-carousel .owl-item .owl-lazy{opacity:0;transition:opacity .4s ease}.owl-carousel .owl-item .owl-lazy:not([src]),.owl-carousel .owl-item .owl-lazy[src^=""]{max-height:0}.owl-carousel .owl-item img.owl-lazy{transform-style:preserve-3d}.owl-carousel .owl-video-wrapper{position:relative;height:100%;background:#000}.owl-carousel .owl-video-play-icon{position:absolute;height:80px;width:80px;left:50%;top:50%;margin-left:-40px;margin-top:-40px;background:url(owl.video.play.png) no-repeat;cursor:pointer;z-index:1;-webkit-backface-visibility:hidden;transition:transform .1s ease}.owl-carousel .owl-video-play-icon:hover{-ms-transform:scale(1.3,1.3);transform:scale(1.3,1.3)}.owl-carousel .owl-video-playing .owl-video-play-icon,.owl-carousel .owl-video-playing .owl-video-tn{display:none}.owl-carousel .owl-video-tn{opacity:0;height:100%;background-position:center center;background-repeat:no-repeat;background-size:contain;transition:opacity .4s ease}.owl-carousel .owl-video-frame{position:relative;z-index:1;height:100%;width:100%} \ No newline at end of file diff --git a/static/assets/css/owl.theme.default.min.css b/static/assets/css/owl.theme.default.min.css deleted file mode 100644 index d86a30fa..00000000 --- a/static/assets/css/owl.theme.default.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Owl Carousel v2.3.4 - * Copyright 2013-2018 David Deutsch - * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE - */ - .owl-theme .owl-dots,.owl-theme .owl-nav{text-align:center;-webkit-tap-highlight-color:transparent}.owl-theme .owl-nav{margin-top:10px}.owl-theme .owl-nav [class*=owl-]{color:#FFF;font-size:14px;margin:5px;padding:4px 7px;background:#D6D6D6;display:inline-block;cursor:pointer;border-radius:3px}.owl-theme .owl-nav [class*=owl-]:hover{background:#869791;color:#FFF;text-decoration:none}.owl-theme .owl-nav .disabled{opacity:.5;cursor:default}.owl-theme .owl-nav.disabled+.owl-dots{margin-top:10px}.owl-theme .owl-dots .owl-dot{display:inline-block;zoom:1}.owl-theme .owl-dots .owl-dot span{width:10px;height:10px;margin:5px 7px;background:#D6D6D6;display:block;-webkit-backface-visibility:visible;transition:opacity .2s ease;border-radius:30px}.owl-theme .owl-dots .owl-dot.active span,.owl-theme .owl-dots .owl-dot:hover span{background:#869791} \ No newline at end of file diff --git a/static/assets/css/owl.video.play.png b/static/assets/css/owl.video.play.png deleted file mode 100644 index 5d0218d4..00000000 Binary files a/static/assets/css/owl.video.play.png and /dev/null differ diff --git a/static/assets/css/rangeSlider.min.css b/static/assets/css/rangeSlider.min.css deleted file mode 100644 index 0ed3591f..00000000 --- a/static/assets/css/rangeSlider.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/* -!Ion.RangeSlider, 2.3.0, © Denis Ineshin, 2010 - 2018, IonDen.com, Build date: 2018-12-12 00:07:25 -*/ -.irs{position:relative;display:block;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:12px;font-family:Arial,sans-serif}.irs-line{position:relative;display:block;overflow:hidden;outline:none !important}.irs-bar{position:absolute;display:block;left:0;width:0}.irs-shadow{position:absolute;display:none;left:0;width:0}.irs-handle{position:absolute;display:block;box-sizing:border-box;cursor:default;z-index:1}.irs-handle.type_last{z-index:2}.irs-min,.irs-max{position:absolute;display:block;cursor:default}.irs-min{left:0}.irs-max{right:0}.irs-from,.irs-to,.irs-single{position:absolute;display:block;top:0;left:0;cursor:default;white-space:nowrap}.irs-grid{position:absolute;display:none;bottom:0;left:0;width:100%;height:20px}.irs-with-grid .irs-grid{display:block}.irs-grid-pol{position:absolute;top:0;left:0;width:1px;height:8px;background:#000}.irs-grid-pol.small{height:4px}.irs-grid-text{position:absolute;bottom:0;left:0;white-space:nowrap;text-align:center;font-size:9px;line-height:9px;padding:0 3px;color:#000}.irs-disable-mask{position:absolute;display:block;top:0;left:-1%;width:102%;height:100%;cursor:default;background:rgba(0,0,0,0);z-index:2}.lt-ie9 .irs-disable-mask{background:#000;filter:alpha(opacity=0);cursor:not-allowed}.irs-disabled{opacity:.4}.irs-hidden-input{position:absolute !important;display:block !important;top:0 !important;left:0 !important;width:0 !important;height:0 !important;font-size:0 !important;line-height:0 !important;padding:0 !important;margin:0 !important;overflow:hidden;outline:none !important;z-index:-9999 !important;background:none !important;border-style:solid !important;border-color:transparent !important}.irs--flat{height:40px}.irs--flat.irs-with-grid{height:60px}.irs--flat .irs-line{top:25px;height:12px;background-color:#e1e4e9;border-radius:4px}.irs--flat .irs-bar{top:25px;height:12px;background-color:#ed5565}.irs--flat .irs-bar--single{border-radius:4px 0 0 4px}.irs--flat .irs-shadow{height:1px;bottom:16px;background-color:#e1e4e9}.irs--flat .irs-handle{top:22px;width:16px;height:18px;background-color:transparent}.irs--flat .irs-handle>i:first-child{position:absolute;display:block;top:0;left:50%;width:2px;height:100%;margin-left:-1px;background-color:#da4453}.irs--flat .irs-handle.state_hover>i:first-child,.irs--flat .irs-handle:hover>i:first-child{background-color:#a43540}.irs--flat .irs-min,.irs--flat .irs-max{top:0;padding:1px 3px;color:#999;font-size:10px;line-height:1.333;text-shadow:none;background-color:#e1e4e9;border-radius:4px}.irs--flat .irs-from,.irs--flat .irs-to,.irs--flat .irs-single{color:white;font-size:10px;line-height:1.333;text-shadow:none;padding:1px 5px;background-color:#ed5565;border-radius:4px}.irs--flat .irs-from:before,.irs--flat .irs-to:before,.irs--flat .irs-single:before{position:absolute;display:block;content:"";bottom:-6px;left:50%;width:0;height:0;margin-left:-3px;overflow:hidden;border:3px solid transparent;border-top-color:#ed5565}.irs--flat .irs-grid-pol{background-color:#e1e4e9}.irs--flat .irs-grid-text{color:#999}.irs--big{height:55px}.irs--big.irs-with-grid{height:70px}.irs--big .irs-line{top:33px;height:12px;background-color:white;background:linear-gradient(to bottom, #ddd -50%, white 150%);border:1px solid #ccc;border-radius:12px}.irs--big .irs-bar{top:33px;height:12px;background-color:#92bce0;border:1px solid #428bca;background:linear-gradient(to bottom, #ffffff 0%, #428bca 30%, #b9d4ec 100%);box-shadow:inset 0 0 1px 1px rgba(255,255,255,0.5)}.irs--big .irs-bar--single{border-radius:12px 0 0 12px}.irs--big .irs-shadow{height:1px;bottom:16px;background-color:rgba(66,139,202,0.5)}.irs--big .irs-handle{top:25px;width:30px;height:30px;border:1px solid rgba(0,0,0,0.3);background-color:#cbcfd5;background:linear-gradient(to bottom, white 0%, #B4B9BE 30%, white 100%);box-shadow:1px 1px 2px rgba(0,0,0,0.2),inset 0 0 3px 1px white;border-radius:30px}.irs--big .irs-handle.state_hover,.irs--big .irs-handle:hover{border-color:rgba(0,0,0,0.45);background-color:#939ba7;background:linear-gradient(to bottom, white 0%, #919BA5 30%, white 100%)}.irs--big .irs-min,.irs--big .irs-max{top:0;padding:1px 5px;color:white;text-shadow:none;background-color:#9f9f9f;border-radius:3px}.irs--big .irs-from,.irs--big .irs-to,.irs--big .irs-single{color:white;text-shadow:none;padding:1px 5px;background-color:#428bca;background:linear-gradient(to bottom, #428bca 0%, #3071a9 100%);border-radius:3px}.irs--big .irs-grid-pol{background-color:#428bca}.irs--big .irs-grid-text{color:#428bca}.irs--modern{height:55px}.irs--modern.irs-with-grid{height:55px}.irs--modern .irs-line{top:25px;height:5px;background-color:#d1d6e0;background:linear-gradient(to bottom, #e0e4ea 0%, #d1d6e0 100%);border:1px solid #a3adc1;border-bottom-width:0;border-radius:5px}.irs--modern .irs-bar{top:25px;height:5px;background:#20b426;background:linear-gradient(to bottom, #20b426 0%, #18891d 100%)}.irs--modern .irs-bar--single{border-radius:5px 0 0 5px}.irs--modern .irs-shadow{height:1px;bottom:21px;background-color:rgba(209,214,224,0.5)}.irs--modern .irs-handle{top:37px;width:12px;height:13px;border:1px solid #a3adc1;border-top-width:0;box-shadow:1px 1px 1px rgba(0,0,0,0.1);border-radius:0 0 3px 3px}.irs--modern .irs-handle>i:nth-child(1){position:absolute;display:block;top:-4px;left:1px;width:6px;height:6px;border:1px solid #a3adc1;background:white;transform:rotate(45deg)}.irs--modern .irs-handle>i:nth-child(2){position:absolute;display:block;box-sizing:border-box;top:0;left:0;width:10px;height:12px;background:#e9e6e6;background:linear-gradient(to bottom, white 0%, #e9e6e6 100%);border-radius:0 0 3px 3px}.irs--modern .irs-handle>i:nth-child(3){position:absolute;display:block;box-sizing:border-box;top:3px;left:3px;width:4px;height:5px;border-left:1px solid #a3adc1;border-right:1px solid #a3adc1}.irs--modern .irs-handle.state_hover,.irs--modern .irs-handle:hover{border-color:#7685a2;background:#c3c7cd;background:linear-gradient(to bottom, #ffffff 0%, #919ba5 30%, #ffffff 100%)}.irs--modern .irs-handle.state_hover>i:nth-child(1),.irs--modern .irs-handle:hover>i:nth-child(1){border-color:#7685a2}.irs--modern .irs-handle.state_hover>i:nth-child(3),.irs--modern .irs-handle:hover>i:nth-child(3){border-color:#48536a}.irs--modern .irs-min,.irs--modern .irs-max{top:0;font-size:10px;line-height:1.333;text-shadow:none;padding:1px 5px;color:white;background-color:#d1d6e0;border-radius:5px}.irs--modern .irs-from,.irs--modern .irs-to,.irs--modern .irs-single{font-size:10px;line-height:1.333;text-shadow:none;padding:1px 5px;background-color:#20b426;color:white;border-radius:5px}.irs--modern .irs-from:before,.irs--modern .irs-to:before,.irs--modern .irs-single:before{position:absolute;display:block;content:"";bottom:-6px;left:50%;width:0;height:0;margin-left:-3px;overflow:hidden;border:3px solid transparent;border-top-color:#20b426}.irs--modern .irs-grid{height:25px}.irs--modern .irs-grid-pol{background-color:#dedede}.irs--modern .irs-grid-text{color:silver;font-size:13px}.irs--sharp{height:50px;font-size:12px;line-height:1}.irs--sharp.irs-with-grid{height:57px}.irs--sharp .irs-line{top:30px;height:2px;background-color:black;border-radius:2px}.irs--sharp .irs-bar{top:30px;height:2px;background-color:#ee22fa}.irs--sharp .irs-bar--single{border-radius:2px 0 0 2px}.irs--sharp .irs-shadow{height:1px;bottom:21px;background-color:rgba(0,0,0,0.5)}.irs--sharp .irs-handle{top:25px;width:10px;height:10px;background-color:#a804b2}.irs--sharp .irs-handle>i:first-child{position:absolute;display:block;top:100%;left:0;width:0;height:0;border:5px solid transparent;border-top-color:#a804b2}.irs--sharp .irs-handle.state_hover,.irs--sharp .irs-handle:hover{background-color:black}.irs--sharp .irs-handle.state_hover>i:first-child,.irs--sharp .irs-handle:hover>i:first-child{border-top-color:black}.irs--sharp .irs-min,.irs--sharp .irs-max{color:white;font-size:14px;line-height:1;top:0;padding:3px 4px;opacity:.4;background-color:#a804b2;border-radius:2px}.irs--sharp .irs-from,.irs--sharp .irs-to,.irs--sharp .irs-single{font-size:14px;line-height:1;text-shadow:none;padding:3px 4px;background-color:#a804b2;color:white;border-radius:2px}.irs--sharp .irs-from:before,.irs--sharp .irs-to:before,.irs--sharp .irs-single:before{position:absolute;display:block;content:"";bottom:-6px;left:50%;width:0;height:0;margin-left:-3px;overflow:hidden;border:3px solid transparent;border-top-color:#a804b2}.irs--sharp .irs-grid{height:25px}.irs--sharp .irs-grid-pol{background-color:#dedede}.irs--sharp .irs-grid-text{color:silver;font-size:13px}.irs--round{height:50px}.irs--round.irs-with-grid{height:65px}.irs--round .irs-line{top:36px;height:4px;background-color:#dee4ec;border-radius:4px}.irs--round .irs-bar{top:36px;height:4px;background-color:#006cfa}.irs--round .irs-bar--single{border-radius:4px 0 0 4px}.irs--round .irs-shadow{height:4px;bottom:21px;background-color:rgba(222,228,236,0.5)}.irs--round .irs-handle{top:26px;width:24px;height:24px;border:4px solid #006cfa;background-color:white;border-radius:24px;box-shadow:0 1px 3px rgba(0,0,255,0.3)}.irs--round .irs-handle.state_hover,.irs--round .irs-handle:hover{background-color:#f0f6ff}.irs--round .irs-min,.irs--round .irs-max{color:#333;font-size:14px;line-height:1;top:0;padding:3px 5px;background-color:rgba(0,0,0,0.1);border-radius:4px}.irs--round .irs-from,.irs--round .irs-to,.irs--round .irs-single{font-size:14px;line-height:1;text-shadow:none;padding:3px 5px;background-color:#006cfa;color:white;border-radius:4px}.irs--round .irs-from:before,.irs--round .irs-to:before,.irs--round .irs-single:before{position:absolute;display:block;content:"";bottom:-6px;left:50%;width:0;height:0;margin-left:-3px;overflow:hidden;border:3px solid transparent;border-top-color:#006cfa}.irs--round .irs-grid{height:25px}.irs--round .irs-grid-pol{background-color:#dedede}.irs--round .irs-grid-text{color:silver;font-size:13px}.irs--square{height:50px}.irs--square.irs-with-grid{height:60px}.irs--square .irs-line{top:31px;height:4px;background-color:#dedede}.irs--square .irs-bar{top:31px;height:4px;background-color:black}.irs--square .irs-shadow{height:2px;bottom:21px;background-color:#dedede}.irs--square .irs-handle{top:25px;width:16px;height:16px;border:3px solid black;background-color:white;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.irs--square .irs-handle.state_hover,.irs--square .irs-handle:hover{background-color:#f0f6ff}.irs--square .irs-min,.irs--square .irs-max{color:#333;font-size:14px;line-height:1;top:0;padding:3px 5px;background-color:rgba(0,0,0,0.1)}.irs--square .irs-from,.irs--square .irs-to,.irs--square .irs-single{font-size:14px;line-height:1;text-shadow:none;padding:3px 5px;background-color:black;color:white}.irs--square .irs-grid{height:25px}.irs--square .irs-grid-pol{background-color:#dedede}.irs--square .irs-grid-text{color:silver;font-size:11px} \ No newline at end of file diff --git a/static/assets/css/responsive.css b/static/assets/css/responsive.css deleted file mode 100644 index 3ce36649..00000000 --- a/static/assets/css/responsive.css +++ /dev/null @@ -1,4956 +0,0 @@ -/* Max width 767px */ -@media only screen and (max-width: 767px) { - body { - font-size: 13.5px; - } - p { - font-size: 13.5px; - } - .ptb-100 { - padding-top: 60px; - padding-bottom: 60px; - } - .pt-100 { - padding-top: 60px; - } - .pb-100 { - padding-bottom: 60px; - } - .ptb-70 { - padding-top: 30px; - padding-bottom: 30px; - } - .pt-70 { - padding-top: 30px; - } - .pb-70 { - padding-bottom: 30px; - } - .default-btn { - padding: 11px 40px 11px 25px; - font-size: 13.5px; - } - .default-btn i { - right: 25px; - font-size: 12px; - } - .default-btn:hover i { - right: 20px; - } - .section-title { - max-width: 100%; - margin-left: 0; - margin-right: 0; - margin-bottom: 40px; - margin-top: -2px; - } - .section-title .sub-title { - font-size: 13px; - } - .section-title h2 { - font-size: 22px; - } - .section-title p { - max-width: 100%; - margin-top: 12px; - } - .section-title.style-two { - padding-left: 0; - text-align: center; - margin-bottom: 40px; - } - .section-title.style-two h2 { - font-size: 22px; - } - .section-title.style-two h2::before { - display: none; - } - .section-title.style-two p { - max-width: 100%; - } - .section-title.style-three h2 span { - padding-left: 10px; - padding-right: 10px; - } - .section-title.style-four { - text-align: center; - margin-bottom: 40px; - } - .section-title.style-four h2 { - font-size: 22px; - } - .section-title.style-four::before { - left: 50%; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); - } - .form-control { - font-size: 13.5px; - } - .top-header-area { - text-align: center; - } - .top-header-area .container-fluid { - max-width: 100%; - } - .top-header-area.top-header-style-two .container-fluid { - max-width: 100%; - } - .top-header-area.top-header-style-three .container-fluid { - max-width: 100%; - } - .top-header-contact-info { - text-align: center !important; - } - .top-header-contact-info .email { - padding-left: 20px; - margin-right: 10px; - margin-top: 10px; - } - .top-header-contact-info .email i { - top: 4.5px; - } - .top-header-contact-info .number { - padding-left: 20px; - margin-top: 10px; - } - .top-header-contact-info .number i { - top: 4px; - } - .top-header-contact-info .lang-switcher { - padding-left: 18px; - margin-left: 10px; - margin-top: 10px; - } - .search-overlay .search-overlay-form { - max-width: 290px; - width: 290px; - } - .search-overlay .search-overlay-form form .input-search { - height: 50px; - font-size: 15px; - } - .search-overlay .search-overlay-form form button { - font-size: 18px; - } - .header-area.p-responsive-relative { - position: relative; - } - .startup-banner-area .container-fluid { - max-width: 100%; - padding-left: 0; - padding-right: 0; - } - .startup-banner-inner { - padding-top: 60px; - padding-left: 15px; - padding-right: 15px; - padding-bottom: 60px; - } - .startup-banner-content { - padding-right: 0; - text-align: center; - } - .startup-banner-content .sub-title { - font-size: 13px; - } - .startup-banner-content h1 { - margin-bottom: 18px; - font-size: 25px; - } - .startup-banner-content p { - max-width: 100%; - } - .startup-banner-content .default-btn { - margin-top: 5px; - } - .startup-banner-image { - padding-left: 0; - margin-top: 30px; - } - .marketing-banner-area { - padding-top: 60px; - padding-bottom: 60px; - } - .marketing-banner-content { - text-align: center; - padding-right: 0; - } - .marketing-banner-content .sub-title { - font-size: 13px; - } - .marketing-banner-content h1 { - margin-bottom: 18px; - font-size: 25px; - } - .marketing-banner-content p { - max-width: 100%; - } - .marketing-banner-content .default-btn { - margin-top: 5px; - } - .marketing-banner-image { - margin-top: 30px; - } - .business-banner-area { - padding-top: 60px; - padding-bottom: 60px; - } - .business-banner-area .container { - max-width: 100%; - } - .business-banner-area::before { - display: none; - } - .business-banner-content { - padding-right: 0; - text-align: center; - max-width: 100%; - } - .business-banner-content .sub-title { - font-size: 13px; - } - .business-banner-content h1 { - margin-bottom: 12px; - font-size: 25px; - } - .business-banner-content p { - max-width: 100%; - } - .business-banner-content .default-btn { - margin-top: 5px; - } - .business-banner-image { - margin-top: 30px; - } - .business-banner-image img { - border-radius: 0; - } - .app-banner-area { - padding-top: 60px; - padding-bottom: 60px; - } - .app-banner-area .container { - max-width: 100%; - } - .app-banner-content { - text-align: center; - } - .app-banner-content h1 { - margin-bottom: 20px; - font-size: 25px; - } - .app-banner-content .btn-box { - margin-top: 25px; - } - .app-banner-content .btn-box .store-btn { - padding: 10px 10px 10px 45px; - font-size: 12px; - text-align: start; - margin-left: 3px; - margin-right: 3px; - } - .app-banner-content .btn-box .store-btn:first-child { - margin-left: 0; - } - .app-banner-content .btn-box .store-btn img { - left: 10px; - width: 25px; - } - .app-banner-content .btn-box .store-btn span { - font-size: 14px; - } - .app-banner-image { - padding-left: 0; - margin-top: 35px; - } - .saas-banner-area { - padding-top: 60px; - } - .saas-banner-area .container-fluid { - padding-left: 15px; - padding-right: 15px; - } - .saas-banner-content { - text-align: center; - max-width: 100%; - margin-left: 0; - } - .saas-banner-content .sub-title { - font-size: 13px; - } - .saas-banner-content h1 { - margin-bottom: 18px; - font-size: 25px; - } - .saas-banner-content p { - max-width: 100%; - } - .saas-banner-content .default-btn { - margin-top: 5px; - } - .saas-banner-image { - margin-top: 35px; - } - .personal-portfolio-banner-area { - padding-top: 60px; - padding-bottom: 60px; - } - .personal-portfolio-banner-area .container { - max-width: 100%; - } - .personal-portfolio-banner-content { - text-align: center; - } - .personal-portfolio-banner-content .sub-title { - font-size: 13px; - padding-left: 0; - } - .personal-portfolio-banner-content .sub-title::before { - display: none; - } - .personal-portfolio-banner-content h1 { - margin-bottom: 15px; - font-size: 25px; - } - .personal-portfolio-banner-content h4 { - margin-bottom: 15px; - font-size: 16px; - } - .personal-portfolio-banner-content p { - max-width: 100%; - } - .personal-portfolio-banner-image { - margin-top: 30px; - } - .consulting-banner-area { - padding-top: 235px; - padding-bottom: 60px; - } - .consulting-banner-area .container { - max-width: 100%; - } - .consulting-banner-area::before { - display: block; - } - .consulting-banner-content { - text-align: center; - max-width: 100%; - } - .consulting-banner-content h1 { - margin-bottom: 15px; - font-size: 25px; - } - .consulting-banner-content p { - max-width: 100%; - } - .consulting-banner-content .default-btn { - margin-top: 5px; - } - .it-agency-banner-area .it-agency-inner-banner { - padding-top: 230px; - padding-bottom: 60px; - } - .it-agency-banner-content { - max-width: 100%; - } - .it-agency-banner-content h1 { - margin-bottom: 15px; - font-size: 25px; - } - .it-agency-banner-content p { - max-width: 100%; - } - .it-agency-banner-content .default-btn { - margin-top: 5px; - } - .banner-video-box { - max-width: 100%; - margin-top: 60px; - } - .banner-video-box .video-btn { - width: 60px; - height: 60px; - font-size: 25px; - } - .shop-banner-area { - background-position: left center; - padding-top: 60px; - padding-bottom: 60px; - } - .shop-banner-content .sub-title { - font-size: 15px; - margin-bottom: 8px; - } - .shop-banner-content h1 { - margin-bottom: 12px; - font-size: 25px; - } - .shop-banner-content h4 { - font-size: 16px; - } - .shop-banner-content .default-btn { - margin-top: 20px; - } - .blog-banner-area { - padding-top: 60px; - padding-bottom: 60px; - } - .blog-banner-item { - margin-bottom: 30px; - } - .blog-banner-item .content { - max-width: 100%; - padding: 20px 15px; - top: 0; - background-color: white; - position: relative; - -webkit-transform: translateY(0); - transform: translateY(0); - } - .blog-banner-item .content h3 { - font-size: 16px; - } - .blog-banner-item .content .meta li a i { - top: 6px; - } - .blog-banner-item-list .single-blog-banner-item .image { - height: auto; - background-image: unset !important; - } - .blog-banner-item-list .single-blog-banner-item .image img { - display: inline-block; - } - .blog-banner-item-list .single-blog-banner-item .content { - padding-left: 0; - padding-bottom: 0; - padding-top: 20px; - } - .blog-banner-item-list .single-blog-banner-item .content h3 { - margin-bottom: 12px; - font-size: 16px; - } - .blog-banner-item-list .single-blog-banner-item .content .date i { - top: 2px; - } - .furniture-banner-item { - padding-top: 60px; - padding-bottom: 95px; - } - .furniture-banner-content { - padding: 20px 15px; - max-width: 100%; - text-align: center; - border-width: 2px; - } - .furniture-banner-content h1 { - margin-bottom: 12px; - font-size: 25px; - } - .furniture-banner-content .default-btn { - margin-top: 5px; - } - .furniture-banner-slides.owl-theme .owl-dots { - top: auto; - -webkit-transform: translateY(0); - transform: translateY(0); - bottom: 30px; - left: 0; - right: 0; - } - .furniture-banner-slides.owl-theme .owl-dots .owl-dot { - display: inline-block; - margin: 0 5px; - } - .furniture-banner-slides.owl-theme .owl-dots .owl-dot span { - width: 35px; - } - .minimal-portfolio-banner-area { - padding-top: 150px; - padding-bottom: 140px; - } - .minimal-portfolio-banner-content { - max-width: 100%; - } - .minimal-portfolio-banner-content .typewrite { - margin-bottom: 25px; - padding: 6px 15px; - font-size: 13px; - } - .minimal-portfolio-banner-content h1 { - font-size: 25px; - margin-bottom: 25px; - } - .shape1, .shape13, .shape2, .shape3, .shape4, .shape5, .shape6, .shape7, .shape8, .shape9, .shape16, .shape17, .divider, .shape18, .shape19 { - display: none !important; - } - .shape10 { - max-width: 290px; - } - .features-area { - padding-top: 60px; - padding-bottom: 30px; - } - .single-features-box { - padding-left: 0; - padding-right: 0; - } - .single-features-box .icon { - font-size: 40px; - margin-bottom: 20px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-features-box h3 { - font-size: 16px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-features-box p { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-features-box .link-btn { - left: 0; - bottom: 0 !important; - opacity: 1; - -webkit-transform: unset; - transform: unset; - margin-top: 15px; - position: relative; - visibility: visible; - } - .single-features-box .link-btn i { - top: 5px; - font-size: 11px; - } - .features-box { - padding: 20px 15px; - } - .features-box h3 { - font-size: 16px; - } - .features-box .icon { - width: 60px; - height: 60px; - font-size: 30px; - line-height: 60px; - margin-bottom: 20px; - } - .single-features-item { - text-align: center; - padding-left: 0; - } - .single-features-item .icon { - position: relative; - font-size: 35px; - display: inline-block; - margin-bottom: 15px; - } - .single-features-item h3 { - font-size: 16px; - } - .single-features-item h3 br { - display: none; - } - .shape15 { - display: none; - } - .features-item { - padding: 20px 15px; - } - .features-item h3 { - font-size: 16px; - } - .features-item .icon { - font-size: 25px; - margin-right: 10px; - } - .startup-about-area { - border-bottom: 1px solid #eeeeee; - margin-bottom: 60px; - } - .startup-about-area .container-fluid { - max-width: 100%; - } - .startup-about-image { - margin-right: 0; - padding: 0; - } - .startup-about-image .video-btn { - left: 50%; - width: 60px; - height: 60px; - font-size: 22px; - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); - } - .startup-about-image .video-btn::after, .startup-about-image .video-btn::before { - border-width: 1px; - } - .startup-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - max-width: 100%; - } - .startup-about-content .sub-title { - font-size: 13px; - } - .startup-about-content h2 { - font-size: 22px; - } - .startup-about-content .features-list { - padding-left: 0; - list-style-type: none; - } - .startup-about-content .features-list li { - font-weight: 600; - } - .marketing-about-image { - margin-right: 0; - } - .marketing-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - } - .marketing-about-content .sub-title { - font-size: 13px; - } - .marketing-about-content h2 { - font-size: 22px; - } - .marketing-about-content .features-list { - padding-left: 0; - list-style-type: none; - } - .marketing-about-content .features-list li { - font-weight: 600; - } - .business-about-image { - padding-right: 0; - } - .business-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - } - .business-about-content .sub-title { - font-size: 13px; - } - .business-about-content h2 { - font-size: 22px; - } - .shape11, .shape12 { - display: none; - } - .app-about-content { - padding-right: 0; - } - .app-about-content h3 { - margin-bottom: 15px; - font-size: 16px; - } - .app-about-content .default-btn { - margin-top: 5px; - } - .app-about-image { - padding-left: 0; - margin-top: 30px; - } - .saas-about-image { - padding-right: 0; - margin-bottom: 30px; - } - .saas-about-content { - padding-left: 0; - } - .saas-about-content .sub-title { - font-size: 13px; - } - .saas-about-content h2 { - font-size: 22px; - } - .it-agency-about-image { - margin-right: 0; - text-align: end; - padding-right: 0; - padding-bottom: 0; - } - .it-agency-about-image .main-image { - padding-bottom: 100px; - padding-right: 0; - padding-left: 80px; - } - .it-agency-about-image .main-image img:nth-child(2) { - right: 0; - left: 0; - position: relative; - top: 0; - } - .it-agency-about-image .main-image img:nth-child(1) { - left: 0; - bottom: 0; - z-index: 2; - max-width: 200px; - position: absolute; - } - .it-agency-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - } - .it-agency-about-content .sub-title { - font-size: 13px; - } - .it-agency-about-content h2 { - font-size: 22px; - } - .it-agency-about-content .features-list { - padding-left: 0; - list-style-type: none; - } - .it-agency-about-content .features-list li { - font-weight: 600; - } - .single-services-box .content { - padding: 15px; - } - .single-services-box .content .icon { - font-size: 35px; - margin-bottom: 15px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-services-box .content h3 { - font-size: 16px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-services-box .content p { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-services-box .content .link-btn { - left: 0; - bottom: 0 !important; - opacity: 1; - -webkit-transform: unset; - transform: unset; - margin-top: 13px; - position: relative; - visibility: visible; - } - .single-services-box .content .link-btn i { - top: 5px; - font-size: 11px; - } - .single-services-box::before { - background: -webkit-gradient(linear, left bottom, left top, color-stop(0, rgba(35, 35, 37, 0.9)), color-stop(50%, rgba(35, 35, 37, 0.7)), color-stop(75%, rgba(22, 22, 23, 0)), to(rgba(0, 0, 0, 0))); - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.7) 50%, rgba(22, 22, 23, 0) 75%, rgba(0, 0, 0, 0) 100%); - } - .single-services-item .content { - padding-top: 20px; - padding-left: 0; - padding-right: 0; - } - .single-services-item .content h3 { - font-size: 16px; - } - .services-tabs .nav { - margin-bottom: 20px; - border-bottom: 1px solid #eeeeee; - padding-bottom: 10px; - } - .services-tabs .nav .nav-item { - margin-left: 10px; - margin-right: 10px; - margin-bottom: 20px; - } - .services-tabs .nav .nav-item .nav-link { - font-size: 14px; - } - .services-tabs .nav .nav-item .nav-link .icon { - width: 60px; - height: 60px; - line-height: 60px; - font-size: 25px; - padding-right: 5px; - margin-bottom: 12px; - } - .single-main-services .content { - padding-right: 0; - text-align: center; - margin-bottom: 30px; - } - .single-main-services .content h3 { - font-size: 19px; - } - .single-main-services .content p { - max-width: 100%; - } - .services-box { - padding: 20px 15px; - } - .services-box .icon { - font-size: 40px; - margin-bottom: 20px; - } - .services-box h3 { - font-size: 16px; - } - .services-item h3 { - font-size: 16px; - margin-top: 20px; - } - .services-item .hover-content { - padding: 20px 15px; - } - .services-item .hover-content .link-btn i { - top: 6px; - } - .col-lg-3:nth-child(2) .services-box, .col-lg-3:nth-child(4) .services-box { - margin-top: 0; - } - .col-lg-3:nth-child(5) .services-box, .col-lg-3:nth-child(7) .services-box { - margin-top: 0; - } - .single-it-services-box { - padding: 20px 15px; - text-align: center; - } - .single-it-services-box .icon { - left: 0; - top: 0; - font-size: 35px; - display: block; - position: relative; - margin-bottom: 20px; - } - .single-it-services-box h3 { - font-size: 16px; - } - .services-details-desc h3 { - font-size: 17px; - } - .screenshot-wrap .screenshot-frame { - display: none; - } - .screenshot-slides.owl-theme { - padding-top: 40px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-] { - left: 15px; - width: 30px; - line-height: 30px; - height: 30px; - font-size: 15px; - top: 0; - -webkit-transform: translateY(0); - transform: translateY(0); - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 15px; - } - .expertise-box { - text-align: center; - } - .expertise-box .icon { - width: 60px; - height: 60px; - font-size: 30px; - line-height: 60px; - margin-left: auto; - margin-right: auto; - margin-bottom: 20px; - } - .expertise-box h3 { - font-size: 16px; - } - .categories-box h3 { - font-size: 16px; - padding-left: 15px; - padding-right: 15px; - } - .categories-slides.owl-theme .owl-nav { - opacity: 1; - visibility: visible; - margin-bottom: 30px; - } - .categories-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 5px; - position: relative; - left: 0; - top: 0; - width: 30px; - height: 30px; - line-height: 30px; - -webkit-transform: unset; - transform: unset; - } - .categories-slides.owl-theme .owl-nav [class*=owl-].owl-next { - right: 0; - } - .who-we-are-content { - padding: 20px 15px; - text-align: center; - } - .who-we-are-content .sub-title { - font-size: 13px; - } - .who-we-are-content h2 { - font-size: 16px; - } - .who-we-are-content .default-btn { - margin-top: 5px; - } - .who-we-are-image .video-btn { - width: 60px; - height: 60px; - font-size: 20px; - } - .categories-area { - border-bottom: 1px solid #eeeeee; - margin-bottom: 55px; - } - .single-categories-box h3 { - top: 15px; - left: 15px; - font-size: 15px; - padding: 7px 20px; - } - .startup-works-area { - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #eeeeee; - padding-top: 60px; - margin-bottom: 60px; - } - .startup-works-area .section-title { - padding-top: 0; - text-align: center; - margin-bottom: 40px; - } - .startup-works-area .section-title h2 { - font-size: 22px; - } - .startup-works-content { - text-align: center; - position: relative; - border-top: 1px solid #eeeeee; - margin-top: 15px; - padding-top: 50px; - padding-right: 0; - } - .startup-works-content h3 { - font-size: 16px; - margin-bottom: 12px; - } - .startup-works-content .custom-owl-nav { - margin-top: 0; - } - .startup-works-content .custom-owl-nav [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - position: absolute; - left: 0; - top: -15px; - } - .startup-works-content .custom-owl-nav [class*=owl-].custom-owl-next { - left: auto; - right: 0; - } - .startup-works-image { - margin-left: 0; - margin-top: 15px; - padding-top: 0; - padding-right: 0; - padding-bottom: 0; - } - .single-works-box .works-image .works-overlay h3 { - font-size: 16px; - } - .works-box h3 { - opacity: 1; - visibility: visible; - top: auto; - -webkit-transform: translateY(0); - transform: translateY(0); - bottom: 0; - font-size: 16px; - padding: 10px; - margin-left: 0; - margin-right: 0; - margin-top: 0; - } - .works-box-btn { - margin-top: 5px; - } - .single-works-item .content { - padding-left: 15px; - padding-right: 15px; - } - .single-works-item .content .link-btn { - width: 40px; - height: 40px; - line-height: 40px; - } - .single-works-item .content h3 { - font-size: 16px; - } - .how-it-works-content { - padding-right: 0; - } - .how-it-works-content ul li { - padding-left: 50px; - } - .how-it-works-content ul li i { - font-size: 30px; - } - .how-it-works-content ul li h3 { - font-size: 16px; - } - .how-it-works-image { - margin-top: 30px; - } - .single-process-box { - padding: 25px 15px; - } - .single-process-box h3 { - font-size: 16px; - } - .funfacts-inner { - padding-left: 15px; - padding-right: 15px; - } - .funfacts-inner.border-radius.ptb-70 { - padding-bottom: 10px; - } - .single-funfacts-box { - text-align: center; - padding: 20px 10px; - } - .single-funfacts-box .icon { - position: relative; - top: 0; - line-height: 1; - -webkit-transform: translateY(0); - transform: translateY(0); - left: 0; - font-size: 30px; - margin-bottom: 15px; - display: block; - } - .single-funfacts-box h3 { - font-size: 30px; - } - .single-funfacts-box.without-bg-color { - border-right: none; - margin-right: 0; - padding: 0; - } - .single-funfacts-box.text-center { - border-right: none; - padding: 0; - } - .single-products-box .image .buttons-list li { - -webkit-transform: translateY(0); - transform: translateY(0); - visibility: visible; - opacity: 1; - } - .single-products-box .image .buttons-list li a .tooltip-label::before { - bottom: -12px; - } - .single-products-box .image .buttons-list li a:hover .tooltip-label { - top: -40px; - } - .single-products-box .content h3 { - font-size: 16px; - } - .single-products-box .content .rating { - margin-bottom: 10px; - font-size: 12px; - } - .single-products-box .content .price span { - font-size: 14px; - } - .startup-testimonials-area .container-fluid { - max-width: 100%; - } - .startup-testimonials-image { - padding: 0; - } - .startup-testimonials-image .main-image { - -webkit-animation: unset !important; - animation: unset !important; - } - .startup-testimonials-content { - padding-left: 15px; - padding-right: 15px; - padding-bottom: 30px; - padding-top: 30px; - } - .startup-testimonials-content .icon { - margin-bottom: 15px; - font-size: 30px; - } - .startup-testimonials-content p { - font-size: 15px; - } - .startup-testimonials-content .info { - margin-top: 20px; - padding-top: 20px; - } - .startup-testimonials-content .info h3 { - font-size: 15px; - margin-bottom: 7px; - } - .startup-testimonials-slides.owl-theme .owl-nav { - right: 0; - bottom: 0; - -webkit-transform: unset; - transform: unset; - position: relative; - padding-bottom: 30px; - } - .startup-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - .marketing-testimonials-image { - margin-left: 0; - margin-right: 0; - margin-bottom: 30px; - } - .marketing-testimonials-content { - padding-bottom: 0; - } - .marketing-testimonials-content .icon { - margin-bottom: 15px; - font-size: 30px; - } - .marketing-testimonials-content p { - font-size: 15px; - } - .marketing-testimonials-content .info { - margin-top: 20px; - } - .marketing-testimonials-content .info h3 { - font-size: 15px; - margin-bottom: 7px; - } - .marketing-testimonials-slides.owl-theme .owl-nav { - right: 0; - bottom: 0; - margin-top: 25px; - -webkit-transform: unset; - transform: unset; - position: relative; - } - .marketing-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - .business-testimonials-image { - margin-right: 0; - margin-bottom: 40px; - } - .business-testimonials-content .icon { - margin-bottom: 15px; - font-size: 30px; - } - .business-testimonials-content p { - font-size: 15px; - } - .business-testimonials-content .info { - margin-top: 20px; - } - .business-testimonials-content .info h3 { - font-size: 15px; - margin-bottom: 7px; - } - .testimonials-slides.owl-theme { - padding-left: 0; - padding-right: 0; - } - .testimonials-slides.owl-theme .owl-nav { - margin-top: 30px; - } - .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - padding: 6px 20px 6px !important; - left: 0; - top: 0; - -webkit-transform: unset; - transform: unset; - position: relative; - font-size: 13px; - margin: 0 4px; - } - .testimonials-inner { - padding-left: 15px; - padding-right: 15px; - } - .testimonials-inner .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 5px; - font-size: 15px; - width: 35px; - height: 35px; - line-height: 35px; - } - .testimonials-inner.it-agency-color { - padding-left: 0; - padding-right: 0; - } - .testimonials-inner.minimal-portfolio-color { - padding-left: 0; - padding-right: 0; - } - .testimonials-inner.shop-color { - padding-left: 0; - padding-right: 0; - } - .testimonials-item { - max-width: 100%; - } - .testimonials-item .icon { - margin-bottom: 15px; - font-size: 30px; - } - .testimonials-item p { - font-size: 15px; - } - .testimonials-item .info { - margin-top: 20px; - } - .testimonials-item .info img { - width: 70px !important; - height: 70px; - } - .testimonials-item .info .title h3 { - font-size: 15px; - margin-bottom: 7px; - } - .saas-testimonials-slides.owl-theme { - margin-top: 30px; - padding-left: 0; - padding-right: 0; - } - .saas-testimonials-slides.owl-theme .owl-nav { - margin-top: 20px; - } - .saas-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - top: 0; - left: 0; - margin: 0 6px; - font-size: 20px; - -webkit-transform: unset; - transform: unset; - position: relative; - } - .saas-testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-next { - right: 0; - } - .saas-testimonials-item { - padding: 20px 15px; - text-align: center; - } - .saas-testimonials-item .info { - margin-bottom: 20px; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - text-align: start; - } - .saas-testimonials-item .info img { - width: 65px !important; - height: 65px; - } - .saas-testimonials-item .info .title { - text-align: start; - } - .saas-testimonials-item .info .title h3 { - font-size: 15px; - } - .saas-testimonials-item p { - font-size: 14px; - } - .testimonials-box { - padding: 20px 15px; - } - .testimonials-box .icon { - width: 60px; - height: 60px; - line-height: 60px; - font-size: 25px; - margin-bottom: 15px; - } - .testimonials-box p { - font-size: 14px; - } - .testimonials-box .info { - margin-top: 20px; - } - .testimonials-box .info h3 { - font-size: 15px; - } - .single-skills-box { - padding: 20px; - } - .short-brief-area .row .col-lg-6:last-child .short-brief-content { - border-bottom: none; - margin-bottom: 0; - } - .short-brief-content { - border-bottom: 1px solid #eeeeee; - padding-bottom: 30px; - } - .short-brief-content .timeline-content h3 { - font-size: 16px; - margin-bottom: 12px; - } - .short-brief-content .timeline-item { - padding-left: 30px; - } - .short-brief-content .timeline-info .sub-title { - margin-bottom: 10px; - } - .short-brief-content .timeline-info .sub-title span { - font-size: 15px; - } - .video-box .video-btn { - top: 50%; - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); - width: 55px; - height: 55px; - font-size: 20px; - } - .single-team-member .content { - padding: 20px 15px; - } - .single-team-member .content::before { - opacity: 1; - visibility: visible; - } - .single-team-member .content h3 { - font-size: 16px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-team-member .content span { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-team-member .content .social { - bottom: auto; - opacity: 1; - visibility: visible; - top: -30px; - } - .single-team-member .content .social li a { - font-size: 15px; - } - .single-team-box .image .social { - margin-bottom: 0; - opacity: 1; - visibility: visible; - padding: 20px; - } - .single-team-box .content { - margin-top: 20px; - } - .single-team-box .content h3 { - font-size: 16px; - } - .team-member .content { - padding: 20px 15px; - margin-top: 0; - } - .team-member .content::before { - opacity: 1; - visibility: visible; - } - .team-member .content h3 { - font-size: 16px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .team-member .content span { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .team-member .content .social { - visibility: visible; - bottom: auto; - opacity: 1; - top: -30px; - } - .team-member .content .social li a { - font-size: 15px; - } - .team-box .content { - visibility: visible; - position: relative; - padding: 20px 15px; - opacity: 1; - bottom: 0; - } - .team-box .content h3 { - font-size: 16px; - } - .team-box .content .social li a { - font-size: 17px; - } - .single-pricing-box { - text-align: center; - padding: 20px 15px; - } - .single-pricing-box .pricing-header { - margin-bottom: 20px; - padding-bottom: 20px; - } - .single-pricing-box .pricing-header h3 { - font-size: 30px; - } - .single-pricing-box .pricing-header h3 span { - font-size: 13px; - } - .single-pricing-box .features-list { - max-width: 176px; - text-align: start; - margin-bottom: 20px; - margin-left: auto; - margin-right: auto; - } - .single-pricing-box .features-list li { - padding-left: 20px; - } - .single-pricing-box .features-list li i { - top: 3.5px; - } - .pricing-box { - padding: 25px; - } - .pricing-box .pricing-header { - margin: -25px -25px 25px; - padding: 25px; - } - .pricing-box .pricing-header h3 { - font-size: 16px; - } - .pricing-box .pricing-header .price { - font-size: 30px; - } - .pricing-box .pricing-header .price span { - font-size: 13px; - } - .pricing-box .features-list { - margin-bottom: 25px; - } - .pricing-box.bg-black .pricing-header { - padding: 0 0 25px; - } - .pricing-tabs { - margin-top: 0; - } - .pricing-tabs .nav { - margin-bottom: 40px; - } - .pricing-tabs .nav .nav-item .nav-link { - padding: 11px 30px; - font-size: 14px; - } - .pricing-item { - padding: 20px 15px; - } - .pricing-item .pricing-header { - margin: -20px 0 20px; - padding: 20px 15px; - } - .pricing-item .pricing-header h3 { - font-size: 16px; - } - .pricing-item .pricing-header .price { - font-size: 30px; - } - .pricing-item .pricing-header .price span { - font-size: 13px; - } - .pricing-item .features-list { - margin-bottom: 20px; - } - .single-clients-box a { - padding: 10px; - } - .services-details-content { - text-align: center; - } - .services-details-content h2 { - font-size: 22px; - } - .services-details-video { - margin-left: 0; - margin-top: 30px; - } - .services-details-video iframe { - height: 162px; - } - .showMoreItemsList + .button-box button { - margin-top: 0; - padding: 11px 30px; - font-size: 14px; - } - .single-featured-video-box .content h3 { - font-size: 16px; - } - .featured-video-item-list .single-featured-video-item .image { - height: auto; - background-image: unset !important; - } - .featured-video-item-list .single-featured-video-item .image img { - display: inline-block; - } - .featured-video-item-list .single-featured-video-item .content { - padding-top: 20px; - padding-left: 0; - padding-bottom: 0; - } - .featured-video-item-list .single-featured-video-item .content h3 { - font-size: 16px; - } - .latest-posts-area .widget-area { - margin-top: 10px; - margin-bottom: 30px; - } - .single-latest-post-box .post-content { - padding-top: 20px; - padding-left: 0; - padding-right: 0; - } - .single-latest-post-box .post-content h3 { - font-size: 16px; - } - .single-latest-post-box .post-content .link-btn i { - top: 6px; - } - .what-we-do-content { - text-align: center; - } - .what-we-do-content .section-title { - text-align: center; - margin-bottom: 0; - } - .what-we-do-content .section-title p { - margin-top: 12px; - } - .what-we-do-content .single-what-we-do-box { - margin-top: 30px; - } - .single-what-we-do-box .icon { - font-size: 35px; - margin-bottom: 20px; - } - .single-what-we-do-box h3 { - font-size: 16px; - } - .what-we-do-image { - margin-top: 30px; - } - .shorting-menu { - margin-bottom: 15px; - } - .shorting-menu .filter { - font-size: 14px; - margin-bottom: 15px; - margin-right: 10px; - margin-left: 10px; - } - .works-slides-style-one.owl-theme .owl-nav, .works-slides-style-two.owl-theme .owl-nav { - opacity: 1; - visibility: visible; - } - .works-slides-style-one.owl-theme .owl-nav [class*=owl-], .works-slides-style-two.owl-theme .owl-nav [class*=owl-] { - left: 15px; - width: 35px; - height: 35px; - line-height: 35px; - font-size: 16px; - } - .works-slides-style-one.owl-theme .owl-nav [class*=owl-].owl-next, .works-slides-style-two.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 15px; - } - .works-slides-style-one.owl-theme .owl-dots, .works-slides-style-two.owl-theme .owl-dots { - margin-top: 30px; - } - .works-slides-style-one.owl-theme .owl-dots .owl-dot span, .works-slides-style-two.owl-theme .owl-dots .owl-dot span { - width: 15px; - height: 15px; - margin: 0 3px; - } - .portfolio-details-desc h3 { - font-size: 17px; - } - .portfolio-details-desc .image { - padding-right: 0; - } - .portfolio-details-desc .content { - margin-top: 0; - margin-bottom: 15px; - } - .portfolio-details-info { - padding: 20px; - margin-left: 0; - margin-top: 30px; - } - .portfolio-details-info ul li .icon { - top: 6px; - font-size: 20px; - } - .portfolio-details-info ul li span { - margin-bottom: 8px; - font-size: 15px; - } - .portfolio-details-slides.owl-theme .owl-nav { - opacity: 1; - visibility: visible; - } - .portfolio-details-slides.owl-theme .owl-nav [class*=owl-] { - left: 15px; - width: 35px; - height: 35px; - line-height: 35px; - font-size: 16px; - } - .portfolio-details-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 15px; - } - .portfolio-details-left a { - padding-left: 20px; - font-size: 13px; - } - .menu-icon { - font-size: 18px; - } - .portfolio-details-right a { - padding-right: 20px; - font-size: 13px; - } - .widget-area { - margin-top: 40px; - } - .widget-area .widget .widget-title { - font-size: 16px; - padding-bottom: 10px; - } - .widget-area .widget_service_categories ul li a i { - font-size: 11px; - } - .widget-area .widget_about_author .about-author { - padding: 20px 15px; - } - .widget-area .widget_noke_posts_thumb .item { - padding-left: 95px; - } - .widget-area .widget_noke_posts_thumb .item .thumb { - width: 80px; - } - .widget-area .widget_noke_posts_thumb .item .thumb .fullimage { - width: 80px; - } - .widget-area .widget_noke_posts_thumb .item .info .title { - font-size: 13px; - } - .widget-area .widget_recent_posts .item .title { - font-size: 14px; - } - .widget-area .widget_newsletter { - padding: 20px 15px; - } - .widget-area .widget_newsletter h4 { - font-size: 17px; - } - .widget-area .widget_tag_cloud .tagcloud a { - font-size: 13px !important; - } - .widget-area.extra-padding { - padding-left: 0; - } - .widget-area.extra-padding.left-sidebar { - padding-right: 0; - margin-top: 0; - margin-bottom: 40px; - } - .widget-area .widget_price_filter .collection_filter_by_price { - padding-right: 15px; - } - .widget-area .widget_colors ul li a span:nth-child(1) { - width: 15px; - height: 15px; - } - .widget-area .widget_top_rated_products .item .info .title { - margin-bottom: 6px; - font-size: 13px; - } - .products-details-desc { - padding-left: 0; - margin-top: 30px; - } - .products-details-desc h3 { - margin-bottom: 15px; - font-size: 16px; - } - .products-details-desc .products-review .rating { - font-size: 13px; - } - .products-details-desc .price { - font-size: 14px; - } - .products-details-tabs { - margin-top: 30px; - } - .products-details-tabs .single-tabs-box h2 { - font-size: 17px; - } - .products-details-tabs .single-tabs-box .inner-box h3 { - font-size: 16px; - margin-bottom: 15px; - } - .products-details-tabs .single-tabs-box .inner-box .table-striped tbody tr td { - font-size: 13px; - white-space: nowrap; - } - .products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review { - padding-left: 0; - } - .products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review .sub-comment { - margin-bottom: 6px; - } - .products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review img { - top: 0; - left: 0; - position: relative; - margin-bottom: 15px; - } - .products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form button { - margin-top: 20px; - font-size: 14px; - } - .cart-table-desc table thead tr th { - font-size: 14px; - white-space: nowrap; - } - .cart-table-desc table tbody tr td { - padding-right: 20px; - white-space: nowrap; - font-size: 13px; - text-align: center; - } - .cart-buttons-desc .shopping-coupon-code .form-control { - padding-left: 10px; - } - .cart-buttons-desc .shopping-coupon-code button { - padding: 0 15px; - font-size: 12px; - } - .cart-buttons-desc .text-end { - text-align: center !important; - margin-top: 30px; - } - .cart-totals-desc { - margin-top: 30px; - padding: 20px 15px; - } - .cart-totals-desc h3 { - font-size: 16px; - padding-top: 15px; - padding-left: 15px; - padding-right: 15px; - padding-bottom: 14px; - margin-left: -15px; - margin-right: -15px; - margin-bottom: 20px; - margin-top: -20px; - } - .cart-totals-desc .default-btn { - margin-top: 20px; - } - .billing-details-desc { - padding: 20px 15px; - } - .billing-details-desc h3 { - font-size: 16px; - padding-top: 15px; - padding-left: 15px; - padding-right: 15px; - padding-bottom: 14px; - margin-left: -15px; - margin-right: -15px; - margin-bottom: 20px; - margin-top: -20px; - } - .billing-details-desc .form-group .nice-select { - font-size: 13px; - } - .billing-details-desc .form-group .nice-select .list .option { - font-size: 14px; - } - .order-details-desc { - margin-top: 30px; - padding: 20px 15px; - } - .order-details-desc h3 { - font-size: 16px; - padding-top: 15px; - padding-left: 15px; - padding-right: 15px; - padding-bottom: 14px; - margin-left: -15px; - margin-right: -15px; - margin-bottom: 20px; - margin-top: -20px; - } - .order-details-desc .default-btn { - margin-top: 20px; - } - .blog-details-desc .article-content .entry-meta { - margin-top: -10px; - } - .blog-details-desc .article-content .entry-meta ul li { - margin-right: 10px; - margin-top: 10px; - } - .blog-details-desc .article-content h3 { - font-size: 16px; - line-height: 1.5; - margin-bottom: 12px; - margin-top: 20px; - } - .blog-details-desc .article-footer { - text-align: center; - } - .blog-details-desc .article-footer .article-tags { - -webkit-box-flex: 0; - -ms-flex: 0 0 100%; - flex: 0 0 100%; - max-width: 100%; - } - .blog-details-desc .article-footer .article-tags .d-flex { - display: block !important; - } - .blog-details-desc .article-footer .article-tags p { - display: block; - margin-bottom: 0; - } - .blog-details-desc .article-footer .article-tags a { - margin-top: 10px; - } - .blog-details-desc .article-footer .article-share { - -webkit-box-flex: 0; - -ms-flex: 0 0 100%; - flex: 0 0 100%; - max-width: 100%; - margin-top: 20px; - } - .blog-details-desc .article-footer .article-share .social { - text-align: center; - } - blockquote { - padding: 20px !important; - } - blockquote::after, blockquote::after { - width: 1px; - } - blockquote p { - font-size: 14px !important; - } - .comments-area { - margin-top: 25px; - } - .comments-area .comments-title { - font-size: 16px; - } - .comments-area .comment-author { - font-size: 14px; - } - .comments-area .comment-author .avatar { - top: 0; - left: 0; - display: block; - position: relative; - margin-bottom: 12px; - } - .comments-area .comment-body .reply a { - font-size: 12px; - } - .comments-area .comment-body { - padding-left: 0; - } - .comments-area .comment-respond { - margin-top: 25px; - } - .comments-area .comment-respond .comment-reply-title { - font-size: 16px; - } - .comments-area .comment-respond .comment-form-author { - width: 100%; - padding-right: 0; - } - .comments-area .comment-respond .comment-form-email { - width: 100%; - padding-left: 0; - } - .comments-area .comment-respond input[type="date"], .comments-area .comment-respond input[type="time"], .comments-area .comment-respond input[type="datetime-local"], .comments-area .comment-respond input[type="week"], .comments-area .comment-respond input[type="month"], .comments-area .comment-respond input[type="text"], .comments-area .comment-respond input[type="email"], .comments-area .comment-respond input[type="url"], .comments-area .comment-respond input[type="password"], .comments-area .comment-respond input[type="search"], .comments-area .comment-respond input[type="tel"], .comments-area .comment-respond input[type="number"], .comments-area .comment-respond textarea { - font-size: 14px; - padding-left: 12px; - } - .comments-area .comment-respond textarea { - padding-top: 12px; - } - .comments-area .comment-respond .comment-form-cookies-consent label { - font-size: 14px; - } - .comments-area .comment-respond .form-submit input { - font-size: 13px; - } - .faq-accordion .accordion-item .accordion-button { - padding: 15px 15px 15px 40px; - font-size: 14px; - } - .faq-accordion .accordion-item .accordion-button::after { - left: 15px; - top: 20px; - width: 12px; - height: 12px; - background-size: 12px; - } - .faq-accordion .accordion-item .accordion-body { - padding: 0 15px 15px 40px; - } - .facility-content { - padding-right: 0; - margin-bottom: 30px; - } - .facility-content .item { - padding-left: 0; - text-align: center; - } - .facility-content .item i { - line-height: 1; - font-size: 35px; - position: relative; - margin-bottom: 20px; - display: inline-block; - } - .facility-content .item h3 { - font-size: 16px; - } - .cta-content { - max-width: 100%; - } - .cta-content h2 { - font-size: 22px; - } - .cta-content p { - max-width: 100%; - margin-top: 15px; - } - .cta-content .default-btn { - margin-top: 10px; - } - .call-to-action-content { - text-align: center; - } - .call-to-action-content h2 { - font-size: 22px; - } - .call-to-action-btn { - text-align: center; - margin-top: 25px; - } - .call-to-action-inner { - padding-left: 15px; - padding-right: 15px; - } - .profile-authentication-area { - height: auto; - padding-bottom: 70px; - padding-top: 130px; - } - .signin-form { - max-width: 100%; - text-align: center; - padding: 20px 15px; - } - .signin-form h2 { - font-size: 25px; - margin-bottom: 25px; - } - .signin-form form .lost-your-password-wrap { - text-align: start; - margin-top: 15px; - } - .signin-form form .form-group { - margin-bottom: 15px; - } - .signin-form form button { - padding: 12px 30px; - font-size: 14px; - } - .signin-form form .dont-account { - margin-top: 20px; - } - .signin-form form .sign-in-with-button { - margin-top: 20px; - } - .signup-form { - max-width: 100%; - text-align: center; - padding: 20px 15px; - } - .signup-form h2 { - font-size: 25px; - margin-bottom: 25px; - } - .signup-form form .lost-your-password-wrap { - text-align: start; - margin-top: 15px; - } - .signup-form form .form-group { - margin-bottom: 15px; - } - .signup-form form button { - padding: 12px 30px; - font-size: 14px; - } - .signup-form form .dont-account { - margin-top: 20px; - } - .signup-form form .sign-in-with-button { - margin-top: 20px; - } - .noke-grid-sorting { - margin-bottom: 30px; - text-align: center; - } - .noke-grid-sorting .ordering { - text-align: center; - margin-top: 15px; - } - .noke-grid-sorting .ordering select { - font-size: 14px; - } - .app-download-area .col-lg-6:nth-child(1) { - -webkit-box-ordinal-group: 3; - -ms-flex-order: 2; - order: 2; - } - .app-download-area .col-lg-6:nth-child(2) { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .app-download-content { - text-align: center; - } - .app-download-content .sub-title { - font-size: 13px; - } - .app-download-content h2 { - font-size: 22px; - } - .app-download-content .btn-box { - margin-top: 25px; - } - .app-download-content .btn-box .store-btn { - padding: 10px 10px 10px 45px; - font-size: 12px; - text-align: start; - margin-left: 3px; - margin-right: 3px; - } - .app-download-content .btn-box .store-btn:first-child { - margin-left: 0; - } - .app-download-content .btn-box .store-btn img { - left: 10px; - width: 25px; - } - .app-download-content .btn-box .store-btn span { - font-size: 14px; - } - .app-download-image { - padding-right: 0; - margin-top: 30px; - } - .trending-style-image { - margin-right: 0; - padding-bottom: 0; - padding-right: 0; - } - .trending-style-image img:nth-child(2) { - position: relative; - margin-top: 30px; - width: auto; - } - .trending-style-content { - text-align: center; - padding-left: 0; - margin-top: 30px; - } - .trending-style-content .sub-title { - font-size: 13px; - } - .trending-style-content h2 { - margin-bottom: 13px; - font-size: 22px; - } - .trending-style-content h3 { - font-size: 15px; - margin-bottom: 13px; - } - .trending-style-content .default-btn { - margin-top: 5px; - } - .page-title-area { - padding-top: 60px; - padding-bottom: 70px; - } - .page-title-area.style-three { - padding-top: 60px; - padding-bottom: 60px; - } - .page-title-area.style-four { - padding-top: 60px; - padding-bottom: 60px; - } - .page-title-area.style-four::before { - background-image: unset !important; - background-color: rgba(255, 255, 255, 0.85); - } - .page-title-area.style-two { - padding-bottom: 60px; - padding-top: 245px; - } - .page-title-content.text-start { - text-align: center !important; - } - .page-title-content h2 { - font-size: 25px; - } - .page-title-content ul { - margin-top: 10px; - } - .page-title-content ul li { - font-size: 14px; - } - .privacy-policy-content h3 { - font-size: 16px; - margin-bottom: 13px; - } - .terms-conditions-content h3 { - font-size: 16px; - margin-bottom: 13px; - } - .error-content h3 { - font-size: 20px; - margin-bottom: 15px; - margin-top: 30px; - } - .error-content .default-btn { - margin-top: 20px; - } - .coming-soon-image { - height: auto; - background-image: unset !important; - } - .coming-soon-image img { - display: inline-block; - } - .coming-soon-content { - height: auto; - padding-top: 60px; - padding-bottom: 60px; - padding-left: 15px; - padding-right: 15px; - } - .coming-soon-content h2 { - font-size: 25px; - margin-top: 25px; - } - .coming-soon-content #timer { - margin-top: 10px; - } - .coming-soon-content #timer div { - width: 95px; - height: 100px; - font-size: 30px; - margin-top: 20px; - } - .coming-soon-content #timer div span { - font-size: 14px; - } - .coming-soon-content #timer div:first-child { - margin-left: 10px; - } - .coming-soon-content #timer div:last-child { - margin-right: 10px; - } - .coming-soon-content form { - max-width: 100%; - margin-top: 30px; - } - .coming-soon-content form .default-btn { - position: relative; - right: 0; - bottom: 0; - display: block; - width: 100%; - padding: 13px 20px; - margin-top: 15px; - } - .coming-soon-content form .input-newsletter { - padding: 0 0 0 15px; - height: 50px; - font-size: 13px; - } - .single-blog-post .content { - padding: 15px; - } - .single-blog-post .content .meta li i { - top: 3px; - } - .single-blog-post .content h3 { - font-size: 16px; - } - .single-post-item .post-content { - padding: 20px 15px; - } - .single-post-item .post-content .meta { - margin-bottom: 3px; - } - .single-post-item .post-content .meta li { - padding-left: 19px; - margin-right: 10px; - margin-bottom: 10px; - } - .single-post-item .post-content .tag { - margin-bottom: 12px; - } - .single-post-item .post-content h3 { - font-size: 16px; - } - .single-post-item .post-content .link-btn i { - top: 6px; - } - .single-post-item.without-box-shadow .post-content { - padding-left: 0; - padding-right: 0; - } - .blog-section-title { - text-align: center; - margin-bottom: 40px; - } - .blog-section-title .sub-title { - font-size: 13px; - } - .blog-section-title h2 { - font-size: 22px; - } - .blog-section-title .default-btn { - margin-top: 5px; - } - .pagination-area { - margin-top: 10px; - } - .pagination-area .page-numbers { - width: 35px; - height: 35px; - line-height: 33px; - font-size: 15px; - } - .single-contact-info-box { - padding: 20px 15px; - text-align: center; - } - .single-contact-info-box .icon { - font-size: 30px; - margin-bottom: 20px; - } - .single-contact-info-box h3 { - font-size: 16px; - margin-bottom: 12px; - } - .single-contact-info-box.style-two .icon { - width: 60px; - height: 60px; - font-size: 25px; - margin-bottom: 20px; - } - .contact-form { - padding: 20px; - } - .footer-area { - padding-top: 60px; - } - .footer-area.marketing-footer .footer-top { - margin-bottom: 60px; - padding-bottom: 0; - } - .footer-area.business-footer .footer-top { - margin-bottom: 60px; - padding-top: 50px; - padding-bottom: 50px; - } - .footer-top { - text-align: center; - padding-bottom: 30px; - margin-bottom: 30px; - } - .footer-top .logo { - margin-bottom: 25px; - } - .footer-top .content.d-flex { - display: block !important; - } - .footer-top .content h3 { - font-size: 16px; - margin-bottom: 25px; - } - .single-footer-widget h3 { - font-size: 16px; - } - .single-footer-widget.pl-4 { - padding-left: 0; - } - .single-footer-widget.pl-2 { - padding-left: 0; - } - .single-footer-widget.style-two h3 { - padding-left: 0; - } - .single-footer-widget.style-two h3::before { - display: none; - } - .copyright-area { - margin-top: 30px; - padding-top: 20px; - padding-bottom: 20px; - } - .copyright-area .text-start { - text-align: center !important; - margin-bottom: 15px; - } - .copyright-area .social-links { - text-align: center; - margin-top: 15px; - } - .go-top { - width: 38px; - height: 38px; - font-size: 15px; - } - .productsQuickView .modal-dialog { - max-width: 100%; - } - .productsQuickView .modal-content { - padding: 15px; - } - .productsQuickView .modal-content button.btn-close { - font-size: 15px; - } - .productsQuickView .modal-content .products-content { - margin-top: 25px; - } - .productsQuickView .modal-content .products-content h3 { - font-size: 16px; - } - .productsQuickView .modal-content .products-content .price { - font-size: 14px; - } - .productsQuickView .modal-content .products-content .products-add-to-cart { - margin-top: 20px; - } - .productsQuickView .modal-content .products-content .social-share { - margin-top: 25px; - } -} - -/* Min width 576px to Max width 767px */ -@media only screen and (min-width: 576px) and (max-width: 767px) { - .top-header-area .container-fluid { - max-width: 540px; - } - .top-header-area.top-header-style-two .container-fluid { - max-width: 540px; - } - .top-header-area.top-header-style-three .container-fluid { - max-width: 540px; - } - .startup-banner-inner { - padding-left: 25px; - padding-right: 25px; - } - .startup-banner-content { - max-width: 540px; - margin-left: auto; - margin-right: auto; - padding-left: 15px; - padding-right: 15px; - } - .startup-banner-image { - max-width: 540px; - margin-left: auto; - margin-right: auto; - padding-left: 15px; - padding-right: 15px; - } - .business-banner-area .container { - max-width: 540px; - } - .app-banner-area .container { - max-width: 540px; - } - .saas-banner-area .container-fluid { - max-width: 540px; - } - .personal-portfolio-banner-area .container { - max-width: 540px; - } - .consulting-banner-area .container { - max-width: 540px; - } - .screenshot-wrap .screenshot-frame { - display: block; - height: 500px; - width: 240px; - top: 40px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-] { - left: 0; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 0; - } - .startup-about-area .container-fluid { - max-width: 540px; - } - .single-services-box .content { - padding: 25px; - } - .single-blog-post .content { - padding: 25px; - } - .works-area .container-fluid { - max-width: 540px; - } - .facility-content { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - margin-right: -15px; - margin-left: -15px; - margin-bottom: 0; - } - .facility-content .item { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; - margin-bottom: 30px !important; - padding-left: 15px; - padding-right: 15px; - } - .startup-testimonials-area .container-fluid { - max-width: 540px; - } - .trending-style-image { - padding-right: 100px; - padding-bottom: 80px; - } - .trending-style-image img:nth-child(2) { - position: absolute; - margin-top: 0; - width: 250px; - } - .productsQuickView .modal-dialog { - max-width: 430px; - } - .productsQuickView .modal-content { - padding: 30px; - } -} - -/* Min width 768px to Max width 991px */ -@media only screen and (min-width: 768px) and (max-width: 991px) { - body { - font-size: 14.5px; - } - p { - font-size: 14.5px; - } - .ptb-100 { - padding-top: 80px; - padding-bottom: 80px; - } - .pt-100 { - padding-top: 80px; - } - .pb-100 { - padding-bottom: 80px; - } - .ptb-70 { - padding-top: 50px; - padding-bottom: 50px; - } - .pt-70 { - padding-top: 50px; - } - .pb-70 { - padding-bottom: 50px; - } - .default-btn { - font-size: 14.5px; - } - .section-title { - max-width: 550px; - margin-bottom: 50px; - } - .section-title h2 { - font-size: 30px; - } - .section-title.style-two { - text-align: center; - padding-left: 0; - margin-left: auto; - margin-right: auto; - margin-bottom: 50px; - } - .section-title.style-two h2::before { - display: none; - } - .section-title.style-four { - text-align: center; - } - .section-title.style-four h2 { - font-size: 25px; - } - .section-title.style-four::before { - left: 50%; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); - } - .top-header-area .container-fluid { - max-width: 720px; - } - .top-header-area.top-header-style-two .container-fluid { - max-width: 720px; - } - .top-header-area.top-header-style-three .container-fluid { - max-width: 720px; - } - .noke-responsive-nav .others-option { - display: -webkit-box !important; - display: -ms-flexbox !important; - display: flex !important; - } - .others-option-for-responsive { - display: none; - } - .startup-banner-area .container-fluid { - max-width: 100%; - padding-left: 0; - padding-right: 0; - } - .startup-banner-inner { - padding-top: 80px; - padding-bottom: 80px; - padding-left: 25px; - padding-right: 25px; - } - .startup-banner-content { - max-width: 720px; - text-align: center; - margin-left: auto; - margin-right: auto; - padding-left: 15px; - padding-right: 15px; - } - .startup-banner-content p { - margin-left: auto; - margin-right: auto; - } - .startup-banner-content h1 { - margin-bottom: 20px; - font-size: 35px; - } - .startup-banner-content .default-btn { - margin-top: 5px; - } - .startup-banner-image { - max-width: 720px; - padding-left: 15px; - padding-right: 15px; - margin-left: auto; - margin-right: auto; - margin-top: 30px; - } - .marketing-banner-area { - padding-top: 70px; - padding-bottom: 70px; - } - .marketing-banner-content { - padding-right: 0; - text-align: center; - } - .marketing-banner-content p { - margin-left: auto; - margin-right: auto; - } - .marketing-banner-content h1 { - margin-bottom: 15px; - font-size: 35px; - } - .marketing-banner-content .default-btn { - margin-top: 5px; - } - .marketing-banner-image { - margin-top: 30px; - } - .business-banner-area { - padding-top: 80px; - padding-bottom: 80px; - } - .business-banner-area .container { - max-width: 720px; - } - .business-banner-area::before { - display: none; - } - .business-banner-content { - padding-right: 0; - text-align: center; - max-width: 100%; - } - .business-banner-content p { - margin-left: auto; - margin-right: auto; - } - .business-banner-content h1 { - margin-bottom: 15px; - font-size: 36px; - } - .business-banner-content .default-btn { - margin-top: 5px; - } - .business-banner-image { - margin-top: 30px; - } - .app-banner-area { - padding-top: 80px; - padding-bottom: 80px; - } - .app-banner-area .container { - max-width: 720px; - } - .app-banner-content { - text-align: center; - } - .app-banner-content p { - margin-left: auto; - margin-right: auto; - } - .app-banner-content h1 { - margin-bottom: 15px; - font-size: 40px; - } - .app-banner-content .btn-box { - margin-top: 20px; - } - .app-banner-content .btn-box .store-btn { - text-align: start; - } - .app-banner-content .btn-box .store-btn span { - font-size: 16px; - } - .app-banner-image { - padding-left: 0; - margin-top: 35px; - } - .personal-portfolio-banner-area { - padding-top: 80px; - padding-bottom: 80px; - } - .personal-portfolio-banner-area .container { - max-width: 720px; - } - .personal-portfolio-banner-content { - text-align: center; - } - .personal-portfolio-banner-content .sub-title { - padding-left: 0; - font-size: 16px; - } - .personal-portfolio-banner-content .sub-title::before { - display: none; - } - .personal-portfolio-banner-content p { - margin-left: auto; - margin-right: auto; - } - .personal-portfolio-banner-content h4 { - margin-bottom: 15px; - font-size: 20px; - } - .personal-portfolio-banner-content h1 { - margin-bottom: 15px; - font-size: 40px; - } - .personal-portfolio-banner-content .default-btn { - margin-top: 5px; - } - .personal-portfolio-banner-image { - margin-top: 30px; - } - .personal-portfolio-banner-image img { - width: 100%; - } - .shape1, .shape2, .shape3, .shape4, .shape5, .shape6, .shape7, .shape8, .shape9, .shape16, .shape17, .shape18, .shape19 { - display: none !important; - } - .saas-banner-area { - padding-top: 80px; - } - .saas-banner-area .container-fluid { - max-width: 720px; - padding-left: 15px; - padding-right: 15px; - } - .saas-banner-content { - margin-left: 0; - text-align: center; - max-width: 100%; - } - .saas-banner-content h1 { - margin-bottom: 15px; - font-size: 40px; - } - .saas-banner-content p { - margin-left: auto; - margin-right: auto; - } - .saas-banner-image { - margin-top: 35px; - } - .consulting-banner-area { - padding-top: 215px; - padding-bottom: 80px; - } - .consulting-banner-area .container { - max-width: 720px; - } - .consulting-banner-area::before { - display: block; - } - .consulting-banner-content { - text-align: center; - max-width: 100%; - } - .consulting-banner-content h1 { - margin-bottom: 15px; - font-size: 35px; - } - .consulting-banner-content p { - margin-left: auto; - margin-right: auto; - } - .consulting-banner-content .default-btn { - margin-top: 5px; - } - .it-agency-banner-area .it-agency-inner-banner { - padding-top: 200px; - padding-bottom: 230px; - } - .it-agency-banner-content { - max-width: 100%; - } - .it-agency-banner-content h1 { - font-size: 40px; - margin-bottom: 15px; - } - .it-agency-banner-content .default-btn { - margin-top: 8px; - } - .banner-video-box { - max-width: 100%; - margin-top: -130px; - } - .shop-banner-area { - padding-top: 100px; - padding-bottom: 100px; - } - .shop-banner-content .sub-title { - font-size: 20px; - } - .shop-banner-content h1 { - margin-bottom: 15px; - font-size: 40px; - } - .shop-banner-content h4 { - font-size: 20px; - } - .shop-banner-content .default-btn { - margin-top: 25px; - } - .blog-banner-area { - padding-top: 80px; - padding-bottom: 80px; - } - .blog-banner-item { - margin-bottom: 30px; - } - .blog-banner-item .content { - padding: 25px; - max-width: 510px; - } - .blog-banner-item .content h3 { - font-size: 20px; - } - .blog-banner-item-list .single-blog-banner-item .image { - background-image: unset !important; - height: auto; - } - .blog-banner-item-list .single-blog-banner-item .image img { - display: inline-block; - } - .blog-banner-item-list .single-blog-banner-item .content { - padding-top: 20px; - padding-bottom: 0; - padding-left: 0; - } - .blog-banner-item-list .single-blog-banner-item .content h3 { - font-size: 18px; - } - .furniture-banner-item { - padding-top: 100px; - padding-bottom: 100px; - } - .furniture-banner-content { - border-left-width: 3px; - padding: 30px; - } - .furniture-banner-content h1 { - font-size: 35px; - } - .minimal-portfolio-banner-area { - padding-bottom: 170px; - padding-top: 200px; - } - .minimal-portfolio-banner-content { - max-width: 600px; - } - .minimal-portfolio-banner-content .typewrite { - padding: 8px 20px; - margin-bottom: 25px; - font-size: 15px; - } - .minimal-portfolio-banner-content h1 { - font-size: 35px; - margin-bottom: 25px; - } - .features-area.ptb-70 { - padding-bottom: 20px; - } - .single-features-box { - padding-left: 0; - padding-right: 0; - } - .single-features-box .icon { - font-size: 40px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-features-box h3 { - font-size: 18px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-features-box p { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-features-box .link-btn { - left: 0; - opacity: 1; - -webkit-transform: unset; - transform: unset; - margin-top: 13px; - position: relative; - visibility: visible; - bottom: 0 !important; - } - .single-features-box .link-btn i { - top: 6.5px; - } - .features-box { - padding: 25px 20px; - } - .features-box h3 { - font-size: 18px; - } - .features-box .icon { - width: 80px; - height: 80px; - font-size: 40px; - line-height: 80px; - } - .single-features-item { - padding-left: 55px; - } - .single-features-item .icon { - font-size: 40px; - } - .single-features-item h3 { - font-size: 17px; - } - .features-item { - padding: 25px 20px; - } - .features-item .icon { - font-size: 30px; - } - .features-item h3 { - font-size: 18px; - } - .startup-about-area { - border-bottom: 1px solid #eeeeee; - margin-bottom: 80px; - } - .startup-about-area .container-fluid { - max-width: 720px; - } - .startup-about-image { - margin-right: 0; - padding: 0; - } - .startup-about-image .video-btn { - left: 50%; - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); - } - .startup-about-content { - padding-left: 0; - text-align: center; - max-width: 100%; - margin-top: 30px; - } - .startup-about-content h2 { - font-size: 30px; - } - .startup-about-content .features-list { - padding-left: 0; - list-style-type: none; - } - .startup-about-content .features-list li { - font-weight: 600; - } - .marketing-about-image { - margin-right: 0; - } - .marketing-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - } - .marketing-about-content h2 { - font-size: 30px; - } - .marketing-about-content .features-list { - padding-left: 0; - list-style-type: none; - } - .marketing-about-content .features-list li { - font-weight: 600; - } - .business-about-image { - padding-right: 0; - margin-bottom: 30px; - } - .business-about-content { - padding-left: 0; - text-align: center; - } - .business-about-content h2 { - font-size: 30px; - } - .app-about-content { - padding-right: 0; - } - .app-about-content h3 { - font-size: 19px; - margin-bottom: 15px; - } - .app-about-image { - padding-left: 0; - margin-top: 30px; - } - .saas-about-image { - padding-right: 0; - margin-bottom: 30px; - } - .saas-about-content { - padding-left: 0; - } - .saas-about-content h2 { - font-size: 30px; - } - .it-agency-about-image { - padding-right: 0; - margin-right: 0; - padding-bottom: 0; - } - .it-agency-about-content { - padding-left: 0; - text-align: center; - margin-top: 35px; - } - .it-agency-about-content h2 { - font-size: 30px; - } - .it-agency-about-content .features-list { - padding-left: 0; - list-style-type: none; - } - .it-agency-about-content .features-list li { - font-weight: 600; - } - .expertise-box { - text-align: center; - } - .expertise-box .icon { - width: 70px; - height: 70px; - line-height: 70px; - font-size: 35px; - margin-left: auto; - margin-right: auto; - } - .expertise-box h3 { - font-size: 18px; - } - .who-we-are-content { - text-align: center; - padding: 30px; - } - .who-we-are-content h2 { - font-size: 20px; - } - .who-we-are-content .default-btn { - margin-top: 5px; - } - .single-services-box .content { - padding: 20px; - } - .single-services-box .content .icon { - font-size: 40px; - margin-bottom: 15px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-services-box .content h3 { - font-size: 18px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-services-box .content p { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-services-box .content .link-btn { - left: 0; - opacity: 1; - -webkit-transform: unset; - transform: unset; - margin-top: 13px; - position: relative; - visibility: visible; - bottom: 0 !important; - } - .single-services-box .content .link-btn i { - top: 6px; - } - .single-services-box::before { - background: -webkit-gradient(linear, left bottom, left top, color-stop(0, rgba(35, 35, 37, 0.9)), color-stop(50%, rgba(35, 35, 37, 0.7)), color-stop(75%, rgba(22, 22, 23, 0)), to(rgba(0, 0, 0, 0))); - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.7) 50%, rgba(22, 22, 23, 0) 75%, rgba(0, 0, 0, 0) 100%); - } - .single-services-item .content { - padding-left: 0; - padding-right: 0; - } - .single-services-item .content h3 { - font-size: 18px; - } - .services-tabs .nav { - margin-bottom: 35px; - border-bottom: 1px solid #eeeeee; - padding-bottom: 20px; - } - .services-tabs .nav .nav-item { - margin-left: 15px; - margin-right: 15px; - margin-bottom: 30px; - } - .services-tabs .nav .nav-item .nav-link { - font-size: 16px; - } - .services-tabs .nav .nav-item .nav-link .icon { - width: 70px; - height: 70px; - font-size: 30px; - line-height: 70px; - margin-bottom: 15px; - } - .single-main-services .content { - padding-right: 0; - text-align: center; - } - .single-main-services .content h3 { - font-size: 21px; - } - .single-main-services .content p { - max-width: 100%; - } - .single-main-services .image { - margin-top: 30px; - } - .services-box { - padding: 25px 20px; - } - .services-box .icon { - font-size: 50px; - } - .services-box h3 { - font-size: 18px; - } - .col-lg-3:nth-child(2) .services-box, .col-lg-3:nth-child(4) .services-box { - margin-top: 0; - } - .col-lg-3:nth-child(5) .services-box, .col-lg-3:nth-child(7) .services-box { - margin-top: 0; - } - .services-item h3 { - font-size: 18px; - margin-top: 20px; - } - .services-item .hover-content { - padding: 20px; - } - .single-it-services-box { - padding: 25px 20px 25px 70px; - } - .single-it-services-box .icon { - left: 20px; - top: 25px; - font-size: 35px; - } - .single-it-services-box h3 { - font-size: 18px; - } - .single-categories-box h3 { - font-size: 16px; - left: 20px; - top: 20px; - } - .categories-box h3 { - font-size: 17px; - padding-left: 15px; - padding-right: 15px; - } - .categories-slides.owl-theme .owl-nav { - opacity: 1; - visibility: visible; - margin-bottom: 30px; - } - .categories-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 5px; - position: relative; - left: 0; - top: 0; - -webkit-transform: unset; - transform: unset; - } - .categories-slides.owl-theme .owl-nav [class*=owl-].owl-next { - right: 0; - } - .startup-works-area { - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #eeeeee; - padding-top: 80px; - margin-bottom: 80px; - } - .startup-works-area .section-title { - padding-top: 0; - text-align: center; - margin-bottom: 50px; - margin-left: auto; - margin-right: auto; - } - .startup-works-area .section-title h2 { - font-size: 30px; - } - .startup-works-content { - margin-top: 18px; - position: relative; - text-align: center; - border-top: 1px solid #eeeeee; - padding-right: 0; - padding-top: 55px; - } - .startup-works-content h3 { - font-size: 18px; - } - .startup-works-content .custom-owl-nav { - margin-top: 0; - } - .startup-works-content .custom-owl-nav [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - position: absolute; - left: 0; - top: -18px; - } - .startup-works-content .custom-owl-nav [class*=owl-].custom-owl-next { - left: auto; - right: 0; - } - .startup-works-image { - padding: 0; - margin-left: 0; - margin-top: 15px; - } - .single-works-box .works-image .works-overlay h3 { - font-size: 18px; - } - .works-box h3 { - top: auto; - -webkit-transform: translateY(0); - transform: translateY(0); - bottom: 0; - font-size: 18px; - opacity: 1; - visibility: visible; - margin-top: 0; - margin-right: 0; - margin-left: 0; - } - .single-featured-video-box .content h3 { - font-size: 18px; - } - .featured-video-item-list .single-featured-video-item .image { - height: auto; - background-image: unset !important; - } - .featured-video-item-list .single-featured-video-item .image img { - display: inline-block; - } - .featured-video-item-list .single-featured-video-item .content { - padding-bottom: 0; - padding-left: 0; - padding-top: 20px; - } - .featured-video-item-list .single-featured-video-item .content h3 { - font-size: 18px; - } - .latest-posts-area .widget-area { - margin-top: 10px; - } - .single-latest-post-box .post-content { - padding-top: 20px; - padding-right: 0; - padding-left: 0; - } - .single-latest-post-box .post-content h3 { - font-size: 18px; - } - .facility-content { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - padding-right: 0; - margin-right: -15px; - margin-left: -15px; - margin-bottom: 0; - } - .facility-content .item { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; - margin-bottom: 30px !important; - text-align: center; - padding-left: 15px; - padding-right: 15px; - } - .facility-content .item i { - line-height: 1; - position: relative; - margin-bottom: 20px; - display: inline-block; - font-size: 40px; - } - .facility-content .item h3 { - font-size: 18px; - } - .short-brief-area .row .col-lg-6:last-child .short-brief-content { - border-bottom: none; - margin-bottom: 0; - } - .short-brief-content { - border-bottom: 1px solid #eeeeee; - padding-bottom: 30px; - } - .short-brief-content .timeline-content h3 { - font-size: 18px; - margin-bottom: 12px; - } - .short-brief-content .timeline-info .sub-title { - margin-bottom: 10px; - } - .short-brief-content .timeline-info .sub-title span { - font-size: 17px; - } - .shorting-menu .filter { - font-size: 15px; - margin-left: 15px; - margin-right: 15px; - } - .widget-area { - margin-top: 40px; - } - .widget-area .widget .widget-title { - font-size: 17px; - } - .widget-area .widget_about_author .about-author { - padding: 25px; - } - .widget-area .widget_noke_posts_thumb .item .info .title { - font-size: 14px; - } - .widget-area .widget_newsletter h4 { - font-size: 18px; - } - .widget-area .widget_tag_cloud .tagcloud a { - font-size: 13px !important; - } - .widget-area .widget_recent_posts .item .title { - font-size: 14px; - } - .widget-area.extra-padding { - padding-left: 0; - } - .widget-area.extra-padding.left-sidebar { - padding-right: 0; - margin-top: 0; - margin-bottom: 40px; - } - .services-details-content { - text-align: center; - } - .services-details-content h2 { - font-size: 30px; - } - .services-details-video { - margin-left: 0; - margin-top: 30px; - } - .services-details-video iframe { - height: 387px; - } - .single-funfacts-box { - text-align: center; - padding: 30px; - } - .single-funfacts-box .icon { - position: relative; - left: 0; - line-height: 1; - top: 0; - -webkit-transform: translateY(0); - transform: translateY(0); - font-size: 40px; - display: block; - margin-bottom: 15px; - } - .single-funfacts-box h3 { - margin-bottom: 5px; - font-size: 35px; - } - .single-funfacts-box.without-bg-color { - margin-right: -15px; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 0; - } - .single-funfacts-box.text-center { - padding: 0; - border-right: none; - } - .funfacts-inner { - padding-left: 20px; - padding-right: 20px; - } - .funfacts-inner.border-radius.ptb-70 { - padding-bottom: 20px; - } - .single-skills-box { - padding: 25px 20px; - } - .how-it-works-content { - padding-right: 0; - } - .how-it-works-content ul li { - padding-left: 60px; - } - .how-it-works-content ul li h3 { - font-size: 18px; - } - .how-it-works-content ul li i { - font-size: 40px; - } - .how-it-works-image { - margin-top: 30px; - } - .short-brief-content h3 { - font-size: 18px; - margin-bottom: 13px; - } - .screenshot-wrap .screenshot-frame { - border-radius: 20px; - width: 215px; - height: 448px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-] { - left: -27px; - width: 30px; - line-height: 30px; - height: 30px; - font-size: 15px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: -27px; - } - .single-post-item .post-content { - padding: 20px 0 0; - } - .single-clients-box a { - padding: 20px; - } - .what-we-do-content .section-title { - text-align: center; - margin-bottom: 0; - } - .what-we-do-content .section-title p { - max-width: 540px; - margin-left: auto; - margin-right: auto; - } - .what-we-do-content .single-what-we-do-box { - margin-top: 30px; - } - .single-what-we-do-box { - text-align: center; - } - .single-what-we-do-box .icon { - font-size: 35px; - margin-bottom: 25px; - } - .single-what-we-do-box h3 { - font-size: 18px; - } - .what-we-do-image { - margin-top: 30px; - } - .single-process-box { - padding: 30px 20px; - } - .single-process-box h3 { - font-size: 18px; - } - .startup-testimonials-area .container-fluid { - max-width: 720px; - } - .startup-testimonials-content { - padding: 40px 30px; - } - .startup-testimonials-content .icon { - font-size: 40px; - } - .startup-testimonials-content p { - font-size: 16px; - } - .startup-testimonials-content .info h3 { - font-size: 16px; - } - .startup-testimonials-image { - padding: 0; - } - .startup-testimonials-slides.owl-theme .owl-nav { - margin-top: 0; - position: relative; - right: 0; - bottom: 0; - -webkit-transform: unset; - transform: unset; - padding-bottom: 40px; - } - .startup-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - .marketing-testimonials-image { - margin-left: 30px; - margin-right: 30px; - margin-bottom: 40px; - } - .marketing-testimonials-content { - padding-bottom: 0; - } - .marketing-testimonials-content .icon { - font-size: 40px; - } - .marketing-testimonials-content p { - font-size: 16px; - } - .marketing-testimonials-content .info h3 { - font-size: 16px; - } - .marketing-testimonials-slides.owl-theme .owl-nav { - margin-top: 30px; - position: relative; - right: 0; - bottom: 0; - -webkit-transform: unset; - transform: unset; - } - .marketing-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - .business-testimonials-area { - background-repeat: repeat; - background-size: contain; - } - .business-testimonials-image { - margin-right: 0; - margin-bottom: 35px; - } - .business-testimonials-content .icon { - font-size: 40px; - } - .business-testimonials-content p { - font-size: 16px; - } - .business-testimonials-content .info h3 { - font-size: 16px; - } - .testimonials-slides.owl-theme { - padding-left: 90px; - padding-right: 90px; - } - .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - .testimonials-inner { - padding-left: 25px; - padding-right: 25px; - } - .testimonials-inner .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - font-size: 17px; - width: 40px; - height: 40px; - line-height: 40px; - } - .testimonials-item .icon { - font-size: 40px; - } - .testimonials-item p { - font-size: 16px; - } - .testimonials-item .info { - margin-top: 20px; - } - .testimonials-item .info .title h3 { - font-size: 16px; - } - .saas-testimonials-item { - padding: 25px; - } - .saas-testimonials-item .info .title h3 { - font-size: 16px; - } - .saas-testimonials-item p { - font-size: 15px; - } - .saas-testimonials-slides.owl-theme { - margin-top: -50px; - padding-left: 40px; - padding-right: 40px; - } - .saas-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - left: 0; - font-size: 18px; - } - .saas-testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 0; - } - .testimonials-box { - padding: 25px 20px; - } - .testimonials-box .icon { - width: 70px; - height: 70px; - font-size: 30px; - line-height: 70px; - } - .testimonials-box p { - font-size: 15px; - } - .testimonials-box .info { - margin-top: 20px; - } - .testimonials-box .info h3 { - font-size: 16px; - } - .single-products-box .image .buttons-list li { - -webkit-transform: translateY(0); - transform: translateY(0); - visibility: visible; - opacity: 1; - } - .single-products-box .image .buttons-list li a .tooltip-label::before { - bottom: -12px; - } - .single-products-box .image .buttons-list li a:hover .tooltip-label { - top: -40px; - } - .single-products-box .content h3 { - font-size: 18px; - } - .single-products-box .content .rating { - margin-bottom: 10px; - font-size: 15px; - } - .single-products-box .content .price span { - font-size: 16px; - } - .trending-style-image { - margin-right: 0; - } - .trending-style-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - } - .trending-style-content .sub-title { - font-size: 13px; - } - .trending-style-content h2 { - font-size: 30px; - } - .trending-style-content h3 { - font-size: 17px; - } - .single-pricing-box { - padding: 30px; - } - .single-pricing-box .pricing-header { - margin-bottom: 20px; - padding-bottom: 20px; - } - .single-pricing-box .pricing-header h3 { - font-size: 35px; - } - .pricing-box .pricing-header h3 { - font-size: 18px; - } - .pricing-box .pricing-header .price { - font-size: 35px; - } - .pricing-tabs .nav .nav-item .nav-link { - font-size: 14px; - } - .pricing-item .pricing-header h3 { - font-size: 18px; - } - .pricing-item .pricing-header .price { - font-size: 35px; - } - .single-team-member .content::before { - opacity: 1; - visibility: visible; - } - .single-team-member .content h3 { - font-size: 18px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-team-member .content span { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .single-team-member .content .social { - bottom: auto; - opacity: 1; - visibility: visible; - top: -30px; - } - .single-team-member .content .social li a { - font-size: 15px; - } - .single-team-box .image .social { - margin-bottom: 0; - opacity: 1; - visibility: visible; - padding: 20px; - } - .single-team-box .content { - margin-top: 20px; - } - .single-team-box .content h3 { - font-size: 18px; - } - .team-member .content { - margin-top: 0; - } - .team-member .content::before { - opacity: 1; - visibility: visible; - } - .team-member .content h3 { - font-size: 18px; - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .team-member .content span { - -webkit-transform: translateY(0) !important; - transform: translateY(0) !important; - } - .team-member .content .social { - bottom: auto; - opacity: 1; - visibility: visible; - top: -30px; - } - .team-member .content .social li a { - font-size: 15px; - } - .team-box .content { - visibility: visible; - position: relative; - opacity: 1; - bottom: 0; - } - .team-box .content h3 { - font-size: 18px; - } - .video-box .video-btn { - width: 70px; - height: 70px; - font-size: 25px; - } - .cta-content h2 { - font-size: 30px; - } - .cta-content p { - margin-top: 15px; - } - .cta-content .default-btn { - margin-top: 10px; - } - .call-to-action-content { - text-align: center; - } - .call-to-action-content h2 { - font-size: 30px; - } - .call-to-action-btn { - text-align: center; - margin-top: 30px; - } - .call-to-action-inner { - padding-left: 25px; - padding-right: 25px; - } - .blog-section-title { - text-align: center; - margin-bottom: 50px; - } - .blog-section-title h2 { - font-size: 30px; - } - .single-blog-post .content { - padding: 20px; - } - .single-blog-post .content h3 { - font-size: 18px; - } - .single-post-item .post-content .meta { - margin-bottom: 3px; - } - .single-post-item .post-content .meta li { - padding-left: 20px; - margin-right: 10px; - margin-bottom: 10px; - } - .single-post-item .post-content h3 { - margin-bottom: 12px; - font-size: 18px; - } - .single-post-item .post-content .link-btn i { - top: 6px; - } - .app-download-area .col-lg-6:nth-child(1) { - -webkit-box-ordinal-group: 3; - -ms-flex-order: 2; - order: 2; - } - .app-download-area .col-lg-6:nth-child(2) { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .app-download-content { - text-align: center; - margin-bottom: 35px; - } - .app-download-content h2 { - font-size: 30px; - } - .app-download-content p { - max-width: 540px; - margin-left: auto; - margin-right: auto; - } - .app-download-content .btn-box .store-btn { - text-align: start; - } - .app-download-content .btn-box .store-btn span { - font-size: 16px; - } - .app-download-image { - padding-right: 0; - } - .works-area .container-fluid { - max-width: 720px; - } - .single-works-item .content h3 { - font-size: 18px; - } - .portfolio-details-desc h3 { - font-size: 20px; - } - .portfolio-details-info { - padding: 30px; - margin-left: 0; - margin-top: 30px; - } - .portfolio-details-info ul li { - padding-left: 38px; - } - .portfolio-details-info ul li span { - font-size: 16px; - } - .portfolio-details-slides.owl-theme .owl-nav { - opacity: 1; - visibility: visible; - } - .page-title-area { - padding-top: 100px; - padding-bottom: 110px; - } - .page-title-area.style-four { - padding-top: 100px; - padding-bottom: 100px; - } - .page-title-area.style-two { - padding-top: 210px; - padding-bottom: 100px; - } - .page-title-area.style-three { - padding-top: 240px; - padding-bottom: 100px; - } - .page-title-content h2 { - margin-bottom: 0; - font-size: 35px; - } - .page-title-content ul { - margin-top: 15px; - } - .page-title-content ul li { - font-size: 15px; - } - .pagination-area { - margin-top: 15px; - } - .pagination-area .page-numbers { - width: 40px; - height: 40px; - line-height: 38px; - font-size: 16px; - } - .services-details-desc h3 { - font-size: 20px; - } - .faq-accordion .accordion-item .accordion-button { - font-size: 16px; - } - .products-details-desc { - padding-left: 0; - margin-top: 30px; - } - .products-details-desc h3 { - font-size: 18px; - } - .products-details-desc .products-review .rating { - font-size: 13px; - } - .products-details-desc .price { - font-size: 15px; - } - .products-details-tabs { - margin-top: 40px; - } - .products-details-tabs .single-tabs-box h2 { - font-size: 20px; - } - .products-details-tabs .single-tabs-box .inner-box h3 { - font-size: 18px; - } - .products-details-tabs .single-tabs-box .inner-box .table-striped tbody tr td { - font-size: 15px; - } - .cart-table-desc table tbody tr td { - white-space: nowrap; - } - .cart-totals-desc { - margin-top: 30px; - } - .billing-details-desc h3 { - font-size: 18px; - } - .order-details-desc { - margin-top: 30px; - } - .order-details-desc h3 { - font-size: 18px; - } - .profile-authentication-area { - height: auto; - padding-bottom: 100px; - padding-top: 160px; - } - .signin-form { - padding: 35px; - text-align: center; - } - .signin-form form .remember-me-wrap { - text-align: start; - } - .signup-form { - padding: 35px; - text-align: center; - } - .signup-form form .remember-me-wrap { - text-align: start; - } - .privacy-policy-content h3 { - font-size: 18px; - } - .terms-conditions-content h3 { - font-size: 18px; - } - .error-content h3 { - font-size: 22px; - margin-bottom: 15px; - margin-top: 35px; - } - .error-content .default-btn { - margin-top: 20px; - } - .coming-soon-image { - height: auto; - background-image: unset !important; - } - .coming-soon-image img { - display: inline-block; - } - .coming-soon-content { - height: auto; - padding-top: 100px; - padding-bottom: 100px; - } - .coming-soon-content h2 { - font-size: 30px; - } - .works-slides-style-one.owl-theme .owl-nav, .works-slides-style-two.owl-theme .owl-nav { - opacity: 1; - visibility: visible; - } - .blog-details-desc .article-content h3 { - font-size: 20px; - } - .comments-area .comments-title { - font-size: 20px; - } - .comments-area .comment-respond .comment-reply-title { - font-size: 20px; - } - .single-contact-info-box { - padding: 30px; - text-align: center; - } - .single-contact-info-box .icon { - font-size: 35px; - margin-bottom: 20px; - } - .single-contact-info-box h3 { - font-size: 18px; - margin-bottom: 12px; - } - .single-contact-info-box.style-two .icon { - width: 70px; - height: 70px; - font-size: 30px; - margin-bottom: 20px; - } - .footer-area { - padding-top: 80px; - } - .footer-area.business-footer .footer-top { - margin-bottom: 80px; - padding-top: 60px; - padding-bottom: 60px; - } - .footer-top { - text-align: center; - margin-bottom: 50px; - padding-bottom: 50px; - } - .footer-top .logo { - margin-bottom: 30px; - } - .footer-top .content { - text-align: start; - } - .footer-top .content h3 { - font-size: 18px; - } - .single-footer-widget h3 { - font-size: 18px; - } - .single-footer-widget.pl-4 { - padding-left: 0; - } - .single-footer-widget.pl-2 { - padding-left: 0; - } - .single-footer-widget.style-two h3 { - padding-left: 0; - } - .single-footer-widget.style-two h3::before { - display: none; - } - .copyright-area { - margin-top: 50px; - } - .copyright-area .text-start { - text-align: center !important; - margin-bottom: 15px; - } - .copyright-area .social-links { - text-align: center; - margin-top: 15px; - } - .productsQuickView .modal-dialog { - max-width: 700px; - } - .productsQuickView .modal-content { - padding: 20px; - } - .productsQuickView .modal-content button.btn-close { - font-size: 15px; - } - .productsQuickView .modal-content .products-content h3 { - font-size: 18px; - } - .productsQuickView .modal-content .products-content .price { - font-size: 15px; - } -} - -/* Min width 992px to Max width 1199px */ -@media only screen and (min-width: 992px) and (max-width: 1199px) { - body { - font-size: 15px; - } - p { - font-size: 15px; - } - .ptb-100 { - padding-top: 90px; - padding-bottom: 90px; - } - .pt-100 { - padding-top: 90px; - } - .pb-100 { - padding-bottom: 90px; - } - .ptb-70 { - padding-top: 60px; - padding-bottom: 60px; - } - .pt-70 { - padding-top: 60px; - } - .pb-70 { - padding-bottom: 60px; - } - .top-header-area .container-fluid { - max-width: 960px; - } - .top-header-area.top-header-style-two .container-fluid { - max-width: 960px; - } - .top-header-area.top-header-style-three .container-fluid { - max-width: 960px; - } - .noke-responsive-nav .others-option { - display: -webkit-box !important; - display: -ms-flexbox !important; - display: flex !important; - } - .others-option-for-responsive { - display: none; - } - .startup-banner-area .container-fluid { - max-width: 960px; - } - .startup-banner-content { - padding-right: 0; - } - .startup-banner-content h1 { - font-size: 50px; - } - .startup-banner-inner { - padding-top: 100px; - padding-bottom: 100px; - padding-left: 30px; - padding-right: 30px; - } - .startup-banner-image { - padding-left: 0; - } - .marketing-banner-content { - padding-right: 0; - } - .marketing-banner-content h1 { - font-size: 50px; - margin-bottom: 20px; - } - .marketing-banner-content p { - max-width: 100%; - } - .business-banner-area { - padding-top: 90px; - padding-bottom: 90px; - } - .business-banner-area .container { - max-width: 960px; - } - .business-banner-area::before { - opacity: 0.3; - } - .business-banner-content { - padding-right: 0; - max-width: 100%; - } - .business-banner-content h1 { - font-size: 48px; - margin-bottom: 20px; - } - .business-banner-content p { - max-width: 100%; - } - .app-banner-area .container { - max-width: 990px; - } - .app-banner-content h1 { - font-size: 50px; - margin-bottom: 20px; - } - .app-banner-content p { - max-width: 100%; - } - .app-banner-content .btn-box { - margin-top: 20px; - } - .app-banner-content .btn-box .store-btn span { - font-size: 16px; - } - .app-banner-image { - padding-left: 0; - } - .saas-banner-area .container-fluid { - max-width: 990px; - padding-left: 15px; - padding-right: 15px; - } - .saas-banner-content { - margin-left: 0; - max-width: 100%; - } - .saas-banner-content h1 { - font-size: 50px; - margin-bottom: 20px; - } - .saas-banner-content p { - max-width: 100%; - } - .personal-portfolio-banner-area .container { - max-width: 960px; - } - .personal-portfolio-banner-content h1 { - font-size: 52px; - } - .personal-portfolio-banner-content h4 { - font-size: 25px; - } - .consulting-banner-area { - padding-top: 270px; - padding-bottom: 150px; - } - .consulting-banner-area .container { - max-width: 960px; - } - .consulting-banner-content { - max-width: 635px; - } - .consulting-banner-content h1 { - font-size: 40px; - margin-bottom: 20px; - } - .consulting-banner-content .default-btn { - margin-top: 5px; - } - .it-agency-banner-area .it-agency-inner-banner { - padding-top: 250px; - padding-bottom: 300px; - } - .it-agency-banner-content { - max-width: 100%; - } - .it-agency-banner-content h1 { - font-size: 50px; - } - .banner-video-box { - max-width: 100%; - } - .shop-banner-area { - padding-top: 150px; - padding-bottom: 150px; - } - .shop-banner-content .sub-title { - font-size: 25px; - } - .shop-banner-content h1 { - font-size: 50px; - } - .shop-banner-content h4 { - font-size: 25px; - } - .blog-banner-item .content { - padding: 20px; - max-width: 480px; - } - .blog-banner-item .content h3 { - font-size: 22px; - } - .blog-banner-item-list .single-blog-banner-item .content { - padding-top: 0; - padding-bottom: 0; - padding-left: 15px; - } - .blog-banner-item-list .single-blog-banner-item .content h3 { - margin-bottom: 12px; - font-size: 15px; - } - .furniture-banner-item { - padding-top: 150px; - padding-bottom: 150px; - } - .furniture-banner-content h1 { - margin-bottom: 15px; - font-size: 40px; - } - .minimal-portfolio-banner-area { - padding-top: 230px; - padding-bottom: 200px; - } - .minimal-portfolio-banner-content { - max-width: 730px; - } - .minimal-portfolio-banner-content .typewrite { - font-size: 18px; - } - .minimal-portfolio-banner-content h1 { - font-size: 50px; - margin-bottom: 30px; - } - .single-features-box { - padding-left: 0; - padding-right: 0; - } - .single-features-item { - padding-left: 65px; - } - .features-item { - padding: 25px; - } - .startup-about-area .container-fluid { - max-width: 960px; - } - .startup-about-image { - margin-right: 0; - padding-top: 60px; - padding-left: 29px; - padding-bottom: 60px; - } - .startup-about-image .video-btn { - left: 55%; - -webkit-transform: translateY(-50%) translateX(-55%); - transform: translateY(-50%) translateX(-55%); - } - .startup-about-content { - padding-left: 0; - max-width: 100%; - } - .startup-about-content h2 { - font-size: 38px; - } - .marketing-about-image { - margin-right: 0; - } - .marketing-about-content { - padding-left: 0; - } - .business-about-image { - padding-right: 0; - } - .business-about-content { - padding-left: 0; - } - .saas-about-image { - padding-right: 0; - } - .saas-about-content { - padding-left: 0; - } - .it-agency-about-image { - margin-right: 0; - padding: 0; - } - .it-agency-about-image .main-image { - padding-bottom: 100px; - } - .it-agency-about-image .main-image img:nth-child(2) { - max-width: 300px; - } - .it-agency-about-content { - padding-left: 0; - } - .startup-works-area .section-title { - max-width: 650px; - text-align: center; - padding-top: 0; - margin-left: auto; - margin-right: auto; - margin-bottom: 60px; - } - .startup-works-area .section-title h2 { - font-size: 40px; - } - .startup-works-content { - padding-top: 0; - padding-right: 0; - } - .startup-works-image { - margin-left: 0; - padding: 0; - } - .startup-works-slides .row { - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - } - .single-services-item .content { - padding-left: 0; - padding-right: 0; - } - .services-tabs .nav { - margin-bottom: 50px; - } - .services-tabs .nav .nav-item { - margin-left: 16px; - margin-right: 16px; - } - .services-tabs .nav .nav-item .nav-link { - font-size: 17px; - } - .single-main-services .content { - padding-right: 0; - } - .single-main-services .content h3 { - font-size: 30px; - } - .single-main-services .content p { - max-width: 100%; - } - .col-lg-3:nth-child(2) .services-box, .col-lg-3:nth-child(4) .services-box { - margin-top: 0; - } - .col-lg-3:nth-child(5) .services-box, .col-lg-3:nth-child(7) .services-box { - margin-top: 0; - } - .services-item h3 { - font-size: 20px; - } - .services-item .hover-content { - padding: 20px; - } - .facility-content { - padding-right: 0; - } - .single-funfacts-box { - padding-top: 30px; - padding-bottom: 30px; - padding-left: 75px; - padding-right: 20px; - } - .single-funfacts-box .icon { - left: 20px; - font-size: 40px; - } - .single-funfacts-box h3 { - font-size: 35px; - } - .single-funfacts-box .sub-title { - font-size: 14.5px; - } - .single-funfacts-box.without-bg-color { - margin-right: 0; - padding-left: 60px; - } - .app-screenshot-area::before { - height: 100px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-] { - left: -25px; - width: 35px; - line-height: 35px; - height: 35px; - font-size: 15px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: -25px; - } - .screenshot-wrap .screenshot-frame { - width: 175px; - height: 355px; - } - .categories-box h3 { - padding-left: 20px; - padding-right: 20px; - } - .categories-slides.owl-theme .owl-nav [class*=owl-] { - left: -40px; - } - .categories-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: -40px; - } - .startup-testimonials-area .container-fluid { - max-width: 960px; - } - .startup-testimonials-image { - background-color: transparent; - padding-top: 0; - padding-bottom: 23px; - padding-left: 0; - } - .startup-testimonials-image .main-image { - -webkit-animation: unset !important; - animation: unset !important; - margin-bottom: 15px; - } - .startup-testimonials-content { - padding-left: 0; - padding-right: 0; - padding-bottom: 30px; - padding-top: 30px; - } - .startup-testimonials-content p { - font-size: 15px; - } - .startup-testimonials-content .info { - margin-top: 20px; - padding-top: 20px; - } - .startup-testimonials-content .info h3 { - font-size: 17px; - } - .startup-testimonials-slides.owl-theme .owl-nav { - bottom: 0; - right: 80%; - -webkit-transform: translateX(80%); - transform: translateX(80%); - } - .marketing-testimonials-image { - margin-right: 30px; - margin-left: 0; - } - .marketing-testimonials-content { - padding-bottom: 50px; - } - .marketing-testimonials-content p { - font-size: 16px; - } - .marketing-testimonials-content .info { - margin-top: 20px; - } - .marketing-testimonials-content .info h3 { - font-size: 17px; - } - .marketing-testimonials-slides.owl-theme .owl-nav { - bottom: 40px; - right: 20%; - -webkit-transform: translateX(20%); - transform: translateX(20%); - } - .business-testimonials-image { - margin-right: 0; - } - .testimonials-slides.owl-theme { - padding-left: 100px; - padding-right: 100px; - } - .business-testimonials-content p { - font-size: 18px; - } - .saas-testimonials-slides.owl-theme { - margin-top: -65px; - } - .testimonials-box { - padding: 20px; - } - .shape1, .shape2, .shape3, .shape4, .shape5, .shape6, .shape7, .shape8, .shape9, .shape16 { - display: none !important; - } - .page-title-area { - padding-top: 100px; - padding-bottom: 110px; - } - .page-title-area.style-three { - padding-top: 250px; - padding-bottom: 110px; - } - .page-title-area.style-two { - padding-bottom: 100px; - padding-top: 220px; - } - .page-title-area.style-four { - padding-top: 120px; - padding-bottom: 120px; - } - .who-we-are-content { - padding: 25px; - } - .who-we-are-content h2 { - font-size: 20px; - } - .who-we-are-content .default-btn { - margin-top: 5px; - } - .app-download-area .row.align-items-center { - -webkit-box-align: unset !important; - -ms-flex-align: unset !important; - align-items: unset !important; - } - .call-to-action-inner { - padding-left: 30px; - padding-right: 30px; - } - .services-details-content h2 { - font-size: 38px; - } - .services-details-video { - margin-left: 0; - } - .services-details-video iframe { - height: 251px; - } - .single-pricing-box { - padding: 30px; - } - .single-pricing-box .pricing-header h3 { - font-size: 40px; - } - .featured-video-item-list .single-featured-video-item .content { - padding-top: 5px; - padding-bottom: 5px; - padding-left: 15px; - } - .featured-video-item-list .single-featured-video-item .content h3 { - font-size: 17px; - } - .single-clients-box a { - padding: 20px; - } - .coming-soon-content { - padding-left: 15px; - padding-right: 15px; - } - .coming-soon-content #timer div { - width: 100px; - height: 100px; - font-size: 30px; - } - .coming-soon-content form { - max-width: 100%; - } - .trending-style-content { - padding-left: 0; - } - .trending-style-image { - padding-right: 80px; - margin-right: 0; - } - .trending-style-image img:nth-child(2) { - width: 250px; - } - .widget-area .widget_about_author .about-author { - padding: 20px; - } - .widget-area .widget_noke_posts_thumb .item { - padding-left: 95px; - } - .widget-area .widget_noke_posts_thumb .item .thumb { - width: 80px; - } - .widget-area .widget_noke_posts_thumb .item .thumb .fullimage { - width: 80px; - } - .widget-area .widget_noke_posts_thumb .item .info .title { - font-size: 14px; - } - .blog-section-title h2 { - font-size: 35px; - } - .single-blog-post .content .meta li i { - top: 2px; - } - .single-post-item .post-content { - padding: 20px; - } - .single-post-item .post-content .meta { - margin-bottom: 3px; - } - .single-post-item .post-content .meta li { - margin-right: 10px; - margin-bottom: 10px; - } - .portfolio-details-info { - padding: 20px; - } - .products-details-desc { - padding-left: 0; - } - .single-contact-info-box { - padding: 25px; - } - .footer-area { - padding-top: 90px; - } - .footer-top { - padding-bottom: 60px; - margin-bottom: 60px; - } - .single-footer-widget.pl-4 { - padding-left: 0; - } - .single-footer-widget.pl-2 { - padding-left: 0; - } - .single-footer-widget.style-two h3 { - padding-left: 40px; - } - .single-footer-widget.style-two h3::before { - width: 25px; - } - .copyright-area { - margin-top: 60px; - } - .productsQuickView .modal-dialog { - max-width: 830px; - } - .productsQuickView .modal-content { - padding: 25px; - } -} - -/* Min width 1200px to Max width 1355px */ -@media only screen and (min-width: 1200px) and (max-width: 1355px) { - .top-header-area .container-fluid { - max-width: 1140px; - } - .top-header-area.top-header-style-two .container-fluid { - max-width: 1140px; - } - .top-header-area.top-header-style-three .container-fluid { - max-width: 1140px; - } - .navbar-area.navbar-style-three .noke-nav .container-fluid { - max-width: 1140px; - } - .navbar-area.navbar-style-two .noke-nav .container-fluid { - max-width: 1140px; - } - .navbar-area.navbar-style-four .noke-nav .container-fluid { - max-width: 1140px; - } - .navbar-area.navbar-style-five .noke-nav .container-fluid { - max-width: 1140px; - } - .noke-nav .container-fluid { - max-width: 1140px; - } - .startup-banner-area .container-fluid { - max-width: 100%; - } - .startup-banner-inner { - padding: 100px 40px; - } - .startup-banner-content h1 { - font-size: 55px; - } - .marketing-banner-content h1 { - font-size: 55px; - } - .marketing-banner-content p { - max-width: 510px; - } - .business-banner-area .container { - max-width: 1140px; - } - .business-banner-content { - padding-right: 0; - max-width: 100%; - } - .business-banner-content h1 { - font-size: 55px; - } - .app-banner-area .container { - max-width: 1140px; - } - .app-banner-content h1 { - font-size: 49px; - } - .personal-portfolio-banner-area .container { - max-width: 1140px; - } - .personal-portfolio-banner-content h1 { - font-size: 60px; - } - .personal-portfolio-banner-content h4 { - font-size: 30px; - } - .consulting-banner-area .container { - max-width: 1140px; - } - .consulting-banner-content { - max-width: 715px; - } - .consulting-banner-content h1 { - font-size: 45px; - } - .it-agency-banner-content h1 { - font-size: 55px; - } - .blog-banner-item-list .single-blog-banner-item .content { - padding-bottom: 3.3px; - padding-top: 3.3px; - } - .blog-banner-item-list .single-blog-banner-item .content h3 { - font-size: 16px; - } - .startup-about-area .container-fluid { - max-width: 100%; - } - .it-agency-about-image .main-image img:nth-child(2) { - max-width: 300px; - } - .startup-testimonials-area .container-fluid { - max-width: 100%; - } - .marketing-testimonials-slides.owl-theme .owl-nav { - right: 20%; - bottom: 55px; - -webkit-transform: translateX(20%); - transform: translateX(20%); - } - .services-tabs .nav .nav-item { - margin-left: 25px; - margin-right: 25px; - } - .single-main-services .content h3 { - font-size: 35px; - } - .who-we-are-content h2 { - font-size: 20px; - } - .app-screenshot-area::before { - height: 150px; - } - .screenshot-wrap .screenshot-frame { - width: 210px; - height: 430px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-] { - left: -34px; - } - .screenshot-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: -34px; - } - .services-box { - padding: 30px 20px; - } - .startup-works-area .section-title { - padding-top: 20px; - margin-bottom: -180px; - } - .startup-works-content { - padding-top: 210px; - } - .featured-video-item-list .single-featured-video-item .content { - padding-top: 6px; - padding-bottom: 6px; - } - .featured-video-item-list .single-featured-video-item .content h3 { - font-size: 17px; - } - .call-to-action-content h2 { - font-size: 35px; - } - .single-post-item .post-content .meta { - margin-bottom: 3px; - } - .single-post-item .post-content .meta li { - margin-right: 10px; - margin-bottom: 10px; - } - .facility-content { - padding-right: 0; - } - .single-funfacts-box.without-bg-color { - margin-right: 15px; - } -} - -/* Min width 1535px */ -@media only screen and (min-width: 1550px) { - .top-header-area.top-header-style-two .container-fluid { - max-width: 1470px; - } - .navbar-area.navbar-style-three .noke-nav .container-fluid { - max-width: 1450px; - } - .navbar-area.navbar-style-four .noke-nav .container-fluid { - max-width: 1470px; - } - .noke-nav .container-fluid { - max-width: 1470px; - } - .startup-banner-area .container-fluid { - max-width: 1470px; - } - .startup-banner-inner { - padding-top: 140px; - padding-bottom: 140px; - padding-left: 120px; - padding-right: 120px; - } - .shape3 { - right: 34.3%; - bottom: 7%; - } - .startup-about-area .container-fluid { - max-width: 1470px; - } - .startup-about-image { - padding-left: 100px; - padding-bottom: 100px; - } - .startup-works-image { - padding-top: 100px; - padding-right: 100px; - padding-bottom: 100px; - } - .shape6 { - top: 67px; - } - .startup-testimonials-area .container-fluid { - max-width: 1470px; - } - .startup-testimonials-image { - padding-top: 100px; - padding-left: 100px; - padding-bottom: 100px; - } - .shape7 { - left: 40px; - top: 30px; - } - .shape8 { - bottom: 70px; - left: 70px; - } - .shape9 { - right: 0; - top: 85px; - } - .shape16 { - right: 150px; - } -} -/*# sourceMappingURL=responsive.css.map */ \ No newline at end of file diff --git a/static/assets/css/responsive.css.map b/static/assets/css/responsive.css.map deleted file mode 100644 index 8b137891..00000000 --- a/static/assets/css/responsive.css.map +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/assets/css/responsive.scss b/static/assets/css/responsive.scss deleted file mode 100644 index 269a7e15..00000000 --- a/static/assets/css/responsive.scss +++ /dev/null @@ -1,6664 +0,0 @@ -$media: screen; -$feature_min: min-width; -$feature_max: max-width; -$value_one: 576px; -$value_two: 767px; -$value_three: 768px; -$value_four: 991px; -$value_five: 992px; -$value_six: 1199px; -$value_seven: 1200px; -$value_eight: 1355px; -$value_nine: 1550px; - -/* Max width 767px */ -@media only #{$media} and ($feature_max : $value_two) { - - body { - font-size: 13.5px; - } - p { - font-size: 13.5px; - } - .ptb-100 { - padding: { - top: 60px; - bottom: 60px; - }; - } - .pt-100 { - padding-top: 60px; - } - .pb-100 { - padding-bottom: 60px; - } - .ptb-70 { - padding: { - top: 30px; - bottom: 30px; - }; - } - .pt-70 { - padding-top: 30px; - } - .pb-70 { - padding-bottom: 30px; - } - .default-btn { - padding: 11px 40px 11px 25px; - font-size: 13.5px; - - i { - right: 25px; - font-size: 12px; - } - &:hover { - i { - right: 20px; - } - } - } - .section-title { - max-width: 100%; - margin: { - left: 0; - right: 0; - bottom: 40px; - top: -2px; - }; - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - p { - max-width: 100%; - margin-top: 12px; - } - &.style-two { - padding-left: 0; - text-align: center; - margin-bottom: 40px; - - h2 { - font-size: 22px; - - &::before { - display: none; - } - } - p { - max-width: 100%; - } - } - &.style-three { - h2 { - span { - padding: { - left: 10px; - right: 10px; - }; - } - } - } - &.style-four { - text-align: center; - margin-bottom: 40px; - - h2 { - font-size: 22px; - } - &::before { - left: 50%; - transform: translateX(-50%); - } - } - } - .form-control { - font-size: 13.5px; - } - - .top-header-area { - text-align: center; - - .container-fluid { - max-width: 100%; - } - &.top-header-style-two { - .container-fluid { - max-width: 100%; - } - } - &.top-header-style-three { - .container-fluid { - max-width: 100%; - } - } - } - .top-header-contact-info { - text-align: center !important; - - .email { - padding-left: 20px; - margin: { - right: 10px; - top: 10px; - }; - i { - top: 4.5px; - } - } - .number { - padding-left: 20px; - margin-top: 10px; - - i { - top: 4px; - } - } - .lang-switcher { - padding-left: 18px; - margin: { - left: 10px; - top: 10px; - }; - } - } - - .search-overlay { - .search-overlay-form { - max-width: 290px; - width: 290px; - - form { - .input-search { - height: 50px; - font-size: 15px; - } - button { - font-size: 18px; - } - } - } - } - - .header-area { - &.p-responsive-relative { - position: relative; - } - } - - .startup-banner-area { - .container-fluid { - max-width: 100%; - padding: { - left: 0; - right: 0; - }; - } - } - .startup-banner-inner { - padding: { - top: 60px; - left: 15px; - right: 15px; - bottom: 60px; - }; - } - .startup-banner-content { - padding-right: 0; - text-align: center; - - .sub-title { - font-size: 13px; - } - h1 { - margin-bottom: 18px; - font-size: 25px; - } - p { - max-width: 100%; - } - .default-btn { - margin-top: 5px; - } - } - .startup-banner-image { - padding-left: 0; - margin-top: 30px; - } - .marketing-banner-area { - padding: { - top: 60px; - bottom: 60px; - }; - } - .marketing-banner-content { - text-align: center; - padding-right: 0; - - .sub-title { - font-size: 13px; - } - h1 { - margin-bottom: 18px; - font-size: 25px; - } - p { - max-width: 100%; - } - .default-btn { - margin-top: 5px; - } - } - .marketing-banner-image { - margin-top: 30px; - } - .business-banner-area { - padding: { - top: 60px; - bottom: 60px; - }; - .container { - max-width: 100%; - } - &::before { - display: none; - } - } - .business-banner-content { - padding-right: 0; - text-align: center; - max-width: 100%; - - .sub-title { - font-size: 13px; - } - h1 { - margin-bottom: 12px; - font-size: 25px; - } - p { - max-width: 100%; - } - .default-btn { - margin-top: 5px; - } - } - .business-banner-image { - margin-top: 30px; - - img { - border-radius: 0; - } - } - .app-banner-area { - padding: { - top: 60px; - bottom: 60px; - }; - .container { - max-width: 100%; - } - } - .app-banner-content { - text-align: center; - - h1 { - margin-bottom: 20px; - font-size: 25px; - } - .btn-box { - margin-top: 25px; - - .store-btn { - padding: 10px 10px 10px 45px; - font-size: 12px; - text-align: start; - margin: { - left: 3px; - right: 3px; - }; - &:first-child { - margin-left: 0; - } - img { - left: 10px; - width: 25px; - } - span { - font-size: 14px; - } - } - } - } - .app-banner-image { - padding-left: 0; - margin-top: 35px; - } - .saas-banner-area { - padding-top: 60px; - - .container-fluid { - padding: { - left: 15px; - right: 15px; - }; - } - } - .saas-banner-content { - text-align: center; - max-width: 100%; - margin-left: 0; - - .sub-title { - font-size: 13px; - } - h1 { - margin-bottom: 18px; - font-size: 25px; - } - p { - max-width: 100%; - } - .default-btn { - margin-top: 5px; - } - } - .saas-banner-image { - margin-top: 35px; - } - .personal-portfolio-banner-area { - padding: { - top: 60px; - bottom: 60px; - }; - .container { - max-width: 100%; - } - } - .personal-portfolio-banner-content { - text-align: center; - - .sub-title { - font-size: 13px; - padding-left: 0; - - &::before { - display: none; - } - } - h1 { - margin-bottom: 15px; - font-size: 25px; - } - h4 { - margin-bottom: 15px; - font-size: 16px; - } - p { - max-width: 100%; - } - } - .personal-portfolio-banner-image { - margin-top: 30px; - } - .consulting-banner-area { - padding: { - top: 235px; - bottom: 60px; - }; - .container { - max-width: 100%; - } - &::before { - display: block; - } - } - .consulting-banner-content { - text-align: center; - max-width: 100%; - - h1 { - margin-bottom: 15px; - font-size: 25px; - } - p { - max-width: 100%; - } - .default-btn { - margin-top: 5px; - } - } - .it-agency-banner-area { - .it-agency-inner-banner { - padding: { - top: 230px; - bottom: 60px; - }; - } - } - .it-agency-banner-content { - max-width: 100%; - - h1 { - margin-bottom: 15px; - font-size: 25px; - } - p { - max-width: 100%; - } - .default-btn { - margin-top: 5px; - } - } - .banner-video-box { - max-width: 100%; - margin-top: 60px; - - .video-btn { - width: 60px; - height: 60px; - font-size: 25px; - } - } - .shop-banner-area { - background-position: left center; - padding: { - top: 60px; - bottom: 60px; - }; - } - .shop-banner-content { - .sub-title { - font-size: 15px; - margin-bottom: 8px; - } - h1 { - margin-bottom: 12px; - font-size: 25px; - } - h4 { - font-size: 16px; - } - .default-btn { - margin-top: 20px; - } - } - .blog-banner-area { - padding: { - top: 60px; - bottom: 60px; - }; - } - .blog-banner-item { - margin-bottom: 30px; - - .content { - max-width: 100%; - padding: 20px 15px; - top: 0; - background-color: rgba(255, 255, 255, 1); - position: relative; - transform: translateY(0); - - h3 { - font-size: 16px; - } - .meta { - li { - a { - i { - top: 6px; - } - } - } - } - } - } - .blog-banner-item-list { - .single-blog-banner-item { - .image { - height: auto; - background-image: unset !important; - - img { - display: inline-block; - } - } - .content { - padding: { - left: 0; - bottom: 0; - top: 20px; - }; - h3 { - margin-bottom: 12px; - font-size: 16px; - } - .date { - i { - top: 2px; - } - } - } - } - } - .furniture-banner-item { - padding: { - top: 60px; - bottom: 95px; - }; - } - .furniture-banner-content { - padding: 20px 15px; - max-width: 100%; - text-align: center; - border-width: 2px; - - h1 { - margin-bottom: 12px; - font-size: 25px; - } - .default-btn { - margin-top: 5px; - } - } - .furniture-banner-slides { - &.owl-theme { - .owl-dots { - top: auto; - transform: translateY(0); - bottom: 30px; - left: 0; - right: 0; - - .owl-dot { - display: inline-block; - margin: 0 5px; - - span { - width: 35px; - } - } - } - } - } - .minimal-portfolio-banner-area { - padding: { - top: 150px; - bottom: 140px; - }; - } - .minimal-portfolio-banner-content { - max-width: 100%; - - .typewrite { - margin-bottom: 25px; - padding: 6px 15px; - font-size: 13px; - } - h1 { - font-size: 25px; - margin-bottom: 25px; - } - } - .shape1, .shape13, .shape2, .shape3, .shape4, .shape5, .shape6, .shape7, .shape8, .shape9, .shape16, .shape17, .divider, .shape18, .shape19 { - display: none !important; - } - .shape10 { - max-width: 290px; - } - - .features-area { - padding: { - top: 60px; - bottom: 30px; - }; - } - .single-features-box { - padding: { - left: 0; - right: 0; - }; - .icon { - font-size: 40px; - margin-bottom: 20px; - transform: translateY(0) !important; - } - h3 { - font-size: 16px; - transform: translateY(0) !important; - } - p { - transform: translateY(0) !important; - } - .link-btn { - left: 0; - bottom: 0 !important; - opacity: 1; - transform: unset; - margin-top: 15px; - position: relative; - visibility: visible; - - i { - top: 5px; - font-size: 11px; - } - } - } - .features-box { - padding: 20px 15px; - - h3 { - font-size: 16px; - } - .icon { - width: 60px; - height: 60px; - font-size: 30px; - line-height: 60px; - margin-bottom: 20px; - } - } - .single-features-item { - text-align: center; - padding-left: 0; - - .icon { - position: relative; - font-size: 35px; - display: inline-block; - margin-bottom: 15px; - } - h3 { - font-size: 16px; - - br { - display: none; - } - } - } - .shape15 { - display: none; - } - .features-item { - padding: 20px 15px; - - h3 { - font-size: 16px; - } - .icon { - font-size: 25px; - margin-right: 10px; - } - } - - .startup-about-area { - border-bottom: 1px solid #eeeeee; - margin-bottom: 60px; - - .container-fluid { - max-width: 100%; - } - } - .startup-about-image { - margin-right: 0; - padding: 0; - - .video-btn { - left: 50%; - width: 60px; - height: 60px; - font-size: 22px; - transform: translateY(-50%) translateX(-50%); - - &::after, &::before { - border-width: 1px; - } - } - } - .startup-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - max-width: 100%; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - .features-list { - padding-left: 0; - list-style-type: none; - - li { - font-weight: 600; - } - } - } - .marketing-about-image { - margin-right: 0; - } - .marketing-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - .features-list { - padding-left: 0; - list-style-type: none; - - li { - font-weight: 600; - } - } - } - .business-about-image { - padding-right: 0; - } - .business-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - } - .shape11, .shape12 { - display: none; - } - .app-about-content { - padding-right: 0; - - h3 { - margin-bottom: 15px; - font-size: 16px; - } - .default-btn { - margin-top: 5px; - } - } - .app-about-image { - padding-left: 0; - margin-top: 30px; - } - .saas-about-image { - padding-right: 0; - margin-bottom: 30px; - } - .saas-about-content { - padding-left: 0; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - } - .it-agency-about-image { - margin-right: 0; - text-align: end; - padding: { - right: 0; - bottom: 0; - }; - .main-image { - padding: { - bottom: 100px; - right: 0; - left: 80px; - }; - img { - &:nth-child(2) { - right: 0; - left: 0; - position: relative; - top: 0; - } - &:nth-child(1) { - left: 0; - bottom: 0; - z-index: 2; - max-width: 200px; - position: absolute; - } - } - } - } - .it-agency-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - .features-list { - padding-left: 0; - list-style-type: none; - - li { - font-weight: 600; - } - } - } - - .single-services-box { - .content { - padding: 15px; - - .icon { - font-size: 35px; - margin-bottom: 15px; - transform: translateY(0) !important; - } - h3 { - font-size: 16px; - transform: translateY(0) !important; - } - p { - transform: translateY(0) !important; - } - .link-btn { - left: 0; - bottom: 0 !important; - opacity: 1; - transform: unset; - margin-top: 13px; - position: relative; - visibility: visible; - - i { - top: 5px; - font-size: 11px; - } - } - } - &::before { - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.7) 50%, rgba(22, 22, 23, 0) 75%, rgba(0, 0, 0, 0) 100%); - } - } - .single-services-item { - .content { - padding: { - top: 20px; - left: 0; - right: 0; - }; - h3 { - font-size: 16px; - } - } - } - .services-tabs { - .nav { - margin-bottom: 20px; - border-bottom: 1px solid #eeeeee; - padding-bottom: 10px; - - .nav-item { - margin: { - left: 10px; - right: 10px; - bottom: 20px; - }; - .nav-link { - font-size: 14px; - - .icon { - width: 60px; - height: 60px; - line-height: 60px; - font-size: 25px; - padding-right: 5px; - margin-bottom: 12px; - } - } - } - } - } - .single-main-services { - .content { - padding-right: 0; - text-align: center; - margin-bottom: 30px; - - h3 { - font-size: 19px; - } - p { - max-width: 100%; - } - } - } - .services-box { - padding: 20px 15px; - - .icon { - font-size: 40px; - margin-bottom: 20px; - } - h3 { - font-size: 16px; - } - } - .services-item { - h3 { - font-size: 16px; - margin-top: 20px; - } - .hover-content { - padding: 20px 15px; - - .link-btn { - i { - top: 6px; - } - } - } - } - .col-lg-3 { - &:nth-child(2), &:nth-child(4) { - .services-box { - margin-top: 0; - } - } - &:nth-child(5), &:nth-child(7) { - .services-box { - margin-top: 0; - } - } - } - .single-it-services-box { - padding: 20px 15px; - text-align: center; - - .icon { - left: 0; - top: 0; - font-size: 35px; - display: block; - position: relative; - margin-bottom: 20px; - } - h3 { - font-size: 16px; - } - } - - .services-details-desc { - h3 { - font-size: 17px; - } - } - - .screenshot-wrap { - .screenshot-frame { - display: none; - } - } - .screenshot-slides { - &.owl-theme { - padding-top: 40px; - - .owl-nav { - [class*=owl-] { - left: 15px; - width: 30px; - line-height: 30px; - height: 30px; - font-size: 15px; - top: 0; - transform: translateY(0); - - &.owl-next { - left: auto; - right: 15px; - } - } - } - } - } - - .expertise-box { - text-align: center; - - .icon { - width: 60px; - height: 60px; - font-size: 30px; - line-height: 60px; - margin: { - left: auto; - right: auto; - bottom: 20px; - }; - } - h3 { - font-size: 16px; - } - } - - .categories-box { - h3 { - font-size: 16px; - padding: { - left: 15px; - right: 15px; - }; - } - } - .categories-slides { - &.owl-theme { - .owl-nav { - opacity: 1; - visibility: visible; - margin-bottom: 30px; - - [class*=owl-] { - margin: 0 5px; - position: relative; - left: 0; - top: 0; - width: 30px; - height: 30px; - line-height: 30px; - transform: unset; - - &.owl-next { - right: 0; - } - } - } - } - } - - .who-we-are-content { - padding: 20px 15px; - text-align: center; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 16px; - } - .default-btn { - margin-top: 5px; - } - } - .who-we-are-image { - .video-btn { - width: 60px; - height: 60px; - font-size: 20px; - } - } - - .categories-area { - border-bottom: 1px solid #eeeeee; - margin-bottom: 55px; - } - .single-categories-box { - h3 { - top: 15px; - left: 15px; - font-size: 15px; - padding: 7px 20px; - } - } - - .startup-works-area { - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #eeeeee; - padding-top: 60px; - margin-bottom: 60px; - - .section-title { - padding-top: 0; - text-align: center; - margin-bottom: 40px; - - h2 { - font-size: 22px; - } - } - } - .startup-works-content { - text-align: center; - position: relative; - border-top: 1px solid #eeeeee; - margin-top: 15px; - padding: { - top: 50px; - right: 0; - }; - h3 { - font-size: 16px; - margin-bottom: 12px; - } - .custom-owl-nav { - margin-top: 0; - - [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - position: absolute; - left: 0; - top: -15px; - - &.custom-owl-next { - left: auto; - right: 0; - } - } - } - } - .startup-works-image { - margin: { - left: 0; - top: 15px; - }; - padding: { - top: 0; - right: 0; - bottom: 0; - }; - } - .single-works-box { - .works-image { - .works-overlay { - h3 { - font-size: 16px; - } - } - } - } - .works-box { - h3 { - opacity: 1; - visibility: visible; - top: auto; - transform: translateY(0); - bottom: 0; - font-size: 16px; - padding: 10px; - margin: { - left: 0; - right: 0; - top: 0; - }; - } - } - .works-box-btn { - margin-top: 5px; - } - .single-works-item { - .content { - padding: { - left: 15px; - right: 15px; - }; - .link-btn { - width: 40px; - height: 40px; - line-height: 40px; - } - h3 { - font-size: 16px; - } - } - } - - .how-it-works-content { - padding-right: 0; - - ul { - li { - padding-left: 50px; - - i { - font-size: 30px; - } - h3 { - font-size: 16px; - } - } - } - } - .how-it-works-image { - margin-top: 30px; - } - - .single-process-box { - padding: 25px 15px; - - h3 { - font-size: 16px; - } - } - - .funfacts-inner { - padding: { - left: 15px; - right: 15px; - }; - &.border-radius { - &.ptb-70 { - padding-bottom: 10px; - } - } - } - .single-funfacts-box { - text-align: center; - padding: 20px 10px; - - .icon { - position: relative; - top: 0; - line-height: 1; - transform: translateY(0); - left: 0; - font-size: 30px; - margin-bottom: 15px; - display: block; - } - h3 { - font-size: 30px; - } - &.without-bg-color { - border-right: none; - margin-right: 0; - padding: 0; - } - &.text-center { - border-right: none; - padding: 0; - } - } - - .single-products-box { - .image { - .buttons-list { - li { - transform: translateY(0); - visibility: visible; - opacity: 1; - - a { - .tooltip-label { - &::before { - bottom: -12px; - } - } - &:hover { - .tooltip-label { - top: -40px; - } - } - } - } - } - } - .content { - h3 { - font-size: 16px; - } - .rating { - margin-bottom: 10px; - font-size: 12px; - } - .price { - span { - font-size: 14px; - } - } - } - } - - .startup-testimonials-area { - .container-fluid { - max-width: 100%; - } - } - .startup-testimonials-image { - padding: 0; - - .main-image { - animation: unset !important; - } - } - .startup-testimonials-content { - padding: { - left: 15px; - right: 15px; - bottom: 30px; - top: 30px; - }; - .icon { - margin-bottom: 15px; - font-size: 30px; - } - p { - font-size: 15px; - } - .info { - margin-top: 20px; - padding-top: 20px; - - h3 { - font-size: 15px; - margin-bottom: 7px; - } - } - } - .startup-testimonials-slides { - &.owl-theme { - .owl-nav { - right: 0; - bottom: 0; - transform: unset; - position: relative; - padding-bottom: 30px; - - [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - } - } - } - .marketing-testimonials-image { - margin: { - left: 0; - right: 0; - bottom: 30px; - }; - } - .marketing-testimonials-content { - padding-bottom: 0; - - .icon { - margin-bottom: 15px; - font-size: 30px; - } - p { - font-size: 15px; - } - .info { - margin-top: 20px; - - h3 { - font-size: 15px; - margin-bottom: 7px; - } - } - } - .marketing-testimonials-slides { - &.owl-theme { - .owl-nav { - right: 0; - bottom: 0; - margin-top: 25px; - transform: unset; - position: relative; - - [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - } - } - } - .business-testimonials-image { - margin: { - right: 0; - bottom: 40px; - }; - } - .business-testimonials-content { - .icon { - margin-bottom: 15px; - font-size: 30px; - } - p { - font-size: 15px; - } - .info { - margin-top: 20px; - - h3 { - font-size: 15px; - margin-bottom: 7px; - } - } - } - .testimonials-slides { - &.owl-theme { - padding: { - left: 0; - right: 0; - }; - .owl-nav { - margin-top: 30px; - - [class*=owl-] { - padding: 6px 20px 6px !important; - left: 0; - top: 0; - transform: unset; - position: relative; - font-size: 13px; - margin: 0 4px; - } - } - } - } - .testimonials-inner { - padding: { - left: 15px; - right: 15px; - }; - .testimonials-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - margin: 0 5px; - font-size: 15px; - width: 35px; - height: 35px; - line-height: 35px; - } - } - } - } - &.it-agency-color { - padding: { - left: 0; - right: 0; - }; - } - &.minimal-portfolio-color { - padding: { - left: 0; - right: 0; - }; - } - &.shop-color { - padding: { - left: 0; - right: 0; - }; - } - } - .testimonials-item { - max-width: 100%; - - .icon { - margin-bottom: 15px; - font-size: 30px; - } - p { - font-size: 15px; - } - .info { - margin-top: 20px; - - img { - width: 70px !important; - height: 70px; - } - .title { - h3 { - font-size: 15px; - margin-bottom: 7px; - } - } - } - } - .saas-testimonials-slides { - &.owl-theme { - margin-top: 30px; - padding: { - left: 0; - right: 0; - }; - .owl-nav { - margin-top: 20px; - - [class*=owl-] { - top: 0; - left: 0; - margin: 0 6px; - font-size: 20px; - transform: unset; - position: relative; - - &.owl-next { - right: 0; - } - } - } - } - } - .saas-testimonials-item { - padding: 20px 15px; - text-align: center; - - .info { - margin-bottom: 20px; - justify-content: center; - text-align: start; - - img { - width: 65px !important; - height: 65px; - } - .title { - text-align: start; - - h3 { - font-size: 15px; - } - } - } - p { - font-size: 14px; - } - } - .testimonials-box { - padding: 20px 15px; - - .icon { - width: 60px; - height: 60px; - line-height: 60px; - font-size: 25px; - margin-bottom: 15px; - } - p { - font-size: 14px; - } - .info { - margin-top: 20px; - - h3 { - font-size: 15px; - } - } - } - - .single-skills-box { - padding: 20px; - } - - .short-brief-area { - .row { - .col-lg-6 { - &:last-child { - .short-brief-content { - border-bottom: none; - margin-bottom: 0; - } - } - } - } - } - .short-brief-content { - border-bottom: 1px solid #eeeeee; - padding-bottom: 30px; - - .timeline-content { - h3 { - font-size: 16px; - margin-bottom: 12px; - } - } - .timeline-item { - padding-left: 30px; - } - .timeline-info { - .sub-title { - margin-bottom: 10px; - - span { - font-size: 15px; - } - } - } - } - - .video-box { - .video-btn { - top: 50%; - transform: translateY(-50%) translateX(-50%); - width: 55px; - height: 55px; - font-size: 20px; - } - } - - .single-team-member { - .content { - padding: 20px 15px; - - &::before { - opacity: 1; - visibility: visible; - } - h3 { - font-size: 16px; - transform: translateY(0) !important; - } - span { - transform: translateY(0) !important; - } - .social { - bottom: auto; - opacity: 1; - visibility: visible; - top: -30px; - - li { - a { - font-size: 15px; - } - } - } - } - } - .single-team-box { - .image { - .social { - margin-bottom: 0; - opacity: 1; - visibility: visible; - padding: 20px; - } - } - .content { - margin-top: 20px; - - h3 { - font-size: 16px; - } - } - } - .team-member { - .content { - padding: 20px 15px; - margin-top: 0; - - &::before { - opacity: 1; - visibility: visible; - } - h3 { - font-size: 16px; - transform: translateY(0) !important; - } - span { - transform: translateY(0) !important; - } - .social { - visibility: visible; - bottom: auto; - opacity: 1; - top: -30px; - - li { - a { - font-size: 15px; - } - } - } - } - } - .team-box { - .content { - visibility: visible; - position: relative; - padding: 20px 15px; - opacity: 1; - bottom: 0; - - h3 { - font-size: 16px; - } - .social { - li { - a { - font-size: 17px; - } - } - } - } - } - - .single-pricing-box { - text-align: center; - padding: 20px 15px; - - .pricing-header { - margin-bottom: 20px; - padding-bottom: 20px; - - h3 { - font-size: 30px; - - span { - font-size: 13px; - } - } - } - .features-list { - max-width: 176px; - text-align: start; - margin: { - bottom: 20px; - left: auto; - right: auto; - }; - li { - padding-left: 20px; - - i { - top: 3.5px; - } - } - } - } - .pricing-box { - padding: 25px; - - .pricing-header { - margin: -25px -25px 25px; - padding: 25px; - - h3 { - font-size: 16px; - } - .price { - font-size: 30px; - - span { - font-size: 13px; - } - } - } - .features-list { - margin-bottom: 25px; - } - &.bg-black { - .pricing-header { - padding: 0 0 25px; - } - } - } - .pricing-tabs { - margin-top: 0; - - .nav { - margin-bottom: 40px; - - .nav-item { - .nav-link { - padding: 11px 30px; - font-size: 14px; - } - } - } - } - .pricing-item { - padding: 20px 15px; - - .pricing-header { - margin: -20px 0 20px; - padding: 20px 15px; - - h3 { - font-size: 16px; - } - .price { - font-size: 30px; - - span { - font-size: 13px; - } - } - } - .features-list { - margin-bottom: 20px; - } - } - - .single-clients-box { - a { - padding: 10px; - } - } - - .services-details-content { - text-align: center; - - h2 { - font-size: 22px; - } - } - .services-details-video { - margin: { - left: 0; - top: 30px; - }; - iframe { - height: 162px; - } - } - - .showMoreItemsList + .button-box button { - margin-top: 0; - padding: 11px 30px; - font-size: 14px; - } - - .single-featured-video-box { - .content { - h3 { - font-size: 16px; - } - } - } - .featured-video-item-list { - .single-featured-video-item { - .image { - height: auto; - background-image: unset !important; - - img { - display: inline-block; - } - } - .content { - padding: { - top: 20px; - left: 0; - bottom: 0; - }; - h3 { - font-size: 16px; - } - } - } - } - - .latest-posts-area { - .widget-area { - margin: { - top: 10px; - bottom: 30px; - }; - } - } - .single-latest-post-box { - .post-content { - padding: { - top: 20px; - left: 0; - right: 0; - }; - h3 { - font-size: 16px; - } - .link-btn { - i { - top: 6px; - } - } - } - } - - .what-we-do-content { - text-align: center; - - .section-title { - text-align: center; - margin-bottom: 0; - - p { - margin-top: 12px; - } - } - .single-what-we-do-box { - margin-top: 30px; - } - } - .single-what-we-do-box { - .icon { - font-size: 35px; - margin-bottom: 20px; - } - h3 { - font-size: 16px; - } - } - .what-we-do-image { - margin-top: 30px; - } - - .shorting-menu { - margin-bottom: 15px; - - .filter { - font-size: 14px; - margin: { - bottom: 15px; - right: 10px; - left: 10px; - }; - } - } - .works-slides-style-one, .works-slides-style-two { - &.owl-theme { - .owl-nav { - opacity: 1; - visibility: visible; - - [class*=owl-] { - left: 15px; - width: 35px; - height: 35px; - line-height: 35px; - font-size: 16px; - - &.owl-next { - left: auto; - right: 15px; - } - } - } - .owl-dots { - margin-top: 30px; - - .owl-dot { - span { - width: 15px; - height: 15px; - margin: 0 3px; - } - } - } - } - } - - .portfolio-details-desc { - h3 { - font-size: 17px; - } - .image { - padding-right: 0; - } - .content { - margin-top: 0; - margin-bottom: 15px; - } - } - .portfolio-details-info { - padding: 20px; - margin: { - left: 0; - top: 30px; - }; - ul { - li { - .icon { - top: 6px; - font-size: 20px; - } - span { - margin-bottom: 8px; - font-size: 15px; - } - } - } - } - .portfolio-details-slides { - &.owl-theme { - .owl-nav { - opacity: 1; - visibility: visible; - - [class*=owl-] { - left: 15px; - width: 35px; - height: 35px; - line-height: 35px; - font-size: 16px; - - &.owl-next { - left: auto; - right: 15px; - } - } - } - } - } - - .portfolio-details-left { - a { - padding-left: 20px; - font-size: 13px; - } - } - .menu-icon { - font-size: 18px; - } - .portfolio-details-right { - a { - padding-right: 20px; - font-size: 13px; - } - } - - .widget-area { - margin-top: 40px; - - .widget { - .widget-title { - font-size: 16px; - padding-bottom: 10px; - } - } - .widget_service_categories { - ul { - li { - a { - i { - font-size: 11px; - } - } - } - } - } - .widget_about_author { - .about-author { - padding: 20px 15px; - } - } - .widget_noke_posts_thumb { - .item { - padding-left: 95px; - - .thumb { - width: 80px; - - .fullimage { - width: 80px; - } - } - .info { - .title { - font-size: 13px; - } - } - } - } - .widget_recent_posts { - .item { - .title { - font-size: 14px; - } - } - } - .widget_newsletter { - padding: 20px 15px; - - h4 { - font-size: 17px; - } - } - .widget_tag_cloud { - .tagcloud { - a { - font-size: 13px !important; - } - } - } - &.extra-padding { - padding-left: 0; - - &.left-sidebar { - padding-right: 0; - margin: { - top: 0; - bottom: 40px; - }; - } - } - .widget_price_filter { - .collection_filter_by_price { - padding-right: 15px; - } - } - .widget_colors { - ul { - li { - a { - span { - &:nth-child(1) { - width: 15px; - height: 15px; - } - } - } - } - } - } - .widget_top_rated_products { - .item { - .info { - .title { - margin-bottom: 6px; - font-size: 13px; - } - } - } - } - } - - .products-details-desc { - padding-left: 0; - margin-top: 30px; - - h3 { - margin-bottom: 15px; - font-size: 16px; - } - .products-review { - .rating { - font-size: 13px; - } - } - .price { - font-size: 14px; - } - } - .products-details-tabs { - margin-top: 30px; - - .single-tabs-box { - h2 { - font-size: 17px; - } - .inner-box { - h3 { - font-size: 16px; - margin-bottom: 15px; - } - .table-striped { - tbody tr td { - font-size: 13px; - white-space: nowrap; - } - } - .products-review-comments { - .user-review { - padding-left: 0; - - .sub-comment { - margin-bottom: 6px; - } - img { - top: 0; - left: 0; - position: relative; - margin-bottom: 15px; - } - } - } - .review-form-wrapper { - form { - button { - margin-top: 20px; - font-size: 14px; - } - } - } - } - } - } - - .cart-table-desc { - table { - thead { - tr { - th { - font-size: 14px; - white-space: nowrap; - } - } - } - tbody { - tr { - td { - padding-right: 20px; - white-space: nowrap; - font-size: 13px; - text-align: center; - } - } - } - } - } - .cart-buttons-desc { - .shopping-coupon-code { - .form-control { - padding-left: 10px; - } - button { - padding: 0 15px; - font-size: 12px; - } - } - .text-end { - text-align: center !important; - margin-top: 30px; - } - } - .cart-totals-desc { - margin-top: 30px; - padding: 20px 15px; - - h3 { - font-size: 16px; - padding: { - top: 15px; - left: 15px; - right: 15px; - bottom: 14px; - }; - margin: { - left: -15px; - right: -15px; - bottom: 20px; - top: -20px; - }; - } - .default-btn { - margin-top: 20px; - } - } - - .billing-details-desc { - padding: 20px 15px; - - h3 { - font-size: 16px; - padding: { - top: 15px; - left: 15px; - right: 15px; - bottom: 14px; - }; - margin: { - left: -15px; - right: -15px; - bottom: 20px; - top: -20px; - }; - } - .form-group { - .nice-select { - font-size: 13px; - - .list { - .option { - font-size: 14px; - } - } - } - } - } - .order-details-desc { - margin-top: 30px; - padding: 20px 15px; - - h3 { - font-size: 16px; - padding: { - top: 15px; - left: 15px; - right: 15px; - bottom: 14px; - }; - margin: { - left: -15px; - right: -15px; - bottom: 20px; - top: -20px; - }; - } - .default-btn { - margin-top: 20px; - } - } - - .blog-details-desc { - .article-content { - .entry-meta { - margin-top: -10px; - - ul { - li { - margin: { - right: 10px; - top: 10px; - }; - } - } - } - h3 { - font-size: 16px; - line-height: 1.5; - margin: { - bottom: 12px; - top: 20px; - }; - } - } - .article-footer { - text-align: center; - - .article-tags { - flex: 0 0 100%; - max-width: 100%; - - .d-flex { - display: block !important; - } - p { - display: block; - margin-bottom: 0; - } - a { - margin-top: 10px; - } - } - .article-share { - flex: 0 0 100%; - max-width: 100%; - margin-top: 20px; - - .social { - text-align: center; - } - } - } - } - blockquote { - padding: 20px !important; - - &::after, &::after { - width: 1px; - } - p { - font-size: 14px !important; - } - } - .comments-area { - margin-top: 25px; - - .comments-title { - font-size: 16px; - } - .comment-author { - font-size: 14px; - - .avatar { - top: 0; - left: 0; - display: block; - position: relative; - margin-bottom: 12px; - } - } - .comment-body { - .reply { - a { - font-size: 12px; - } - } - } - .comment-body { - padding-left: 0; - } - .comment-respond { - margin-top: 25px; - - .comment-reply-title { - font-size: 16px; - } - .comment-form-author { - width: 100%; - padding-right: 0; - } - .comment-form-email { - width: 100%; - padding-left: 0; - } - input[type="date"], input[type="time"], input[type="datetime-local"], input[type="week"], input[type="month"], input[type="text"], input[type="email"], input[type="url"], input[type="password"], input[type="search"], input[type="tel"], input[type="number"], textarea { - font-size: 14px; - padding-left: 12px; - } - textarea { - padding-top: 12px; - } - .comment-form-cookies-consent { - label { - font-size: 14px; - } - } - .form-submit { - input { - font-size: 13px; - } - } - } - } - - .faq-accordion { - .accordion-item { - .accordion-button { - padding: 15px 15px 15px 40px; - font-size: 14px; - - &::after { - left: 15px; - top: 20px; - width: 12px; - height: 12px; - background-size: 12px; - } - } - .accordion-body { - padding: 0 15px 15px 40px; - } - } - } - - .facility-content { - padding-right: 0; - margin-bottom: 30px; - - .item { - padding-left: 0; - text-align: center; - - i { - line-height: 1; - font-size: 35px; - position: relative; - margin-bottom: 20px; - display: inline-block; - } - h3 { - font-size: 16px; - } - } - } - - .cta-content { - max-width: 100%; - - h2 { - font-size: 22px; - } - p { - max-width: 100%; - margin-top: 15px; - } - .default-btn { - margin-top: 10px; - } - } - .call-to-action-content { - text-align: center; - - h2 { - font-size: 22px; - } - } - .call-to-action-btn { - text-align: center; - margin-top: 25px; - } - .call-to-action-inner { - padding: { - left: 15px; - right: 15px; - }; - } - - .profile-authentication-area { - height: auto; - padding: { - bottom: 70px; - top: 130px; - }; - } - .signin-form { - max-width: 100%; - text-align: center; - padding: 20px 15px; - - h2 { - font-size: 25px; - margin-bottom: 25px; - } - form { - .lost-your-password-wrap { - text-align: start; - margin-top: 15px; - } - .form-group { - margin-bottom: 15px; - } - button { - padding: 12px 30px; - font-size: 14px; - } - .dont-account { - margin-top: 20px; - } - .sign-in-with-button { - margin-top: 20px; - } - } - } - .signup-form { - max-width: 100%; - text-align: center; - padding: 20px 15px; - - h2 { - font-size: 25px; - margin-bottom: 25px; - } - form { - .lost-your-password-wrap { - text-align: start; - margin-top: 15px; - } - .form-group { - margin-bottom: 15px; - } - button { - padding: 12px 30px; - font-size: 14px; - } - .dont-account { - margin-top: 20px; - } - .sign-in-with-button { - margin-top: 20px; - } - } - } - - .noke-grid-sorting { - margin-bottom: 30px; - text-align: center; - - .ordering { - text-align: center; - margin-top: 15px; - - select { - font-size: 14px; - } - } - } - - .app-download-area { - .col-lg-6 { - &:nth-child(1) { - order: 2; - } - &:nth-child(2) { - order: 1; - } - } - } - .app-download-content { - text-align: center; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - .btn-box { - margin-top: 25px; - - .store-btn { - padding: 10px 10px 10px 45px; - font-size: 12px; - text-align: start; - margin: { - left: 3px; - right: 3px; - }; - &:first-child { - margin-left: 0; - } - img { - left: 10px; - width: 25px; - } - span { - font-size: 14px; - } - } - } - } - .app-download-image { - padding-right: 0; - margin-top: 30px; - } - - .trending-style-image { - margin-right: 0; - padding: { - bottom: 0; - right: 0; - }; - img { - &:nth-child(2) { - position: relative; - margin-top: 30px; - width: auto; - } - } - } - .trending-style-content { - text-align: center; - padding-left: 0; - margin-top: 30px; - - .sub-title { - font-size: 13px; - } - h2 { - margin-bottom: 13px; - font-size: 22px; - } - h3 { - font-size: 15px; - margin-bottom: 13px; - } - .default-btn { - margin-top: 5px; - } - } - - .page-title-area { - padding: { - top: 60px; - bottom: 70px; - }; - &.style-three { - padding: { - top: 60px; - bottom: 60px; - }; - } - &.style-four { - padding: { - top: 60px; - bottom: 60px; - }; - &::before { - background: { - image: unset !important; - color: rgba(255, 255, 255, .85); - }; - } - } - &.style-two { - padding: { - bottom: 60px; - top: 245px; - }; - } - } - .page-title-content { - &.text-start { - text-align: center !important; - } - h2 { - font-size: 25px; - } - ul { - margin-top: 10px; - - li { - font-size: 14px; - } - } - } - - .privacy-policy-content { - h3 { - font-size: 16px; - margin-bottom: 13px; - } - } - - .terms-conditions-content { - h3 { - font-size: 16px; - margin-bottom: 13px; - } - } - - .error-content { - h3 { - font-size: 20px; - margin: { - bottom: 15px; - top: 30px; - }; - } - .default-btn { - margin-top: 20px; - } - } - - .coming-soon-image { - height: auto; - background-image: unset !important; - - img { - display: inline-block; - } - } - .coming-soon-content { - height: auto; - padding: { - top: 60px; - bottom: 60px; - left: 15px; - right: 15px; - }; - h2 { - font-size: 25px; - margin-top: 25px; - } - #timer { - margin-top: 10px; - - div { - width: 95px; - height: 100px; - font-size: 30px; - margin-top: 20px; - - span { - font-size: 14px; - } - &:first-child { - margin-left: 10px; - } - &:last-child { - margin-right: 10px; - } - } - } - form { - max-width: 100%; - margin-top: 30px; - - .default-btn { - position: relative; - right: 0; - bottom: 0; - display: block; - width: 100%; - padding: 13px 20px; - margin-top: 15px; - } - .input-newsletter { - padding: 0 0 0 15px; - height: 50px; - font-size: 13px; - } - } - } - - .single-blog-post { - .content { - padding: 15px; - - .meta { - li { - i { - top: 3px; - } - } - } - h3 { - font-size: 16px; - } - } - } - .single-post-item { - .post-content { - padding: 20px 15px; - - .meta { - margin-bottom: 3px; - - li { - padding-left: 19px; - margin: { - right: 10px; - bottom: 10px; - }; - } - } - .tag { - margin-bottom: 12px; - } - h3 { - font-size: 16px; - } - .link-btn { - i { - top: 6px; - } - } - } - &.without-box-shadow { - .post-content { - padding: { - left: 0; - right: 0; - }; - } - } - } - .blog-section-title { - text-align: center; - margin-bottom: 40px; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 22px; - } - .default-btn { - margin-top: 5px; - } - } - - .pagination-area { - margin-top: 10px; - - .page-numbers { - width: 35px; - height: 35px; - line-height: 33px; - font-size: 15px; - } - } - - .single-contact-info-box { - padding: 20px 15px; - text-align: center; - - .icon { - font-size: 30px; - margin-bottom: 20px; - } - h3 { - font-size: 16px; - margin-bottom: 12px; - } - &.style-two { - .icon { - width: 60px; - height: 60px; - font-size: 25px; - margin-bottom: 20px; - } - } - } - .contact-form { - padding: 20px; - } - - .footer-area { - padding-top: 60px; - - &.marketing-footer { - .footer-top { - margin-bottom: 60px; - padding-bottom: 0; - } - } - &.business-footer { - .footer-top { - margin-bottom: 60px; - padding: { - top: 50px; - bottom: 50px; - }; - } - } - } - .footer-top { - text-align: center; - padding-bottom: 30px; - margin-bottom: 30px; - - .logo { - margin-bottom: 25px; - } - .content { - &.d-flex { - display: block !important; - } - h3 { - font-size: 16px; - margin-bottom: 25px; - } - } - } - .single-footer-widget { - h3 { - font-size: 16px; - } - &.pl-4 { - padding-left: 0; - } - &.pl-2 { - padding-left: 0; - } - &.style-two { - h3 { - padding-left: 0; - - &::before { - display: none; - } - } - } - } - .copyright-area { - margin-top: 30px; - padding: { - top: 20px; - bottom: 20px; - }; - .text-start { - text-align: center!important; - margin-bottom: 15px; - } - .social-links { - text-align: center; - margin-top: 15px; - } - } - .go-top { - width: 38px; - height: 38px; - font-size: 15px; - } - - .productsQuickView { - .modal-dialog { - max-width: 100%; - } - .modal-content { - padding: 15px; - - button { - &.btn-close { - font-size: 15px; - } - } - .products-content { - margin-top: 25px; - - h3 { - font-size: 16px; - } - .price { - font-size: 14px; - } - .products-add-to-cart { - margin-top: 20px; - } - .social-share { - margin-top: 25px; - } - } - } - } - -} - -/* Min width 576px to Max width 767px */ -@media only #{$media} and ($feature_min : $value_one) and ($feature_max : $value_two) { - - .top-header-area { - .container-fluid { - max-width: 540px; - } - &.top-header-style-two { - .container-fluid { - max-width: 540px; - } - } - &.top-header-style-three { - .container-fluid { - max-width: 540px; - } - } - } - - .startup-banner-inner { - padding: { - left: 25px; - right: 25px; - }; - } - .startup-banner-content { - max-width: 540px; - margin: { - left: auto; - right: auto; - }; - padding: { - left: 15px; - right: 15px; - }; - } - .startup-banner-image { - max-width: 540px; - margin: { - left: auto; - right: auto; - }; - padding: { - left: 15px; - right: 15px; - }; - } - .business-banner-area { - .container { - max-width: 540px; - } - } - .app-banner-area { - .container { - max-width: 540px; - } - } - .saas-banner-area { - .container-fluid { - max-width: 540px; - } - } - .personal-portfolio-banner-area { - .container { - max-width: 540px; - } - } - .consulting-banner-area { - .container { - max-width: 540px; - } - } - - .screenshot-wrap { - .screenshot-frame { - display: block; - height: 500px; - width: 240px; - top: 40px; - } - } - .screenshot-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - left: 0; - - &.owl-next { - left: auto; - right: 0; - } - } - } - } - } - - .startup-about-area { - .container-fluid { - max-width: 540px; - } - } - - .single-services-box { - .content { - padding: 25px; - } - } - - .single-blog-post { - .content { - padding: 25px; - } - } - - .works-area { - .container-fluid { - max-width: 540px; - } - } - - .facility-content { - display: flex; - flex-wrap: wrap; - margin: { - right: -15px; - left: -15px; - bottom: 0; - }; - .item { - flex: 0 0 50%; - max-width: 50%; - margin-bottom: 30px !important; - padding: { - left: 15px; - right: 15px; - }; - } - } - - .startup-testimonials-area { - .container-fluid { - max-width: 540px; - } - } - - .trending-style-image { - padding: { - right: 100px; - bottom: 80px; - }; - img { - &:nth-child(2) { - position: absolute; - margin-top: 0; - width: 250px; - } - } - } - - .productsQuickView { - .modal-dialog { - max-width: 430px; - } - .modal-content { - padding: 30px; - } - } - -} - -/* Min width 768px to Max width 991px */ -@media only #{$media} and ($feature_min : $value_three) and ($feature_max : $value_four) { - - body { - font-size: 14.5px; - } - p { - font-size: 14.5px; - } - .ptb-100 { - padding: { - top: 80px; - bottom: 80px; - }; - } - .pt-100 { - padding-top: 80px; - } - .pb-100 { - padding-bottom: 80px; - } - .ptb-70 { - padding: { - top: 50px; - bottom: 50px; - }; - } - .pt-70 { - padding-top: 50px; - } - .pb-70 { - padding-bottom: 50px; - } - .default-btn { - font-size: 14.5px; - } - .section-title { - max-width: 550px; - margin-bottom: 50px; - - h2 { - font-size: 30px; - } - &.style-two { - text-align: center; - padding-left: 0; - margin: { - left: auto; - right: auto; - bottom: 50px; - }; - h2 { - &::before { - display: none; - } - } - } - &.style-four { - text-align: center; - - h2 { - font-size: 25px; - } - &::before { - left: 50%; - transform: translateX(-50%); - } - } - } - - .top-header-area { - .container-fluid { - max-width: 720px; - } - &.top-header-style-two { - .container-fluid { - max-width: 720px; - } - } - &.top-header-style-three { - .container-fluid { - max-width: 720px; - } - } - } - - .noke-responsive-nav { - .others-option { - display: flex !important; - } - } - .others-option-for-responsive { - display: none; - } - - .startup-banner-area { - .container-fluid { - max-width: 100%; - padding: { - left: 0; - right: 0; - }; - } - } - .startup-banner-inner { - padding: { - top: 80px; - bottom: 80px; - left: 25px; - right: 25px; - }; - } - .startup-banner-content { - max-width: 720px; - text-align: center; - margin: { - left: auto; - right: auto; - }; - padding: { - left: 15px; - right: 15px; - }; - p { - margin: { - left: auto; - right: auto; - }; - } - h1 { - margin-bottom: 20px; - font-size: 35px; - } - .default-btn { - margin-top: 5px; - } - } - .startup-banner-image { - max-width: 720px; - padding: { - left: 15px; - right: 15px; - }; - margin: { - left: auto; - right: auto; - top: 30px; - }; - } - .marketing-banner-area { - padding: { - top: 70px; - bottom: 70px; - }; - } - .marketing-banner-content { - padding-right: 0; - text-align: center; - - p { - margin: { - left: auto; - right: auto; - }; - } - h1 { - margin-bottom: 15px; - font-size: 35px; - } - .default-btn { - margin-top: 5px; - } - } - .marketing-banner-image { - margin-top: 30px; - } - .business-banner-area { - padding: { - top: 80px; - bottom: 80px; - }; - .container { - max-width: 720px; - } - &::before { - display: none; - } - } - .business-banner-content { - padding-right: 0; - text-align: center; - max-width: 100%; - - p { - margin: { - left: auto; - right: auto; - }; - } - h1 { - margin-bottom: 15px; - font-size: 36px; - } - .default-btn { - margin-top: 5px; - } - } - .business-banner-image { - margin-top: 30px; - } - .app-banner-area { - padding: { - top: 80px; - bottom: 80px; - }; - .container { - max-width: 720px; - } - } - .app-banner-content { - text-align: center; - - p { - margin: { - left: auto; - right: auto; - }; - } - h1 { - margin-bottom: 15px; - font-size: 40px; - } - .btn-box { - margin-top: 20px; - - .store-btn { - text-align: start; - - span { - font-size: 16px; - } - } - } - } - .app-banner-image { - padding-left: 0; - margin-top: 35px; - } - .personal-portfolio-banner-area { - padding: { - top: 80px; - bottom: 80px; - }; - .container { - max-width: 720px; - } - } - .personal-portfolio-banner-content { - text-align: center; - - .sub-title { - padding-left: 0; - font-size: 16px; - - &::before { - display: none; - } - } - p { - margin: { - left: auto; - right: auto; - }; - } - h4 { - margin-bottom: 15px; - font-size: 20px; - } - h1 { - margin-bottom: 15px; - font-size: 40px; - } - .default-btn { - margin-top: 5px; - } - } - .personal-portfolio-banner-image { - margin-top: 30px; - - img { - width: 100%; - } - } - .shape1, .shape2, .shape3, .shape4, .shape5, .shape6, .shape7, .shape8, .shape9, .shape16, .shape17, .shape18, .shape19 { - display: none !important; - } - .saas-banner-area { - padding-top: 80px; - - .container-fluid { - max-width: 720px; - padding: { - left: 15px; - right: 15px; - }; - } - } - .saas-banner-content { - margin-left: 0; - text-align: center; - max-width: 100%; - - h1 { - margin-bottom: 15px; - font-size: 40px; - } - p { - margin: { - left: auto; - right: auto; - }; - } - } - .saas-banner-image { - margin-top: 35px; - } - .consulting-banner-area { - padding: { - top: 215px; - bottom: 80px; - }; - .container { - max-width: 720px; - } - &::before { - display: block; - } - } - .consulting-banner-content { - text-align: center; - max-width: 100%; - - h1 { - margin-bottom: 15px; - font-size: 35px; - } - p { - margin: { - left: auto; - right: auto; - }; - } - .default-btn { - margin-top: 5px; - } - } - .it-agency-banner-area { - .it-agency-inner-banner { - padding: { - top: 200px; - bottom: 230px; - }; - } - } - .it-agency-banner-content { - max-width: 100%; - - h1 { - font-size: 40px; - margin-bottom: 15px; - } - .default-btn { - margin-top: 8px; - } - } - .banner-video-box { - max-width: 100%; - margin-top: -130px; - } - .shop-banner-area { - padding: { - top: 100px; - bottom: 100px; - }; - } - .shop-banner-content { - .sub-title { - font-size: 20px; - } - h1 { - margin-bottom: 15px; - font-size: 40px; - } - h4 { - font-size: 20px; - } - .default-btn { - margin-top: 25px; - } - } - .blog-banner-area { - padding: { - top: 80px; - bottom: 80px; - }; - } - .blog-banner-item { - margin-bottom: 30px; - - .content { - padding: 25px; - max-width: 510px; - - h3 { - font-size: 20px; - } - } - } - .blog-banner-item-list { - .single-blog-banner-item { - .image { - background-image: unset !important; - height: auto; - - img { - display: inline-block; - } - } - .content { - padding: { - top: 20px; - bottom: 0; - left: 0; - }; - h3 { - font-size: 18px; - } - } - } - } - .furniture-banner-item { - padding: { - top: 100px; - bottom: 100px; - }; - } - .furniture-banner-content { - border-left-width: 3px; - padding: 30px; - - h1 { - font-size: 35px; - } - } - .minimal-portfolio-banner-area { - padding: { - bottom: 170px; - top: 200px; - }; - } - .minimal-portfolio-banner-content { - max-width: 600px; - - .typewrite { - padding: 8px 20px; - margin-bottom: 25px; - font-size: 15px; - } - h1 { - font-size: 35px; - margin-bottom: 25px; - } - } - - .features-area { - &.ptb-70 { - padding-bottom: 20px; - } - } - .single-features-box { - padding: { - left: 0; - right: 0; - }; - .icon { - font-size: 40px; - transform: translateY(0) !important; - } - h3 { - font-size: 18px; - transform: translateY(0) !important; - } - p { - transform: translateY(0) !important; - } - .link-btn { - left: 0; - opacity: 1; - transform: unset; - margin-top: 13px; - position: relative; - visibility: visible; - bottom: 0 !important; - - i { - top: 6.5px; - } - } - } - .features-box { - padding: 25px 20px; - - h3 { - font-size: 18px; - } - .icon { - width: 80px; - height: 80px; - font-size: 40px; - line-height: 80px; - } - } - .single-features-item { - padding-left: 55px; - - .icon { - font-size: 40px; - } - h3 { - font-size: 17px; - } - } - .features-item { - padding: 25px 20px; - - .icon { - font-size: 30px; - } - h3 { - font-size: 18px; - } - } - - .startup-about-area { - border-bottom: 1px solid #eeeeee; - margin-bottom: 80px; - - .container-fluid { - max-width: 720px; - } - } - .startup-about-image { - margin-right: 0; - padding: 0; - - .video-btn { - left: 50%; - transform: translateY(-50%) translateX(-50%); - } - } - .startup-about-content { - padding-left: 0; - text-align: center; - max-width: 100%; - margin-top: 30px; - - h2 { - font-size: 30px; - } - .features-list { - padding-left: 0; - list-style-type: none; - - li { - font-weight: 600; - } - } - } - .marketing-about-image { - margin-right: 0; - } - .marketing-about-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - - h2 { - font-size: 30px; - } - .features-list { - padding-left: 0; - list-style-type: none; - - li { - font-weight: 600; - } - } - } - .business-about-image { - padding-right: 0; - margin-bottom: 30px; - } - .business-about-content { - padding-left: 0; - text-align: center; - - h2 { - font-size: 30px; - } - } - .app-about-content { - padding-right: 0; - - h3 { - font-size: 19px; - margin-bottom: 15px; - } - } - .app-about-image { - padding-left: 0; - margin-top: 30px; - } - .saas-about-image { - padding-right: 0; - margin-bottom: 30px; - } - .saas-about-content { - padding-left: 0; - - h2 { - font-size: 30px; - } - } - .it-agency-about-image { - padding-right: 0; - margin-right: 0; - padding-bottom: 0; - } - .it-agency-about-content { - padding-left: 0; - text-align: center; - margin-top: 35px; - - h2 { - font-size: 30px; - } - .features-list { - padding-left: 0; - list-style-type: none; - - li { - font-weight: 600; - } - } - } - - .expertise-box { - text-align: center; - - .icon { - width: 70px; - height: 70px; - line-height: 70px; - font-size: 35px; - margin: { - left: auto; - right: auto; - }; - } - h3 { - font-size: 18px; - } - } - - .who-we-are-content { - text-align: center; - padding: 30px; - - h2 { - font-size: 20px; - } - .default-btn { - margin-top: 5px; - } - } - - .single-services-box { - .content { - padding: 20px; - - .icon { - font-size: 40px; - margin-bottom: 15px; - transform: translateY(0) !important; - } - h3 { - font-size: 18px; - transform: translateY(0) !important; - } - p { - transform: translateY(0) !important; - } - .link-btn { - left: 0; - opacity: 1; - transform: unset; - margin-top: 13px; - position: relative; - visibility: visible; - bottom: 0 !important; - - i { - top: 6px; - } - } - } - &::before { - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.7) 50%, rgba(22, 22, 23, 0) 75%, rgba(0, 0, 0, 0) 100%); - } - } - .single-services-item { - .content { - padding: { - left: 0; - right: 0; - }; - h3 { - font-size: 18px; - } - } - } - .services-tabs { - .nav { - margin-bottom: 35px; - border-bottom: 1px solid #eeeeee; - padding-bottom: 20px; - - .nav-item { - margin: { - left: 15px; - right: 15px; - bottom: 30px; - }; - .nav-link { - font-size: 16px; - - .icon { - width: 70px; - height: 70px; - font-size: 30px; - line-height: 70px; - margin-bottom: 15px; - } - } - } - } - } - .single-main-services { - .content { - padding-right: 0; - text-align: center; - - h3 { - font-size: 21px; - } - p { - max-width: 100%; - } - } - .image { - margin-top: 30px; - } - } - .services-box { - padding: 25px 20px; - - .icon { - font-size: 50px; - } - h3 { - font-size: 18px; - } - } - .col-lg-3 { - &:nth-child(2), &:nth-child(4) { - .services-box { - margin-top: 0; - } - } - &:nth-child(5), &:nth-child(7) { - .services-box { - margin-top: 0; - } - } - } - .services-item { - h3 { - font-size: 18px; - margin-top: 20px; - } - .hover-content { - padding: 20px; - } - } - .single-it-services-box { - padding: 25px 20px 25px 70px; - - .icon { - left: 20px; - top: 25px; - font-size: 35px; - } - h3 { - font-size: 18px; - } - } - - .single-categories-box { - h3 { - font-size: 16px; - left: 20px; - top: 20px; - } - } - .categories-box { - h3 { - font-size: 17px; - padding: { - left: 15px; - right: 15px; - }; - } - } - .categories-slides { - &.owl-theme { - .owl-nav { - opacity: 1; - visibility: visible; - margin-bottom: 30px; - - [class*=owl-] { - margin: 0 5px; - position: relative; - left: 0; - top: 0; - transform: unset; - - &.owl-next { - right: 0; - } - } - } - } - } - - .startup-works-area { - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #eeeeee; - padding-top: 80px; - margin-bottom: 80px; - - .section-title { - padding-top: 0; - text-align: center; - margin: { - bottom: 50px; - left: auto; - right: auto; - }; - h2 { - font-size: 30px; - } - } - } - .startup-works-content { - margin-top: 18px; - position: relative; - text-align: center; - border-top: 1px solid #eeeeee; - padding: { - right: 0; - top: 55px; - }; - h3 { - font-size: 18px; - } - .custom-owl-nav { - margin-top: 0; - - [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - position: absolute; - left: 0; - top: -18px; - - &.custom-owl-next { - left: auto; - right: 0; - } - } - } - } - .startup-works-image { - padding: 0; - margin: { - left: 0; - top: 15px; - }; - } - .single-works-box { - .works-image { - .works-overlay { - h3 { - font-size: 18px; - } - } - } - } - .works-box { - h3 { - top: auto; - transform: translateY(0); - bottom: 0; - font-size: 18px; - opacity: 1; - visibility: visible; - margin: { - top: 0; - right: 0; - left: 0; - }; - } - } - - .single-featured-video-box { - .content { - h3 { - font-size: 18px; - } - } - } - .featured-video-item-list { - .single-featured-video-item { - .image { - height: auto; - background-image: unset !important; - - img { - display: inline-block; - } - } - .content { - padding: { - bottom: 0; - left: 0; - top: 20px; - }; - h3 { - font-size: 18px; - } - } - } - } - - .latest-posts-area { - .widget-area { - margin-top: 10px; - } - } - .single-latest-post-box { - .post-content { - padding: { - top: 20px; - right: 0; - left: 0; - }; - h3 { - font-size: 18px; - } - } - } - - .facility-content { - display: flex; - flex-wrap: wrap; - padding-right: 0; - margin: { - right: -15px; - left: -15px; - bottom: 0; - }; - .item { - flex: 0 0 50%; - max-width: 50%; - margin-bottom: 30px !important; - text-align: center; - padding: { - left: 15px; - right: 15px; - }; - i { - line-height: 1; - position: relative; - margin-bottom: 20px; - display: inline-block; - font-size: 40px; - } - h3 { - font-size: 18px; - } - } - } - - .short-brief-area { - .row { - .col-lg-6 { - &:last-child { - .short-brief-content { - border-bottom: none; - margin-bottom: 0; - } - } - } - } - } - .short-brief-content { - border-bottom: 1px solid #eeeeee; - padding-bottom: 30px; - - .timeline-content { - h3 { - font-size: 18px; - margin-bottom: 12px; - } - } - .timeline-info { - .sub-title { - margin-bottom: 10px; - - span { - font-size: 17px; - } - } - } - } - - .shorting-menu { - .filter { - font-size: 15px; - margin: { - left: 15px; - right: 15px; - }; - } - } - - .widget-area { - margin-top: 40px; - - .widget { - .widget-title { - font-size: 17px; - } - } - .widget_about_author { - .about-author { - padding: 25px; - } - } - .widget_noke_posts_thumb { - .item { - .info { - .title { - font-size: 14px; - } - } - } - } - .widget_newsletter { - h4 { - font-size: 18px; - } - } - .widget_tag_cloud { - .tagcloud { - a { - font-size: 13px !important; - } - } - } - .widget_recent_posts { - .item { - .title { - font-size: 14px; - } - } - } - &.extra-padding { - padding-left: 0; - - &.left-sidebar { - padding-right: 0; - margin: { - top: 0; - bottom: 40px; - }; - } - } - } - - .services-details-content { - text-align: center; - - h2 { - font-size: 30px; - } - } - .services-details-video { - margin: { - left: 0; - top: 30px; - }; - iframe { - height: 387px; - } - } - - .single-funfacts-box { - text-align: center; - padding: 30px; - - .icon { - position: relative; - left: 0; - line-height: 1; - top: 0; - transform: translateY(0); - font-size: 40px; - display: block; - margin-bottom: 15px; - } - h3 { - margin-bottom: 5px; - font-size: 35px; - } - &.without-bg-color { - margin-right: -15px; - padding: { - top: 5px; - bottom: 5px; - left: 0; - }; - } - &.text-center { - padding: 0; - border-right: none; - } - } - .funfacts-inner { - padding: { - left: 20px; - right: 20px; - }; - &.border-radius { - &.ptb-70 { - padding-bottom: 20px; - } - } - } - - .single-skills-box { - padding: 25px 20px; - } - - .how-it-works-content { - padding-right: 0; - - ul { - li { - padding-left: 60px; - - h3 { - font-size: 18px; - } - i { - font-size: 40px; - } - } - } - } - .how-it-works-image { - margin-top: 30px; - } - - .short-brief-content { - h3 { - font-size: 18px; - margin-bottom: 13px; - } - } - - .screenshot-wrap { - .screenshot-frame { - border-radius: 20px; - width: 215px; - height: 448px; - } - } - .screenshot-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - left: -27px; - width: 30px; - line-height: 30px; - height: 30px; - font-size: 15px; - - &.owl-next { - left: auto; - right: -27px; - } - } - } - } - } - - .single-post-item { - .post-content { - padding: 20px 0 0; - } - } - - .single-clients-box { - a { - padding: 20px; - } - } - - .what-we-do-content { - .section-title { - text-align: center; - margin-bottom: 0; - - p { - max-width: 540px; - margin: { - left: auto; - right: auto; - }; - } - } - .single-what-we-do-box { - margin-top: 30px; - } - } - .single-what-we-do-box { - text-align: center; - - .icon { - font-size: 35px; - margin-bottom: 25px; - } - h3 { - font-size: 18px; - } - } - .what-we-do-image { - margin-top: 30px; - } - - .single-process-box { - padding: 30px 20px; - - h3 { - font-size: 18px; - } - } - - .startup-testimonials-area { - .container-fluid { - max-width: 720px; - } - } - .startup-testimonials-content { - padding: 40px 30px; - - .icon { - font-size: 40px; - } - p { - font-size: 16px; - } - .info { - h3 { - font-size: 16px; - } - } - } - .startup-testimonials-image { - padding: 0; - } - .startup-testimonials-slides { - &.owl-theme { - .owl-nav { - margin-top: 0; - position: relative; - right: 0; - bottom: 0; - transform: unset; - padding-bottom: 40px; - - [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - } - } - } - .marketing-testimonials-image { - margin: { - left: 30px; - right: 30px; - bottom: 40px; - }; - } - .marketing-testimonials-content { - padding-bottom: 0; - - .icon { - font-size: 40px; - } - p { - font-size: 16px; - } - .info { - h3 { - font-size: 16px; - } - } - } - .marketing-testimonials-slides { - &.owl-theme { - .owl-nav { - margin-top: 30px; - position: relative; - right: 0; - bottom: 0; - transform: unset; - - [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - } - } - } - .business-testimonials-area { - background: { - repeat: repeat; - size: contain; - }; - } - .business-testimonials-image { - margin: { - right: 0; - bottom: 35px; - }; - } - .business-testimonials-content { - .icon { - font-size: 40px; - } - p { - font-size: 16px; - } - .info { - h3 { - font-size: 16px; - } - } - } - .testimonials-slides { - &.owl-theme { - padding: { - left: 90px; - right: 90px; - }; - .owl-nav { - [class*=owl-] { - margin: 0 3px; - font-size: 13px; - padding: 6px 20px 6px !important; - } - } - } - } - .testimonials-inner { - padding: { - left: 25px; - right: 25px; - }; - .testimonials-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - font-size: 17px; - width: 40px; - height: 40px; - line-height: 40px; - } - } - } - } - } - .testimonials-item { - .icon { - font-size: 40px; - } - p { - font-size: 16px; - } - .info { - margin-top: 20px; - - .title { - h3 { - font-size: 16px; - } - } - } - } - .saas-testimonials-item { - padding: 25px; - - .info { - .title { - h3 { - font-size: 16px; - } - } - } - p { - font-size: 15px; - } - } - .saas-testimonials-slides { - &.owl-theme { - margin-top: -50px; - padding: { - left: 40px; - right: 40px; - }; - .owl-nav { - [class*=owl-] { - left: 0; - font-size: 18px; - - &.owl-next { - left: auto; - right: 0; - } - } - } - } - } - .testimonials-box { - padding: 25px 20px; - - .icon { - width: 70px; - height: 70px; - font-size: 30px; - line-height: 70px; - } - p { - font-size: 15px; - } - .info { - margin-top: 20px; - - h3 { - font-size: 16px; - } - } - } - - .single-products-box { - .image { - .buttons-list { - li { - transform: translateY(0); - visibility: visible; - opacity: 1; - - a { - .tooltip-label { - &::before { - bottom: -12px; - } - } - &:hover { - .tooltip-label { - top: -40px; - } - } - } - } - } - } - .content { - h3 { - font-size: 18px; - } - .rating { - margin-bottom: 10px; - font-size: 15px; - } - .price { - span { - font-size: 16px; - } - } - } - } - - .trending-style-image { - margin-right: 0; - } - .trending-style-content { - padding-left: 0; - text-align: center; - margin-top: 30px; - - .sub-title { - font-size: 13px; - } - h2 { - font-size: 30px; - } - h3 { - font-size: 17px; - } - } - - .single-pricing-box { - padding: 30px; - - .pricing-header { - margin-bottom: 20px; - padding-bottom: 20px; - - h3 { - font-size: 35px; - } - } - } - .pricing-box { - .pricing-header { - h3 { - font-size: 18px; - } - .price { - font-size: 35px; - } - } - } - .pricing-tabs { - .nav { - .nav-item { - .nav-link { - font-size: 14px; - } - } - } - } - .pricing-item { - .pricing-header { - h3 { - font-size: 18px; - } - .price { - font-size: 35px; - } - } - } - - .single-team-member { - .content { - &::before { - opacity: 1; - visibility: visible; - } - h3 { - font-size: 18px; - transform: translateY(0) !important; - } - span { - transform: translateY(0) !important; - } - .social { - bottom: auto; - opacity: 1; - visibility: visible; - top: -30px; - - li { - a { - font-size: 15px; - } - } - } - } - } - .single-team-box { - .image { - .social { - margin-bottom: 0; - opacity: 1; - visibility: visible; - padding: 20px; - } - } - .content { - margin-top: 20px; - - h3 { - font-size: 18px; - } - } - } - .team-member { - .content { - margin-top: 0; - - &::before { - opacity: 1; - visibility: visible; - } - h3 { - font-size: 18px; - transform: translateY(0) !important; - } - span { - transform: translateY(0) !important; - } - .social { - bottom: auto; - opacity: 1; - visibility: visible; - top: -30px; - - li { - a { - font-size: 15px; - } - } - } - } - } - .team-box { - .content { - visibility: visible; - position: relative; - opacity: 1; - bottom: 0; - - h3 { - font-size: 18px; - } - } - } - - .video-box { - .video-btn { - width: 70px; - height: 70px; - font-size: 25px; - } - } - - .cta-content { - h2 { - font-size: 30px; - } - p { - margin-top: 15px; - } - .default-btn { - margin-top: 10px; - } - } - .call-to-action-content { - text-align: center; - - h2 { - font-size: 30px; - } - } - .call-to-action-btn { - text-align: center; - margin-top: 30px; - } - .call-to-action-inner { - padding: { - left: 25px; - right: 25px; - }; - } - - .blog-section-title { - text-align: center; - margin-bottom: 50px; - - h2 { - font-size: 30px; - } - } - .single-blog-post { - .content { - padding: 20px; - - h3 { - font-size: 18px; - } - } - } - .single-post-item { - .post-content { - .meta { - margin-bottom: 3px; - - li { - padding-left: 20px; - margin: { - right: 10px; - bottom: 10px; - }; - } - } - h3 { - margin-bottom: 12px; - font-size: 18px; - } - .link-btn { - i { - top: 6px; - } - } - } - } - - .app-download-area { - .col-lg-6 { - &:nth-child(1) { - order: 2; - } - &:nth-child(2) { - order: 1; - } - } - } - .app-download-content { - text-align: center; - margin-bottom: 35px; - - h2 { - font-size: 30px; - } - p { - max-width: 540px; - margin: { - left: auto; - right: auto; - }; - } - .btn-box { - .store-btn { - text-align: start; - - span { - font-size: 16px; - } - } - } - } - .app-download-image { - padding-right: 0; - } - - .works-area { - .container-fluid { - max-width: 720px; - } - } - .single-works-item { - .content { - h3 { - font-size: 18px; - } - } - } - - .portfolio-details-desc { - h3 { - font-size: 20px; - } - } - .portfolio-details-info { - padding: 30px; - margin: { - left: 0; - top: 30px; - }; - ul { - li { - padding-left: 38px; - - span { - font-size: 16px; - } - } - } - } - .portfolio-details-slides { - &.owl-theme { - .owl-nav { - opacity: 1; - visibility: visible; - } - } - } - - .page-title-area { - padding: { - top: 100px; - bottom: 110px; - }; - &.style-four { - padding: { - top: 100px; - bottom: 100px; - }; - } - &.style-two { - padding: { - top: 210px; - bottom: 100px; - }; - } - &.style-three { - padding: { - top: 240px; - bottom: 100px; - }; - } - } - .page-title-content { - h2 { - margin-bottom: 0; - font-size: 35px; - } - ul { - margin-top: 15px; - - li { - font-size: 15px; - } - } - } - - .pagination-area { - margin-top: 15px; - - .page-numbers { - width: 40px; - height: 40px; - line-height: 38px; - font-size: 16px; - } - } - - .services-details-desc { - h3 { - font-size: 20px; - } - } - - .faq-accordion { - .accordion-item { - .accordion-button { - font-size: 16px; - } - } - } - - .products-details-desc { - padding-left: 0; - margin-top: 30px; - - h3 { - font-size: 18px; - } - .products-review { - .rating { - font-size: 13px; - } - } - .price { - font-size: 15px; - } - } - .products-details-tabs { - margin-top: 40px; - - .single-tabs-box { - h2 { - font-size: 20px; - } - .inner-box { - h3 { - font-size: 18px; - } - .table-striped { - tbody { - tr { - td { - font-size: 15px; - } - } - } - } - } - } - } - - .cart-table-desc { - table { - tbody { - tr { - td { - white-space: nowrap; - } - } - } - } - } - .cart-totals-desc { - margin-top: 30px; - } - - .billing-details-desc { - h3 { - font-size: 18px; - } - } - .order-details-desc { - margin-top: 30px; - - h3 { - font-size: 18px; - } - } - - .profile-authentication-area { - height: auto; - padding: { - bottom: 100px; - top: 160px; - }; - } - .signin-form { - padding: 35px; - text-align: center; - - form { - .remember-me-wrap { - text-align: start; - } - } - } - .signup-form { - padding: 35px; - text-align: center; - - form { - .remember-me-wrap { - text-align: start; - } - } - } - - .privacy-policy-content { - h3 { - font-size: 18px; - } - } - - .terms-conditions-content { - h3 { - font-size: 18px; - } - } - - .error-content { - h3 { - font-size: 22px; - margin: { - bottom: 15px; - top: 35px; - }; - } - .default-btn { - margin-top: 20px; - } - } - - .coming-soon-image { - height: auto; - background-image: unset !important; - - img { - display: inline-block; - } - } - .coming-soon-content { - height: auto; - padding: { - top: 100px; - bottom: 100px; - }; - h2 { - font-size: 30px; - } - } - - .works-slides-style-one, .works-slides-style-two { - &.owl-theme { - .owl-nav { - opacity: 1; - visibility: visible; - } - } - } - - .blog-details-desc { - .article-content { - h3 { - font-size: 20px; - } - } - } - .comments-area { - .comments-title { - font-size: 20px; - } - .comment-respond { - .comment-reply-title { - font-size: 20px; - } - } - } - - .single-contact-info-box { - padding: 30px; - text-align: center; - - .icon { - font-size: 35px; - margin-bottom: 20px; - } - h3 { - font-size: 18px; - margin-bottom: 12px; - } - &.style-two { - .icon { - width: 70px; - height: 70px; - font-size: 30px; - margin-bottom: 20px; - } - } - } - - .footer-area { - padding-top: 80px; - - &.business-footer { - .footer-top { - margin-bottom: 80px; - padding: { - top: 60px; - bottom: 60px; - }; - } - } - } - .footer-top { - text-align: center; - margin-bottom: 50px; - padding-bottom: 50px; - - .logo { - margin-bottom: 30px; - } - .content { - text-align: start; - - h3 { - font-size: 18px; - } - } - } - .single-footer-widget { - h3 { - font-size: 18px; - } - &.pl-4 { - padding-left: 0; - } - &.pl-2 { - padding-left: 0; - } - &.style-two { - h3 { - padding-left: 0; - - &::before { - display: none; - } - } - } - } - .copyright-area { - margin-top: 50px; - - .text-start { - text-align: center !important; - margin-bottom: 15px; - } - .social-links { - text-align: center; - margin-top: 15px; - } - } - - .productsQuickView { - .modal-dialog { - max-width: 700px; - } - .modal-content { - padding: 20px; - - button { - &.btn-close { - font-size: 15px; - } - } - .products-content { - h3 { - font-size: 18px; - } - .price { - font-size: 15px; - } - } - } - } - -} - -/* Min width 992px to Max width 1199px */ -@media only #{$media} and ($feature_min : $value_five) and ($feature_max : $value_six) { - - body { - font-size: 15px; - } - p { - font-size: 15px; - } - .ptb-100 { - padding: { - top: 90px; - bottom: 90px; - }; - } - .pt-100 { - padding-top: 90px; - } - .pb-100 { - padding-bottom: 90px; - } - .ptb-70 { - padding: { - top: 60px; - bottom: 60px; - }; - } - .pt-70 { - padding-top: 60px; - } - .pb-70 { - padding-bottom: 60px; - } - - .top-header-area { - .container-fluid { - max-width: 960px; - } - &.top-header-style-two { - .container-fluid { - max-width: 960px; - } - } - &.top-header-style-three { - .container-fluid { - max-width: 960px; - } - } - } - - .noke-responsive-nav { - .others-option { - display: flex !important; - } - } - .others-option-for-responsive { - display: none; - } - - .startup-banner-area { - .container-fluid { - max-width: 960px; - } - } - .startup-banner-content { - padding-right: 0; - - h1 { - font-size: 50px; - } - } - .startup-banner-inner { - padding: { - top: 100px; - bottom: 100px; - left: 30px; - right: 30px; - }; - } - .startup-banner-image { - padding-left: 0; - } - .marketing-banner-content { - padding-right: 0; - - h1 { - font-size: 50px; - margin-bottom: 20px; - } - p { - max-width: 100%; - } - } - .business-banner-area { - padding: { - top: 90px; - bottom: 90px; - }; - .container { - max-width: 960px; - } - &::before { - opacity: 0.3; - } - } - .business-banner-content { - padding-right: 0; - max-width: 100%; - - h1 { - font-size: 48px; - margin-bottom: 20px; - } - p { - max-width: 100%; - } - } - .app-banner-area { - .container { - max-width: 990px; - } - } - .app-banner-content { - h1 { - font-size: 50px; - margin-bottom: 20px; - } - p { - max-width: 100%; - } - .btn-box { - margin-top: 20px; - - .store-btn span { - font-size: 16px; - } - } - } - .app-banner-image { - padding-left: 0; - } - .saas-banner-area { - .container-fluid { - max-width: 990px; - padding: { - left: 15px; - right: 15px; - }; - } - } - .saas-banner-content { - margin-left: 0; - max-width: 100%; - - h1 { - font-size: 50px; - margin-bottom: 20px; - } - p { - max-width: 100%; - } - } - .personal-portfolio-banner-area { - .container { - max-width: 960px; - } - } - .personal-portfolio-banner-content { - h1 { - font-size: 52px; - } - h4 { - font-size: 25px; - } - } - .consulting-banner-area { - padding: { - top: 270px; - bottom: 150px; - }; - .container { - max-width: 960px; - } - } - .consulting-banner-content { - max-width: 635px; - - h1 { - font-size: 40px; - margin-bottom: 20px; - } - .default-btn { - margin-top: 5px; - } - } - .it-agency-banner-area { - .it-agency-inner-banner { - padding: { - top: 250px; - bottom: 300px; - }; - } - } - .it-agency-banner-content { - max-width: 100%; - - h1 { - font-size: 50px; - } - } - .banner-video-box { - max-width: 100%; - } - .shop-banner-area { - padding: { - top: 150px; - bottom: 150px; - }; - } - .shop-banner-content { - .sub-title { - font-size: 25px; - } - h1 { - font-size: 50px; - } - h4 { - font-size: 25px; - } - } - .blog-banner-item { - .content { - padding: 20px; - max-width: 480px; - - h3 { - font-size: 22px; - } - } - } - .blog-banner-item-list { - .single-blog-banner-item { - .content { - padding: { - top: 0; - bottom: 0; - left: 15px; - }; - h3 { - margin-bottom: 12px; - font-size: 15px; - } - } - } - } - .furniture-banner-item { - padding: { - top: 150px; - bottom: 150px; - }; - } - .furniture-banner-content { - h1 { - margin-bottom: 15px; - font-size: 40px; - } - } - .minimal-portfolio-banner-area { - padding: { - top: 230px; - bottom: 200px; - }; - } - .minimal-portfolio-banner-content { - max-width: 730px; - - .typewrite { - font-size: 18px; - } - h1 { - font-size: 50px; - margin-bottom: 30px; - } - } - - .single-features-box { - padding-left: 0; - padding-right: 0; - } - .single-features-item { - padding-left: 65px; - } - .features-item { - padding: 25px; - } - - .startup-about-area { - .container-fluid { - max-width: 960px; - } - } - .startup-about-image { - margin-right: 0; - padding: { - top: 60px; - left: 29px; - bottom: 60px; - }; - .video-btn { - left: 55%; - transform: translateY(-50%) translateX(-55%); - } - } - .startup-about-content { - padding-left: 0; - max-width: 100%; - - h2 { - font-size: 38px; - } - } - .marketing-about-image { - margin-right: 0; - } - .marketing-about-content { - padding-left: 0; - } - .business-about-image { - padding-right: 0; - } - .business-about-content { - padding-left: 0; - } - .saas-about-image { - padding-right: 0; - } - .saas-about-content { - padding-left: 0; - } - .it-agency-about-image { - margin-right: 0; - padding: 0; - - .main-image { - padding-bottom: 100px; - - img { - &:nth-child(2) { - max-width: 300px; - } - } - } - } - .it-agency-about-content { - padding-left: 0; - } - - .startup-works-area { - .section-title { - max-width: 650px; - text-align: center; - padding-top: 0; - margin: { - left: auto; - right: auto; - bottom: 60px; - }; - h2 { - font-size: 40px; - } - } - } - .startup-works-content { - padding-top: 0; - padding-right: 0; - } - .startup-works-image { - margin-left: 0; - padding: 0; - } - .startup-works-slides { - .row { - align-items: center; - } - } - - .single-services-item { - .content { - padding: { - left: 0; - right: 0; - }; - } - } - .services-tabs { - .nav { - margin-bottom: 50px; - - .nav-item { - margin: { - left: 16px; - right: 16px; - }; - .nav-link { - font-size: 17px; - } - } - } - } - .single-main-services { - .content { - padding-right: 0; - - h3 { - font-size: 30px; - } - p { - max-width: 100%; - } - } - } - .col-lg-3 { - &:nth-child(2), &:nth-child(4) { - .services-box { - margin-top: 0; - } - } - &:nth-child(5), &:nth-child(7) { - .services-box { - margin-top: 0; - } - } - } - .services-item { - h3 { - font-size: 20px; - } - .hover-content { - padding: 20px; - } - } - - .facility-content { - padding-right: 0; - } - - .single-funfacts-box { - padding: { - top: 30px; - bottom: 30px; - left: 75px; - right: 20px; - }; - .icon { - left: 20px; - font-size: 40px; - } - h3 { - font-size: 35px; - } - .sub-title { - font-size: 14.5px; - } - &.without-bg-color { - margin-right: 0; - padding-left: 60px; - } - } - - .app-screenshot-area { - &::before { - height: 100px; - } - } - .screenshot-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - left: -25px; - width: 35px; - line-height: 35px; - height: 35px; - font-size: 15px; - - &.owl-next { - left: auto; - right: -25px; - } - } - } - } - } - .screenshot-wrap { - .screenshot-frame { - width: 175px; - height: 355px; - } - } - - .categories-box { - h3 { - padding: { - left: 20px; - right: 20px; - }; - } - } - .categories-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - left: -40px; - - &.owl-next { - left: auto; - right: -40px; - } - } - } - } - } - - .startup-testimonials-area { - .container-fluid { - max-width: 960px; - } - } - .startup-testimonials-image { - background-color: transparent; - padding: { - top: 0; - bottom: 23px; - left: 0; - }; - .main-image { - animation: unset !important; - margin-bottom: 15px; - } - } - .startup-testimonials-content { - padding: { - left: 0; - right: 0; - bottom: 30px; - top: 30px; - }; - p { - font-size: 15px; - } - .info { - margin-top: 20px; - padding-top: 20px; - - h3 { - font-size: 17px; - } - } - } - .startup-testimonials-slides { - &.owl-theme { - .owl-nav { - bottom: 0; - right: 80%; - transform: translateX(80%); - } - } - } - .marketing-testimonials-image { - margin: { - right: 30px; - left: 0; - }; - } - .marketing-testimonials-content { - padding-bottom: 50px; - - p { - font-size: 16px; - } - .info { - margin-top: 20px; - - h3 { - font-size: 17px; - } - } - } - .marketing-testimonials-slides { - &.owl-theme { - .owl-nav { - bottom: 40px; - right: 20%; - transform: translateX(20%); - } - } - } - .business-testimonials-image { - margin-right: 0; - } - .testimonials-slides { - &.owl-theme { - padding: { - left: 100px; - right: 100px; - }; - } - } - .business-testimonials-content { - p { - font-size: 18px; - } - } - .saas-testimonials-slides { - &.owl-theme { - margin-top: -65px; - } - } - .testimonials-box { - padding: 20px; - } - .shape1, .shape2, .shape3, .shape4, .shape5, .shape6, .shape7, .shape8, .shape9, .shape16 { - display: none !important; - } - - .page-title-area { - padding: { - top: 100px; - bottom: 110px; - }; - &.style-three { - padding: { - top: 250px; - bottom: 110px; - }; - } - &.style-two { - padding: { - bottom: 100px; - top: 220px; - }; - } - &.style-four { - padding: { - top: 120px; - bottom: 120px; - }; - } - } - - .who-we-are-content { - padding: 25px; - - h2 { - font-size: 20px; - } - .default-btn { - margin-top: 5px; - } - } - - .app-download-area { - .row { - &.align-items-center { - align-items: unset !important; - } - } - } - - .call-to-action-inner { - padding: { - left: 30px; - right: 30px; - }; - } - - .services-details-content { - h2 { - font-size: 38px; - } - } - .services-details-video { - margin-left: 0; - - iframe { - height: 251px; - } - } - - .single-pricing-box { - padding: 30px; - - .pricing-header { - h3 { - font-size: 40px; - } - } - } - - .featured-video-item-list { - .single-featured-video-item { - .content { - padding: { - top: 5px; - bottom: 5px; - left: 15px; - }; - h3 { - font-size: 17px; - } - } - } - } - - .single-clients-box { - a { - padding: 20px; - } - } - - .coming-soon-content { - padding: { - left: 15px; - right: 15px; - }; - #timer { - div { - width: 100px; - height: 100px; - font-size: 30px; - } - } - form { - max-width: 100%; - } - } - - .trending-style-content { - padding-left: 0; - } - .trending-style-image { - padding-right: 80px; - margin-right: 0; - - img { - &:nth-child(2) { - width: 250px; - } - } - } - - .widget-area { - .widget_about_author { - .about-author { - padding: 20px; - } - } - .widget_noke_posts_thumb { - .item { - padding-left: 95px; - - .thumb { - width: 80px; - - .fullimage { - width: 80px; - } - } - .info { - .title { - font-size: 14px; - } - } - } - } - } - - .blog-section-title { - h2 { - font-size: 35px; - } - } - .single-blog-post { - .content { - .meta { - li { - i { - top: 2px; - } - } - } - } - } - .single-post-item { - .post-content { - padding: 20px; - - .meta { - margin-bottom: 3px; - - li { - margin: { - right: 10px; - bottom: 10px; - }; - } - } - } - } - - .portfolio-details-info { - padding: 20px; - } - - .products-details-desc { - padding-left: 0; - } - - .single-contact-info-box { - padding: 25px; - } - - .footer-area { - padding-top: 90px; - } - .footer-top { - padding-bottom: 60px; - margin-bottom: 60px; - } - .single-footer-widget { - &.pl-4 { - padding-left: 0; - } - &.pl-2 { - padding-left: 0; - } - &.style-two { - h3 { - padding-left: 40px; - - &::before { - width: 25px; - } - } - } - } - .copyright-area { - margin-top: 60px; - } - - .productsQuickView { - .modal-dialog { - max-width: 830px; - } - .modal-content { - padding: 25px; - } - } - -} - -/* Min width 1200px to Max width 1355px */ -@media only #{$media} and ($feature_min : $value_seven) and ($feature_max : $value_eight) { - - .top-header-area { - .container-fluid { - max-width: 1140px; - } - &.top-header-style-two { - .container-fluid { - max-width: 1140px; - } - } - &.top-header-style-three { - .container-fluid { - max-width: 1140px; - } - } - } - - .navbar-area { - &.navbar-style-three { - .noke-nav { - .container-fluid { - max-width: 1140px; - } - } - } - &.navbar-style-two { - .noke-nav { - .container-fluid { - max-width: 1140px; - } - } - } - &.navbar-style-four { - .noke-nav { - .container-fluid { - max-width: 1140px; - } - } - } - &.navbar-style-five { - .noke-nav { - .container-fluid { - max-width: 1140px; - } - } - } - } - .noke-nav { - .container-fluid { - max-width: 1140px; - } - } - - .startup-banner-area { - .container-fluid { - max-width: 100%; - } - } - .startup-banner-inner { - padding: 100px 40px; - } - .startup-banner-content { - h1 { - font-size: 55px; - } - } - .marketing-banner-content { - h1 { - font-size: 55px; - } - p { - max-width: 510px; - } - } - .business-banner-area { - .container { - max-width: 1140px; - } - } - .business-banner-content { - padding-right: 0; - max-width: 100%; - - h1 { - font-size: 55px; - } - } - .app-banner-area { - .container { - max-width: 1140px; - } - } - .app-banner-content { - h1 { - font-size: 49px; - } - } - .personal-portfolio-banner-area { - .container { - max-width: 1140px; - } - } - .personal-portfolio-banner-content { - h1 { - font-size: 60px; - } - h4 { - font-size: 30px; - } - } - .consulting-banner-area { - .container { - max-width: 1140px; - } - } - .consulting-banner-content { - max-width: 715px; - - h1 { - font-size: 45px; - } - } - .it-agency-banner-content { - h1 { - font-size: 55px; - } - } - .blog-banner-item-list { - .single-blog-banner-item { - .content { - padding: { - bottom: 3.3px; - top: 3.3px; - }; - h3 { - font-size: 16px; - } - } - } - } - - .startup-about-area { - .container-fluid { - max-width: 100%; - } - } - .it-agency-about-image { - .main-image { - img:nth-child(2) { - max-width: 300px; - } - } - } - - .startup-testimonials-area { - .container-fluid { - max-width: 100%; - } - } - .marketing-testimonials-slides { - &.owl-theme { - .owl-nav { - right: 20%; - bottom: 55px; - transform: translateX(20%); - } - } - } - - .services-tabs { - .nav { - .nav-item { - margin: { - left: 25px; - right: 25px; - }; - } - } - } - .single-main-services { - .content { - h3 { - font-size: 35px; - } - } - } - - .who-we-are-content { - h2 { - font-size: 20px; - } - } - - .app-screenshot-area { - &::before { - height: 150px; - } - } - .screenshot-wrap { - .screenshot-frame { - width: 210px; - height: 430px; - } - } - .screenshot-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - left: -34px; - - &.owl-next { - left: auto; - right: -34px; - } - } - } - } - } - - .services-box { - padding: 30px 20px; - } - - .startup-works-area { - .section-title { - padding-top: 20px; - margin-bottom: -180px; - } - } - .startup-works-content { - padding-top: 210px; - } - - .featured-video-item-list { - .single-featured-video-item { - .content { - padding: { - top: 6px; - bottom: 6px; - }; - h3 { - font-size: 17px; - } - } - } - } - - .call-to-action-content { - h2 { - font-size: 35px; - } - } - - .single-post-item { - .post-content { - .meta { - margin-bottom: 3px; - - li { - margin: { - right: 10px; - bottom: 10px; - }; - } - } - } - } - - .facility-content { - padding-right: 0; - } - - .single-funfacts-box { - &.without-bg-color { - margin-right: 15px; - } - } - -} - -/* Min width 1535px */ -@media only #{$media} and ($feature_min : $value_nine) { - - .top-header-area { - &.top-header-style-two { - .container-fluid { - max-width: 1470px; - } - } - } - - .navbar-area { - &.navbar-style-three { - .noke-nav { - .container-fluid { - max-width: 1450px; - } - } - } - &.navbar-style-four { - .noke-nav { - .container-fluid { - max-width: 1470px; - } - } - } - } - .noke-nav { - .container-fluid { - max-width: 1470px; - } - } - - .startup-banner-area { - .container-fluid { - max-width: 1470px; - } - } - .startup-banner-inner { - padding: { - top: 140px; - bottom: 140px; - left: 120px; - right: 120px; - }; - } - .shape3 { - right: 34.3%; - bottom: 7%; - } - - .startup-about-area { - .container-fluid { - max-width: 1470px; - } - } - .startup-about-image { - padding: { - left: 100px; - bottom: 100px; - }; - } - - .startup-works-image { - padding: { - top: 100px; - right: 100px; - bottom: 100px; - }; - } - .shape6 { - top: 67px; - } - - .startup-testimonials-area { - .container-fluid { - max-width: 1470px; - } - } - .startup-testimonials-image { - padding: { - top: 100px; - left: 100px; - bottom: 100px; - }; - } - .shape7 { - left: 40px; - top: 30px; - } - .shape8 { - bottom: 70px; - left: 70px; - } - .shape9 { - right: 0; - top: 85px; - } - .shape16 { - right: 150px; - } - -} \ No newline at end of file diff --git a/static/assets/css/showMoreItems-theme.min.css b/static/assets/css/showMoreItems-theme.min.css deleted file mode 100644 index 195b28de..00000000 --- a/static/assets/css/showMoreItems-theme.min.css +++ /dev/null @@ -1 +0,0 @@ -.showMoreItemsList{height: auto;overflow: hidden;list-style: none;}.showMoreItemsList .item{float: left;width: 33.33%;box-sizing: border-box;padding: 0 8px;}.showMoreItemsList .item .box{max-width: 400px;margin: 0 auto 30px;}.showMoreItemsList .item .name{text-align: center;}.showMoreItemsList .item .pic img{max-width: 100%}.showMoreItemsList + .button-box{text-align: center;clear: both;margin-bottom: 50px;}.showMoreItemsList + .button-box button{padding: 8px 20px;border:none;box-shadow: 0 0 0;outline: 0;cursor: pointer;background: #db3434; color: #fff; text-align: center; text-decoration: none; width: 158px; border-radius: 30px;}@media screen and (max-width:1280px){.showMoreItemsList .item{width: 50%;}}@media screen and (max-width:600px){.showMoreItemsList .item{width: 100%;}} \ No newline at end of file diff --git a/static/assets/css/style.css b/static/assets/css/style.css deleted file mode 100644 index e1ca08e9..00000000 --- a/static/assets/css/style.css +++ /dev/null @@ -1,15936 +0,0 @@ -/* -@File: Noke Template Styles - -* This file contains the styling for the actual template, this -is the file you need to edit to change the look of the -template. - -This files table contents are outlined below>>>>> - -******************************************* -******************************************* - -** - Default CSS -** - Top Header Area CSS -** - Navbar Area CSS -** - Search Overlay CSS -** - Startup Banner Area CSS -** - Marketing Banner Area CSS -** - Business Banner Area CSS -** - App Banner Area CSS -** - Saas Banner Area CSS -** - Personal Portfolio Banner Area CSS -** - Consulting Banner Area CSS -** - IT Agency Banner Area CSS -** - Shop Banner Area CSS -** - Blog Banner Area CSS -** - Furniture Banner Area CSS -** - Minimal Portfolio Banner Area CSS -** - Features Area CSS -** - About Area CSS -** - Services Area CSS -** - Services Details Area CSS -** - Works Area CSS -** - What We Do Area CSS -** - Categories Area CSS -** - Who We Are Area CSS -** - Video Area CSS -** - Process Area CSS -** - Funfacts Area CSS -** - Products Area CSS -** - Products Details Area CSS -** - Cart Area CSS -** - Checkout Area CSS -** - Testimonials Area CSS -** - Team Area CSS -** - How It Works Area CSS -** - Skills Area CSS -** - Expertise Area CSS -** - CTA Area CSS -** - Timeline/Short Brief Area CSS -** - App Download Area CSS -** - Trending Style Area CSS -** - Pricing Area CSS -** - App Screenshot Area CSS -** - Facility Area CSS -** - Latest Posts Area CSS -** - Partner Area CSS -** - Clients Area CSS -** - Featured Video Area CSS -** - Blog Area CSS -** - Blog Details Area CSS -** - Widget Area CSS -** - Profile Authentication Area CSS -** - Instagram Area CSS -** - Page Title Area CSS -** - Pagination Area CSS -** - Gallery Area CSS -** - Portfolio Details Area CSS -** - Portfolio Details Navigation Area CSS -** - Privacy Policy Area CSS -** - Terms & Conditions Area CSS -** - FAQ Area CSS -** - 404 Error Area CSS -** - Coming Soon Area CSS -** - Contact Area CSS -** - Footer Area CSS -** - Go Top CSS -** - Modal CSS -*/ -/*================================================ -Default CSS -=================================================*/ -@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); -:root { - --fontFamily: 'Manrope', sans-serif; - --headingFontFamily: 'Poppins', sans-serif; - --startupMainColor: #fd5444; - --marketingMainColor: #2f4eac; - --businessMainColor: #5d3dec; - --businessGradientColor: linear-gradient(to right, #4651e6, #5749ed, #693ff3, #7c30f7, #8f11fa); - --appMainColor: #0042bb; - --appGradientColor: linear-gradient(to right bottom, #0042bb, #093ea8, #113a96, #163584, #1a3172); - --saasMainColor: #296dfe; - --saasGradientColor: linear-gradient(to right top, #257ef7, #0094fb, #00a7f5, #00b6ea, #17c3dd); - --personalPortfolioMainColor: #111111; - --itAgencyMainColor: #ff4a17; - --itAgencyGradientColor: linear-gradient(to right, #ff4a17, #fd5d15, #fb6d16, #f87b1a, #f68821); - --consultingMainColor: #4eaf4e; - --shopMainColor: #111111; - --minimalPortfolioColor: #4ab19d; - --optionalColor: #6f6f6f; - --whiteColor: #ffffff; - --blackColor: #2d2d37; - --fontSize: 15.5px; - --transition: .5s; -} - -body { - margin: 0; - padding: 0; - color: var(--blackColor); - font-size: var(--fontSize); - font-family: var(--fontFamily); -} - -.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { - color: var(--blackColor); - font-weight: 600; - font-family: var(--headingFontFamily); -} - -a { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--blackColor); - text-decoration: none; - outline: 0 !important; -} - -a:hover { - color: var(--optionalColor); - text-decoration: none; -} - -:focus { - outline: 0 !important; -} - -.d-table { - width: 100%; - height: 100%; -} - -.d-table-cell { - vertical-align: middle; -} - -img { - max-width: 100%; - height: auto; -} - -p { - color: var(--optionalColor); - margin-bottom: 15px; - line-height: 1.8; - font-weight: 500; - font-size: var(--fontSize); -} - -p:last-child { - margin-bottom: 0; -} - -.ptb-100 { - padding-top: 100px; - padding-bottom: 100px; -} - -.pt-100 { - padding-top: 100px; -} - -.pb-100 { - padding-bottom: 100px; -} - -.ptb-70 { - padding-top: 70px; - padding-bottom: 70px; -} - -.pt-70 { - padding-top: 70px; -} - -.pb-70 { - padding-bottom: 70px; -} - -.bg-f9f4f0 { - background-color: #f9f4f0; -} - -.bg-faf7f2 { - background-color: #faf7f2; -} - -.bg-1b2232 { - background-color: #1b2232; -} - -.bg-f3f4f7 { - background-color: #f3f4f7; -} - -.bg-f9f9f9 { - background-color: #f9f9f9; -} - -.bg-dark { - background-color: #06080e !important; -} - -.bg-dark .section-title { - color: var(--whiteColor); -} - -.bg-dark .section-title h2 { - color: var(--whiteColor); -} - -.bg-dark .section-title p { - color: var(--whiteColor); -} - -.bg-f6f7f9 { - background-color: #f6f7f9; -} - -/*section-title*/ -.section-title { - text-align: center; - max-width: 655px; - margin-bottom: 60px; - margin-left: auto; - margin-right: auto; -} - -.section-title h2 { - margin-bottom: 0; - font-size: 40px; -} - -.section-title p { - max-width: 540px; - margin-top: 15px; - margin-left: auto; - margin-right: auto; -} - -.section-title .sub-title { - display: block; - margin-bottom: 12px; - color: var(--startupMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.section-title .sub-title.marketing-color { - color: var(--marketingMainColor); -} - -.section-title .sub-title.business-color { - color: var(--businessMainColor); -} - -.section-title .sub-title.app-color { - color: var(--appMainColor); -} - -.section-title .sub-title.saas-color { - color: var(--saasMainColor); -} - -.section-title .sub-title.consulting-color { - color: var(--consultingMainColor); -} - -.section-title .sub-title.it-agency-color { - color: var(--itAgencyMainColor); -} - -.section-title .sub-title.minimal-portfolio-color { - color: var(--minimalPortfolioColor); -} - -.section-title.style-two { - text-align: start; - padding-left: 90px; - margin-left: 0; - margin-right: 0; - margin-bottom: 50px; -} - -.section-title.style-two h2 { - position: relative; - font-size: 30px; -} - -.section-title.style-two h2::before { - left: -90px; - top: 50%; - width: 70px; - height: 1px; - content: ''; - position: absolute; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - background-color: var(--personalPortfolioMainColor); -} - -.section-title.style-two p { - margin-left: 0; - margin-right: 0; -} - -.section-title.style-three { - max-width: 100%; -} - -.section-title.style-three h2 { - position: relative; - z-index: 1; -} - -.section-title.style-three h2 span { - display: inline-block; - background-color: var(--whiteColor); - padding-left: 30px; - padding-right: 30px; -} - -.section-title.style-three h2::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - background-color: #cfcfcf; - height: 1px; - z-index: -1; -} - -.section-title.style-four { - border-bottom: 1px solid #eeeeee; - padding-bottom: 12px; - position: relative; - text-align: start; - max-width: 100%; - margin-left: 0; - margin-right: 0; - margin-bottom: 45px; -} - -.section-title.style-four h2 { - font-size: 30px; -} - -.section-title.style-four::before { - content: ''; - position: absolute; - left: 0; - bottom: -1px; - width: 70px; - height: 1px; - background-color: var(--blackColor); -} - -/*default-btn*/ -.default-btn { - -webkit-box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - -webkit-transition: var(--transition); - transition: var(--transition); - display: inline-block; - background-color: var(--startupMainColor); - color: var(--whiteColor); - position: relative; - border-radius: 5px; - border: none; - padding: 13px 50px 13px 35px; - font-size: var(--fontSize); - font-weight: 700; -} - -.default-btn i { - position: absolute; - right: 35px; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - margin-top: 1px; - font-size: 14px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.default-btn:hover { - color: var(--whiteColor); - background-color: var(--startupMainColor); - -webkit-box-shadow: 0 0.5rem 1.2rem rgba(0, 0, 0, 0.1), 0 2rem 3rem rgba(0, 0, 0, 0.15); - box-shadow: 0 0.5rem 1.2rem rgba(0, 0, 0, 0.1), 0 2rem 3rem rgba(0, 0, 0, 0.15); -} - -.default-btn:hover i { - right: 30px; -} - -.default-btn.marketing-color { - background-color: var(--marketingMainColor); - color: var(--whiteColor); -} - -.default-btn.marketing-color:hover { - color: var(--whiteColor); - background-color: var(--marketingMainColor); -} - -.default-btn.business-color { - background: var(--businessGradientColor); -} - -.default-btn.app-color { - background: var(--appGradientColor); -} - -.default-btn.saas-color { - background: var(--saasGradientColor); -} - -.default-btn.personal-portfolio-color { - background: var(--personalPortfolioMainColor); -} - -.default-btn.consulting-color { - background: var(--consultingMainColor); -} - -.default-btn.it-agency-color { - background: var(--itAgencyGradientColor); -} - -.default-btn.shop-color { - background: var(--shopMainColor); -} - -.default-btn.minimal-portfolio-color { - background: var(--minimalPortfolioColor); -} - -/*form-control*/ -.form-control { - border-radius: 0; - background-color: #f5f5f5 !important; - -webkit-box-shadow: unset !important; - box-shadow: unset !important; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none !important; - height: 50px; - padding-left: 15px; - color: var(--blackColor); - font-size: var(--fontSize); - font-weight: 500; -} - -.form-control::-webkit-input-placeholder { - color: #999999; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.form-control:-ms-input-placeholder { - color: #999999; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.form-control::-ms-input-placeholder { - color: #999999; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.form-control::placeholder { - color: #999999; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.form-control:focus::-webkit-input-placeholder { - color: transparent; -} - -.form-control:focus:-ms-input-placeholder { - color: transparent; -} - -.form-control:focus::-ms-input-placeholder { - color: transparent; -} - -.form-control:focus::placeholder { - color: transparent; -} - -textarea.form-control { - padding-top: 15px; - height: auto; -} - -/*================================================ -Top Header Area CSS -=================================================*/ -.header-area.p-absolute { - position: absolute; - left: 0; - right: 0; - top: 0; - height: auto; - z-index: 3; -} - -.header-area.p-absolute .navbar-area { - background-color: transparent; -} - -.top-header-area { - background-color: var(--startupMainColor); - padding-top: 12px; - padding-bottom: 12px; -} - -.top-header-area .container-fluid { - max-width: 1400px; -} - -.top-header-area.business-color { - background: var(--businessGradientColor); -} - -.top-header-area.saas-color { - background: var(--saasGradientColor); -} - -.top-header-area.top-header-style-two .container-fluid { - max-width: 1470px; -} - -.top-header-area.top-header-style-three .container-fluid { - max-width: 1450px; -} - -.top-header-area.it-agency-color { - background: var(--itAgencyGradientColor); -} - -.top-header-area.bg-f3f4f7 { - background: #f3f4f7; -} - -.top-header-area.bg-f3f4f7 .top-header-social-links li a { - color: var(--blackColor); -} - -.top-header-area.bg-f3f4f7 .top-header-social-links li a:hover { - color: var(--startupMainColor); -} - -.top-header-area.bg-f3f4f7 .top-header-contact-info { - color: var(--blackColor); -} - -.top-header-area.bg-f3f4f7 .top-header-contact-info .number { - color: var(--blackColor); -} - -.top-header-area.bg-f3f4f7 .top-header-contact-info .email { - color: var(--blackColor); -} - -.top-header-area.bg-f3f4f7 .top-header-contact-info .lang-switcher label { - color: var(--startupMainColor); -} - -.top-header-area.bg-f3f4f7 .top-header-contact-info .lang-switcher select { - color: var(--blackColor); -} - -.top-header-area.bg-f3f4f7 .top-header-contact-info .lang-switcher select option { - color: var(--blackColor); -} - -.top-header-area.bg-f3f4f7.consulting-color .top-header-social-links li a:hover { - color: var(--consultingMainColor); -} - -.top-header-area.bg-f3f4f7.consulting-color .top-header-contact-info .lang-switcher label { - color: var(--consultingMainColor); -} - -.top-header-area.bg-f3f4f7.consulting-color .top-header-info .number i { - color: var(--consultingMainColor); -} - -.top-header-area.bg-f3f4f7.consulting-color .top-header-info .email i { - color: var(--consultingMainColor); -} - -.top-header-area.shop-color { - background: var(--shopMainColor); -} - -.top-header-info .number { - display: inline-block; - position: relative; - padding-left: 25px; -} - -.top-header-info .number i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); -} - -.top-header-info .email { - display: inline-block; - position: relative; - padding-left: 25px; - margin-right: 20px; -} - -.top-header-info .email i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); -} - -.top-header-social-links { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.top-header-social-links li { - display: inline-block; - margin-right: 15px; -} - -.top-header-social-links li a { - display: block; - color: var(--whiteColor); -} - -.top-header-social-links li a:hover { - opacity: 0.8; -} - -.top-header-social-links li:last-child { - margin-right: 0; -} - -.top-header-contact-info { - color: var(--whiteColor); -} - -.top-header-contact-info .number { - display: inline-block; - color: var(--whiteColor); - position: relative; - padding-left: 25px; -} - -.top-header-contact-info .number i { - position: absolute; - left: 0; - top: 4px; -} - -.top-header-contact-info .email { - display: inline-block; - color: var(--whiteColor); - position: relative; - padding-left: 25px; - margin-right: 20px; -} - -.top-header-contact-info .email i { - position: absolute; - left: 0; - top: 4px; -} - -.top-header-contact-info .lang-switcher { - display: inline-block; - position: relative; - margin-left: 20px; - padding-left: 20px; -} - -.top-header-contact-info .lang-switcher label { - display: block; - margin-bottom: 0; - position: absolute; - left: 0; - top: 1px; -} - -.top-header-contact-info .lang-switcher select { - cursor: pointer; - background-color: transparent; - color: var(--whiteColor); - border: none; -} - -.top-header-contact-info .lang-switcher select option { - color: var(--blackColor); -} - -/*================================================ -Navbar Area CSS -=================================================*/ -.navbar-area { - padding: 5px 0; - position: relative; - background-color: var(--whiteColor); -} - -.navbar-area.p-absolute { - position: absolute; - left: 0; - right: 0; - top: 0; - height: auto; - z-index: 9; -} - -.navbar-area.is-sticky { - top: 0; - left: 0; - width: 100%; - z-index: 999; - position: fixed; - background-color: var(--whiteColor) !important; - -webkit-box-shadow: 0 2px 28px 0 rgba(0, 0, 0, 0.09); - box-shadow: 0 2px 28px 0 rgba(0, 0, 0, 0.09); - -webkit-animation: 500ms ease-in-out 0s normal none 1 running fadeInDown; - animation: 500ms ease-in-out 0s normal none 1 running fadeInDown; -} - -.navbar-area.is-sticky.bg-dark { - background-color: #06080e !important; - -webkit-box-shadow: 0 2px 28px 0 rgba(255, 255, 255, 0.05); - box-shadow: 0 2px 28px 0 rgba(255, 255, 255, 0.05); -} - -.navbar-area.bg-f9faff { - background-color: #f9faff; -} - -.navbar-area.navbar-style-two .noke-nav .container-fluid { - max-width: 1400px; -} - -.navbar-area.navbar-style-three .noke-nav .container-fluid { - max-width: 1450px; -} - -.navbar-area.navbar-style-four .noke-nav .container-fluid { - max-width: 1470px; -} - -.navbar-area.navbar-style-five { - padding-top: 0; - padding-bottom: 0; -} - -.navbar-area.navbar-style-five .noke-nav .container-fluid { - max-width: 1450px; -} - -.navbar-area.navbar-style-five .noke-nav .navbar { - background-color: var(--whiteColor); - padding-left: 25px; - padding-right: 25px; -} - -.navbar-area.bg-dark { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item a { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item a:hover, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item a:focus, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item:hover a, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .navbar-area.bg-dark .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .option-item .search-icon { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .option-item .social-links li a { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .option-item .social-links li a:hover { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search::-webkit-input-placeholder { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search:-ms-input-placeholder { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search::-ms-input-placeholder { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search::placeholder { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search:focus::-webkit-input-placeholder { - color: transparent; -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search:focus:-ms-input-placeholder { - color: transparent; -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search:focus::-ms-input-placeholder { - color: transparent; -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box .input-search:focus::placeholder { - color: transparent; -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box button { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .search-box button:hover { - color: var(--startupMainColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .lang-switcher { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .lang-switcher select { - color: var(--whiteColor); -} - -.navbar-area.bg-dark .noke-nav .navbar .others-option .lang-switcher select option { - color: var(--blackColor); -} - -.navbar-area.bg-dark .others-option-for-responsive .dot-menu .inner .circle { - background-color: var(--whiteColor); -} - -.navbar-area.bg-dark .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--startupMainColor); -} - -.navbar-area.color-white.is-sticky { - background-color: #06080e !important; - -webkit-box-shadow: 0 2px 28px 0 rgba(255, 255, 255, 0.05); - box-shadow: 0 2px 28px 0 rgba(255, 255, 255, 0.05); -} - -.noke-responsive-nav { - display: none; -} - -.noke-nav .container-fluid { - max-width: 1400px; -} - -.noke-nav .navbar { - padding: 0; - position: inherit; -} - -.noke-nav .navbar .navbar-brand { - font-size: inherit; - line-height: 1; - padding: 0; -} - -.noke-nav .navbar ul { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; -} - -.noke-nav .navbar .navbar-nav { - margin-left: auto; - position: relative; -} - -.noke-nav .navbar .navbar-nav .nav-item { - position: relative; - margin-left: 15px; - margin-right: 15px; -} - -.noke-nav .navbar .navbar-nav .nav-item a { - position: relative; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); - font-weight: 700; - font-size: var(--fontSize); - padding-left: 0; - padding-right: 0; - padding-top: 30px; - padding-bottom: 30px; -} - -.noke-nav .navbar .navbar-nav .nav-item a:hover, .noke-nav .navbar .navbar-nav .nav-item a:focus, .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-toggle { - padding-right: 14px; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-toggle::after { - display: none; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-toggle::before { - -webkit-font-smoothing: antialiased; - text-rendering: auto; - position: absolute; - content: "\f078"; - line-height: 1; - top: 37px; - right: 0; - font-family: "Font Awesome 5 Free"; - -webkit-font-variant-ligatures: normal; - font-variant-ligatures: normal; - font-variant-caps: normal; - font-style: normal; - font-weight: 900; - font-size: 10px; -} - -.noke-nav .navbar .navbar-nav .nav-item:last-child { - margin-right: 0; -} - -.noke-nav .navbar .navbar-nav .nav-item:first-child { - margin-left: 0; -} - -.noke-nav .navbar .navbar-nav .nav-item:hover a, .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu { - left: 0; - top: 80px; - opacity: 0; - z-index: 99; - border: none; - width: 250px; - margin-top: 0; - display: block; - padding: 10px 0; - border-radius: 0; - position: absolute; - visibility: hidden; - background: var(--whiteColor); - -webkit-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - -webkit-box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.1); - box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.1); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li { - margin: 0; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - display: block; - padding: 9px 20px; - position: relative; - color: var(--blackColor); - font-size: 14.5px; - font-weight: 600; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a i { - top: 50%; - margin: 0; - right: 15px; - font-size: 11px; - position: absolute; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus { - color: var(--startupMainColor); - padding-left: 25px; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu { - top: 0; - opacity: 0; - left: auto; - right: 250px; - margin-top: 0; - visibility: hidden; - -webkit-transform: translateX(0); - transform: translateX(0); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); - padding-left: 20px; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus { - color: var(--startupMainColor); - padding-left: 25px; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu { - top: 0; - opacity: 0; - right: 250px; - visibility: hidden; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); - padding-left: 20px; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus { - padding-left: 25px; - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu { - top: 0; - opacity: 0; - right: 250px; - visibility: hidden; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - padding-left: 20px; - color: var(--blackColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus { - color: var(--startupMainColor); - padding-left: 25px; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li:hover .dropdown-menu { - opacity: 1; - visibility: visible; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li:hover .dropdown-menu { - opacity: 1; - visibility: visible; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li:hover .dropdown-menu { - opacity: 1; - visibility: visible; -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li:hover .dropdown-menu { - opacity: 1; - margin-top: 0; - visibility: visible; -} - -.noke-nav .navbar .navbar-nav .nav-item:hover .dropdown-menu { - opacity: 1; - visibility: visible; -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu { - position: unset; -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu { - left: 0; - top: auto; - width: 100%; - padding: 25px; - margin-top: 0; - position: absolute; - -webkit-transform: unset !important; - transform: unset !important; -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title { - position: relative; - border-bottom: 1px solid #eeeeee; - padding-bottom: 8px; - text-transform: uppercase; - margin-bottom: 25px; - margin-top: 30px; - font-size: var(--fontSize); - font-weight: 600; -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - width: 50px; - height: 1px; - content: ''; - position: absolute; - left: 0; - bottom: -1px; - background-color: var(--startupMainColor); -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title:first-child { - margin-top: 0; -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu a { - border-bottom: none !important; -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a { - padding: 0; - margin-top: 18px; - color: var(--blackColor); -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a .count { - float: right; - width: 20px; - height: 20px; - display: block; - font-size: 12px; - line-height: 20px; - text-align: center; - border-radius: 5px; - background-color: #f4f4f4; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--startupMainColor); -} - -.noke-nav .navbar .others-option { - margin-left: 30px; -} - -.noke-nav .navbar .others-option .option-item { - position: relative; - margin-left: 15px; - top: 1px; -} - -.noke-nav .navbar .others-option .option-item:first-child { - margin-left: 0; -} - -.noke-nav .navbar .others-option .option-item .search-icon { - line-height: 1; - font-size: 18px; - cursor: pointer; - display: inline-block; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); - border-left: 1px solid rgba(0, 0, 0, 0.2); - border-right: 1px solid rgba(0, 0, 0, 0.2); - padding-left: 15px; - padding-right: 15px; -} - -.noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--startupMainColor); -} - -.noke-nav .navbar .others-option .option-item .social-links { - position: relative; - top: -1px; -} - -.noke-nav .navbar .others-option .option-item .social-links span { - display: inline-block; - margin-left: 15px; - line-height: 1; -} - -.noke-nav .navbar .others-option .option-item .social-links span a { - font-size: 17px; - display: block; -} - -.noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--startupMainColor); -} - -.noke-nav .navbar .others-option .option-item .social-links span:first-child { - margin-left: 0; -} - -.noke-nav .navbar .others-option .option-item .link-btn { - display: inline-block; - font-size: 18px; - line-height: 1; -} - -.noke-nav .navbar .others-option .option-item .icon-btn { - position: relative; - font-size: 18px; - line-height: 1; - padding-right: 10px; - margin-left: 5px; -} - -.noke-nav .navbar .others-option .option-item .icon-btn span { - display: inline-block; - position: absolute; - right: 0; - top: -5px; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: red; - text-align: center; - line-height: 17px; - color: var(--whiteColor); - font-weight: 700; - font-size: 9px; -} - -.noke-nav .navbar .others-option .search-box { - position: relative; - width: 200px; -} - -.noke-nav .navbar .others-option .search-box .input-search { - display: block; - width: 100%; - background-color: transparent; - border: none; - border-bottom: 1px solid #eeeeee; - height: auto; - padding-bottom: 8px; - font-size: 14px; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.noke-nav .navbar .others-option .search-box .input-search::-webkit-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.noke-nav .navbar .others-option .search-box .input-search:-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.noke-nav .navbar .others-option .search-box .input-search::-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.noke-nav .navbar .others-option .search-box .input-search::placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--startupMainColor); -} - -.noke-nav .navbar .others-option .search-box .input-search:focus::-webkit-input-placeholder { - color: transparent; -} - -.noke-nav .navbar .others-option .search-box .input-search:focus:-ms-input-placeholder { - color: transparent; -} - -.noke-nav .navbar .others-option .search-box .input-search:focus::-ms-input-placeholder { - color: transparent; -} - -.noke-nav .navbar .others-option .search-box .input-search:focus::placeholder { - color: transparent; -} - -.noke-nav .navbar .others-option .search-box button { - position: absolute; - right: 0; - top: 0; - background-color: transparent; - border: none; - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.noke-nav .navbar .others-option .search-box button:hover { - color: var(--startupMainColor); -} - -.noke-nav .navbar .others-option .lang-switcher { - position: relative; - padding-left: 34px; - position: relative; - top: -2px; - border-left: 1px solid rgba(0, 0, 0, 0.2); -} - -.noke-nav .navbar .others-option .lang-switcher label { - display: block; - margin-bottom: 0; - position: absolute; - left: 15px; - top: 0; -} - -.noke-nav .navbar .others-option .lang-switcher select { - background-color: transparent; - cursor: pointer; - border: none; - margin-top: -3px; -} - -.others-option-for-responsive { - display: none; -} - -.others-option-for-responsive .dot-menu { - top: -37px; - right: 60px; - height: 30px; - z-index: 999; - padding: 0 10px; - cursor: pointer; - position: absolute; -} - -.others-option-for-responsive .dot-menu .inner { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - height: 30px; -} - -.others-option-for-responsive .dot-menu .inner .circle { - height: 5px; - width: 5px; - border-radius: 100%; - margin: 0 2px; - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--blackColor); -} - -.others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--startupMainColor); -} - -.others-option-for-responsive .container { - position: relative; -} - -.others-option-for-responsive .container .container { - right: 0; - top: 15px; - opacity: 0; - z-index: 2; - max-width: 300px; - margin-left: auto; - visibility: hidden; - position: absolute; - -webkit-transform: scaleX(0); - transform: scaleX(0); - -webkit-transition: var(--transition); - transition: var(--transition); - padding-left: 15px; - padding-right: 15px; -} - -.others-option-for-responsive .container .container.active { - opacity: 1; - visibility: visible; - -webkit-transform: scaleX(1); - transform: scaleX(1); -} - -.others-option-for-responsive .option-inner { - padding: 15px 20px; - -webkit-box-shadow: 0 15px 40px rgba(0, 0, 0, 0.09); - box-shadow: 0 15px 40px rgba(0, 0, 0, 0.09); - background-color: var(--whiteColor); - text-align: center; -} - -.others-option-for-responsive .option-inner .others-option .option-item { - position: relative; - margin-left: 20px; - display: inline-block; -} - -.others-option-for-responsive .option-inner .others-option .option-item:first-child { - margin-left: 0; -} - -.others-option-for-responsive .option-inner .others-option .option-item .search-icon { - line-height: 1; - font-size: 18px; - cursor: pointer; - display: inline-block; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); - border-right: 1px solid rgba(0, 0, 0, 0.2); - padding-right: 20px; -} - -.others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--startupMainColor); -} - -.others-option-for-responsive .option-inner .others-option .option-item .social-links { - position: relative; - top: -1px; -} - -.others-option-for-responsive .option-inner .others-option .option-item .social-links span { - display: inline-block; - margin-left: 15px; - line-height: 1; -} - -.others-option-for-responsive .option-inner .others-option .option-item .social-links span a { - font-size: 17px; - display: block; -} - -.others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--startupMainColor); -} - -.others-option-for-responsive .option-inner .others-option .option-item .social-links span:first-child { - margin-left: 0; -} - -.others-option-for-responsive .option-inner .others-option .search-box { - position: relative; - width: 100%; -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search { - display: block; - width: 100%; - background-color: transparent; - border: none; - border-bottom: 1px solid #eeeeee; - height: auto; - padding-bottom: 8px; - font-size: 14px; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search::-webkit-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search:-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search::-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search::placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--startupMainColor); -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search:focus::-webkit-input-placeholder { - color: transparent; -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search:focus:-ms-input-placeholder { - color: transparent; -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search:focus::-ms-input-placeholder { - color: transparent; -} - -.others-option-for-responsive .option-inner .others-option .search-box .input-search:focus::placeholder { - color: transparent; -} - -.others-option-for-responsive .option-inner .others-option .search-box button { - position: absolute; - right: 0; - top: 0; - background-color: transparent; - border: none; - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--startupMainColor); -} - -.others-option-for-responsive .option-inner .others-option .lang-switcher { - top: -2px; - position: relative; - padding-left: 25px; - position: relative; - margin-left: -10px; - border-left: 1px solid rgba(0, 0, 0, 0.2); -} - -.others-option-for-responsive .option-inner .others-option .lang-switcher label { - display: block; - margin-bottom: 0; - position: absolute; - left: 10px; - top: 0; -} - -.others-option-for-responsive .option-inner .others-option .lang-switcher select { - background-color: transparent; - cursor: pointer; - border: none; - margin-top: -3px; -} - -.others-option-for-responsive .option-inner .others-option .link-btn { - display: inline-block; - font-size: 18px; - line-height: 1; -} - -.others-option-for-responsive .option-inner .others-option .icon-btn { - position: relative; - font-size: 18px; - line-height: 1; - padding-right: 10px; - margin-left: 5px; -} - -.others-option-for-responsive .option-inner .others-option .icon-btn span { - display: inline-block; - position: absolute; - right: 0; - top: -5px; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: red; - text-align: center; - line-height: 17px; - color: var(--whiteColor); - font-weight: 700; - font-size: 9px; -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .marketing-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .marketing-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .marketing-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .marketing-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--marketingMainColor); -} - -.marketing-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--marketingMainColor); -} - -.marketing-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--marketingMainColor); -} - -.marketing-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--marketingMainColor); -} - -.marketing-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--marketingMainColor); -} - -.marketing-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--marketingMainColor); -} - -.marketing-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--marketingMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .business-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .business-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .business-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .business-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--businessMainColor); -} - -.business-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--businessMainColor); -} - -.business-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--businessMainColor); -} - -.business-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--businessMainColor); -} - -.business-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--businessMainColor); -} - -.business-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--businessMainColor); -} - -.business-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--businessMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .app-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .app-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .app-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .app-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--appMainColor); -} - -.app-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--appMainColor); -} - -.app-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--appMainColor); -} - -.app-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--appMainColor); -} - -.app-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--appMainColor); -} - -.app-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--appMainColor); -} - -.app-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--appMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .personal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--personalPortfolioMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item a:hover, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item a:focus, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item:hover a, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .saas-color.bg-dark .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--saasMainColor); -} - -.saas-color.bg-dark .noke-nav .navbar .others-option .search-box button:hover { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--saasMainColor); -} - -.saas-color.bg-dark .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .others-option-for-responsive .option-inner .others-option .option-item .social-links li a:hover { - color: var(--saasMainColor); -} - -.saas-color.bg-dark .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--saasMainColor); -} - -.saas-color.bg-dark .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--saasMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item a { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item a:hover, .color-white .noke-nav .navbar .navbar-nav .nav-item a:focus, .color-white .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item:hover a, .color-white .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .color-white .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .others-option .option-item .search-icon { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .others-option .option-item .social-links li a { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .option-item .social-links li a:hover { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search::-webkit-input-placeholder { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search:-ms-input-placeholder { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search::-ms-input-placeholder { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search::placeholder { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search:focus::-webkit-input-placeholder { - color: transparent; -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search:focus:-ms-input-placeholder { - color: transparent; -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search:focus::-ms-input-placeholder { - color: transparent; -} - -.color-white .noke-nav .navbar .others-option .search-box .input-search:focus::placeholder { - color: transparent; -} - -.color-white .noke-nav .navbar .others-option .search-box button { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .search-box button:hover { - color: var(--startupMainColor); -} - -.color-white .noke-nav .navbar .others-option .lang-switcher { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .lang-switcher select { - color: var(--whiteColor); -} - -.color-white .noke-nav .navbar .others-option .lang-switcher select option { - color: var(--blackColor); -} - -.color-white .others-option-for-responsive .dot-menu .inner .circle { - background-color: var(--whiteColor); -} - -.color-white .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--startupMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item a { - color: var(--whiteColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item a:hover, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item a:focus, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item:hover a, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .it-agency-color.color-white .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .others-option .option-item .search-icon { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .others-option .option-item .social-links li a:hover { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .noke-nav .navbar .others-option .search-box button:hover { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--itAgencyMainColor); -} - -.it-agency-color.color-white .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--itAgencyMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .consulting-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .consulting-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .consulting-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .consulting-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--consultingMainColor); -} - -.consulting-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--consultingMainColor); -} - -.consulting-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--consultingMainColor); -} - -.consulting-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--consultingMainColor); -} - -.consulting-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--consultingMainColor); -} - -.consulting-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--consultingMainColor); -} - -.consulting-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--consultingMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .shop-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .shop-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .shop-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .shop-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--shopMainColor); -} - -.shop-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--shopMainColor); -} - -.shop-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--shopMainColor); -} - -.shop-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--shopMainColor); -} - -.shop-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--shopMainColor); -} - -.shop-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--shopMainColor); -} - -.shop-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--shopMainColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .blog-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .blog-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .blog-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .blog-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--blackColor); -} - -.blog-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--blackColor); -} - -.blog-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--blackColor); -} - -.blog-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--blackColor); -} - -.blog-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--blackColor); -} - -.blog-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--blackColor); -} - -.blog-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--blackColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item a:hover, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item a:focus, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item a.active { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item:hover a, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.active a { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a { - color: var(--blackColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:hover, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a:focus, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li a.active { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a { - color: var(--blackColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:hover, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a:focus, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li a.active { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu li.active a { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li.active a { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item .dropdown-menu li.active a { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .minimal-portfolio-color .noke-nav .navbar .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .others-option .option-item .search-icon:hover { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .others-option .option-item .social-links span a:hover { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .others-option .search-box .input-search:focus { - border-color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .noke-nav .navbar .others-option .search-box button:hover { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .others-option-for-responsive .dot-menu:hover .inner .circle { - background-color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .others-option-for-responsive .option-inner .others-option .option-item .search-icon:hover { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .others-option-for-responsive .option-inner .others-option .option-item .social-links span a:hover { - color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .others-option-for-responsive .option-inner .others-option .search-box .input-search:focus { - border-color: var(--minimalPortfolioColor); -} - -.minimal-portfolio-color .others-option-for-responsive .option-inner .others-option .search-box button:hover { - color: var(--minimalPortfolioColor); -} - -@media only screen and (max-width: 1199px) { - .header-area.p-absolute .navbar-area.navbar-style-five { - border-bottom: none; - background-color: var(--whiteColor); - } - .navbar-area { - padding-top: 15px; - padding-bottom: 15px; - } - .navbar-area.is-sticky { - padding-top: 15px; - padding-bottom: 15px; - } - .navbar-area.bg-dark { - border-bottom: 1px solid rgba(255, 255, 255, 0.07); - } - .navbar-area.bg-dark .noke-responsive-nav .mean-container a.meanmenu-reveal { - color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .mean-container a.meanmenu-reveal span { - background: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box .input-search { - border-bottom: 1px solid #6e6a6a; - color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box .input-search::-webkit-input-placeholder { - color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box .input-search:-ms-input-placeholder { - color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box .input-search::-ms-input-placeholder { - color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box .input-search::placeholder { - color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box button { - color: var(--whiteColor); - } - .navbar-area.bg-dark .noke-responsive-nav .others-option .search-box button:hover { - color: var(--whiteColor); - } - .navbar-area.color-white { - border-bottom: 1px solid rgba(255, 255, 255, 0.07); - } - .navbar-area.color-white .noke-responsive-nav .mean-container a.meanmenu-reveal { - color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .mean-container a.meanmenu-reveal span { - background: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box .input-search { - border-bottom: 1px solid #6e6a6a; - color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box .input-search::-webkit-input-placeholder { - color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box .input-search:-ms-input-placeholder { - color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box .input-search::-ms-input-placeholder { - color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box .input-search::placeholder { - color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box button { - color: var(--whiteColor); - } - .navbar-area.color-white .noke-responsive-nav .others-option .search-box button:hover { - color: var(--whiteColor); - } - .navbar-area.navbar-style-five { - border-bottom: 1px solid #eeeeee; - padding-top: 15px; - padding-bottom: 15px; - } - .noke-responsive-nav { - display: block; - } - .noke-responsive-nav .noke-responsive-menu { - position: relative; - } - .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav { - margin-top: 63px; - background-color: var(--whiteColor); - } - .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul { - font-size: 14px; - border: none !important; - } - .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a { - color: var(--blackColor); - border-top-color: #DBEEFD; - text-transform: unset; - font-weight: 500; - } - .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a i { - display: none; - } - .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.mean-expand { - width: 50%; - height: 28px; - text-align: end; - padding: 11px !important; - background: transparent !important; - border-left: none !important; - border-bottom: none !important; - } - .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--startupMainColor); - } - .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li li a { - font-size: 14px; - } - .noke-responsive-nav .noke-responsive-menu.mean-container .navbar-nav { - height: 301px; - overflow-y: scroll; - -webkit-box-shadow: 0 7px 13px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 7px 13px 0 rgba(0, 0, 0, 0.1); - } - .noke-responsive-nav .mean-container a.meanmenu-reveal { - top: 0; - padding: 0; - width: 35px; - height: 30px; - padding-top: 13px; - color: var(--blackColor); - font-family: var(--fontFamily); - } - .noke-responsive-nav .mean-container a.meanmenu-reveal span { - background: var(--blackColor); - height: 4px; - margin-top: -6px; - border-radius: 3px; - position: relative; - top: 8px; - } - .noke-responsive-nav .mean-container .mean-bar { - background: transparent; - position: absolute; - z-index: 999; - padding: 0; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .nav-item { - width: 90%; - overflow: hidden; - padding: 1em 5% 5px; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .nav-item .row { - display: block; - -ms-flex-wrap: unset; - flex-wrap: unset; - margin-left: 0; - margin-right: 0; - margin-top: -20px; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .nav-item .row .col { - -ms-flex-preferred-size: unset; - flex-basis: unset; - -webkit-box-flex: unset; - -ms-flex-positive: unset; - flex-grow: unset; - max-width: 100%; - padding-left: 0; - padding-right: 0; - padding-top: 20px; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .nav-item .row .col:last-child { - margin-bottom: 15px; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .nav-item a { - width: 100%; - float: unset; - display: block; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .nav-item a.p-0 { - border: none !important; - margin-top: 15px; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title { - position: relative; - padding-bottom: 8px; - color: var(--blackColor); - text-transform: uppercase; - border-bottom: 1px solid #eeeeee; - margin-bottom: 5px; - margin-top: 25px; - margin-left: 0; - margin-right: 0; - font-size: 12.5px; - font-weight: 600; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - width: 30px; - height: 1px; - content: ''; - position: absolute; - left: 0; - bottom: -1px; - background-color: var(--startupMainColor); - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title:first-child { - margin-top: 0; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu a { - border-bottom: none !important; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .mean-expand { - display: none !important; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu { - width: auto; - overflow: hidden; - display: block !important; - padding-left: 0; - padding-right: 0; - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a { - padding: 0; - border: none; - margin-top: 15px; - font-weight: 500; - color: var(--blackColor); - } - .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--startupMainColor); - } - .noke-responsive-nav .dropdown-toggle::after { - display: none !important; - } - .noke-responsive-nav .others-option { - display: none !important; - position: absolute; - right: 60px; - top: 16px; - } - .noke-responsive-nav .others-option .option-item { - position: relative; - margin-left: 20px; - display: inline-block; - } - .noke-responsive-nav .others-option .option-item:first-child { - margin-left: 0; - } - .noke-responsive-nav .others-option .option-item .search-icon { - line-height: 1; - font-size: 18px; - cursor: pointer; - display: inline-block; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); - border-right: 1px solid rgba(0, 0, 0, 0.2); - padding-right: 20px; - } - .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--startupMainColor); - } - .noke-responsive-nav .others-option .option-item .social-links { - position: relative; - top: -1px; - } - .noke-responsive-nav .others-option .option-item .social-links span { - display: inline-block; - margin-left: 15px; - line-height: 1; - } - .noke-responsive-nav .others-option .option-item .social-links span a { - font-size: 17px; - display: block; - } - .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--startupMainColor); - } - .noke-responsive-nav .others-option .option-item .social-links span:first-child { - margin-left: 0; - } - .noke-responsive-nav .others-option .search-box { - position: relative; - width: 250px; - top: -7px; - } - .noke-responsive-nav .others-option .search-box .input-search { - display: block; - width: 100%; - background-color: transparent; - border: none; - border-bottom: 1px solid #eeeeee; - height: auto; - padding-bottom: 8px; - font-size: 14px; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); - } - .noke-responsive-nav .others-option .search-box .input-search::-webkit-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); - } - .noke-responsive-nav .others-option .search-box .input-search:-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); - } - .noke-responsive-nav .others-option .search-box .input-search::-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); - } - .noke-responsive-nav .others-option .search-box .input-search::placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); - } - .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--startupMainColor); - } - .noke-responsive-nav .others-option .search-box .input-search:focus::-webkit-input-placeholder { - color: transparent; - } - .noke-responsive-nav .others-option .search-box .input-search:focus:-ms-input-placeholder { - color: transparent; - } - .noke-responsive-nav .others-option .search-box .input-search:focus::-ms-input-placeholder { - color: transparent; - } - .noke-responsive-nav .others-option .search-box .input-search:focus::placeholder { - color: transparent; - } - .noke-responsive-nav .others-option .search-box button { - position: absolute; - right: 0; - top: 0; - background-color: transparent; - border: none; - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); - } - .noke-responsive-nav .others-option .search-box button:hover { - color: var(--startupMainColor); - } - .noke-responsive-nav .others-option .lang-switcher { - top: -2px; - position: relative; - padding-left: 25px; - position: relative; - margin-left: -10px; - border-left: 1px solid rgba(0, 0, 0, 0.2); - } - .noke-responsive-nav .others-option .lang-switcher label { - display: block; - margin-bottom: 0; - position: absolute; - left: 10px; - top: 0; - } - .noke-responsive-nav .others-option .lang-switcher select { - background-color: transparent; - cursor: pointer; - border: none; - margin-top: -3px; - } - .noke-responsive-nav .others-option .link-btn { - display: inline-block; - font-size: 18px; - line-height: 1; - } - .noke-responsive-nav .others-option .icon-btn { - position: relative; - font-size: 18px; - line-height: 1; - padding-right: 10px; - margin-left: 5px; - } - .noke-responsive-nav .others-option .icon-btn span { - display: inline-block; - position: absolute; - right: 0; - top: -5px; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: red; - text-align: center; - line-height: 17px; - color: var(--whiteColor); - font-weight: 700; - font-size: 9px; - } - .noke-responsive-nav .logo { - position: relative; - width: 60%; - z-index: 999; - } - .noke-nav { - display: none; - } - .others-option-for-responsive { - display: block; - } - .marketing-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--marketingMainColor); - } - .marketing-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--marketingMainColor); - } - .marketing-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .marketing-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--marketingMainColor); - } - .marketing-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--marketingMainColor); - } - .marketing-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--marketingMainColor); - } - .marketing-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--marketingMainColor); - } - .marketing-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--marketingMainColor); - } - .business-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--businessMainColor); - } - .business-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--businessMainColor); - } - .business-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .business-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--businessMainColor); - } - .business-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--businessMainColor); - } - .business-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--businessMainColor); - } - .business-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--businessMainColor); - } - .business-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--businessMainColor); - } - .app-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--appMainColor); - } - .app-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--appMainColor); - } - .app-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .app-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--appMainColor); - } - .app-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--appMainColor); - } - .app-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--appMainColor); - } - .app-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--appMainColor); - } - .app-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--appMainColor); - } - .it-agency-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--itAgencyMainColor); - } - .it-agency-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--itAgencyMainColor); - } - .it-agency-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .it-agency-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--itAgencyMainColor); - } - .it-agency-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--itAgencyMainColor); - } - .it-agency-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--itAgencyMainColor); - } - .it-agency-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--itAgencyMainColor); - } - .it-agency-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--itAgencyMainColor); - } - .consulting-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--consultingMainColor); - } - .consulting-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--consultingMainColor); - } - .consulting-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .consulting-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--consultingMainColor); - } - .consulting-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--consultingMainColor); - } - .consulting-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--consultingMainColor); - } - .consulting-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--consultingMainColor); - } - .consulting-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--consultingMainColor); - } - .saas-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--saasMainColor); - } - .saas-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--saasMainColor); - } - .saas-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .saas-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--saasMainColor); - } - .saas-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--saasMainColor); - } - .saas-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--saasMainColor); - } - .saas-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--saasMainColor); - } - .saas-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--saasMainColor); - } - .shop-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--shopMainColor); - } - .shop-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--shopMainColor); - } - .shop-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .shop-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--shopMainColor); - } - .shop-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--shopMainColor); - } - .shop-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--shopMainColor); - } - .shop-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--shopMainColor); - } - .shop-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--shopMainColor); - } - .blog-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--personalPortfolioMainColor); - } - .blog-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--personalPortfolioMainColor); - } - .blog-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .blog-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--personalPortfolioMainColor); - } - .blog-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--personalPortfolioMainColor); - } - .blog-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--personalPortfolioMainColor); - } - .blog-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--personalPortfolioMainColor); - } - .blog-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--personalPortfolioMainColor); - } - .personal-portfolio-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--personalPortfolioMainColor); - } - .personal-portfolio-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--personalPortfolioMainColor); - } - .personal-portfolio-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .personal-portfolio-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--personalPortfolioMainColor); - } - .personal-portfolio-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--personalPortfolioMainColor); - } - .personal-portfolio-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--personalPortfolioMainColor); - } - .personal-portfolio-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--personalPortfolioMainColor); - } - .personal-portfolio-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--personalPortfolioMainColor); - } - .minimal-portfolio-color .noke-responsive-nav .noke-responsive-menu.mean-container .mean-nav ul li a.active { - color: var(--minimalPortfolioColor); - } - .minimal-portfolio-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .submenu-title::before { - background-color: var(--minimalPortfolioColor); - } - .minimal-portfolio-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a:hover, .minimal-portfolio-color .noke-responsive-nav .mean-container .navbar-nav .nav-item.megamenu .dropdown-menu .megamenu-submenu li a.active { - color: var(--minimalPortfolioColor); - } - .minimal-portfolio-color .noke-responsive-nav .others-option .option-item .search-icon:hover { - color: var(--minimalPortfolioColor); - } - .minimal-portfolio-color .noke-responsive-nav .others-option .option-item .social-links span a:hover { - color: var(--minimalPortfolioColor); - } - .minimal-portfolio-color .noke-responsive-nav .others-option .search-box .input-search:focus { - border-color: var(--minimalPortfolioColor); - } - .minimal-portfolio-color .noke-responsive-nav .others-option .search-box button:hover { - color: var(--minimalPortfolioColor); - } -} - -/*================================================ -Search Overlay CSS -=================================================*/ -.search-overlay { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 99999; - opacity: 0; - visibility: hidden; - -webkit-transition: all 0.3s ease-in-out; - transition: all 0.3s ease-in-out; - overflow: hidden; -} - -.search-overlay .search-overlay-layer { - position: absolute; - top: 0; - right: 0; - bottom: 0; - z-index: 1; - -webkit-transform: translateX(100%); - transform: translateX(100%); -} - -.search-overlay .search-overlay-layer:nth-child(1) { - left: 0; - background-color: rgba(0, 0, 0, 0.5); - -webkit-transition: all 0.3s ease-in-out 0s; - transition: all 0.3s ease-in-out 0s; -} - -.search-overlay .search-overlay-layer:nth-child(2) { - left: 0; - background-color: rgba(0, 0, 0, 0.4); - -webkit-transition: all 0.3s ease-in-out 0.3s; - transition: all 0.3s ease-in-out 0.3s; -} - -.search-overlay .search-overlay-layer:nth-child(3) { - left: 0; - background-color: rgba(0, 0, 0, 0.7); - -webkit-transition: all 0.9s ease-in-out 0.6s; - transition: all 0.9s ease-in-out 0.6s; -} - -.search-overlay .search-overlay-close { - position: absolute; - top: 40px; - right: 40px; - width: 50px; - z-index: 2; - text-align: center; - cursor: pointer; - padding: 10px; - -webkit-transition: all 0.9s ease-in-out 1.5s; - transition: all 0.9s ease-in-out 1.5s; - opacity: 0; - visibility: hidden; -} - -.search-overlay .search-overlay-close .search-overlay-close-line { - width: 100%; - height: 3px; - float: left; - margin-bottom: 5px; - background-color: var(--whiteColor); - -webkit-transition: all 500ms ease; - transition: all 500ms ease; -} - -.search-overlay .search-overlay-close .search-overlay-close-line:nth-child(1) { - -webkit-transform: rotate(45deg); - transform: rotate(45deg); -} - -.search-overlay .search-overlay-close .search-overlay-close-line:nth-child(2) { - margin-top: -7px; - -webkit-transform: rotate(-45deg); - transform: rotate(-45deg); -} - -.search-overlay .search-overlay-close:hover .search-overlay-close-line { - background: red; - -webkit-transform: rotate(180deg); - transform: rotate(180deg); -} - -.search-overlay .search-overlay-form { - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); - -webkit-transition: all 0.9s ease-in-out 1.4s; - transition: all 0.9s ease-in-out 1.4s; - visibility: hidden; - position: absolute; - max-width: 600px; - width: 600px; - opacity: 0; - z-index: 2; - left: 50%; - top: 50%; -} - -.search-overlay .search-overlay-form form { - position: relative; -} - -.search-overlay .search-overlay-form form .input-search { - display: block; - width: 100%; - height: 70px; - border: none; - background-color: transparent; - color: var(--whiteColor); - padding: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border-bottom: 1px solid rgba(255, 255, 255, 0.4); - font-size: 22px; - font-weight: 500; -} - -.search-overlay .search-overlay-form form .input-search::-webkit-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: rgba(255, 255, 255, 0.6); - font-weight: 100; -} - -.search-overlay .search-overlay-form form .input-search:-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: rgba(255, 255, 255, 0.6); - font-weight: 100; -} - -.search-overlay .search-overlay-form form .input-search::-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: rgba(255, 255, 255, 0.6); - font-weight: 100; -} - -.search-overlay .search-overlay-form form .input-search::placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: rgba(255, 255, 255, 0.6); - font-weight: 100; -} - -.search-overlay .search-overlay-form form .input-search:focus { - border-color: var(--whiteColor); -} - -.search-overlay .search-overlay-form form .input-search:focus::-webkit-input-placeholder { - color: transparent; -} - -.search-overlay .search-overlay-form form .input-search:focus:-ms-input-placeholder { - color: transparent; -} - -.search-overlay .search-overlay-form form .input-search:focus::-ms-input-placeholder { - color: transparent; -} - -.search-overlay .search-overlay-form form .input-search:focus::placeholder { - color: transparent; -} - -.search-overlay .search-overlay-form form button { - position: absolute; - right: 0; - top: 13px; - color: var(--whiteColor); - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: transparent; - border: none; - font-size: 25px; - opacity: 0.7; -} - -.search-overlay .search-overlay-form form button:hover { - opacity: 1; -} - -.search-overlay.search-overlay-active.search-overlay { - opacity: 1; - visibility: visible; -} - -.search-overlay.search-overlay-active.search-overlay .search-overlay-layer { - -webkit-transform: translateX(0); - transform: translateX(0); -} - -.search-overlay.search-overlay-active.search-overlay .search-overlay-close { - opacity: 1; - visibility: visible; -} - -.search-overlay.search-overlay-active.search-overlay .search-overlay-form { - opacity: 1; - visibility: visible; -} - -/*================================================ -Startup Banner Area CSS -=================================================*/ -.startup-banner-area .container-fluid { - max-width: 1400px; -} - -.startup-banner-inner { - position: relative; - z-index: 1; - padding-top: 140px; - padding-bottom: 140px; - padding-left: 100px; - padding-right: 100px; -} - -.startup-banner-content { - padding-right: 15px; -} - -.startup-banner-content .sub-title { - display: block; - margin-bottom: 10px; - color: var(--startupMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.startup-banner-content h1 { - margin-bottom: 25px; - font-size: 60px; -} - -.startup-banner-content p { - max-width: 580px; -} - -.startup-banner-content .default-btn { - margin-top: 10px; -} - -.startup-banner-image { - text-align: center; - padding-left: 15px; -} - -/*================================================ -Marketing Banner Area CSS -=================================================*/ -.marketing-banner-area { - z-index: 1; - position: relative; - border-top: 1px solid #f0f0f0; - border-bottom: 1px solid #f0f0f0; - padding-top: 80px; - padding-bottom: 80px; -} - -.marketing-banner-content { - padding-right: 25px; -} - -.marketing-banner-content .sub-title { - display: block; - margin-bottom: 10px; - color: var(--marketingMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.marketing-banner-content h1 { - margin-bottom: 25px; - font-size: 60px; -} - -.marketing-banner-content p { - max-width: 550px; -} - -.marketing-banner-content .default-btn { - margin-top: 10px; -} - -.marketing-banner-image { - text-align: center; -} - -/*================================================ -Business Banner Area CSS -=================================================*/ -.business-banner-area { - z-index: 1; - position: relative; - padding-top: 100px; - padding-bottom: 100px; -} - -.business-banner-area::before { - -webkit-animation: moveleftbounce 5s linear infinite; - animation: moveleftbounce 5s linear infinite; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - margin-left: 20px; - margin-right: 20px; - background-image: url(../../assets/img/bg2.jpg); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.business-banner-area .container { - max-width: 1400px; -} - -.business-banner-content { - padding-right: 25px; - max-width: 745px; -} - -.business-banner-content .sub-title { - display: block; - margin-bottom: 10px; - color: var(--businessMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.business-banner-content h1 { - margin-bottom: 25px; - font-size: 60px; -} - -.business-banner-content p { - max-width: 550px; -} - -.business-banner-content .default-btn { - margin-top: 10px; -} - -.business-banner-image { - text-align: center; -} - -.business-banner-image img { - border-radius: 0 0 0 100px; -} - -/*================================================ -App Banner Area CSS -=================================================*/ -.app-banner-area { - z-index: 1; - position: relative; - background: var(--appGradientColor); - background-image: url(../../assets/img/bg3.jpg); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; - padding-top: 100px; - padding-bottom: 100px; -} - -.app-banner-area .container { - max-width: 1450px; -} - -.app-banner-content h1 { - margin-bottom: 25px; - font-size: 60px; - color: var(--whiteColor); -} - -.app-banner-content p { - max-width: 550px; - color: var(--whiteColor); -} - -.app-banner-content .btn-box { - margin-top: 35px; -} - -.app-banner-content .btn-box .store-btn { - display: inline-block; - background-color: var(--whiteColor); - position: relative; - color: var(--optionalColor); - padding: 11px 25px 11px 65px; - margin-right: 15px; - border-radius: 5px; - font-size: 13.5px; - font-weight: 600; -} - -.app-banner-content .btn-box .store-btn img { - position: absolute; - left: 15px; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - color: var(--blackColor); - font-size: 40px; - font-weight: normal; -} - -.app-banner-content .btn-box .store-btn span { - display: block; - color: var(--blackColor); - margin-top: 2px; - font-size: 18px; - font-weight: 700; -} - -.app-banner-content .btn-box .store-btn:last-child { - margin-right: 0; -} - -.app-banner-image { - text-align: center; - padding-left: 15px; -} - -/*================================================ -Saas Banner Area CSS -=================================================*/ -.saas-banner-area { - background-color: #06080e; - padding-top: 80px; -} - -.saas-banner-area .container-fluid { - padding-right: 30px; - padding-left: 30px; -} - -.saas-banner-content { - margin-left: auto; - max-width: 710px; -} - -.saas-banner-content h1 { - color: var(--whiteColor); - margin-bottom: 25px; - font-size: 64px; -} - -.saas-banner-content p { - opacity: 0.9; - max-width: 535px; - color: var(--whiteColor); -} - -.saas-banner-content .default-btn { - margin-top: 10px; -} - -.saas-banner-image { - text-align: center; -} - -/*================================================ -Personal Portfolio Banner Area CSS -=================================================*/ -.personal-portfolio-banner-area { - border-bottom: 1px solid #f0f0f0; - position: relative; - z-index: 1; - padding-top: 100px; - padding-bottom: 100px; -} - -.personal-portfolio-banner-area .container { - max-width: 1450px; -} - -.personal-portfolio-banner-content .sub-title { - display: block; - position: relative; - padding-left: 65px; - margin-bottom: 12px; - font-size: 20px; - font-weight: 500; -} - -.personal-portfolio-banner-content .sub-title::before { - left: 0; - top: 50%; - width: 50px; - height: 1px; - content: ''; - position: absolute; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - background-color: var(--personalPortfolioMainColor); -} - -.personal-portfolio-banner-content h1 { - margin-bottom: 15px; - font-size: 80px; - font-weight: 700; -} - -.personal-portfolio-banner-content h4 { - margin-bottom: 20px; - font-size: 40px; - font-weight: 500; -} - -.personal-portfolio-banner-content p { - max-width: 535px; -} - -.personal-portfolio-banner-content .default-btn { - margin-top: 10px; -} - -.personal-portfolio-banner-image { - text-align: center; -} - -/*================================================ -Consulting Banner Area CSS -=================================================*/ -.consulting-banner-area { - position: relative; - z-index: 1; - background-size: cover; - background-repeat: no-repeat; - background-position: left center; - background-image: url(../../assets/img/consulting-banner-bg.jpg); - padding-top: 300px; - padding-bottom: 180px; -} - -.consulting-banner-area .container { - max-width: 1450px; -} - -.consulting-banner-area::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: var(--whiteColor); - opacity: 0.85; - display: none; -} - -.consulting-banner-content { - max-width: 790px; -} - -.consulting-banner-content h1 { - margin-bottom: 25px; - font-size: 50px; - font-weight: 700; -} - -.consulting-banner-content p { - max-width: 580px; -} - -.consulting-banner-content .default-btn { - margin-top: 15px; -} - -/*================================================ -IT Agency Banner Area CSS -=================================================*/ -.it-agency-banner-area .it-agency-inner-banner { - position: relative; - z-index: 1; - background-image: url(../../assets/img/it-agency-banner-bg.jpg); - background-position: center center; - background-size: cover; - background-repeat: no-repeat; - padding-top: 280px; - padding-bottom: 330px; -} - -.it-agency-banner-area .it-agency-inner-banner::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: 0.8; -} - -.it-agency-banner-content { - max-width: 1000px; - text-align: center; - color: var(--whiteColor); - margin-left: auto; - margin-right: auto; -} - -.it-agency-banner-content h1 { - color: var(--whiteColor); - margin-bottom: 20px; - line-height: 1.3; - font-size: 60px; -} - -.it-agency-banner-content p { - color: var(--whiteColor); - max-width: 600px; - opacity: 0.9; - margin-left: auto; - margin-right: auto; -} - -.it-agency-banner-content .default-btn { - margin-top: 15px; -} - -.banner-video-box { - z-index: 1; - max-width: 1000px; - position: relative; - text-align: center; - border-radius: 10px; - margin-left: auto; - margin-right: auto; - margin-top: -130px; -} - -.banner-video-box img { - border-radius: 10px; -} - -.banner-video-box .video-btn { - top: 50%; - left: 50%; - width: 80px; - height: 80px; - font-size: 30px; - border-radius: 50%; - position: absolute; - display: inline-block; - color: var(--whiteColor); - background-color: rgba(255, 74, 23, 0.75); - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); -} - -.banner-video-box .video-btn i { - position: absolute; - left: 0; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.banner-video-box .video-btn::after, .banner-video-box .video-btn::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 1.5px solid var(--whiteColor); -} - -.banner-video-box .video-btn::before { - -webkit-animation: ripple 2s linear infinite; - animation: ripple 2s linear infinite; -} - -.banner-video-box .video-btn::after { - -webkit-animation: ripple 2s linear 1s infinite; - animation: ripple 2s linear 1s infinite; -} - -/*================================================ -Shop Banner Area CSS -=================================================*/ -.shop-banner-area { - padding-top: 200px; - padding-bottom: 200px; - background-image: url(../../assets/img/shop-banner-bg.jpg); - background-position: top center; - background-repeat: no-repeat; - background-size: cover; -} - -.shop-banner-content .sub-title { - display: block; - font-size: 30px; - margin-bottom: 8px; -} - -.shop-banner-content h1 { - margin-bottom: 15px; - font-size: 60px; - font-weight: 700; -} - -.shop-banner-content h4 { - margin-bottom: 0; - font-size: 30px; -} - -.shop-banner-content .default-btn { - margin-top: 30px; -} - -/*================================================ -Blog Banner Area CSS -=================================================*/ -.blog-banner-area { - background-image: url(../../assets/img/bg11.jpg); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; - padding-top: 100px; - padding-bottom: 100px; -} - -.blog-banner-item { - position: relative; - text-align: center; - overflow: hidden; -} - -.blog-banner-item img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.blog-banner-item .content { - position: absolute; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - left: 0; - right: 0; - padding: 30px; - background-color: rgba(255, 255, 255, 0.9); - max-width: 525px; - margin-left: auto; - margin-right: auto; -} - -.blog-banner-item .content .tag { - display: inline-block; - margin-bottom: 12px; - font-weight: 500; -} - -.blog-banner-item .content h3 { - font-size: 25px; - line-height: 1.4; - margin-bottom: 12px; -} - -.blog-banner-item .content p { - margin-bottom: 20px; -} - -.blog-banner-item .content .meta { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.blog-banner-item .content .meta li a { - display: inline-block; - font-weight: 600; - position: relative; - padding-right: 12px; -} - -.blog-banner-item .content .meta li a i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; -} - -.blog-banner-item .content .meta li a::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.blog-banner-item .content .meta li a:hover { - padding-right: 15px; - color: var(--blackColor); -} - -.blog-banner-item .content .meta li a:hover::before { - width: 100%; - right: auto; - left: 0; -} - -.blog-banner-item .content .meta li.date { - padding-left: 20px; - position: relative; -} - -.blog-banner-item .content .meta li.date i { - position: absolute; - left: 0; - top: 3px; -} - -.blog-banner-item:hover img { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -.blog-banner-item-list { - margin-bottom: -30px; -} - -.blog-banner-item-list .single-blog-banner-item { - margin-bottom: 30px; -} - -.blog-banner-item-list .single-blog-banner-item .row { - margin-left: 0; - margin-right: 0; -} - -.blog-banner-item-list .single-blog-banner-item .row .col-lg-4, .blog-banner-item-list .single-blog-banner-item .row .col-lg-8 { - padding-left: 0; - padding-right: 0; -} - -.blog-banner-item-list .single-blog-banner-item .image { - height: 100%; - position: relative; - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.blog-banner-item-list .single-blog-banner-item .image.bg1 { - background-image: url(../../assets/img/blog-banner/blog-banner-list1.jpg); -} - -.blog-banner-item-list .single-blog-banner-item .image.bg2 { - background-image: url(../../assets/img/blog-banner/blog-banner-list2.jpg); -} - -.blog-banner-item-list .single-blog-banner-item .image.bg3 { - background-image: url(../../assets/img/blog-banner/blog-banner-list3.jpg); -} - -.blog-banner-item-list .single-blog-banner-item .image.bg4 { - background-image: url(../../assets/img/blog-banner/blog-banner-list4.jpg); -} - -.blog-banner-item-list .single-blog-banner-item .image img { - display: none; -} - -.blog-banner-item-list .single-blog-banner-item .image .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - display: block; - z-index: 1; -} - -.blog-banner-item-list .single-blog-banner-item .content { - padding-left: 25px; - padding-top: 9.1px; - padding-bottom: 9.1px; -} - -.blog-banner-item-list .single-blog-banner-item .content h3 { - margin-bottom: 15px; - color: var(--whiteColor); - line-height: 1.4; - font-size: 18px; -} - -.blog-banner-item-list .single-blog-banner-item .content h3 a { - color: var(--whiteColor); -} - -.blog-banner-item-list .single-blog-banner-item .content .date { - color: var(--whiteColor); - padding-left: 20px; - position: relative; - opacity: 0.9; -} - -.blog-banner-item-list .single-blog-banner-item .content .date i { - position: absolute; - left: 0; - top: 3px; -} - -/*================================================ -Furniture Banner Area CSS -=================================================*/ -.furniture-banner-item { - padding-top: 200px; - padding-bottom: 200px; - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.furniture-banner-item.bg-item1 { - background-image: url(../../assets/img/furniture-banner-bg1.jpg); -} - -.furniture-banner-item.bg-item2 { - background-image: url(../../assets/img/furniture-banner-bg2.jpg); -} - -.furniture-banner-item.bg-item3 { - background-image: url(../../assets/img/furniture-banner-bg3.jpg); -} - -.furniture-banner-content { - padding: 40px; - max-width: 560px; - background-color: var(--whiteColor); - border-left: 5px solid var(--blackColor); -} - -.furniture-banner-content h1 { - margin-bottom: 15px; - font-size: 45px; -} - -.furniture-banner-content .default-btn { - margin-top: 10px; -} - -.furniture-banner-slides.owl-theme .owl-nav.disabled + .owl-dots { - margin-top: 0; -} - -.furniture-banner-slides.owl-theme .owl-dots { - position: absolute; - right: 15px; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.furniture-banner-slides.owl-theme .owl-dots .owl-dot { - display: block; - margin-top: 10px; - margin-bottom: 10px; -} - -.furniture-banner-slides.owl-theme .owl-dots .owl-dot span { - margin: 0; - border-radius: 0; - width: 50px; - height: 6px; - background-color: var(--whiteColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.furniture-banner-slides.owl-theme .owl-dots .owl-dot:hover span, .furniture-banner-slides.owl-theme .owl-dots .owl-dot.active span { - background-color: var(--blackColor); -} - -/*================================================ -Minimal Portfolio Banner Area CSS -=================================================*/ -.minimal-portfolio-banner-area { - position: relative; - z-index: 1; - padding-top: 250px; - padding-bottom: 220px; -} - -.minimal-portfolio-banner-content { - text-align: center; - max-width: 770px; - margin-left: auto; - margin-right: auto; -} - -.minimal-portfolio-banner-content .typewrite { - margin-bottom: 30px; - display: inline-block; - color: var(--blackColor); - padding: 8px 20px; - line-height: 1; - letter-spacing: 2px; - background-color: #ebebeb; - font-size: 20px; - font-weight: 300; -} - -.minimal-portfolio-banner-content h1 { - font-size: 60px; - margin-bottom: 40px; - text-shadow: -1px 1px 0 var(--minimalPortfolioColor), 1px 1px 0 var(--startupMainColor), 1px -1px 0 var(--consultingMainColor), -1px -1px 0 var(--itAgencyMainColor); -} - -.scroll-container { - left: 50%; - bottom: 0; - width: 30px; - height: 60px; - position: absolute; - border-radius: 15px; - border: 3px solid #eeeeee; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); -} - -.scroller { - width: 16px; - border-radius: 8px; - background-color: var(--minimalPortfolioColor); - position: absolute; - top: 4px; - left: 4px; - bottom: 34px; - -webkit-animation: scroller 1500ms ease-out infinite; - animation: scroller 1500ms ease-out infinite; -} - -/*================================================ -Features Area CSS -=================================================*/ -.features-area { - position: relative; - z-index: 1; -} - -.features-area.ptb-70 { - padding-bottom: 40px; -} - -.single-features-box { - text-align: center; - margin-bottom: 30px; - -webkit-transition: var(--transition); - transition: var(--transition); - position: relative; - padding-left: 15px; - padding-right: 15px; -} - -.single-features-box .icon { - line-height: 1; - display: block; - font-size: 50px; - margin-bottom: 22px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-features-box h3 { - font-size: 20px; - margin-bottom: 12px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-features-box p { - margin-bottom: 0; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-features-box .link-btn { - display: inline-block; - font-weight: 700; - padding-right: 12px; - position: absolute; - left: 50%; - bottom: 0; - opacity: 0; - visibility: hidden; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); -} - -.single-features-box .link-btn i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; -} - -.single-features-box .link-btn::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 2px; - background-color: #eeeeee; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-features-box .link-btn:hover { - color: var(--startupMainColor); - padding-right: 15px; -} - -.single-features-box .link-btn:hover::before { - width: 100%; - right: auto; - left: 0; -} - -.single-features-box:hover .icon { - -webkit-transform: translateY(-20px); - transform: translateY(-20px); - color: var(--startupMainColor); -} - -.single-features-box:hover h3 { - -webkit-transform: translateY(-20px); - transform: translateY(-20px); -} - -.single-features-box:hover p { - -webkit-transform: translateY(-20px); - transform: translateY(-20px); -} - -.single-features-box:hover .link-btn { - opacity: 1; - bottom: -15px; - visibility: visible; -} - -.features-box { - text-align: center; - margin-bottom: 30px; - background-color: var(--whiteColor); - -webkit-box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - padding: 35px 25px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.features-box .icon { - display: block; - width: 100px; - height: 100px; - position: relative; - font-size: 55px; - line-height: 100px; - z-index: 1; - color: var(--appMainColor); - border-radius: 10px; - -webkit-transition: var(--transition); - transition: var(--transition); - margin-left: auto; - margin-right: auto; - margin-bottom: 25px; -} - -.features-box .icon::after { - content: ''; - position: absolute; - -webkit-transition: var(--transition); - transition: var(--transition); - left: 0; - right: 0; - top: 0; - bottom: 0; - border-radius: 10px; - z-index: -1; - background-color: var(--appMainColor); - opacity: .10; -} - -.features-box .icon.bg-7669ff { - color: #7669ff; -} - -.features-box .icon.bg-7669ff::after { - background-color: #7669ff; -} - -.features-box .icon.bg-ff9f07 { - color: #ff9f07; -} - -.features-box .icon.bg-ff9f07::after { - background-color: #ff9f07; -} - -.features-box .icon.bg-89c111 { - color: #89c111; -} - -.features-box .icon.bg-89c111::after { - background-color: #89c111; -} - -.features-box .icon.bg-2ea7ff { - color: #2ea7ff; -} - -.features-box .icon.bg-2ea7ff::after { - background-color: #2ea7ff; -} - -.features-box .icon.bg-ca83ff { - color: #ca83ff; -} - -.features-box .icon.bg-ca83ff::after { - background-color: #ca83ff; -} - -.features-box .icon.bg-ff5f5f { - color: #ff5f5f; -} - -.features-box .icon.bg-ff5f5f::after { - background-color: #ff5f5f; -} - -.features-box h3 { - margin-bottom: 12px; - font-size: 20px; -} - -.features-box:hover { - -webkit-transform: translateY(-10px); - transform: translateY(-10px); -} - -.features-box:hover .icon { - color: var(--whiteColor); -} - -.features-box:hover .icon::after { - opacity: 1; -} - -.features-box.without-box-shadow { - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.single-features-item { - margin-bottom: 30px; - position: relative; - padding-left: 70px; -} - -.single-features-item .icon { - position: absolute; - left: 0; - line-height: 1; - top: 0; - font-size: 45px; - color: var(--consultingMainColor); -} - -.single-features-item h3 { - margin-bottom: 0; - font-size: 20px; -} - -.single-features-item.color-white h3 { - color: var(--whiteColor); -} - -.single-features-item.startup-color .icon { - color: var(--startupMainColor); -} - -.features-item { - margin-bottom: 30px; - -webkit-transition: var(--transition); - transition: var(--transition); - border-radius: 10px; - position: relative; - padding: 30px; - z-index: 1; -} - -.features-item .icon { - display: block; - line-height: 1; - -webkit-transition: var(--transition); - transition: var(--transition); - font-size: 35px; - color: var(--itAgencyMainColor); - margin-right: 15px; -} - -.features-item h3 { - -webkit-transition: var(--transition); - transition: var(--transition); - position: relative; - font-size: 20px; - margin-bottom: 0; -} - -.features-item p { - -webkit-transition: var(--transition); - transition: var(--transition); - margin-top: 15px; -} - -.features-item::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - border-radius: 10px; - background-color: var(--itAgencyMainColor); - z-index: -1; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0.07; -} - -.features-item::after { - content: ''; - position: absolute; - left: 0; - top: 0; - -webkit-transition: .3s; - transition: .3s; - border-radius: 10px; - height: 0; - width: 100%; - z-index: -1; - background: var(--itAgencyGradientColor); -} - -.features-item.bg-6d5fff .icon { - color: #6d5fff; -} - -.features-item.bg-6d5fff::before { - background-color: #6d5fff; -} - -.features-item.bg-ff9f07 .icon { - color: #ff9f07; -} - -.features-item.bg-ff9f07::before { - background-color: #ff9f07; -} - -.features-item.bg-2ea7ff .icon { - color: #2ea7ff; -} - -.features-item.bg-2ea7ff::before { - background-color: #2ea7ff; -} - -.features-item:hover::before { - opacity: 0; - visibility: hidden; -} - -.features-item:hover::after { - height: 100%; -} - -.features-item:hover p { - color: var(--whiteColor); -} - -.features-item:hover h3 { - color: var(--whiteColor); -} - -.features-item:hover h3 a { - color: var(--whiteColor); -} - -.features-item:hover .icon { - color: var(--whiteColor); -} - -/*================================================ -About Area CSS -=================================================*/ -.about-area.with-border { - border-bottom: 1px solid #f0f0f0; -} - -.startup-about-area .container-fluid { - max-width: 1400px; -} - -.startup-about-image { - z-index: 1; - margin-right: 15px; - text-align: center; - position: relative; - background-color: #f9f4f0; - padding-top: 80px; - padding-left: 80px; - padding-bottom: 80px; -} - -.startup-about-image .video-btn { - position: absolute; - left: 57%; - display: inline-block; - top: 50%; - -webkit-transform: translateY(-50%) translateX(-57%); - transform: translateY(-50%) translateX(-57%); - width: 80px; - height: 80px; - background-color: rgba(0, 0, 0, 0.75); - border-radius: 50%; - color: var(--whiteColor); - font-size: 30px; -} - -.startup-about-image .video-btn i { - position: absolute; - left: 0; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.startup-about-image .video-btn::after, .startup-about-image .video-btn::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 2px solid var(--whiteColor); -} - -.startup-about-image .video-btn::before { - -webkit-animation: ripple 2s linear infinite; - animation: ripple 2s linear infinite; -} - -.startup-about-image .video-btn::after { - -webkit-animation: ripple 2s linear 1s infinite; - animation: ripple 2s linear 1s infinite; -} - -.startup-about-content { - padding-left: 15px; - max-width: 600px; -} - -.startup-about-content .sub-title { - display: block; - margin-bottom: 12px; - color: var(--startupMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.startup-about-content h2 { - margin-bottom: 15px; - font-size: 40px; -} - -.startup-about-content p strong { - color: var(--blackColor); -} - -.startup-about-content .features-list { - padding-left: 0; - margin-left: 16px; - margin-bottom: 0; -} - -.startup-about-content .features-list li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; -} - -.startup-about-content .features-list li:last-child { - margin-bottom: 0; -} - -.startup-about-content .default-btn { - margin-top: 5px; -} - -.marketing-about-area { - border-bottom: 1px solid #f0f0f0; -} - -.marketing-about-image { - margin-right: 15px; - text-align: center; -} - -.marketing-about-content { - padding-left: 15px; -} - -.marketing-about-content .sub-title { - display: block; - margin-bottom: 12px; - color: var(--marketingMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.marketing-about-content h2 { - margin-bottom: 15px; - font-size: 40px; -} - -.marketing-about-content .default-btn { - margin-top: 10px; -} - -.marketing-about-content .features-list { - padding-left: 0; - margin-left: 16px; - margin-bottom: 0; -} - -.marketing-about-content .features-list li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; -} - -.marketing-about-content .features-list li:last-child { - margin-bottom: 0; -} - -.business-about-content { - padding-left: 15px; -} - -.business-about-content .sub-title { - display: block; - margin-bottom: 12px; - color: var(--businessMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.business-about-content h2 { - margin-bottom: 15px; - font-size: 40px; -} - -.business-about-content .default-btn { - margin-top: 10px; -} - -.business-about-image { - z-index: 1; - text-align: center; - padding-right: 15px; - position: relative; -} - -.app-about-content { - padding-right: 15px; -} - -.app-about-content h3 { - margin-bottom: 16px; - font-size: 24px; -} - -.app-about-content .features-list { - padding-left: 0; - margin-bottom: 15px; - list-style-type: none; -} - -.app-about-content .features-list li { - margin-bottom: 12px; - position: relative; - color: var(--optionalColor); - font-weight: 500; - padding-left: 24px; -} - -.app-about-content .features-list li i { - position: absolute; - left: 0; - top: 4px; - color: var(--appMainColor); -} - -.app-about-content .features-list li:last-child { - margin-bottom: 0; -} - -.app-about-content .default-btn { - margin-top: 10px; -} - -.app-about-image { - padding-left: 15px; - text-align: center; -} - -.saas-about-content { - padding-left: 15px; -} - -.saas-about-content .sub-title { - display: block; - margin-bottom: 12px; - color: var(--saasMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.saas-about-content h2 { - margin-bottom: 15px; - font-size: 40px; -} - -.saas-about-content .features-list { - padding-left: 0; - margin-bottom: 15px; - list-style-type: none; -} - -.saas-about-content .features-list li { - margin-bottom: 12px; - position: relative; - color: var(--optionalColor); - font-weight: 500; - padding-left: 24px; -} - -.saas-about-content .features-list li i { - position: absolute; - left: 0; - top: 4px; - color: var(--saasMainColor); -} - -.saas-about-content .features-list li:last-child { - margin-bottom: 0; -} - -.saas-about-content .default-btn { - margin-top: 10px; -} - -.saas-about-content.white-color { - color: var(--whiteColor); -} - -.saas-about-content.white-color h2 { - color: var(--whiteColor); -} - -.saas-about-content.white-color p { - color: var(--whiteColor); - opacity: 0.9; -} - -.saas-about-content.white-color .features-list li { - color: var(--whiteColor); - opacity: 0.9; -} - -.saas-about-content.white-color .features-list li i { - color: var(--whiteColor); -} - -.saas-about-image { - padding-right: 15px; - text-align: center; -} - -.it-agency-about-content { - padding-left: 15px; -} - -.it-agency-about-content .sub-title { - display: block; - margin-bottom: 12px; - color: var(--itAgencyMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.it-agency-about-content h2 { - margin-bottom: 15px; - font-size: 40px; -} - -.it-agency-about-content .default-btn { - margin-top: 10px; -} - -.it-agency-about-content .features-list { - padding-left: 0; - margin-left: 16px; - margin-bottom: 15px; -} - -.it-agency-about-content .features-list li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; -} - -.it-agency-about-content .features-list li:last-child { - margin-bottom: 0; -} - -.it-agency-about-image { - text-align: start; - position: relative; - margin-right: 20px; - padding-right: 20px; - padding-bottom: 20px; -} - -.it-agency-about-image .main-image { - position: relative; - padding-bottom: 150px; -} - -.it-agency-about-image .main-image img { - border-radius: 10px; -} - -.it-agency-about-image .main-image img:nth-child(2) { - position: absolute; - right: 0; - bottom: 0; - z-index: 1; -} - -/*================================================ -Services Area CSS -=================================================*/ -.services-area.with-black-border { - border-top: 1px solid #12141a; - border-bottom: 1px solid #12141a; -} - -.single-services-box { - margin-bottom: 30px; - position: relative; - overflow: hidden; -} - -.single-services-box img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-services-box .content { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding: 30px; - z-index: 2; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-services-box .content .icon { - line-height: 1; - display: block; - font-size: 50px; - margin-bottom: 20px; - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--startupMainColor); -} - -.single-services-box .content h3 { - font-size: 20px; - margin-bottom: 10px; - color: var(--whiteColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-services-box .content h3 a { - color: var(--whiteColor); -} - -.single-services-box .content h3 a:hover { - color: var(--startupMainColor); -} - -.single-services-box .content p { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--whiteColor); - margin-bottom: 0; - opacity: 0.9; -} - -.single-services-box .content .link-btn { - font-weight: 700; - padding-right: 12px; - position: absolute; - left: 30px; - display: inline-block; - bottom: 0; - color: var(--whiteColor); - opacity: 0; - visibility: hidden; -} - -.single-services-box .content .link-btn i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; -} - -.single-services-box .content .link-btn::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 2px; - background-color: #eeeeee; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-services-box .content .link-btn:hover { - padding-right: 15px; -} - -.single-services-box .content .link-btn:hover::before { - width: 100%; - right: auto; - left: 0; -} - -.single-services-box::before { - position: absolute; - height: 100%; - content: ""; - width: 100%; - z-index: 1; - left: 0; - top: 0; - background: -webkit-gradient(linear, left bottom, left top, color-stop(0, rgba(35, 35, 37, 0.9)), color-stop(35%, rgba(35, 35, 37, 0.7)), color-stop(60%, rgba(22, 22, 23, 0)), to(rgba(0, 0, 0, 0))); - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.7) 35%, rgba(22, 22, 23, 0) 60%, rgba(0, 0, 0, 0) 100%); -} - -.single-services-box:hover img { - -webkit-transform: scale(1.05); - transform: scale(1.05); -} - -.single-services-box:hover .content .icon { - -webkit-transform: translateY(-30px); - transform: translateY(-30px); -} - -.single-services-box:hover .content p { - -webkit-transform: translateY(-30px); - transform: translateY(-30px); -} - -.single-services-box:hover .content h3 { - -webkit-transform: translateY(-30px); - transform: translateY(-30px); -} - -.single-services-box:hover .content .link-btn { - opacity: 1; - bottom: 25px; - visibility: visible; -} - -.single-services-item { - margin-bottom: 30px; -} - -.single-services-item .content { - padding-top: 25px; - padding-left: 20px; - padding-right: 20px; -} - -.single-services-item .content h3 { - font-size: 20px; - margin-bottom: 10px; -} - -.services-tabs .nav { - padding-left: 0; - margin-bottom: 60px; - border: none; - list-style-type: none; - text-align: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; -} - -.services-tabs .nav .nav-item { - margin-left: 42px; - margin-right: 42px; -} - -.services-tabs .nav .nav-item .nav-link { - display: inline-block; - padding: 0; - border: none; - margin: 0; - font-size: 19px; - font-weight: 700; -} - -.services-tabs .nav .nav-item .nav-link .icon { - display: block; - width: 80px; - height: 80px; - position: relative; - z-index: 1; - line-height: 75px; - color: var(--businessMainColor); - font-weight: normal; - -webkit-transition: var(--transition); - transition: var(--transition); - font-size: 35px; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - padding-right: 10px; - margin-left: auto; - margin-right: auto; - margin-bottom: 15px; -} - -.services-tabs .nav .nav-item .nav-link .icon::after { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - z-index: -1; - bottom: 0; - background: var(--businessGradientColor); - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0.10; -} - -.services-tabs .nav .nav-item .nav-link:hover, .services-tabs .nav .nav-item .nav-link.active { - color: var(--businessMainColor); -} - -.services-tabs .nav .nav-item .nav-link:hover .icon, .services-tabs .nav .nav-item .nav-link.active .icon { - color: var(--whiteColor); -} - -.services-tabs .nav .nav-item .nav-link:hover .icon::after, .services-tabs .nav .nav-item .nav-link.active .icon::after { - opacity: 1; -} - -.services-tabs .nav .nav-item:last-child { - margin-right: 0; -} - -.services-tabs .nav .nav-item:first-child { - margin-left: 0; -} - -.single-main-services { - margin-bottom: 30px; -} - -.single-main-services .content { - padding-right: 50px; -} - -.single-main-services .content h3 { - font-size: 40px; - margin-bottom: 15px; - line-height: 1.4; -} - -.single-main-services .content p { - max-width: 540px; -} - -.single-main-services .content .default-btn { - margin-top: 10px; -} - -.single-main-services .image { - text-align: center; -} - -.services-box { - text-align: center; - margin-bottom: 30px; - border: 1px solid #eeeeee; - background-color: var(--whiteColor); - padding: 35px 30px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.services-box .icon { - font-size: 60px; - line-height: 1; - background: var(--saasGradientColor); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - margin-bottom: 22px; - display: block; -} - -.services-box h3 { - margin-bottom: 12px; - font-size: 20px; -} - -.services-box h3 a:hover { - color: var(--saasMainColor); -} - -.services-box.color-white { - border-color: #272836; - background-color: #040611; -} - -.services-box.color-white h3 { - color: var(--whiteColor); -} - -.services-box.color-white h3 a { - color: var(--whiteColor); -} - -.services-box.color-white p { - opacity: 0.9; - color: var(--whiteColor); -} - -.services-box:hover { - border-color: var(--saasMainColor) !important; -} - -.services-box.startup-color .icon { - background: var(--startupMainColor); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -.services-box.startup-color:hover { - border-color: var(--startupMainColor) !important; -} - -.services-item { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; -} - -.services-item img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.services-item::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - opacity: 0; - visibility: hidden; - z-index: 1; - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--consultingMainColor); -} - -.services-item h3 { - font-size: 20px; - -webkit-transition: var(--transition); - transition: var(--transition); - margin-top: 25px; - margin-bottom: 0; -} - -.services-item .hover-content { - left: 0; - right: 0; - top: 50%; - padding: 30px; - position: absolute; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - z-index: 2; - opacity: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--whiteColor); -} - -.services-item .hover-content h3 { - opacity: 1 !important; - visibility: visible !important; - color: var(--whiteColor); - margin-top: 0; - margin-bottom: 12px; -} - -.services-item .hover-content h3 a { - color: var(--whiteColor); -} - -.services-item .hover-content p { - color: var(--whiteColor); -} - -.services-item .hover-content .link-btn { - font-weight: 700; - position: relative; - display: inline-block; - padding-right: 12px; - color: var(--whiteColor); -} - -.services-item .hover-content .link-btn i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; -} - -.services-item .hover-content .link-btn::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--whiteColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.services-item .hover-content .link-btn:hover { - padding-right: 15px; -} - -.services-item .hover-content .link-btn:hover::before { - width: 100%; - right: auto; - left: 0; -} - -.services-item:hover img { - -webkit-transform: scale(1.7); - transform: scale(1.7); -} - -.services-item:hover h3 { - opacity: 0; - visibility: hidden; -} - -.services-item:hover .hover-content { - opacity: 1; - visibility: visible; -} - -.services-item:hover::before { - opacity: .90; - visibility: visible; -} - -.services-item.startup-color::before { - background-color: var(--startupMainColor); -} - -.col-lg-3:nth-child(2) .services-box, .col-lg-3:nth-child(4) .services-box { - margin-top: 30px; -} - -.col-lg-3:nth-child(5) .services-box, .col-lg-3:nth-child(7) .services-box { - margin-top: -30px; -} - -.single-it-services-box { - margin-bottom: 30px; - border-radius: 10px; - position: relative; - padding: 30px 30px 30px 100px; - z-index: 1; -} - -.single-it-services-box .icon { - position: absolute; - left: 30px; - top: 30px; - line-height: 1; - font-size: 45px; - color: var(--itAgencyMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-it-services-box h3 { - -webkit-transition: var(--transition); - transition: var(--transition); - margin-bottom: 12px; - font-size: 20px; -} - -.single-it-services-box h3 a { - display: inline-block; -} - -.single-it-services-box h3 a:hover { - color: var(--itAgencyGradientColor); -} - -.single-it-services-box p { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-it-services-box::before { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - opacity: .06; - position: absolute; - border-radius: 10px; - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--itAgencyMainColor); -} - -.single-it-services-box.bg-6d5fff .icon { - color: #6d5fff; -} - -.single-it-services-box.bg-6d5fff::before { - background-color: #6d5fff; -} - -.single-it-services-box.bg-ff9f07 .icon { - color: #ff9f07; -} - -.single-it-services-box.bg-ff9f07::before { - background-color: #ff9f07; -} - -.single-it-services-box.bg-89c111 .icon { - color: #89c111; -} - -.single-it-services-box.bg-89c111::before { - background-color: #89c111; -} - -.single-it-services-box.bg-ca83ff .icon { - color: #ca83ff; -} - -.single-it-services-box.bg-ca83ff::before { - background-color: #ca83ff; -} - -.single-it-services-box.bg-ff5f5f .icon { - color: #ff5f5f; -} - -.single-it-services-box.bg-ff5f5f::before { - background-color: #ff5f5f; -} - -.single-it-services-box.bg-2ea7ff .icon { - color: #2ea7ff; -} - -.single-it-services-box.bg-2ea7ff::before { - background-color: #2ea7ff; -} - -.single-it-services-box:hover .icon { - color: var(--whiteColor) !important; -} - -.single-it-services-box:hover h3 { - color: var(--whiteColor) !important; -} - -.single-it-services-box:hover h3 a { - color: var(--whiteColor) !important; -} - -.single-it-services-box:hover p { - color: var(--whiteColor); -} - -.single-it-services-box:hover::before { - opacity: 1; - visibility: visible; -} - -/*================================================ -Services Details Area CSS -=================================================*/ -.services-details-desc h3 { - font-size: 24px; - margin-bottom: 15px; - margin-top: 30px; -} - -.services-details-desc h3:first-child { - margin-top: 0; -} - -.services-details-desc ul li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; -} - -.services-details-desc ul li:last-child { - margin-bottom: 0; -} - -.services-details-desc p strong { - color: var(--blackColor); -} - -.services-details-desc p a { - color: var(--startupMainColor); -} - -.services-details-desc p a:hover { - text-decoration: underline; -} - -.services-details-desc img { - margin-top: 20px; - margin-bottom: 20px; -} - -.services-details-content h2 { - font-size: 40px; - line-height: 1.4; - margin-bottom: 15px; -} - -.services-details-video { - margin-left: 30px; - text-align: center; - background-color: #f5f5f5; -} - -.services-details-video iframe { - width: 100%; - border: none; - height: 336px; - display: block; -} - -.services-details-video .default-btn { - margin-top: 20px; - margin-left: 30px; - margin-right: 30px; - margin-bottom: 20px; -} - -.services-details-overview .overview-content h2 { - font-size: 40px; - line-height: 1.4; - margin-bottom: 15px; -} - -/*================================================ -Works Area CSS -=================================================*/ -.startup-works-area .section-title { - max-width: 100%; - text-align: start; - padding-top: 100px; - margin-left: 0; - margin-right: 0; - margin-bottom: -220px; -} - -.startup-works-area .section-title h2 { - font-size: 35px; -} - -.startup-works-content { - padding-top: 245px; - padding-right: 15px; -} - -.startup-works-content h3 { - font-size: 20px; - margin-bottom: 15px; -} - -.startup-works-content h3 a { - display: inline-block; -} - -.startup-works-content h3 a:hover { - color: var(--startupMainColor); -} - -.startup-works-content .custom-owl-nav { - margin-top: 25px; -} - -.startup-works-content .custom-owl-nav [class*=owl-] { - margin: 0 5px; - background-color: var(--startupMainColor); - color: var(--whiteColor); - padding: 8px 25px 7px !important; - border-radius: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - font-size: var(--fontSize); -} - -.startup-works-content .custom-owl-nav [class*=owl-] i { - line-height: 1; -} - -.startup-works-content .custom-owl-nav [class*=owl-].owl-prev { - margin-left: 0; -} - -.startup-works-content .custom-owl-nav [class*=owl-].owl-next { - margin-right: 0; -} - -.startup-works-content .custom-owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.startup-works-image { - z-index: 1; - margin-left: 15px; - text-align: center; - position: relative; - background-color: #f9f4f0; - padding-top: 80px; - padding-right: 80px; - padding-bottom: 80px; -} - -.startup-works-image .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 1; - display: block; -} - -.single-works-box { - overflow: hidden; - position: relative; - margin-bottom: 30px; -} - -.single-works-box .works-image { - position: relative; -} - -.single-works-box .works-image img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-works-box .works-image .works-overlay { - position: absolute; - overflow: hidden; - top: 10px; - left: 10px; - right: 10px; - bottom: 10px; -} - -.single-works-box .works-image .works-overlay::before { - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - position: absolute; - width: 100%; - bottom: 0; - height: 50%; - background: var(--businessGradientColor); - right: -100%; -} - -.single-works-box .works-image .works-overlay::after { - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - position: absolute; - background: var(--businessGradientColor); - top: 0; - height: 50%; - width: 100%; - left: -100%; -} - -.single-works-box .works-image .works-overlay h3 { - color: transparent; - overflow: hidden; - position: absolute; - top: 30px; - text-align: center; - right: 25px; - z-index: 2; - -webkit-transition: var(--transition); - transition: var(--transition); - font-size: 20px; - margin-bottom: 5px; -} - -.single-works-box .works-image .works-overlay h3::before { - content: ''; - background: var(--blackColor); - position: absolute; - width: 100%; - bottom: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - left: 100%; - top: 0; - right: auto; -} - -.single-works-box .works-image .works-overlay span { - color: transparent; - overflow: hidden; - z-index: 2; - position: absolute; - top: 62px; - display: block; - right: 25px; - -webkit-transition: var(--transition); - transition: var(--transition); - text-align: center; -} - -.single-works-box .works-image .works-overlay span::before { - content: ''; - background: var(--whiteColor); - width: 100%; - top: 0; - bottom: 0; - position: absolute; - -webkit-transition: var(--transition); - transition: var(--transition); - right: 100%; -} - -.single-works-box .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 5; -} - -.single-works-box:hover .works-image img { - -webkit-transform: scale(1.3); - transform: scale(1.3); -} - -.single-works-box:hover .works-image .works-overlay::before { - right: 0; - opacity: 1; - width: 100%; - height: 50%; - -webkit-transition: all .8s; - transition: all .8s; -} - -.single-works-box:hover .works-image .works-overlay::after { - left: 0; - opacity: 1; - -webkit-transition: all .8s; - transition: all .8s; -} - -.single-works-box:hover .works-image .works-overlay span { - color: var(--whiteColor); -} - -.single-works-box:hover .works-image .works-overlay span::before { - right: -100%; -} - -.single-works-box:hover .works-image .works-overlay h3 { - color: var(--whiteColor); -} - -.single-works-box:hover .works-image .works-overlay h3::before { - left: -100%; -} - -.single-works-box.startup-color .works-image .works-overlay::before { - background: var(--startupMainColor); -} - -.single-works-box.startup-color .works-image .works-overlay::after { - background: var(--startupMainColor); -} - -.works-box { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; -} - -.works-box img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.works-box .link-btn { - display: block; - left: 0; - right: 0; - top: 0; - bottom: 0; - position: absolute; - z-index: 2; -} - -.works-box h3 { - margin-bottom: 0; - font-size: 20px; - position: absolute; - left: 0; - right: 0; - top: 50%; - opacity: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - color: var(--whiteColor); - background-color: rgba(0, 0, 0, 0.75); - padding: 15px; - margin-left: 25px; - margin-right: 25px; - margin-top: 25px; -} - -.works-box:hover img { - -webkit-transform: scale(1.05); - transform: scale(1.05); -} - -.works-box:hover h3 { - margin-top: 0; - visibility: visible; - opacity: 1; -} - -.works-box-btn { - text-align: center; - margin-top: 20px; -} - -.single-works-item { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; -} - -.single-works-item img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-works-item::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: var(--startupMainColor); - z-index: 1; - opacity: 0; - visibility: hidden; - -webkit-transition: .3s; - transition: .3s; -} - -.single-works-item .content { - position: absolute; - left: 0; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - margin-top: 15px; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; - z-index: 2; - padding-left: 20px; - padding-right: 20px; -} - -.single-works-item .content .link-btn { - width: 45px; - height: 45px; - line-height: 45px; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - border-radius: 50%; - margin-bottom: 20px; - display: inline-block; -} - -.single-works-item .content .link-btn:hover { - color: var(--blackColor); - background-color: var(--whiteColor); -} - -.single-works-item .content h3 { - font-size: 22px; - color: var(--whiteColor); - margin-bottom: 12px; -} - -.single-works-item .content h3 a { - color: var(--whiteColor); -} - -.single-works-item .content span { - display: block; - color: var(--whiteColor); - opacity: 0.9; -} - -.single-works-item:hover img { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -.single-works-item:hover::before { - opacity: 1; - margin: 10px; - visibility: visible; -} - -.single-works-item:hover .content { - opacity: 1; - visibility: visible; - margin-top: 0; -} - -.single-works-item.minimal-portfolio-color::before { - background-color: var(--minimalPortfolioColor); -} - -.showMoreItemsList + .button-box { - margin-bottom: 0; - text-align: start; -} - -.showMoreItemsList + .button-box button { - background-color: transparent; - border: 1px solid #d6d9dc; - color: var(--blackColor); - padding: 12px 45px; - -webkit-transition: var(--transition); - transition: var(--transition); - display: inline-block; - width: auto; - margin-top: 10px; - border-radius: 0; - font-size: var(--fontSize); - font-weight: 500; -} - -.showMoreItemsList + .button-box button:hover { - background-color: var(--startupMainColor); - border-color: var(--startupMainColor); - color: var(--whiteColor); -} - -.works-slides-style-one.owl-theme .single-works-item, .works-slides-style-one.owl-theme .works-box, .works-slides-style-two.owl-theme .single-works-item, .works-slides-style-two.owl-theme .works-box { - margin-bottom: 0; -} - -.works-slides-style-one.owl-theme .owl-nav, .works-slides-style-two.owl-theme .owl-nav { - opacity: 0; - margin-top: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.works-slides-style-one.owl-theme .owl-nav [class*=owl-], .works-slides-style-two.owl-theme .owl-nav [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--blackColor); - padding: 0 !important; - border-radius: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: 25px; - top: 50%; - width: 45px; - height: 45px; - line-height: 45px; - border-radius: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - font-size: 20px; -} - -.works-slides-style-one.owl-theme .owl-nav [class*=owl-].owl-next, .works-slides-style-two.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 25px; -} - -.works-slides-style-one.owl-theme .owl-nav [class*=owl-] i, .works-slides-style-two.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.works-slides-style-one.owl-theme .owl-nav [class*=owl-]:hover, .works-slides-style-two.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); -} - -.works-slides-style-one.owl-theme:hover .owl-nav, .works-slides-style-two.owl-theme:hover .owl-nav { - opacity: 1; - visibility: visible; -} - -.works-slides-style-one.owl-theme .owl-dots, .works-slides-style-two.owl-theme .owl-dots { - margin-top: 40px; -} - -.works-slides-style-one.owl-theme .owl-dots .owl-dot span, .works-slides-style-two.owl-theme .owl-dots .owl-dot span { - width: 18px; - height: 18px; - border-radius: 50%; - -webkit-transition: var(--transition); - transition: var(--transition); - margin: 0 5px; -} - -.works-slides-style-one.owl-theme .owl-dots .owl-dot:hover span, .works-slides-style-one.owl-theme .owl-dots .owl-dot.active span, .works-slides-style-two.owl-theme .owl-dots .owl-dot:hover span, .works-slides-style-two.owl-theme .owl-dots .owl-dot.active span { - background-color: var(--startupMainColor); -} - -/*================================================ -What We Do Area CSS -=================================================*/ -.what-we-do-content .section-title { - text-align: start; - max-width: 100%; - margin-left: 0; - margin-right: 0; - margin-bottom: 30px; -} - -.what-we-do-content .section-title p { - max-width: 100%; - margin-left: 0; - margin-right: 0; -} - -.what-we-do-content .single-what-we-do-box { - margin-bottom: 0; -} - -.single-what-we-do-box { - margin-bottom: 30px; -} - -.single-what-we-do-box .icon { - display: block; - line-height: 1; - font-size: 45px; - margin-bottom: 25px; - color: var(--consultingMainColor); -} - -.single-what-we-do-box h3 { - font-size: 20px; - margin-bottom: 12px; -} - -/*================================================ -Categories Area CSS -=================================================*/ -.single-categories-box { - margin-bottom: 30px; - overflow: hidden; - position: relative; -} - -.single-categories-box img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-categories-box h3 { - margin-bottom: 0; - font-size: 20px; - position: absolute; - left: 25px; - top: 25px; - background-color: var(--whiteColor); - padding: 7px 20px; - -webkit-clip-path: polygon(5% 0, 100% 0, 95% 100%, 0% 100%); - clip-path: polygon(5% 0, 100% 0, 95% 100%, 0% 100%); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-categories-box .link-btn { - display: block; - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - z-index: 2; -} - -.single-categories-box:hover img { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -.single-categories-box:hover h3 { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.categories-box { - margin-bottom: 30px; - position: relative; - text-align: center; - overflow: hidden; -} - -.categories-box img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.categories-box h3 { - top: 50%; - left: 50%; - margin-bottom: 0; - position: absolute; - -webkit-transform: translateX(-50%) translateY(-50%); - transform: translateX(-50%) translateY(-50%); - font-size: 20px; - z-index: 1; - background-color: rgba(255, 255, 255, 0.8); - -webkit-transition: var(--transition); - transition: var(--transition); - padding-top: 5px; - padding-bottom: 5px; - padding-left: 30px; - padding-right: 30px; -} - -.categories-box .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - display: block; - z-index: 2; -} - -.categories-box:hover img { - -webkit-transform: scale(1.05); - transform: scale(1.05); -} - -.categories-box:hover h3 { - background-color: var(--whiteColor); -} - -.categories-slides.owl-theme .owl-nav { - margin-top: 0; - opacity: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.categories-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0; - background-color: transparent; - color: var(--optionalColor); - padding: 0 !important; - border-radius: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: -45px; - top: 50%; - width: 35px; - height: 35px; - line-height: 35px; - border-radius: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - border: 1px solid var(--optionalColor); - font-size: var(--fontSize); -} - -.categories-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: -45px; -} - -.categories-slides.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.categories-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); -} - -.categories-slides.owl-theme:hover .owl-nav { - opacity: 1; - visibility: visible; -} - -/*================================================ -Who We Are Area CSS -=================================================*/ -.who-we-are-area .row { - margin-left: 0; - margin-right: 0; -} - -.who-we-are-area .row .col-lg-6 { - padding-left: 0; - padding-right: 0; -} - -.who-we-are-content { - padding: 40px; - background-color: #f3f4f7; -} - -.who-we-are-content .sub-title { - display: block; - margin-bottom: 10px; - color: var(--consultingMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.who-we-are-content h2 { - margin-bottom: 15px; - line-height: 1.5; - font-size: 21px; -} - -.who-we-are-content .default-btn { - margin-top: 10px; -} - -.who-we-are-image { - text-align: center; - position: relative; -} - -.who-we-are-image .video-btn { - top: 50%; - left: 50%; - width: 80px; - height: 80px; - font-size: 30px; - border-radius: 50%; - position: absolute; - display: inline-block; - color: var(--blackColor); - background-color: var(--whiteColor); - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); -} - -.who-we-are-image .video-btn i { - position: absolute; - left: 0; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.who-we-are-image .video-btn::after, .who-we-are-image .video-btn::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 1.5px solid var(--saasMainColor); -} - -.who-we-are-image .video-btn::before { - -webkit-animation: ripple 2s linear infinite; - animation: ripple 2s linear infinite; -} - -.who-we-are-image .video-btn::after { - -webkit-animation: ripple 2s linear 1s infinite; - animation: ripple 2s linear 1s infinite; -} - -/*================================================ -Video Area CSS -=================================================*/ -.video-box { - text-align: center; - position: relative; -} - -.video-box img { - width: 100%; -} - -.video-box .video-btn { - position: absolute; - left: 50%; - display: inline-block; - top: 40%; - -webkit-transform: translateY(-40%) translateX(-50%); - transform: translateY(-40%) translateX(-50%); - width: 80px; - height: 80px; - background-color: var(--whiteColor); - border-radius: 50%; - color: var(--blackColor); - font-size: 30px; -} - -.video-box .video-btn i { - position: absolute; - left: 0; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.video-box .video-btn::after, .video-box .video-btn::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 2px solid var(--saasMainColor); -} - -.video-box .video-btn::before { - -webkit-animation: ripple 2s linear infinite; - animation: ripple 2s linear infinite; -} - -.video-box .video-btn::after { - -webkit-animation: ripple 2s linear 1s infinite; - animation: ripple 2s linear 1s infinite; -} - -/*================================================ -Process Area CSS -=================================================*/ -.single-process-box { - margin-bottom: 30px; - text-align: center; - border: 1px dashed #e1e1e1; - -webkit-transition: var(--transition); - transition: var(--transition); - padding: 40px 25px; -} - -.single-process-box img { - margin-bottom: 30px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-process-box h3 { - margin-bottom: 12px; - font-size: 20px; -} - -.single-process-box:hover { - -webkit-box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - border-color: var(--whiteColor); -} - -.single-process-box:hover img { - -webkit-transform: scale(0.9); - transform: scale(0.9); -} - -/*================================================ -Funfacts Area CSS -=================================================*/ -.funfacts-area.bg-business-color { - background: var(--businessGradientColor); -} - -.funfacts-area.bg-startup-color { - background: var(--startupMainColor); -} - -.funfacts-area.bg-startup-color .single-funfacts-box.without-bg-color { - border-color: var(--whiteColor); -} - -.funfacts-area.bg-image { - background-color: var(--blackColor); - background-image: url(../../assets/img/bg8.jpg); - background-position: center center; - background-size: cover; - background-repeat: no-repeat; -} - -.funfacts-area.bg-f9f9f9 { - position: relative; - z-index: 1; -} - -.funfacts-area.bg-f9f9f9::before { - left: 0; - right: 0; - bottom: 0; - height: 50%; - z-index: -1; - content: ''; - position: absolute; - background-color: var(--whiteColor); -} - -.single-funfacts-box { - margin-bottom: 30px; - color: var(--whiteColor); - position: relative; - z-index: 1; - overflow: hidden; - background-color: var(--blackColor); - padding-top: 40px; - padding-bottom: 40px; - padding-left: 85px; - padding-right: 30px; -} - -.single-funfacts-box .icon { - position: absolute; - left: 25px; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - color: var(--startupMainColor); - font-size: 45px; -} - -.single-funfacts-box h3 { - color: var(--whiteColor); - margin-bottom: 4px; - font-size: 40px; -} - -.single-funfacts-box h3 .odometer-formatting-mark { - display: none; -} - -.single-funfacts-box h3 .sign { - position: relative; - top: 1px; -} - -.single-funfacts-box .sub-title { - display: block; - font-weight: 600; -} - -.single-funfacts-box .shape { - position: absolute; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - z-index: -1; -} - -.single-funfacts-box.bg-white { - color: var(--optionalColor); - background-color: var(--whiteColor); - -webkit-box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); -} - -.single-funfacts-box.bg-white .icon.color-fd5444 { - color: #fd5444; -} - -.single-funfacts-box.bg-white .icon.color-3251ad { - color: #3251ad; -} - -.single-funfacts-box.bg-white .icon.color-62bd5e { - color: #62bd5e; -} - -.single-funfacts-box.bg-white .icon.color-ff6720 { - color: #ff6720; -} - -.single-funfacts-box.bg-white h3 { - color: var(--blackColor); -} - -.single-funfacts-box.bg-white .shape { - opacity: .15; -} - -.single-funfacts-box.without-bg-color { - background-color: transparent; - border-right: 1px dashed #a684f5; - margin-right: 40px; - padding-right: 0; - padding-top: 15px; - padding-bottom: 15px; - padding-left: 65px; -} - -.single-funfacts-box.without-bg-color .icon { - left: 0; - color: var(--whiteColor); -} - -.single-funfacts-box.text-center { - border-right: 2px dashed rgba(255, 255, 255, 0.2); - background-color: transparent; - padding-left: 0; - padding-top: 20px; - padding-bottom: 20px; -} - -.single-funfacts-box.consulting-color h3 { - color: var(--consultingMainColor); -} - -.funfacts-inner { - background-color: var(--blackColor); - padding-left: 30px; - padding-right: 30px; -} - -.funfacts-inner.bg-app-image { - background-image: url(../../assets/img/bg4.jpg); - background-position: center center; - background-size: cover; - background-repeat: no-repeat; -} - -.funfacts-inner.bg-app-image .single-funfacts-box.without-bg-color { - padding: 0; - border: none; - text-align: center; - margin-right: 0; -} - -.funfacts-inner.it-agency-color { - background: var(--itAgencyGradientColor); - background-size: cover; - background-repeat: no-repeat; - background-position: center center; - background-image: url(../../assets/img/bg9.jpg); -} - -.funfacts-inner.border-radius { - border-radius: 10px; -} - -.funfacts-inner.border-radius.ptb-70 { - padding-bottom: 40px; -} - -.row .col-lg-3:last-child .single-funfacts-box.without-bg-color, .row .col-lg-3:last-child .single-funfacts-box.text-center { - border-right: none; -} - -/*================================================ -Products Area CSS -=================================================*/ -.products-area.with-top-border { - border-top: 1px solid #eeeeee; -} - -.single-products-box { - margin-bottom: 30px; - text-align: center; -} - -.single-products-box .image { - position: relative; - overflow: hidden; -} - -.single-products-box .image img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-products-box .image .new { - position: absolute; - left: 12px; - top: 12px; - background-color: var(--blackColor); - color: var(--whiteColor); - width: 45px; - height: 45px; - text-align: center; - line-height: 45px; - border-radius: 50%; - z-index: 2; - font-size: 13px; - font-weight: 600; -} - -.single-products-box .image .sale { - position: absolute; - z-index: 2; - right: 12px; - top: 12px; - background-color: red; - color: var(--whiteColor); - width: 45px; - height: 45px; - text-align: center; - line-height: 45px; - border-radius: 50%; - font-size: 13px; - font-weight: 600; -} - -.single-products-box .image .buttons-list { - position: absolute; - text-align: center; - z-index: 1; - left: 0; - right: 0; - bottom: 20px; - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.single-products-box .image .buttons-list li { - opacity: 0; - visibility: hidden; - display: inline-block; - -webkit-transform: translateY(30px); - transform: translateY(30px); - -webkit-transition: all 0.3s ease-in-out 0s; - transition: all 0.3s ease-in-out 0s; - margin-left: 3px; - margin-right: 3px; -} - -.single-products-box .image .buttons-list li a { - display: inline-block; - font-size: 16px; - color: var(--blackColor); - position: relative; - width: 40px; - border-radius: 50%; - line-height: 39px; - background-color: var(--whiteColor); - height: 40px; - border: 1px solid #eeeeee; -} - -.single-products-box .image .buttons-list li a .tooltip-label { - right: 50%; - top: -15px; - -webkit-transform: translateX(50%); - transform: translateX(50%); - position: absolute; - padding: 0 10px; - line-height: 28px; - font-size: 12px; - letter-spacing: .3px; - visibility: hidden; - white-space: nowrap; - opacity: 0; - color: transparent; - background-color: transparent; - -webkit-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.single-products-box .image .buttons-list li a .tooltip-label::before { - content: "\f0dd"; - color: var(--blackColor); - left: 50%; - position: absolute; - -webkit-transform: translateX(-50%) rotate(0deg); - transform: translateX(-50%) rotate(0deg); - -webkit-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; - bottom: -12px; - font-family: "Font Awesome 5 Free"; - -webkit-font-variant-ligatures: normal; - font-variant-ligatures: normal; - font-variant-caps: normal; - font-style: normal; - font-weight: 900; - font-size: 20px; -} - -.single-products-box .image .buttons-list li a:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); -} - -.single-products-box .image .buttons-list li a:hover .tooltip-label { - visibility: visible; - opacity: 1; - top: -45px; -} - -.single-products-box .image .buttons-list li:nth-child(1) { - -webkit-transition-delay: 0.1s; - transition-delay: 0.1s; -} - -.single-products-box .image .buttons-list li:nth-child(2) { - -webkit-transition-delay: 0.2s; - transition-delay: 0.2s; -} - -.single-products-box .image .buttons-list li:nth-child(3) { - -webkit-transition-delay: 0.3s; - transition-delay: 0.3s; -} - -.single-products-box .content { - margin-top: 25px; -} - -.single-products-box .content .rating { - margin-bottom: 12px; -} - -.single-products-box .content .rating i { - color: #fec900; - line-height: 1; -} - -.single-products-box .content h3 { - margin-bottom: 13px; - font-size: 20px; -} - -.single-products-box .content .price span { - color: var(--blackColor); - font-size: 17px; - font-weight: 700; -} - -.single-products-box .content .price span.old-price { - color: var(--optionalColor); - text-decoration: line-through; -} - -.single-products-box:hover .image img { - -webkit-transform: scale(1.05); - transform: scale(1.05); -} - -.single-products-box:hover .image .buttons-list li { - -webkit-transform: translateY(0); - transform: translateY(0); - visibility: visible; - opacity: 1; -} - -.noke-grid-sorting { - margin-bottom: 40px; -} - -.noke-grid-sorting .result-count p .count { - font-weight: 700; - color: var(--blackColor); -} - -.noke-grid-sorting .ordering { - text-align: end; -} - -.noke-grid-sorting .ordering label { - display: inline-block; - color: var(--optionalColor); - margin-bottom: 0; - margin-right: 5px; -} - -.noke-grid-sorting .ordering select { - width: 215px; - height: 45px; - cursor: pointer; - line-height: 45px; - display: inline-block; - background: #f8f8f8; - border-color: #eeeeee; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); - padding-top: 1px; - padding-left: 12px; - font-size: var(--fontSize); - font-weight: 600; -} - -/*================================================ -Products Details Area CSS -=================================================*/ -.products-details-desc { - padding-left: 15px; -} - -.products-details-desc h3 { - margin-bottom: 15px; - font-size: 20px; -} - -.products-details-desc .price { - color: var(--blackColor); - margin-top: 15px; - margin-bottom: 12px; - font-weight: 700; - font-size: 17px; -} - -.products-details-desc .price .old-price { - text-decoration: line-through; - color: #9b9494; - font-weight: normal; -} - -.products-details-desc .products-review .rating { - display: inline-block; - padding-right: 3px; - font-size: 14px; -} - -.products-details-desc .products-review .rating i { - display: inline-block; - color: #f49f0b; - line-height: 1; -} - -.products-details-desc .products-review .rating-count { - display: inline-block; - color: var(--optionalColor); - line-height: initial; - position: relative; - top: 1px; -} - -.products-details-desc .products-review .rating-count:hover { - color: var(--blackColor); -} - -.products-details-desc .products-meta { - margin-top: 20px; -} - -.products-details-desc .products-meta span { - display: block; - font-weight: 500; - margin-bottom: 10px; - color: var(--optionalColor); -} - -.products-details-desc .products-meta span span { - display: inline-block; - margin-bottom: 0; -} - -.products-details-desc .products-meta span span.sku { - color: var(--startupMainColor); -} - -.products-details-desc .products-meta span a { - display: inline-block; - color: var(--blackColor); -} - -.products-details-desc .products-meta span a:hover, .products-details-desc .products-meta span a:focus { - color: var(--startupMainColor); -} - -.products-details-desc .products-meta span.in-stock { - color: green; -} - -.products-details-desc .products-meta span:last-child { - margin-bottom: 0; -} - -.products-details-desc .products-add-to-cart { - margin-top: 25px; -} - -.products-details-desc .products-add-to-cart .quantities { - position: relative; - padding-left: 45px; -} - -.products-details-desc .products-add-to-cart .quantities .sub-title { - left: 0; - top: 50%; - display: block; - font-weight: 600; - position: absolute; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.products-details-desc .products-add-to-cart .input-counter { - display: block; - max-width: 120px; - min-width: 120px; - text-align: center; - position: relative; - margin-bottom: 25px; -} - -.products-details-desc .products-add-to-cart .input-counter span { - top: 0; - width: 35px; - height: 100%; - cursor: pointer; - font-size: 15px; - line-height: 38px; - position: absolute; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.products-details-desc .products-add-to-cart .input-counter span i { - position: relative; - top: 1px; -} - -.products-details-desc .products-add-to-cart .input-counter span.minus-btn { - left: 0; -} - -.products-details-desc .products-add-to-cart .input-counter span.plus-btn { - right: 0; -} - -.products-details-desc .products-add-to-cart .input-counter span:hover { - color: var(--optionalColor); -} - -.products-details-desc .products-add-to-cart .input-counter input { - height: 40px; - outline: 0; - width: 100%; - display: block; - text-align: center; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - font-size: 17px; - font-weight: 600; -} - -.products-details-desc .products-add-to-cart .input-counter input::-webkit-input-placeholder { - color: var(--blackColor); -} - -.products-details-desc .products-add-to-cart .input-counter input:-ms-input-placeholder { - color: var(--blackColor); -} - -.products-details-desc .products-add-to-cart .input-counter input::-ms-input-placeholder { - color: var(--blackColor); -} - -.products-details-desc .products-add-to-cart .input-counter input::placeholder { - color: var(--blackColor); -} - -.products-details-desc .default-btn { - -webkit-box-shadow: unset !important; - box-shadow: unset !important; -} - -.products-details-desc .social-share { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; - margin-top: 30px; -} - -.products-details-desc .social-share li { - display: inline-block; - margin-right: 12px; -} - -.products-details-desc .social-share li span { - font-weight: 600; - position: relative; - top: -1px; -} - -.products-details-desc .social-share li a { - line-height: 1; - font-size: 18px; - color: var(--optionalColor); -} - -.products-details-desc .social-share li a:hover { - color: var(--blackColor); -} - -.products-details-tabs { - margin-top: 50px; -} - -.products-details-tabs .single-tabs-box { - margin-bottom: 40px; -} - -.products-details-tabs .single-tabs-box h2 { - text-align: center; - margin-bottom: 30px; - position: relative; - font-size: 22px; - z-index: 1; -} - -.products-details-tabs .single-tabs-box h2 span { - display: inline-block; - background-color: var(--whiteColor); - padding-left: 20px; - padding-right: 20px; -} - -.products-details-tabs .single-tabs-box h2::before { - left: 0; - right: 0; - top: 50%; - z-index: -1; - height: 1px; - content: ''; - margin-top: 1px; - position: absolute; - background-color: #eeeeee; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.products-details-tabs .single-tabs-box:last-child { - margin-bottom: 0; -} - -.products-details-tabs .single-tabs-box .inner-box { - max-width: 800px; - margin-left: auto; - margin-right: auto; -} - -.products-details-tabs .single-tabs-box .inner-box h3 { - font-size: 20px; - margin-bottom: 15px; -} - -.products-details-tabs .single-tabs-box .inner-box ul { - margin-top: 20px; - margin-bottom: 0; -} - -.products-details-tabs .single-tabs-box .inner-box ul li { - font-weight: 500; - margin-bottom: 12px; - color: var(--optionalColor); -} - -.products-details-tabs .single-tabs-box .inner-box ul li:last-child { - margin-bottom: 0; -} - -.products-details-tabs .single-tabs-box .inner-box .table-striped { - margin-bottom: 0; -} - -.products-details-tabs .single-tabs-box .inner-box .table-striped tbody tr td { - border: none; - padding: 15px; - color: var(--optionalColor); - font-weight: 500; - font-size: var(--fontSize); -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments { - margin-top: 40px; -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review { - position: relative; - margin-bottom: 20px; - padding: 0 0 20px 110px; - border-bottom: 1px solid #eeeeee; -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review img { - top: 0; - left: 0; - width: 90px; - height: 90px; - border-radius: 5px; - position: absolute; -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review .sub-comment { - margin-bottom: 8px; - font-weight: 600; - font-family: var(--headingFontFamily); -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review .review-rating { - display: block; - margin-bottom: 8px; -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review .review-rating .review-stars { - display: inline-block; -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review .review-rating .review-stars i { - color: #cecfd2; - display: inline-block; -} - -.products-details-tabs .single-tabs-box .inner-box .products-review-comments .user-review .review-rating .review-stars i.checked { - color: orange; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper { - margin-top: 30px; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper h3 { - margin-bottom: 10px; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper .comment-notes span { - color: red; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form { - margin-top: 20px; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .form-group { - margin-bottom: 25px; - text-align: start; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating { - text-align: start; - overflow: hidden; - max-width: 115px; - margin-top: -5px; - margin-bottom: 25px; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating label { - float: right; - position: relative; - width: 23px; - height: 23px; - cursor: pointer; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating label:not(:first-of-type) { - padding-right: 5px; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating label:before { - content: "\2605"; - -webkit-transition: var(--transition); - transition: var(--transition); - font-size: 27px; - color: #CCCCCC; - line-height: 1; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating input { - display: none; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating input:checked ~ label:before, .products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating:not(:checked) > label:hover:before, .products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .rating:not(:checked) > label:hover ~ label:before { - color: #f6b500; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent { - text-align: start; - margin-bottom: 0; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:checked, .products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:not(:checked) { - display: none; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:checked + label, .products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:not(:checked) + label { - position: relative; - padding-left: 28px; - cursor: pointer; - line-height: 20px; - display: inline-block; - margin-bottom: 0; - color: var(--optionalColor); -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:checked + label:before, .products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:not(:checked) + label:before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 19px; - height: 19px; - -webkit-transition: all 0.2s ease; - transition: all 0.2s ease; - border: 1px solid #eeeeee; - border-radius: 3px; - background: var(--whiteColor); -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:checked + label:after, .products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:not(:checked) + label:after { - content: ''; - width: 8px; - height: 8px; - background: var(--startupMainColor); - position: absolute; - top: 6.5px; - left: 6px; - -webkit-transition: all 0.2s ease; - transition: all 0.2s ease; -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:not(:checked) + label:after { - opacity: 0; - -webkit-transform: scale(0); - transform: scale(0); -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:checked + label:after { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:hover + label:before { - border-color: var(--startupMainColor); -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form .comment-form-cookies-consent [type="checkbox"]:checked + label:before { - border-color: var(--startupMainColor); -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form button { - margin-top: 22px; - border: none; - display: inline-block; - text-align: center; - overflow: hidden; - color: var(--whiteColor); - background-color: var(--startupMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); - border-radius: 5px; - padding: 13px 40px; - -webkit-box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - font-weight: 700; - font-size: var(--fontSize); -} - -.products-details-tabs .single-tabs-box .inner-box .review-form-wrapper form button:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -/*================================================ -Cart Area CSS -=================================================*/ -.cart-table-desc table { - margin-bottom: 0; -} - -.cart-table-desc table thead tr th { - border: none; - padding: 13px 20px; - vertical-align: middle; - color: var(--whiteColor); - background-color: var(--blackColor); - font-family: var(--headingFontFamily); - font-size: var(--fontSize); - font-weight: 600; -} - -.cart-table-desc table tbody tr td { - color: var(--optionalColor); - vertical-align: middle; - font-size: 15px; - font-weight: 600; - padding-left: 0; - padding-right: 0; - padding-top: 15px; - padding-bottom: 15px; - border-left: none; - border-right: none; - border-color: #ebebeb; -} - -.cart-table-desc table tbody tr td.product-thumbnail a { - display: inline-block; -} - -.cart-table-desc table tbody tr td.product-thumbnail a img { - width: 80px; -} - -.cart-table-desc table tbody tr td.product-name a { - display: inline-block; -} - -.cart-table-desc table tbody tr td.product-subtotal .remove { - top: 2px; - color: red; - line-height: 1; - font-size: 20px; - margin-left: 40px; - position: relative; -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter { - max-width: 120px; - min-width: 120px; - text-align: center; - display: inline-block; - position: relative; -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter span { - top: 0; - width: 35px; - height: 100%; - font-size: 14px; - line-height: 37px; - position: absolute; - cursor: pointer; - color: var(--blackColor); - background-color: transparent; - -webkit-transition: var(--transition); - transition: var(--transition); - border: 1px solid #eeeeee; -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter span i { - position: relative; - top: 1px; -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter span.minus-btn { - left: 0; -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter span.plus-btn { - right: 0; -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter span:hover { - color: var(--mainColor); -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter input { - height: 40px; - outline: 0; - width: 100%; - display: block; - text-align: center; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - font-size: 17px; - font-weight: 600; -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter input::-webkit-input-placeholder { - color: var(--blackColor); -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter input:-ms-input-placeholder { - color: var(--blackColor); -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter input::-ms-input-placeholder { - color: var(--blackColor); -} - -.cart-table-desc table tbody tr td.product-quantity .input-counter input::placeholder { - color: var(--blackColor); -} - -.cart-table-desc table tbody tr td.product-subtotal { - overflow: hidden; -} - -.cart-table-desc table tbody tr:last-child td { - border-bottom: none; -} - -.cart-buttons-desc .shopping-coupon-code { - position: relative; -} - -.cart-buttons-desc .shopping-coupon-code .form-control { - height: 50px; -} - -.cart-buttons-desc .shopping-coupon-code button { - top: 0; - right: 0; - height: 50px; - border: none; - padding: 0 25px; - position: absolute; - color: var(--whiteColor); - border-radius: 0 5px 5px 0; - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--blackColor); - font-size: var(--fontSize); - font-weight: 700; -} - -.cart-buttons-desc .shopping-coupon-code button:hover { - background-color: var(--startupMainColor); -} - -.cart-buttons-desc .default-btn { - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.cart-totals-desc { - padding: 30px; - background-color: #f5f5f5; -} - -.cart-totals-desc h3 { - padding: 16px 20px; - margin-bottom: 30px; - color: var(--whiteColor); - font-size: var(--fontSize); - background-color: var(--blackColor); - margin-top: -30px; - margin-left: -30px; - margin-right: -30px; -} - -.cart-totals-desc ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.cart-totals-desc ul li { - overflow: hidden; - display: block; - margin-bottom: 12px; - padding-bottom: 12px; - font-weight: 500; - color: var(--optionalColor); - border-bottom: 1px solid #ebebeb; -} - -.cart-totals-desc ul li span { - float: right; -} - -.cart-totals-desc ul li:last-child { - font-weight: 700; - color: var(--blackColor); - padding-bottom: 0; - margin-bottom: 0; - border-bottom: none; -} - -.cart-totals-desc .default-btn { - margin-top: 25px; - -webkit-box-shadow: unset; - box-shadow: unset; -} - -/*================================================ -Checkout Area CSS -=================================================*/ -.billing-details-desc { - padding: 30px 30px 10px; - background-color: #f5f5f5; -} - -.billing-details-desc h3 { - font-size: 20px; - margin-bottom: 30px; - color: var(--whiteColor); - background-color: var(--blackColor); - padding-top: 17px; - padding-left: 30px; - padding-right: 30px; - padding-bottom: 17px; - margin-right: -30px; - margin-left: -30px; - margin-top: -30px; -} - -.billing-details-desc .form-group { - margin-bottom: 20px; -} - -.billing-details-desc .form-group label { - display: block; - font-weight: 500; - margin-bottom: 12px; - color: var(--blackColor); -} - -.billing-details-desc .form-group label .required { - color: red; -} - -.billing-details-desc .form-group .form-control { - background-color: var(--whiteColor) !important; -} - -.billing-details-desc .form-group select { - background-color: var(--whiteColor); - height: 50px; - cursor: pointer; - border: none; - display: block; - width: 100%; - padding-left: 15px; -} - -.order-details-desc { - padding: 30px; - background-color: #f5f5f5; -} - -.order-details-desc h3 { - font-size: 20px; - margin-bottom: 30px; - color: var(--whiteColor); - background-color: var(--blackColor); - padding-left: 30px; - padding-right: 30px; - padding-top: 17px; - padding-bottom: 17px; - margin-left: -30px; - margin-right: -30px; - margin-top: -30px; -} - -.order-details-desc .order-details { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.order-details-desc .order-details li { - display: block; - font-weight: 500; - overflow: hidden; - margin-bottom: 12px; - padding-bottom: 12px; - color: var(--optionalColor); - border-bottom: 1px solid #ebebeb; -} - -.order-details-desc .order-details li span { - float: right; -} - -.order-details-desc .order-details li:last-child { - font-weight: 700; - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; - color: var(--blackColor); -} - -.order-details-desc .payment-method { - margin-top: 30px; - margin-bottom: 20px; -} - -.order-details-desc .payment-method p { - margin-bottom: 12px; -} - -.order-details-desc .payment-method p:last-child { - margin-bottom: 0; -} - -.order-details-desc .payment-method p [type="radio"]:checked, .order-details-desc .payment-method p [type="radio"]:not(:checked) { - display: none; -} - -.order-details-desc .payment-method p [type="radio"]:checked + label, .order-details-desc .payment-method p [type="radio"]:not(:checked) + label { - padding-left: 27px; - cursor: pointer; - display: block; - color: var(--blackColor); - position: relative; - margin-bottom: 8px; - font-size: var(--fontSize); - font-weight: 700; -} - -.order-details-desc .payment-method p [type="radio"]:checked + label::before, .order-details-desc .payment-method p [type="radio"]:not(:checked) + label::before { - content: ''; - position: absolute; - left: 0; - top: 5px; - width: 18px; - height: 18px; - border: 1px solid #dddddd; - border-radius: 50%; - background: var(--whiteColor); -} - -.order-details-desc .payment-method p [type="radio"]:checked + label::after, .order-details-desc .payment-method p [type="radio"]:not(:checked) + label::after { - content: ''; - width: 12px; - height: 12px; - background: var(--startupMainColor); - position: absolute; - top: 8px; - left: 3px; - border-radius: 50%; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.order-details-desc .payment-method p [type="radio"]:not(:checked) + label::after { - opacity: 0; - visibility: hidden; - -webkit-transform: scale(0); - transform: scale(0); -} - -.order-details-desc .payment-method p [type="radio"]:checked + label::after { - opacity: 1; - visibility: visible; - -webkit-transform: scale(1); - transform: scale(1); -} - -.order-details-desc .form-check { - color: var(--optionalColor); - font-weight: 500; -} - -.order-details-desc .form-check a { - color: var(--startupMainColor); -} - -.order-details-desc .form-check a:hover { - text-decoration: underline; -} - -.order-details-desc .default-btn { - margin-top: 25px; - display: block; - width: 100%; - -webkit-box-shadow: unset; - box-shadow: unset; - border-radius: 5px; - padding-top: 13px; - padding-bottom: 13px; - padding-left: 30px; - padding-right: 30px; -} - -/*================================================ -Testimonials Area CSS -=================================================*/ -.testimonials-area.bg-image { - background-image: url(../../assets/img/bg10.jpg); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.testimonials-area.bg-image .section-title.style-three h2 span { - background-color: #f7eee8; -} - -.testimonials-inner { - background-color: var(--blackColor); - padding-left: 60px; - padding-right: 60px; -} - -.testimonials-inner.bg-app-image { - background-image: url(../../assets/img/bg5.jpg); - background-position: center center; - background-size: cover; - background-repeat: no-repeat; -} - -.testimonials-inner .testimonials-item { - max-width: 650px; - -webkit-box-shadow: unset; - box-shadow: unset; - background-color: transparent; - padding: 0; - margin-left: auto; - margin-right: auto; -} - -.testimonials-inner .testimonials-item .icon { - color: var(--whiteColor); -} - -.testimonials-inner .testimonials-item p { - color: var(--whiteColor); -} - -.testimonials-inner .testimonials-item .info .title h3 { - color: var(--whiteColor); -} - -.testimonials-inner .testimonials-item .info .title span { - color: var(--whiteColor); - opacity: 0.9; -} - -.testimonials-inner .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--appMainColor); - padding: 0 !important; - border-radius: 50%; - border: none; - font-size: 20px; - width: 50px; - height: 50px; - line-height: 50px; -} - -.testimonials-inner .testimonials-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.testimonials-inner.it-agency-color { - background-color: transparent; -} - -.testimonials-inner.it-agency-color .testimonials-item .icon { - color: var(--itAgencyMainColor); -} - -.testimonials-inner.it-agency-color .testimonials-item p { - color: var(--blackColor); -} - -.testimonials-inner.it-agency-color .testimonials-item .info .title h3 { - color: var(--blackColor); -} - -.testimonials-inner.it-agency-color .testimonials-item .info .title span { - color: var(--optionalColor); - opacity: 1; -} - -.testimonials-inner.it-agency-color .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - background: var(--itAgencyGradientColor); - color: var(--whiteColor); -} - -.testimonials-inner.minimal-portfolio-color { - background-color: transparent; -} - -.testimonials-inner.minimal-portfolio-color .testimonials-item .icon { - color: var(--minimalPortfolioColor); -} - -.testimonials-inner.minimal-portfolio-color .testimonials-item p { - color: var(--blackColor); -} - -.testimonials-inner.minimal-portfolio-color .testimonials-item .info .title h3 { - color: var(--blackColor); -} - -.testimonials-inner.minimal-portfolio-color .testimonials-item .info .title span { - color: var(--optionalColor); - opacity: 1; -} - -.testimonials-inner.minimal-portfolio-color .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - background: #f9f9f9; - color: var(--blackColor); -} - -.testimonials-inner.minimal-portfolio-color .testimonials-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--minimalPortfolioColor); - color: var(--whiteColor); -} - -.testimonials-inner.shop-color { - background-color: transparent; -} - -.testimonials-inner.shop-color .testimonials-item .icon { - color: var(--shopMainColor); -} - -.testimonials-inner.shop-color .testimonials-item p { - color: var(--blackColor); -} - -.testimonials-inner.shop-color .testimonials-item .info .title h3 { - color: var(--blackColor); -} - -.testimonials-inner.shop-color .testimonials-item .info .title span { - color: var(--optionalColor); - opacity: 1; -} - -.testimonials-inner.shop-color .testimonials-slides.owl-theme .owl-nav [class*=owl-] { - background: var(--whiteColor); - color: var(--blackColor); -} - -.testimonials-inner.shop-color .testimonials-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.testimonials-item { - text-align: center; - background-color: var(--whiteColor); - padding: 30px; - -webkit-box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); -} - -.testimonials-item .icon { - line-height: 1; - margin-bottom: 25px; - color: var(--appMainColor); - display: block; - font-size: 45px; -} - -.testimonials-item p { - color: var(--blackColor); - font-size: 20px; - font-weight: 500; -} - -.testimonials-item .info { - margin-top: 30px; - text-align: start; -} - -.testimonials-item .info img { - display: inline-block !important; - width: 80px !important; - height: 80px; - border-radius: 50%; - margin-right: 10px; -} - -.testimonials-item .info .title { - margin-left: 10px; -} - -.testimonials-item .info .title h3 { - font-size: 18px; - margin-bottom: 8px; -} - -.testimonials-item .info .title span { - display: block; - font-weight: 500; - color: var(--appMainColor); - opacity: .85; -} - -.startup-testimonials-area .container-fluid { - max-width: 1400px; -} - -.startup-testimonials-image { - z-index: 1; - text-align: center; - position: relative; - background-color: #2d2d37; - padding-top: 80px; - padding-left: 80px; - padding-bottom: 80px; -} - -.startup-testimonials-image img { - width: auto !important; - display: inline-block !important; -} - -.startup-testimonials-content { - text-align: center; - padding-left: 40px; - padding-right: 40px; - padding-bottom: 100px; -} - -.startup-testimonials-content .icon { - line-height: 1; - margin-bottom: 20px; - color: var(--startupMainColor); - display: block; - font-size: 45px; -} - -.startup-testimonials-content p { - color: var(--blackColor); - font-size: 18px; - font-weight: 600; -} - -.startup-testimonials-content .info { - margin-top: 25px; - padding-top: 25px; - border-top: 1px solid #eeeeee; -} - -.startup-testimonials-content .info h3 { - font-size: 18px; - margin-bottom: 8px; -} - -.startup-testimonials-content .info span { - display: block; - font-weight: 500; -} - -.startup-testimonials-slides.owl-theme .owl-nav { - margin-top: 0; - position: absolute; - right: 22%; - bottom: 105px; - -webkit-transform: translateX(22%); - transform: translateX(22%); -} - -.startup-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 5px; - background-color: var(--startupMainColor); - color: var(--whiteColor); - padding: 8px 25px 7px !important; - border-radius: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - font-size: var(--fontSize); -} - -.startup-testimonials-slides.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.startup-testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-prev { - margin-left: 0; -} - -.startup-testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-next { - margin-right: 0; -} - -.startup-testimonials-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.marketing-testimonials-area { - border-bottom: 1px solid #f0f0f0; -} - -.marketing-testimonials-image { - text-align: center; - position: relative; - z-index: 2; - margin-right: 50px; - margin-left: 50px; -} - -.marketing-testimonials-image::before { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - position: absolute; - border-radius: 50%; - background-color: var(--marketingMainColor); -} - -.marketing-testimonials-image::after { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -2; - content: ''; - position: absolute; - border-radius: 65% 35% 70% 30% / 64% 31% 69% 36%; - background-color: #ffac00; -} - -.marketing-testimonials-image img { - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - display: inline-block !important; - width: auto !important; -} - -.marketing-testimonials-content { - text-align: center; - padding-bottom: 60px; -} - -.marketing-testimonials-content .icon { - line-height: 1; - margin-bottom: 20px; - color: var(--marketingMainColor); - display: block; - font-size: 45px; -} - -.marketing-testimonials-content p { - color: var(--blackColor); - font-size: 18px; - font-weight: 600; -} - -.marketing-testimonials-content .info { - margin-top: 25px; -} - -.marketing-testimonials-content .info h3 { - font-size: 18px; - margin-bottom: 8px; -} - -.marketing-testimonials-content .info span { - display: block; - font-weight: 500; -} - -.marketing-testimonials-slides.owl-theme .owl-nav { - right: 22%; - margin-top: 0; - bottom: 100px; - position: absolute; - -webkit-transform: translateX(22%); - transform: translateX(22%); -} - -.marketing-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0 5px; - background-color: var(--marketingMainColor); - color: var(--whiteColor); - padding: 8px 25px 7px !important; - border-radius: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - font-size: var(--fontSize); -} - -.marketing-testimonials-slides.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.marketing-testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-prev { - margin-left: 0; -} - -.marketing-testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-next { - margin-right: 0; -} - -.marketing-testimonials-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.business-testimonials-area { - background: var(--businessGradientColor); - background-image: url(../../assets/img/bg1.jpg); - background-position: center center; - background-size: cover; - background-repeat: no-repeat; -} - -.business-testimonials-image { - text-align: center; - position: relative; - margin-right: 30px; - z-index: 2; -} - -.business-testimonials-image::before { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - position: absolute; - border-radius: 20% 80% 50% 50% / 20% 50% 50% 80%; - border: 2px solid var(--whiteColor); -} - -.business-testimonials-image::after { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -2; - content: ''; - position: absolute; - border-radius: 65% 35% 70% 30% / 64% 31% 69% 36%; - background-color: #ffac00; -} - -.business-testimonials-image img { - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - display: inline-block !important; - width: auto !important; -} - -.business-testimonials-content { - text-align: center; -} - -.business-testimonials-content .icon { - line-height: 1; - margin-bottom: 25px; - color: var(--whiteColor); - display: block; - font-size: 45px; -} - -.business-testimonials-content p { - color: var(--whiteColor); - font-size: 20px; - font-weight: 500; -} - -.business-testimonials-content .info { - margin-top: 30px; -} - -.business-testimonials-content .info h3 { - font-size: 18px; - margin-bottom: 8px; - color: var(--whiteColor); -} - -.business-testimonials-content .info span { - display: block; - font-weight: 500; - color: var(--whiteColor); - opacity: .85; -} - -.saas-testimonials-item { - border: 1px solid #242532; - background-color: rgba(4, 6, 17, 0.9); - padding: 30px; -} - -.saas-testimonials-item .info { - margin-bottom: 25px; -} - -.saas-testimonials-item .info img { - display: inline-block !important; - width: 80px !important; - height: 80px; - border-radius: 50%; - margin-right: 10px; -} - -.saas-testimonials-item .info .title { - margin-left: 10px; -} - -.saas-testimonials-item .info .title h3 { - font-size: 18px; - margin-bottom: 8px; - color: var(--whiteColor); -} - -.saas-testimonials-item .info .title span { - display: block; - font-weight: 600; - color: var(--saasMainColor); -} - -.saas-testimonials-item p { - font-size: 16px; - color: var(--whiteColor); -} - -.testimonials-slides.owl-theme { - padding-left: 120px; - padding-right: 120px; -} - -.testimonials-slides.owl-theme .owl-nav { - margin-top: 0; -} - -.testimonials-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0; - background-color: transparent; - color: var(--whiteColor); - padding: 8px 25px 6px !important; - border-radius: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - border: 1px solid var(--whiteColor); - font-size: var(--fontSize); -} - -.testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 0; -} - -.testimonials-slides.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.testimonials-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.saas-testimonials-slides.owl-theme { - position: relative; - margin-top: -100px; - z-index: 1; - padding-left: 80px; - padding-right: 80px; -} - -.saas-testimonials-slides.owl-theme .owl-nav { - margin-top: 0; -} - -.saas-testimonials-slides.owl-theme .owl-nav [class*=owl-] { - top: 50%; - margin: 0; - left: 30px; - border: none; - font-size: 20px; - cursor: pointer; - border-radius: 0; - position: absolute; - color: var(--whiteColor); - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: transparent !important; -} - -.saas-testimonials-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 30px; -} - -.saas-testimonials-slides.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.saas-testimonials-slides.owl-theme .owl-nav [class*=owl-]:hover { - color: var(--saasMainColor); -} - -.testimonials-box { - text-align: center; - border: 1px solid #d5edff; - padding: 30px; - margin-bottom: 30px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.testimonials-box .icon { - width: 80px; - height: 80px; - line-height: 82px; - background-color: #d5edff; - border-radius: 50%; - font-size: 35px; - display: block; - color: #2ea7ff; - -webkit-transition: var(--transition); - transition: var(--transition); - margin-left: auto; - margin-right: auto; - margin-bottom: 20px; -} - -.testimonials-box p { - color: var(--blackColor); - font-size: 16px; - font-weight: 500; -} - -.testimonials-box .info { - margin-top: 25px; -} - -.testimonials-box .info h3 { - font-size: 18px; - margin-bottom: 8px; -} - -.testimonials-box .info span { - display: block; - font-weight: 500; - color: var(--personalPortfolioMainColor); - opacity: .85; -} - -.testimonials-box:hover { - border-color: var(--personalPortfolioMainColor) !important; -} - -.testimonials-box:hover .icon { - background-color: #2ea7ff; - color: var(--whiteColor); -} - -.testimonials-box:hover.bg-ca83ff .icon { - color: var(--whiteColor); - background-color: #ca83ff; -} - -.testimonials-box:hover.bg-ff9f07 .icon { - color: var(--whiteColor); - background-color: #ff9f07; -} - -.testimonials-box.bg-ca83ff { - border-color: #faf2ff; -} - -.testimonials-box.bg-ca83ff .icon { - background-color: #faf2ff; - color: #ca83ff; -} - -.testimonials-box.bg-ff9f07 { - border-color: #fff5e6; -} - -.testimonials-box.bg-ff9f07 .icon { - background-color: #fff5e6; - color: #ff9f07; -} - -/*================================================ -Team Area CSS -=================================================*/ -.team-area.with-border { - border-top: 1px solid #f0f0f0; -} - -.single-team-member { - background-color: #f9f4f0; - margin-bottom: 30px; - text-align: center; -} - -.single-team-member .content { - padding: 25px 20px; - z-index: 1; - position: relative; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-team-member .content h3 { - -webkit-transition: var(--transition); - transition: var(--transition); - font-size: 20px; - margin-bottom: 10px; -} - -.single-team-member .content span { - display: block; - color: var(--startupMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-team-member .content .social { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - position: absolute; - left: 0; - right: 0; - bottom: 25px; - opacity: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-team-member .content .social li { - display: inline-block; - margin-left: 7px; - margin-right: 7px; -} - -.single-team-member .content .social li a { - display: block; - font-size: 20px; - color: var(--whiteColor); -} - -.single-team-member .content .social li a:hover { - color: var(--startupMainColor); -} - -.single-team-member .content::before { - content: ''; - position: absolute; - left: 0; - right: 0; - height: 40px; - background-color: var(--blackColor); - top: -40px; - z-index: -1; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; -} - -.single-team-member:hover .content, .single-team-member.active .content { - background-color: var(--blackColor); -} - -.single-team-member:hover .content h3, .single-team-member.active .content h3 { - color: var(--whiteColor); - -webkit-transform: translateY(-40px); - transform: translateY(-40px); -} - -.single-team-member:hover .content span, .single-team-member.active .content span { - color: var(--whiteColor); - -webkit-transform: translateY(-40px); - transform: translateY(-40px); -} - -.single-team-member:hover .content .social, .single-team-member.active .content .social { - opacity: 1; - visibility: visible; -} - -.single-team-member:hover .content::before, .single-team-member.active .content::before { - opacity: 1; - visibility: visible; -} - -.single-team-member.marketing-color .content span { - color: var(--marketingMainColor); -} - -.single-team-member.marketing-color .content .social li a:hover { - color: var(--marketingMainColor); -} - -.single-team-member.marketing-color .content::before { - background-color: var(--marketingMainColor); -} - -.single-team-member.marketing-color:hover .content, .single-team-member.marketing-color.active .content { - background-color: var(--marketingMainColor); -} - -.single-team-member.marketing-color:hover .content span, .single-team-member.marketing-color.active .content span { - color: var(--whiteColor); -} - -.single-team-member.business-color .content { - position: relative; - z-index: 1; -} - -.single-team-member.business-color .content span { - color: var(--businessMainColor); -} - -.single-team-member.business-color .content .social li a:hover { - color: var(--whiteColor); -} - -.single-team-member.business-color .content::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--businessGradientColor); - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; -} - -.single-team-member.business-color .content::before { - background: var(--businessGradientColor); -} - -.single-team-member.business-color:hover .content, .single-team-member.business-color.active .content { - background-color: transparent; -} - -.single-team-member.business-color:hover .content::after, .single-team-member.business-color.active .content::after { - opacity: 1; - visibility: visible; -} - -.single-team-member.business-color:hover .content span, .single-team-member.business-color.active .content span { - color: var(--whiteColor); -} - -.single-team-member.bg-ffefef { - background-color: #ffefef; -} - -.single-team-member.bg-f0efff { - background-color: #f0efff; -} - -.single-team-member.it-agency-color .content { - position: relative; - z-index: 1; -} - -.single-team-member.it-agency-color .content span { - color: var(--itAgencyMainColor); -} - -.single-team-member.it-agency-color .content .social li a:hover { - color: var(--whiteColor); -} - -.single-team-member.it-agency-color .content::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--itAgencyGradientColor); - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; -} - -.single-team-member.it-agency-color .content::before { - background: var(--itAgencyGradientColor); -} - -.single-team-member.it-agency-color:hover .content, .single-team-member.it-agency-color.active .content { - background-color: transparent; -} - -.single-team-member.it-agency-color:hover .content::after, .single-team-member.it-agency-color.active .content::after { - opacity: 1; - visibility: visible; -} - -.single-team-member.it-agency-color:hover .content span, .single-team-member.it-agency-color.active .content span { - color: var(--whiteColor); -} - -.single-team-member.border-radius { - border-radius: 10px; -} - -.single-team-member.border-radius img { - border-radius: 10px 10px 0 0; -} - -.single-team-box { - text-align: center; - margin-bottom: 30px; -} - -.single-team-box .image { - position: relative; -} - -.single-team-box .image .social { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding-left: 0; - margin-bottom: 15px; - list-style-type: none; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; - padding: 25px; -} - -.single-team-box .image .social li { - display: inline-block; - margin-left: 2px; - margin-right: 2px; -} - -.single-team-box .image .social li a { - display: block; - width: 35px; - height: 35px; - border-radius: 50%; - line-height: 35px; - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.single-team-box .image .social li a:hover { - background-color: var(--startupMainColor); - color: var(--whiteColor); -} - -.single-team-box .content { - margin-top: 22px; -} - -.single-team-box .content h3 { - font-size: 20px; - margin-bottom: 8px; -} - -.single-team-box .content span { - display: block; - color: var(--startupMainColor); -} - -.single-team-box:hover .image .social { - opacity: 1; - margin-bottom: 0; - visibility: visible; -} - -.single-team-box.consulting-color .image .social li a:hover { - background-color: var(--consultingMainColor); -} - -.single-team-box.consulting-color .content span { - color: var(--consultingMainColor); -} - -.team-member { - margin-bottom: 30px; - text-align: center; -} - -.team-member .content { - z-index: 1; - padding: 25px 20px; - position: relative; - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--whiteColor); - -webkit-box-shadow: 0 5px 20px rgba(34, 34, 34, 0.1); - box-shadow: 0 5px 20px rgba(34, 34, 34, 0.1); - margin-left: 15px; - margin-top: -30px; - margin-right: 15px; -} - -.team-member .content h3 { - -webkit-transition: var(--transition); - transition: var(--transition); - font-size: 20px; - margin-bottom: 10px; -} - -.team-member .content span { - display: block; - color: var(--startupMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.team-member .content .social { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - position: absolute; - left: 0; - right: 0; - bottom: 25px; - opacity: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.team-member .content .social li { - display: inline-block; - margin-left: 7px; - margin-right: 7px; -} - -.team-member .content .social li a { - display: block; - font-size: 20px; - color: var(--whiteColor); -} - -.team-member .content .social li a:hover { - color: var(--startupMainColor); -} - -.team-member .content::before { - content: ''; - position: absolute; - left: 0; - right: 0; - height: 40px; - background-color: var(--blackColor); - top: -40px; - z-index: -1; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; -} - -.team-member:hover .content, .team-member.active .content { - background-color: var(--blackColor); -} - -.team-member:hover .content h3, .team-member.active .content h3 { - color: var(--whiteColor); - -webkit-transform: translateY(-40px); - transform: translateY(-40px); -} - -.team-member:hover .content span, .team-member.active .content span { - color: var(--whiteColor); - -webkit-transform: translateY(-40px); - transform: translateY(-40px); -} - -.team-member:hover .content .social, .team-member.active .content .social { - opacity: 1; - visibility: visible; -} - -.team-member:hover .content::before, .team-member.active .content::before { - opacity: 1; - visibility: visible; -} - -.team-member.marketing-color .content span { - color: var(--marketingMainColor); -} - -.team-member.marketing-color .content .social li a:hover { - color: var(--whiteColor); -} - -.team-member.marketing-color .content::before { - background-color: var(--marketingMainColor); -} - -.team-member.marketing-color:hover .content, .team-member.marketing-color.active .content { - background-color: var(--marketingMainColor); -} - -.team-member.marketing-color:hover .content span, .team-member.marketing-color.active .content span { - color: var(--whiteColor); -} - -.team-member.business-color .content { - position: relative; - z-index: 1; -} - -.team-member.business-color .content span { - color: var(--businessMainColor); -} - -.team-member.business-color .content .social li a:hover { - color: var(--whiteColor); -} - -.team-member.business-color .content::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--businessGradientColor); - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; -} - -.team-member.business-color .content::before { - background: var(--businessGradientColor); -} - -.team-member.business-color:hover .content, .team-member.business-color.active .content { - background-color: transparent; -} - -.team-member.business-color:hover .content::after, .team-member.business-color.active .content::after { - opacity: 1; - visibility: visible; -} - -.team-member.business-color:hover .content span, .team-member.business-color.active .content span { - color: var(--whiteColor); -} - -.team-member.bg-ffefef { - background-color: #ffefef; -} - -.team-member.bg-f0efff { - background-color: #f0efff; -} - -.team-member.it-agency-color .content { - position: relative; - z-index: 1; -} - -.team-member.it-agency-color .content span { - color: var(--itAgencyMainColor); -} - -.team-member.it-agency-color .content .social li a:hover { - color: var(--whiteColor); -} - -.team-member.it-agency-color .content::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--itAgencyGradientColor); - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; -} - -.team-member.it-agency-color .content::before { - background: var(--itAgencyGradientColor); -} - -.team-member.it-agency-color:hover .content, .team-member.it-agency-color.active .content { - background-color: transparent; -} - -.team-member.it-agency-color:hover .content::after, .team-member.it-agency-color.active .content::after { - opacity: 1; - visibility: visible; -} - -.team-member.it-agency-color:hover .content span, .team-member.it-agency-color.active .content span { - color: var(--whiteColor); -} - -.team-member.border-radius { - border-radius: 10px; -} - -.team-member.border-radius img { - border-radius: 10px 10px 0 0; -} - -.team-box { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; -} - -.team-box img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.team-box .content { - left: 0; - right: 0; - z-index: 1; - bottom: -100px; - padding: 25px; - -webkit-transition: .3s; - transition: .3s; - position: absolute; - opacity: 0; - visibility: hidden; - background: var(--startupMainColor); -} - -.team-box .content h3 { - font-size: 20px; - margin-bottom: 12px; - color: var(--whiteColor); -} - -.team-box .content span { - display: block; - color: var(--whiteColor); - margin-bottom: 15px; - font-weight: 600; -} - -.team-box .content .social { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.team-box .content .social li { - display: inline-block; - margin-left: 7px; - margin-right: 7px; -} - -.team-box .content .social li a { - display: block; - font-size: 20px; - color: var(--whiteColor); -} - -.team-box .content .social li a:hover { - color: var(--whiteColor); - opacity: 0.85; -} - -.team-box:hover img, .team-box.active img { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -.team-box:hover .content, .team-box.active .content { - bottom: 0; - opacity: 1; - visibility: visible; -} - -.team-box.business-color .content { - background: var(--businessGradientColor); -} - -/*================================================ -How It Works Area CSS -=================================================*/ -.how-it-works-area .section-title { - max-width: 668px; -} - -.how-it-works-content { - padding-right: 30px; -} - -.how-it-works-content ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.how-it-works-content ul li { - margin-bottom: 30px; - position: relative; - padding-left: 65px; -} - -.how-it-works-content ul li i { - position: absolute; - left: 0; - top: 0; - font-size: 45px; - background: var(--saasGradientColor); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -.how-it-works-content ul li h3 { - font-size: 20px; - margin-bottom: 12px; -} - -.how-it-works-content ul li:last-child { - margin-bottom: 0; -} - -.how-it-works-content.white-color ul li { - color: var(--whiteColor); -} - -.how-it-works-content.white-color ul li h3 { - color: var(--whiteColor); -} - -.how-it-works-content.white-color ul li p { - color: var(--whiteColor); - opacity: 0.9; -} - -.how-it-works-image { - text-align: center; -} - -/*================================================ -Skills Area CSS -=================================================*/ -.skills-area.with-border { - border-top: 1px solid #f0f0f0; -} - -.single-skills-box { - margin-bottom: 30px; - text-align: center; - padding: 30px; - background-color: #ffefef; -} - -.single-skills-box.bg-eaf6ff { - background-color: #eaf6ff; -} - -.single-skills-box.bg-fff5e6 { - background-color: #fff5e6; -} - -.single-skills-box.bg-f0efff { - background-color: #f0efff; -} - -/*================================================ -Expertise Area CSS -=================================================*/ -.expertise-area { - border-bottom: 1px solid #f0f0f0; -} - -.expertise-box { - margin-bottom: 30px; -} - -.expertise-box .icon { - display: block; - width: 80px; - height: 80px; - font-size: 40px; - line-height: 80px; - color: var(--personalPortfolioMainColor); - border-radius: 10px; - -webkit-transition: var(--transition); - transition: var(--transition); - margin-bottom: 25px; - text-align: center; - background-color: #f0efff; -} - -.expertise-box .icon.bg-fff5e6 { - background-color: #fff5e6; -} - -.expertise-box .icon.bg-f3f9e7 { - background-color: #f3f9e7; -} - -.expertise-box .icon.bg-ffefef { - background-color: #ffefef; -} - -.expertise-box .icon.bg-eaf6ff { - background-color: #eaf6ff; -} - -.expertise-box .icon.bg-faf2ff { - background-color: #faf2ff; -} - -.expertise-box h3 { - font-size: 20px; - margin-bottom: 12px; -} - -.expertise-box:hover .icon { - background-color: var(--personalPortfolioMainColor); - color: var(--whiteColor); -} - -/*================================================ -CTA Area CSS -=================================================*/ -.cta-area { - position: relative; - z-index: 1; - background-color: var(--blackColor); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; - background-image: url(../../assets/img/cta-bg1.jpg); -} - -.cta-area::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: .75; -} - -.cta-content { - text-align: center; - max-width: 720px; - margin-left: auto; - margin-right: auto; -} - -.cta-content h2 { - color: var(--whiteColor); - margin-bottom: 0; - line-height: 1.4; - font-size: 40px; -} - -.cta-content p { - color: var(--whiteColor); - max-width: 600px; - opacity: 0.9; - margin-right: auto; - margin-left: auto; - margin-top: 20px; -} - -.cta-content .default-btn { - margin-top: 15px; -} - -.cta-content .default-btn:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.cta-content .default-btn:hover i { - color: var(--blackColor); -} - -.call-to-action-area.bg-image { - background-image: url(../../assets/img/bg7.jpg); - background-position: right center; - background-repeat: no-repeat; - background-size: cover; -} - -.call-to-action-content { - color: var(--whiteColor); -} - -.call-to-action-content h2 { - color: var(--whiteColor); - margin-bottom: 0; - line-height: 1.4; - font-size: 37px; -} - -.call-to-action-btn { - text-align: end; -} - -.call-to-action-btn .default-btn:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.call-to-action-btn .default-btn:hover i { - color: var(--blackColor); -} - -.call-to-action-inner { - background-color: var(--blackColor); - border-radius: 10px; - padding-left: 50px; - padding-right: 50px; -} - -.call-to-action-inner.it-agency-color { - background: var(--itAgencyGradientColor); -} - -.call-to-action-inner.it-agency-color .default-btn { - background: var(--whiteColor); - color: var(--blackColor); -} - -.call-to-action-inner.it-agency-color .default-btn i { - color: var(--blackColor); -} - -.call-to-action-inner.it-agency-color .default-btn:hover { - background: var(--blackColor); - color: var(--whiteColor); -} - -.call-to-action-inner.it-agency-color .default-btn:hover i { - color: var(--whiteColor); -} - -.call-to-action-inner.startup-color { - background: var(--startupMainColor); -} - -.call-to-action-inner.startup-color .default-btn { - background: var(--whiteColor); - color: var(--blackColor); -} - -.call-to-action-inner.startup-color .default-btn i { - color: var(--blackColor); -} - -.call-to-action-inner.startup-color .default-btn:hover { - background: var(--blackColor); - color: var(--whiteColor); -} - -.call-to-action-inner.startup-color .default-btn:hover i { - color: var(--whiteColor); -} - -/*================================================ -Timeline/Short Brief Area CSS -=================================================*/ -.short-brief-area { - border-bottom: 1px solid #f0f0f0; -} - -.short-brief-content { - margin-bottom: 30px; -} - -.short-brief-content .timeline { - width: 100%; - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.short-brief-content .timeline-item { - padding-left: 40px; - position: relative; -} - -.short-brief-content .timeline-item:last-child .timeline-content { - padding-bottom: 0; -} - -.short-brief-content .timeline-item:not(.period):hover .timeline-marker::before { - background: transparent; - border: 3px solid var(--blackColor); -} - -.short-brief-content .timeline-marker { - position: absolute; - width: 15px; - bottom: 0; - top: 0; - left: 0; -} - -.short-brief-content .timeline-marker::before { - -webkit-transition: background 0.3s ease-in-out, border 0.3s ease-in-out; - transition: background 0.3s ease-in-out, border 0.3s ease-in-out; - background: var(--blackColor); - border: 3px solid transparent; - border-radius: 100%; - position: absolute; - display: block; - height: 15px; - width: 15px; - content: ""; - top: 4px; - left: 0; -} - -.short-brief-content .timeline-marker::after { - background: #eeeeee; - position: absolute; - display: block; - content: ""; - top: 24px; - bottom: 0; - left: 6px; - width: 3px; -} - -.short-brief-content .timeline-content { - padding-bottom: 30px; -} - -.short-brief-content .timeline-content h3 { - font-size: 20px; - margin-bottom: 13px; -} - -.short-brief-content .timeline-info .sub-title { - color: var(--optionalColor); - margin-bottom: 12px; - font-weight: 500; - display: block; -} - -.short-brief-content .timeline-info .sub-title span { - display: inline-block; - font-size: 18px; - font-weight: 700; -} - -/*================================================ -App Download Area CSS -=================================================*/ -.app-download-area { - background-image: url(../../assets/img/bg6.jpg); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.app-download-image { - text-align: center; - padding-right: 30px; -} - -.app-download-content .sub-title { - display: block; - margin-bottom: 12px; - color: var(--businessMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.app-download-content h2 { - margin-bottom: 15px; - font-size: 40px; -} - -.app-download-content .btn-box { - margin-top: 25px; -} - -.app-download-content .btn-box .store-btn { - display: inline-block; - background-color: var(--whiteColor); - position: relative; - color: var(--optionalColor); - padding: 11px 30px 11px 70px; - margin-right: 15px; - border-radius: 5px; - -webkit-box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.02), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.04); - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.02), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.04); - font-size: 13.5px; - font-weight: 600; -} - -.app-download-content .btn-box .store-btn img { - position: absolute; - left: 18px; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - color: var(--blackColor); - font-size: 40px; - font-weight: normal; -} - -.app-download-content .btn-box .store-btn span { - display: block; - color: var(--blackColor); - margin-top: 2px; - font-size: 18px; - font-weight: 700; -} - -.app-download-content .btn-box .store-btn:last-child { - margin-right: 0; -} - -/*================================================ -Trending Style Area CSS -=================================================*/ -.trending-style-image { - position: relative; - margin-right: 15px; - padding-bottom: 100px; - padding-right: 150px; -} - -.trending-style-image img:nth-child(2) { - position: absolute; - right: 0; - bottom: 0; - width: 315px; -} - -.trending-style-content { - padding-left: 15px; -} - -.trending-style-content .sub-title { - display: block; - margin-bottom: 12px; - color: var(--shopMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.trending-style-content h2 { - margin-bottom: 16px; - font-size: 40px; -} - -.trending-style-content h3 { - font-size: 20px; - line-height: 1.4; - margin-bottom: 15px; -} - -.trending-style-content .default-btn { - margin-top: 10px; -} - -/*================================================ -Pricing Area CSS -=================================================*/ -.pricing-area.with-black-border { - border-top: 1px solid #12141a; -} - -.single-pricing-box { - padding: 40px; - margin-bottom: 30px; - border: 1px dashed #eeeeee; - background-color: var(--whiteColor); - -webkit-box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-pricing-box .pricing-header { - margin-bottom: 25px; - padding-bottom: 25px; - border-bottom: 1px solid #eeeeee; -} - -.single-pricing-box .pricing-header h3 { - color: var(--marketingMainColor); - margin-bottom: 12px; - line-height: 1; - font-weight: 700; - font-size: 45px; -} - -.single-pricing-box .pricing-header h3 span { - color: var(--optionalColor); - font-size: var(--fontSize); - font-weight: normal; -} - -.single-pricing-box .pricing-header .sub-title { - display: block; - font-weight: 700; - color: var(--blackColor); -} - -.single-pricing-box .features-list { - margin-bottom: 25px; - padding-left: 0; - list-style-type: none; -} - -.single-pricing-box .features-list li { - margin-bottom: 12px; - color: var(--optionalColor); - font-family: var(--headingFontFamily); - position: relative; - padding-left: 25px; -} - -.single-pricing-box .features-list li i { - position: absolute; - left: 0; - top: 5px; - color: var(--marketingMainColor); -} - -.single-pricing-box .features-list li i.close { - color: red; -} - -.single-pricing-box .features-list li:last-child { - margin-bottom: 0; -} - -.single-pricing-box .default-btn { - display: block; - width: 100%; - text-align: center; - padding-left: 35px; - padding-right: 35px; - padding-top: 14px; - padding-bottom: 14px; -} - -.single-pricing-box .default-btn:hover { - color: var(--whiteColor); - background-color: #ffac00; -} - -.single-pricing-box:hover { - border-color: var(--marketingMainColor); -} - -.pricing-box { - margin-bottom: 30px; - text-align: center; - background-color: var(--whiteColor); - -webkit-box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - padding: 30px; -} - -.pricing-box .pricing-header { - background: var(--appMainColor); - margin: -30px -30px 30px; - padding: 30px; - position: relative; - z-index: 1; - overflow: hidden; -} - -.pricing-box .pricing-header h3 { - margin-bottom: 15px; - font-size: 20px; - color: var(--whiteColor); -} - -.pricing-box .pricing-header .price { - color: var(--whiteColor); - margin-bottom: 12px; - line-height: 1; - font-weight: 700; - font-size: 45px; -} - -.pricing-box .pricing-header .price span { - opacity: 0.9; - font-size: var(--fontSize); - font-weight: normal; -} - -.pricing-box .pricing-header::before { - width: 400px; - height: 400px; - content: ''; - position: absolute; - left: 50%; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); - border-radius: 50%; - bottom: 10px; - background-color: #0084ff; - opacity: 0.1; - z-index: -1; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.pricing-box .features-list { - padding-left: 0; - margin-bottom: 30px; - list-style-type: none; -} - -.pricing-box .features-list li { - margin-bottom: 12px; - color: var(--optionalColor); - font-family: var(--headingFontFamily); - position: relative; -} - -.pricing-box .features-list li.unactive { - text-decoration: line-through; - color: #9b9dbb; -} - -.pricing-box .features-list li:last-child { - margin-bottom: 0; -} - -.pricing-box .default-btn { - background: transparent; - color: var(--blackColor); - border: 1px solid var(--appMainColor); - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.pricing-box .default-btn i { - color: var(--blackColor); -} - -.pricing-box .default-btn:hover { - background: var(--appMainColor); - border-color: var(--appMainColor); - color: var(--whiteColor); -} - -.pricing-box .default-btn:hover i { - color: var(--whiteColor); -} - -.pricing-box.saas-color .pricing-header { - background: var(--saasGradientColor); -} - -.pricing-box.saas-color .pricing-header::before { - background-color: var(--appMainColor); -} - -.pricing-box.saas-color .default-btn { - border-color: #4d4d57; -} - -.pricing-box.saas-color .default-btn:hover { - background: var(--saasMainColor); - border-color: var(--saasMainColor); -} - -.pricing-box.bg-black { - -webkit-box-shadow: unset; - box-shadow: unset; - background-color: #040611; - border: 1px solid #272836; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.pricing-box.bg-black .pricing-header { - -webkit-transition: var(--transition); - transition: var(--transition); - background: transparent; - border-bottom: 1px solid #272836; - padding: 0 0 30px; - margin-top: 0; - margin-left: 0; - margin-right: 0; -} - -.pricing-box.bg-black .pricing-header::before { - display: none; -} - -.pricing-box.bg-black .features-list { - color: var(--whiteColor); -} - -.pricing-box.bg-black .features-list li { - color: var(--whiteColor); -} - -.pricing-box.bg-black .features-list li.unactive { - color: #5c5656; -} - -.pricing-box.bg-black .default-btn { - color: var(--whiteColor); -} - -.pricing-box.bg-black .default-btn i { - color: var(--whiteColor); -} - -.pricing-box:hover .pricing-header::before { - bottom: -185%; -} - -.pricing-box.active .default-btn { - -webkit-box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - background: var(--appGradientColor); - color: var(--whiteColor); - border: none; -} - -.pricing-box.active .default-btn i { - color: var(--whiteColor); -} - -.pricing-box.active .default-btn.saas-color { - background: var(--saasGradientColor); -} - -.pricing-tabs { - margin-top: -10px; -} - -.pricing-tabs .nav { - padding-left: 0; - margin-bottom: 50px; - list-style-type: none; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - border-bottom: none; -} - -.pricing-tabs .nav .nav-item .nav-link { - background-color: var(--whiteColor); - position: relative; - z-index: 1; - color: var(--blackColor); - border-radius: 0; - border: none; - padding: 11px 35px; - font-size: var(--fontSize); - font-weight: 700; -} - -.pricing-tabs .nav .nav-item .nav-link::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--saasGradientColor); - -webkit-transition: var(--transition); - transition: var(--transition); - opacity: 0; - visibility: hidden; -} - -.pricing-tabs .nav .nav-item .nav-link:hover, .pricing-tabs .nav .nav-item .nav-link.active { - color: var(--whiteColor); -} - -.pricing-tabs .nav .nav-item .nav-link:hover::before, .pricing-tabs .nav .nav-item .nav-link.active::before { - opacity: 1; - visibility: visible; -} - -.pricing-tabs .nav .nav-item:first-child .nav-link { - border-radius: 5px 0 0 5px; -} - -.pricing-tabs .nav .nav-item:first-child .nav-link::before { - border-radius: 5px 0 0 5px; -} - -.pricing-tabs .nav .nav-item:last-child .nav-link { - border-radius: 0 5px 5px 0; -} - -.pricing-tabs .nav .nav-item:last-child .nav-link::before { - border-radius: 0 5px 5px 0; -} - -.pricing-item { - padding: 30px; - overflow: hidden; - position: relative; - text-align: center; - margin-bottom: 30px; - background-color: #f3f4f7; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.pricing-item .pricing-header { - border-bottom: 1px solid #e5e4e4; - -webkit-transition: var(--transition); - transition: var(--transition); - margin: -30px 0 30px; - padding: 30px; -} - -.pricing-item .pricing-header h3 { - -webkit-transition: var(--transition); - transition: var(--transition); - margin-bottom: 15px; - font-size: 20px; -} - -.pricing-item .pricing-header .price { - color: var(--consultingMainColor); - margin-bottom: 12px; - line-height: 1; - font-weight: 700; - font-size: 45px; -} - -.pricing-item .pricing-header .price span { - opacity: 0.9; - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); - font-size: var(--fontSize); - font-weight: normal; -} - -.pricing-item .features-list { - padding-left: 0; - margin-bottom: 30px; - list-style-type: none; -} - -.pricing-item .features-list li { - margin-bottom: 12.5px; - font-family: var(--headingFontFamily); -} - -.pricing-item .features-list li.unactive { - text-decoration: line-through; - color: #b0b1c0; -} - -.pricing-item .features-list li:last-child { - margin-bottom: 0; -} - -.pricing-item .ribbon { - top: -10px; - right: -25px; - width: 150px; - height: 150px; - overflow: hidden; - position: absolute; -} - -.pricing-item .ribbon span { - background-color: var(--whiteColor); - text-transform: uppercase; - color: var(--blackColor); - -webkit-transform: rotate(45deg); - transform: rotate(45deg); - text-align: center; - position: absolute; - display: block; - width: 225px; - left: -25px; - top: 30px; - padding-top: 8px; - padding-bottom: 8px; - font-size: var(--fontSize); - font-weight: 700; -} - -.pricing-item.active, .pricing-item:hover { - color: var(--whiteColor); - background-color: var(--blackColor); -} - -.pricing-item.active .pricing-header, .pricing-item:hover .pricing-header { - border-bottom-color: #464c59; -} - -.pricing-item.active .pricing-header h3, .pricing-item:hover .pricing-header h3 { - color: var(--whiteColor); -} - -.pricing-item.active .pricing-header .price span, .pricing-item:hover .pricing-header .price span { - color: var(--whiteColor); -} - -.pricing-item.active .default-btn, .pricing-item:hover .default-btn { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.pricing-item.active .default-btn i, .pricing-item:hover .default-btn i { - color: var(--blackColor); -} - -.pricing-item.border-radius { - border-radius: 10px; - background-color: #f9f9f9; -} - -.pricing-item.it-agency-color .pricing-header { - border-bottom: none; - background-color: #fff5e6; - margin-left: -30px; - margin-right: -30px; -} - -.pricing-item.it-agency-color .pricing-header h3 { - color: var(--blackColor); -} - -.pricing-item.it-agency-color .pricing-header .price { - color: var(--blackColor); -} - -.pricing-item.it-agency-color .features-list { - color: var(--blackColor); -} - -.pricing-item.it-agency-color .features-list li { - color: var(--blackColor); -} - -.pricing-item.it-agency-color .features-list li.unactive { - color: #b0b1c0; -} - -.pricing-item.it-agency-color .default-btn { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.pricing-item.it-agency-color .default-btn i { - color: var(--whiteColor); -} - -.pricing-item.it-agency-color:hover .pricing-header .price span { - color: var(--optionalColor); -} - -.pricing-item.it-agency-color.active { - background-color: #f9f9f9; -} - -.pricing-item.it-agency-color.active .pricing-header { - background: var(--itAgencyGradientColor); -} - -.pricing-item.it-agency-color.active .pricing-header h3 { - color: var(--whiteColor); -} - -.pricing-item.it-agency-color.active .pricing-header .price { - color: var(--whiteColor); -} - -.pricing-item.it-agency-color.active .pricing-header .price span { - color: var(--whiteColor); -} - -.pricing-item.it-agency-color.active .default-btn { - background: var(--itAgencyGradientColor); - color: var(--whiteColor); -} - -.pricing-item.it-agency-color.active .default-btn i { - color: var(--whiteColor); -} - -.pricing-item.startup-color .pricing-header .price { - color: var(--startupMainColor); -} - -/*================================================ -App Screenshot Area CSS -=================================================*/ -.app-screenshot-area { - background: var(--appGradientColor); - position: relative; - overflow: hidden; - z-index: 1; -} - -.app-screenshot-area .section-title h2 { - color: var(--whiteColor); -} - -.app-screenshot-area .section-title p { - color: var(--whiteColor); -} - -.app-screenshot-area::before { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 200px; - z-index: -1; - background-color: var(--whiteColor); -} - -.app-screenshot-area .overlay { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -2; -} - -.app-screenshot-area .overlay::before { - width: 650px; - height: 650px; - position: absolute; - left: -3%; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - top: -25%; - content: ''; - background-color: #0084ff; - opacity: 0.10; - -webkit-animation: border-transform 10s linear infinite alternate forwards; - animation: border-transform 10s linear infinite alternate forwards; -} - -.app-screenshot-area .overlay::after { - right: -3%; - bottom: 0; - content: ''; - opacity: 0.10; - width: 600px; - height: 600px; - position: absolute; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - background-color: #0084ff; - -webkit-animation: border-transform 10s linear infinite alternate forwards; - animation: border-transform 10s linear infinite alternate forwards; -} - -.single-sccrenshot-item { - text-align: center; -} - -.screenshot-wrap { - position: relative; -} - -.screenshot-wrap .screenshot-frame { - z-index: 2; - border-radius: 20px; - display: block; - width: 241px; - height: 504px; - position: absolute; - top: 0; - bottom: 0; - margin: 10px 0; - left: 50%; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); - background-image: url(../../assets/img/screenshot/frame.png); - background-repeat: no-repeat; - background-size: 100% 100%; -} - -.screenshot-slides { - margin-top: -10px; - margin-bottom: -10px; -} - -.screenshot-slides.owl-theme .owl-nav { - margin-top: 0; -} - -.screenshot-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--appMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: -40px; - top: 50%; - width: 40px; - line-height: 40px; - height: 40px; - border-radius: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - font-size: 18px; -} - -.screenshot-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: -40px; -} - -.screenshot-slides.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.screenshot-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -/*================================================ -Facility Area CSS -=================================================*/ -.facility-content { - padding-right: 50px; -} - -.facility-content .item { - margin-bottom: 30px; - position: relative; - padding-left: 65px; -} - -.facility-content .item:last-child { - margin-bottom: 0; -} - -.facility-content .item i { - position: absolute; - font-size: 45px; - left: 0; - top: 0; -} - -.facility-content .item h3 { - font-size: 20px; - margin-bottom: 12px; -} - -/*================================================ -Latest Posts Area CSS -=================================================*/ -.latest-posts-area .section-title.style-four { - padding-bottom: 8px; - margin-top: -7px; - margin-bottom: 30px; -} - -.single-latest-post-box { - margin-bottom: 30px; -} - -.single-latest-post-box .post-image { - overflow: hidden; -} - -.single-latest-post-box .post-image img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-latest-post-box .post-content { - padding-top: 25px; - padding-left: 25px; - padding-right: 25px; -} - -.single-latest-post-box .post-content .tag { - margin-bottom: 12px; - display: inline-block; - position: relative; - font-weight: 600; - color: var(--blackColor); -} - -.single-latest-post-box .post-content .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; -} - -.single-latest-post-box .post-content .meta li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); -} - -.single-latest-post-box .post-content .meta li i { - position: absolute; - left: 0; - top: 4px; - color: var(--blackColor); -} - -.single-latest-post-box .post-content .meta li a { - color: var(--optionalColor); -} - -.single-latest-post-box .post-content .meta li a:hover { - color: var(--blackColor); -} - -.single-latest-post-box .post-content h3 { - margin-bottom: 12px; - line-height: 1.4; - font-size: 25px; -} - -.single-latest-post-box .post-content .link-btn { - font-weight: 600; - position: relative; - display: inline-block; - padding-right: 12px; -} - -.single-latest-post-box .post-content .link-btn i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; -} - -.single-latest-post-box .post-content .link-btn::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-latest-post-box .post-content .link-btn:hover { - padding-right: 15px; - color: var(--blackColor); -} - -.single-latest-post-box .post-content .link-btn:hover::before { - width: 100%; - right: auto; - left: 0; -} - -.single-latest-post-box:hover .post-image img { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -/*================================================ -Partner Area CSS -=================================================*/ -.partner-area.bg-business-color { - background: var(--businessGradientColor); -} - -.partner-area.with-top-border { - border-top: 1px solid #f0f0f0; -} - -.partner-area.bg-it-agency-color { - background: var(--itAgencyGradientColor); -} - -.partner-area.bg-1b2232 .section-title h2 { - color: var(--whiteColor); -} - -.partner-item { - text-align: center; -} - -.partner-item a img { - width: auto !important; - display: inline-block !important; -} - -/*================================================ -Clients Area CSS -=================================================*/ -.single-clients-box { - margin-bottom: 30px; - text-align: center; - background-color: var(--whiteColor); -} - -.single-clients-box a { - padding: 30px; -} - -.single-clients-box a img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-clients-box:hover a img { - -webkit-transform: scale(1.05); - transform: scale(1.05); -} - -/*================================================ -Featured Video Area CSS -=================================================*/ -.single-featured-video-box { - margin-bottom: 30px; -} - -.single-featured-video-box .image { - position: relative; - overflow: hidden; -} - -.single-featured-video-box .image img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-featured-video-box .image .video-btn { - position: absolute; - left: 15px; - bottom: 15px; - color: var(--whiteColor); - font-weight: 600; -} - -.single-featured-video-box .image .video-btn i { - display: inline-block; - width: 40px; - height: 40px; - color: var(--blackColor); - background-color: var(--whiteColor); - text-align: center; - border-radius: 50%; - line-height: 41px; - margin-right: 10px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-featured-video-box .image .video-btn:hover i { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.single-featured-video-box .content { - margin-top: 25px; -} - -.single-featured-video-box .content .tag { - margin-bottom: 12px; - display: inline-block; - position: relative; - font-weight: 600; - color: var(--blackColor); -} - -.single-featured-video-box .content .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; -} - -.single-featured-video-box .content .meta li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); -} - -.single-featured-video-box .content .meta li i { - position: absolute; - left: 0; - top: 4px; - color: var(--blackColor); -} - -.single-featured-video-box .content .meta li a { - color: var(--optionalColor); -} - -.single-featured-video-box .content .meta li a:hover { - color: var(--blackColor); -} - -.single-featured-video-box .content h3 { - margin-bottom: 0; - line-height: 1.4; - font-size: 25px; -} - -.single-featured-video-box:hover img { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -.featured-video-item-list .single-featured-video-item { - margin-bottom: 30px; -} - -.featured-video-item-list .single-featured-video-item .row { - margin-left: 0; - margin-right: 0; -} - -.featured-video-item-list .single-featured-video-item .row .col-lg-4, .featured-video-item-list .single-featured-video-item .row .col-lg-8 { - padding-left: 0; - padding-right: 0; -} - -.featured-video-item-list .single-featured-video-item .image { - height: 100%; - position: relative; - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.featured-video-item-list .single-featured-video-item .image.bg1 { - background-image: url(../../assets/img/featured-video/featured-video2.jpg); -} - -.featured-video-item-list .single-featured-video-item .image.bg2 { - background-image: url(../../assets/img/featured-video/featured-video3.jpg); -} - -.featured-video-item-list .single-featured-video-item .image.bg3 { - background-image: url(../../assets/img/featured-video/featured-video4.jpg); -} - -.featured-video-item-list .single-featured-video-item .image.bg4 { - background-image: url(../../assets/img/featured-video/featured-video5.jpg); -} - -.featured-video-item-list .single-featured-video-item .image img { - display: none; -} - -.featured-video-item-list .single-featured-video-item .image .link-btn { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 1; - display: block; - position: absolute; -} - -.featured-video-item-list .single-featured-video-item .image .video-btn { - color: var(--whiteColor); - position: absolute; - bottom: 10px; - left: 10px; - z-index: 2; - font-size: 14px; - font-weight: 600; -} - -.featured-video-item-list .single-featured-video-item .image .video-btn i { - display: inline-block; - width: 35px; - height: 35px; - color: var(--blackColor); - background-color: var(--whiteColor); - text-align: center; - border-radius: 50%; - line-height: 36px; - margin-right: 5px; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.featured-video-item-list .single-featured-video-item .image .video-btn:hover i { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.featured-video-item-list .single-featured-video-item .content { - padding-left: 25px; - padding-top: 9.5px; - padding-bottom: 9.5px; -} - -.featured-video-item-list .single-featured-video-item .content .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; -} - -.featured-video-item-list .single-featured-video-item .content .meta li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); -} - -.featured-video-item-list .single-featured-video-item .content .meta li i { - position: absolute; - left: 0; - top: 4px; - color: var(--blackColor); -} - -.featured-video-item-list .single-featured-video-item .content .meta li a { - color: var(--optionalColor); -} - -.featured-video-item-list .single-featured-video-item .content .meta li a:hover { - color: var(--blackColor); -} - -.featured-video-item-list .single-featured-video-item .content h3 { - margin-bottom: 0; - line-height: 1.4; - font-size: 20px; -} - -/*================================================ -Blog Area CSS -=================================================*/ -.blog-area.with-border { - border-top: 1px solid #f0f0f0; -} - -.blog-area.with-black-border { - border-top: 1px solid #12141a; -} - -.single-blog-post { - margin-bottom: 30px; - position: relative; - overflow: hidden; -} - -.single-blog-post img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-blog-post .content { - left: 0; - right: 0; - bottom: 0; - z-index: 2; - padding: 25px; - position: absolute; -} - -.single-blog-post .content .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; -} - -.single-blog-post .content .meta li { - display: inline-block; - margin-right: 15px; - color: var(--whiteColor); - position: relative; - padding-left: 22px; -} - -.single-blog-post .content .meta li i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); -} - -.single-blog-post .content .meta li a { - color: var(--whiteColor); -} - -.single-blog-post .content h3 { - color: var(--whiteColor); - margin-bottom: 13px; - font-size: 20px; - line-height: 1.4; -} - -.single-blog-post .content h3 a { - color: var(--whiteColor); -} - -.single-blog-post .content h3 a:hover { - color: var(--startupMainColor); -} - -.single-blog-post .content .link-btn { - font-weight: 500; - position: relative; - opacity: 0.85; - color: var(--whiteColor); - padding-right: 12px; - padding-bottom: 2px; -} - -.single-blog-post .content .link-btn i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; -} - -.single-blog-post .content .link-btn::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: #eeeeee; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-blog-post .content .link-btn:hover { - opacity: 1; - padding-right: 15px; -} - -.single-blog-post .content .link-btn:hover::before { - width: 100%; - right: auto; - left: 0; -} - -.single-blog-post::before { - position: absolute; - height: 100%; - content: ""; - width: 100%; - z-index: 1; - left: 0; - top: 0; - background: -webkit-gradient(linear, left bottom, left top, color-stop(0, rgba(35, 35, 37, 0.9)), color-stop(50%, rgba(35, 35, 37, 0.5)), color-stop(75%, rgba(22, 22, 23, 0)), to(rgba(0, 0, 0, 0))); - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.5) 50%, rgba(22, 22, 23, 0) 75%, rgba(0, 0, 0, 0) 100%); -} - -.single-blog-post:hover img { - -webkit-transform: scale(1.05); - transform: scale(1.05); -} - -.blog-section-title { - margin-bottom: 30px; -} - -.blog-section-title h2 { - margin-bottom: 0; - font-size: 40px; -} - -.blog-section-title p { - margin-top: 15px; -} - -.blog-section-title .sub-title { - display: block; - margin-bottom: 12px; - color: var(--startupMainColor); - font-family: var(--headingFontFamily); - font-weight: 600; - font-size: 14px; -} - -.blog-section-title .sub-title.marketing-color { - color: var(--marketingMainColor); -} - -.blog-section-title .default-btn { - margin-top: 10px; -} - -.single-post-item { - margin-bottom: 30px; - background-color: var(--whiteColor); - -webkit-box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, 0.1); -} - -.single-post-item .post-content { - padding: 25px; -} - -.single-post-item .post-content .tag { - margin-bottom: 14px; - display: inline-block; - position: relative; - padding-left: 24px; - font-weight: 600; - color: var(--appMainColor); -} - -.single-post-item .post-content .tag i { - position: absolute; - left: 0; - top: 4px; -} - -.single-post-item .post-content .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; -} - -.single-post-item .post-content .meta li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); -} - -.single-post-item .post-content .meta li i { - position: absolute; - left: 0; - top: 4px; - color: var(--marketingMainColor); -} - -.single-post-item .post-content .meta li a { - color: var(--optionalColor); -} - -.single-post-item .post-content .meta li a:hover { - color: var(--marketingMainColor); -} - -.single-post-item .post-content h3 { - margin-bottom: 14px; - line-height: 1.4; - font-size: 20px; -} - -.single-post-item .post-content .link-btn { - font-weight: 600; - position: relative; - display: inline-block; - padding-right: 12px; -} - -.single-post-item .post-content .link-btn i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; -} - -.single-post-item .post-content .link-btn::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--marketingMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-post-item .post-content .link-btn:hover { - padding-right: 15px; - color: var(--marketingMainColor); -} - -.single-post-item .post-content .link-btn:hover::before { - width: 100%; - right: auto; - left: 0; -} - -.single-post-item.border-radius { - border-radius: 10px; -} - -.single-post-item.border-radius .post-image { - border-radius: 10px 10px 0 0; -} - -.single-post-item.border-radius .post-image a { - border-radius: 10px 10px 0 0; -} - -.single-post-item.border-radius .post-image a img { - border-radius: 10px 10px 0 0; -} - -.single-post-item.business-color .post-content .meta li i { - color: var(--businessMainColor); -} - -.single-post-item.business-color .post-content .meta li a:hover { - color: var(--businessMainColor); -} - -.single-post-item.business-color .post-content .link-btn::before { - background-color: var(--businessMainColor); -} - -.single-post-item.business-color .post-content .link-btn:hover { - color: var(--businessMainColor); -} - -.single-post-item.app-color .post-content .meta li i { - color: var(--appMainColor); -} - -.single-post-item.app-color .post-content .meta li a:hover { - color: var(--appMainColor); -} - -.single-post-item.app-color .post-content .link-btn::before { - background-color: var(--appMainColor); -} - -.single-post-item.app-color .post-content .link-btn:hover { - color: var(--appMainColor); -} - -.single-post-item.saas-color .post-content .meta li i { - color: var(--saasMainColor); -} - -.single-post-item.saas-color .post-content .meta li a:hover { - color: var(--saasMainColor); -} - -.single-post-item.saas-color .post-content .link-btn::before { - background-color: var(--saasMainColor); -} - -.single-post-item.saas-color .post-content .link-btn:hover { - color: var(--saasMainColor); -} - -.single-post-item.bg-black { - background-color: #040611; - -webkit-box-shadow: unset; - box-shadow: unset; - border: 1px solid #272836; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-post-item.bg-black .post-content .tag { - color: var(--saasMainColor); -} - -.single-post-item.bg-black .post-content .meta { - color: var(--whiteColor); -} - -.single-post-item.bg-black .post-content .meta li { - color: var(--whiteColor); -} - -.single-post-item.bg-black .post-content .meta li i { - color: var(--saasMainColor); -} - -.single-post-item.bg-black .post-content .meta li a { - color: var(--whiteColor); -} - -.single-post-item.bg-black .post-content .meta li a:hover { - color: var(--saasMainColor); -} - -.single-post-item.bg-black .post-content h3 { - color: var(--whiteColor); -} - -.single-post-item.bg-black .post-content h3 a { - color: var(--whiteColor); -} - -.single-post-item.bg-black .post-content h3 a:hover { - color: var(--saasMainColor); -} - -.single-post-item.bg-black .post-content p { - color: var(--whiteColor); - opacity: 0.9; -} - -.single-post-item.bg-black .post-content .link-btn { - color: var(--whiteColor); -} - -.single-post-item.bg-black .post-content .link-btn::before { - background: var(--saasGradientColor); -} - -.single-post-item.bg-black .post-content .link-btn:hover { - color: var(--whiteColor); -} - -.single-post-item.bg-black:hover { - border-color: var(--whiteColor); -} - -.single-post-item.bg-faf2ff { - background-color: #faf2ff; - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.single-post-item.bg-fff5e6 { - background-color: #fff5e6; - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.single-post-item.bg-eaf6ff { - background-color: #eaf6ff; - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.single-post-item.personal-portfolio-color .post-content .meta li i { - color: var(--personalPortfolioMainColor); -} - -.single-post-item.personal-portfolio-color .post-content .meta li a:hover { - color: var(--personalPortfolioMainColor); -} - -.single-post-item.personal-portfolio-color .post-content .link-btn::before { - background-color: var(--personalPortfolioMainColor); -} - -.single-post-item.personal-portfolio-color .post-content .link-btn:hover { - color: var(--personalPortfolioMainColor); -} - -.single-post-item.consulting-color .post-content .meta li i { - color: var(--consultingMainColor); -} - -.single-post-item.consulting-color .post-content .meta li a:hover { - color: var(--consultingMainColor); -} - -.single-post-item.consulting-color .post-content .link-btn::before { - background-color: var(--consultingMainColor); -} - -.single-post-item.consulting-color .post-content .link-btn:hover { - color: var(--consultingMainColor); -} - -.single-post-item.it-agency-color .post-content .tag { - color: var(--optionalColor); -} - -.single-post-item.it-agency-color .post-content .tag i { - color: var(--itAgencyMainColor); -} - -.single-post-item.it-agency-color .post-content .meta li i { - color: var(--itAgencyMainColor); -} - -.single-post-item.it-agency-color .post-content .meta li a:hover { - color: var(--itAgencyMainColor); -} - -.single-post-item.it-agency-color .post-content .link-btn::before { - background-color: var(--itAgencyMainColor); -} - -.single-post-item.it-agency-color .post-content .link-btn:hover { - color: var(--itAgencyMainColor); -} - -.single-post-item.without-box-shadow { - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.single-post-item.without-box-shadow .post-content { - padding-bottom: 0; -} - -.single-post-item.without-box-shadow .post-content .meta li i { - color: var(--blackColor); -} - -.single-post-item.without-box-shadow .post-content .meta li a:hover { - color: var(--blackColor); -} - -.single-post-item.without-box-shadow .post-content .link-btn::before { - background-color: var(--blackColor); -} - -.single-post-item.without-box-shadow .post-content .link-btn:hover { - color: var(--blackColor); -} - -.single-post-item.startup-color .post-content .tag { - color: var(--startupMainColor); -} - -.single-post-item.startup-color .post-content .meta li i { - color: var(--startupMainColor); -} - -.single-post-item.startup-color .post-content .meta li a:hover { - color: var(--startupMainColor); -} - -.single-post-item.startup-color .post-content .link-btn::before { - background-color: var(--startupMainColor); -} - -.single-post-item.startup-color .post-content .link-btn:hover { - color: var(--startupMainColor); -} - -/*================================================ -Blog Details Area CSS -=================================================*/ -.blog-details-desc .article-content { - margin-top: 30px; -} - -.blog-details-desc .article-content .entry-meta { - margin-bottom: 15px; -} - -.blog-details-desc .article-content .entry-meta ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.blog-details-desc .article-content .entry-meta ul li { - display: inline-block; - margin-right: 20px; - color: var(--optionalColor); - font-weight: 500; - position: relative; - padding-left: 22px; -} - -.blog-details-desc .article-content .entry-meta ul li i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); -} - -.blog-details-desc .article-content .entry-meta ul li:last-child { - margin-right: 0; -} - -.blog-details-desc .article-content h3 { - font-size: 22px; - margin-bottom: 15px; - margin-top: 25px; -} - -.blog-details-desc .article-content .wp-block-gallery.columns-3 { - padding-left: 0; - list-style-type: none; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - margin-right: -10px; - margin-left: -10px; - margin-bottom: 30px; - margin-top: 30px; -} - -.blog-details-desc .article-content .wp-block-gallery.columns-3 li { - -webkit-box-flex: 0; - -ms-flex: 0 0 33.3333%; - flex: 0 0 33.3333%; - max-width: 33.3333%; - padding-right: 10px; - padding-left: 10px; -} - -.blog-details-desc .article-content .wp-block-gallery.columns-3 li figure { - margin-bottom: 0; -} - -.blog-details-desc .article-content .features-list { - margin-top: 25px; - margin-bottom: 30px; -} - -.blog-details-desc .article-content .features-list li { - margin-bottom: 15px; - color: var(--optionalColor); - font-weight: 500; -} - -.blog-details-desc .article-content .features-list li:last-child { - margin-bottom: 0; -} - -.blog-details-desc .article-footer { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - margin-top: 30px; - padding-top: 30px; - border-top: 1px solid #eeeeee; -} - -.blog-details-desc .article-footer .article-tags { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; -} - -.blog-details-desc .article-footer .article-tags p { - line-height: 1; - margin-bottom: 0; - color: var(--blackColor); - font-weight: 600; - text-transform: uppercase; -} - -.blog-details-desc .article-footer .article-tags a { - display: inline-block; - margin-left: 10px; - background-color: #f9f9f9; - color: var(--blackColor); - border-radius: 30px; - padding: 7px 20px; -} - -.blog-details-desc .article-footer .article-tags a:hover { - background-color: var(--startupMainColor); - color: var(--whiteColor); -} - -.blog-details-desc .article-footer .article-share { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; -} - -.blog-details-desc .article-footer .article-share .social { - padding-left: 0; - list-style-type: none; - text-align: end; - margin-bottom: 0; -} - -.blog-details-desc .article-footer .article-share .social li { - display: inline-block; -} - -.blog-details-desc .article-footer .article-share .social li span { - display: inline-block; - margin-right: 3px; - font-weight: 600; -} - -.blog-details-desc .article-footer .article-share .social li a { - display: block; - width: 32px; - height: 32px; - line-height: 32px; - border-radius: 50%; - background-color: var(--startupMainColor); - color: var(--whiteColor); - border: 1px solid var(--startupMainColor); - text-align: center; - font-size: 14px; - margin-left: 2px; -} - -.blog-details-desc .article-footer .article-share .social li a:hover, .blog-details-desc .article-footer .article-share .social li a:focus { - color: var(--startupMainColor); - background-color: transparent; -} - -.blog-details-desc .article-footer .article-share .social li a.facebook { - background-color: #3b5998; - border-color: #3b5998; - color: var(--whiteColor); -} - -.blog-details-desc .article-footer .article-share .social li a.facebook:hover, .blog-details-desc .article-footer .article-share .social li a.facebook:focus { - color: #3b5998; - background-color: transparent; -} - -.blog-details-desc .article-footer .article-share .social li a.twitter { - background-color: #1da1f2; - border-color: #1da1f2; - color: var(--whiteColor); -} - -.blog-details-desc .article-footer .article-share .social li a.twitter:hover, .blog-details-desc .article-footer .article-share .social li a.twitter:focus { - color: #1da1f2; - background-color: transparent; -} - -.blog-details-desc .article-footer .article-share .social li a.linkedin { - background-color: #007bb5; - border-color: #007bb5; - color: var(--whiteColor); -} - -.blog-details-desc .article-footer .article-share .social li a.linkedin:hover, .blog-details-desc .article-footer .article-share .social li a.linkedin:focus { - color: #007bb5; - background-color: transparent; -} - -.blog-details-desc .article-footer .article-share .social li a.instagram { - background-color: #c13584; - border-color: #c13584; - color: var(--whiteColor); -} - -.blog-details-desc .article-footer .article-share .social li a.instagram:hover, .blog-details-desc .article-footer .article-share .social li a.instagram:focus { - color: #c13584; - background-color: transparent; -} - -blockquote, .blockquote { - z-index: 1; - overflow: hidden; - position: relative; - background-color: #fafafa; - padding: 40px 50px !important; - margin-bottom: 20px; - margin-top: 20px; -} - -blockquote p, .blockquote p { - line-height: 1.7; - margin-bottom: 0; - color: var(--blackColor); - font-weight: 700; - font-style: italic; - font-size: 17px !important; -} - -blockquote cite, .blockquote cite { - display: none; -} - -blockquote::after, .blockquote::after { - top: 0; - left: 0; - bottom: 0; - width: 3px; - content: ''; - position: absolute; - background-color: var(--startupMainColor); - margin-top: 20px; - margin-bottom: 20px; -} - -.comments-area { - margin-top: 30px; -} - -.comments-area .comments-title { - font-size: 22px; - margin-bottom: 30px; - line-height: initial; -} - -.comments-area ol, .comments-area ul { - padding: 0; - margin: 0; - list-style-type: none; -} - -.comments-area .comment-list { - padding: 0; - margin: 0; - list-style-type: none; -} - -.comments-area .children { - margin-left: 20px; -} - -.comments-area .comment-body { - border-bottom: 1px dashed #eeeeee; - padding-left: 70px; - color: var(--blackColor); - margin-bottom: 20px; - padding-bottom: 20px; -} - -.comments-area .comment-body .reply { - margin-top: 15px; -} - -.comments-area .comment-body .reply a { - border: 1px dashed #ded9d9; - color: var(--blackColor); - display: inline-block; - padding: 5px 20px 6px; - border-radius: 30px; - font-size: 13px; - font-weight: 700; -} - -.comments-area .comment-body .reply a:hover { - color: var(--whiteColor); - background-color: var(--startupMainColor); - border-color: var(--startupMainColor); -} - -.comments-area .comment-author { - font-size: 16px; - margin-bottom: 6px; - position: relative; - z-index: 2; -} - -.comments-area .comment-author .avatar { - height: 50px; - left: -65px; - position: absolute; - width: 50px; - top: 4px; -} - -.comments-area .comment-author .says { - display: none; -} - -.comments-area .comment-metadata { - margin-bottom: .7em; - color: var(--optionalColor); - letter-spacing: 0.01em; - font-size: 13px; - font-weight: 500; -} - -.comments-area .comment-respond { - margin-top: 30px; -} - -.comments-area .comment-respond .comment-reply-title { - margin-bottom: 0; - font-size: 22px; -} - -.comments-area .comment-respond .comment-reply-title #cancel-comment-reply-link { - display: inline-block; -} - -.comments-area .comment-respond .comment-form { - overflow: hidden; -} - -.comments-area .comment-respond .comment-notes { - margin-bottom: 20px; - margin-top: 10px; -} - -.comments-area .comment-respond .comment-notes .required { - color: red; -} - -.comments-area .comment-respond .comment-form-comment { - float: left; - width: 100%; -} - -.comments-area .comment-respond label { - display: none; -} - -.comments-area .comment-respond input[type="date"], .comments-area .comment-respond input[type="time"], .comments-area .comment-respond input[type="datetime-local"], .comments-area .comment-respond input[type="week"], .comments-area .comment-respond input[type="month"], .comments-area .comment-respond input[type="text"], .comments-area .comment-respond input[type="email"], .comments-area .comment-respond input[type="url"], .comments-area .comment-respond input[type="password"], .comments-area .comment-respond input[type="search"], .comments-area .comment-respond input[type="tel"], .comments-area .comment-respond input[type="number"], .comments-area .comment-respond textarea { - outline: 0; - width: 100%; - border: none; - height: 50px; - display: block; - font-size: 15px; - border-radius: 3px; - padding: 1px 0 0 15px; - color: var(--blackColor); - background-color: #f9f9f9; -} - -.comments-area .comment-respond input[type="date"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="time"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="datetime-local"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="week"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="month"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="text"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="email"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="url"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="password"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="search"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="tel"]::-webkit-input-placeholder, .comments-area .comment-respond input[type="number"]::-webkit-input-placeholder, .comments-area .comment-respond textarea::-webkit-input-placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.comments-area .comment-respond input[type="date"]:-ms-input-placeholder, .comments-area .comment-respond input[type="time"]:-ms-input-placeholder, .comments-area .comment-respond input[type="datetime-local"]:-ms-input-placeholder, .comments-area .comment-respond input[type="week"]:-ms-input-placeholder, .comments-area .comment-respond input[type="month"]:-ms-input-placeholder, .comments-area .comment-respond input[type="text"]:-ms-input-placeholder, .comments-area .comment-respond input[type="email"]:-ms-input-placeholder, .comments-area .comment-respond input[type="url"]:-ms-input-placeholder, .comments-area .comment-respond input[type="password"]:-ms-input-placeholder, .comments-area .comment-respond input[type="search"]:-ms-input-placeholder, .comments-area .comment-respond input[type="tel"]:-ms-input-placeholder, .comments-area .comment-respond input[type="number"]:-ms-input-placeholder, .comments-area .comment-respond textarea:-ms-input-placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.comments-area .comment-respond input[type="date"]::-ms-input-placeholder, .comments-area .comment-respond input[type="time"]::-ms-input-placeholder, .comments-area .comment-respond input[type="datetime-local"]::-ms-input-placeholder, .comments-area .comment-respond input[type="week"]::-ms-input-placeholder, .comments-area .comment-respond input[type="month"]::-ms-input-placeholder, .comments-area .comment-respond input[type="text"]::-ms-input-placeholder, .comments-area .comment-respond input[type="email"]::-ms-input-placeholder, .comments-area .comment-respond input[type="url"]::-ms-input-placeholder, .comments-area .comment-respond input[type="password"]::-ms-input-placeholder, .comments-area .comment-respond input[type="search"]::-ms-input-placeholder, .comments-area .comment-respond input[type="tel"]::-ms-input-placeholder, .comments-area .comment-respond input[type="number"]::-ms-input-placeholder, .comments-area .comment-respond textarea::-ms-input-placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.comments-area .comment-respond input[type="date"]::placeholder, .comments-area .comment-respond input[type="time"]::placeholder, .comments-area .comment-respond input[type="datetime-local"]::placeholder, .comments-area .comment-respond input[type="week"]::placeholder, .comments-area .comment-respond input[type="month"]::placeholder, .comments-area .comment-respond input[type="text"]::placeholder, .comments-area .comment-respond input[type="email"]::placeholder, .comments-area .comment-respond input[type="url"]::placeholder, .comments-area .comment-respond input[type="password"]::placeholder, .comments-area .comment-respond input[type="search"]::placeholder, .comments-area .comment-respond input[type="tel"]::placeholder, .comments-area .comment-respond input[type="number"]::placeholder, .comments-area .comment-respond textarea::placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.comments-area .comment-respond input[type="date"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="time"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="datetime-local"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="week"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="month"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="text"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="email"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="url"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="password"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="search"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="tel"]:focus::-webkit-input-placeholder, .comments-area .comment-respond input[type="number"]:focus::-webkit-input-placeholder, .comments-area .comment-respond textarea:focus::-webkit-input-placeholder { - color: transparent; -} - -.comments-area .comment-respond input[type="date"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="time"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="datetime-local"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="week"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="month"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="text"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="email"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="url"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="password"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="search"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="tel"]:focus:-ms-input-placeholder, .comments-area .comment-respond input[type="number"]:focus:-ms-input-placeholder, .comments-area .comment-respond textarea:focus:-ms-input-placeholder { - color: transparent; -} - -.comments-area .comment-respond input[type="date"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="time"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="datetime-local"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="week"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="month"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="text"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="email"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="url"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="password"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="search"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="tel"]:focus::-ms-input-placeholder, .comments-area .comment-respond input[type="number"]:focus::-ms-input-placeholder, .comments-area .comment-respond textarea:focus::-ms-input-placeholder { - color: transparent; -} - -.comments-area .comment-respond input[type="date"]:focus::placeholder, .comments-area .comment-respond input[type="time"]:focus::placeholder, .comments-area .comment-respond input[type="datetime-local"]:focus::placeholder, .comments-area .comment-respond input[type="week"]:focus::placeholder, .comments-area .comment-respond input[type="month"]:focus::placeholder, .comments-area .comment-respond input[type="text"]:focus::placeholder, .comments-area .comment-respond input[type="email"]:focus::placeholder, .comments-area .comment-respond input[type="url"]:focus::placeholder, .comments-area .comment-respond input[type="password"]:focus::placeholder, .comments-area .comment-respond input[type="search"]:focus::placeholder, .comments-area .comment-respond input[type="tel"]:focus::placeholder, .comments-area .comment-respond input[type="number"]:focus::placeholder, .comments-area .comment-respond textarea:focus::placeholder { - color: transparent; -} - -.comments-area .comment-respond textarea { - height: auto !important; - padding-top: 15px; -} - -.comments-area .comment-respond .comment-form-author { - float: left; - width: 50%; - padding-right: 10px; - margin-bottom: 20px; -} - -.comments-area .comment-respond .comment-form-email { - float: left; - width: 50%; - padding-left: 12px; - margin-bottom: 20px; -} - -.comments-area .comment-respond .comment-form-url { - float: left; - width: 100%; - margin-bottom: 20px; -} - -.comments-area .comment-respond .comment-form-cookies-consent { - width: 100%; - float: left; - position: relative; - padding-left: 20px; - margin-bottom: 20px; - margin-top: 15px; -} - -.comments-area .comment-respond .comment-form-cookies-consent input { - position: absolute; - left: 0; - top: 6px; -} - -.comments-area .comment-respond .comment-form-cookies-consent label { - margin: 0; - display: inline-block; - color: var(--optionalColor); - font-weight: normal; - font-size: var(--fontSize); -} - -.comments-area .comment-respond .form-submit { - float: left; - width: 100%; -} - -.comments-area .comment-respond .form-submit input { - border: none; - cursor: pointer; - border-radius: 4px; - padding: 10px 30px; - display: inline-block; - color: var(--whiteColor); - background: var(--startupMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); - font-weight: 700; - font-size: var(--fontSize); -} - -.comments-area .comment-respond .form-submit input:hover, .comments-area .comment-respond .form-submit input:focus { - color: var(--whiteColor); - background-color: var(--blackColor); -} - -/*================================================ -Widget Area CSS -=================================================*/ -.widget-area .widget { - margin-bottom: 35px; -} - -.widget-area .widget .widget-title { - border-bottom: 1px solid #eeeeee; - padding-bottom: 12px; - margin-bottom: 30px; - position: relative; - font-size: 20px; -} - -.widget-area .widget .widget-title::before { - left: 0; - width: 70px; - height: 1px; - content: ''; - bottom: -1px; - position: absolute; - background-color: var(--blackColor); -} - -.widget-area .widget:last-child { - margin-bottom: 0; -} - -.widget-area .widget_search { - -webkit-box-shadow: 0px 0px 29px 0px rgba(102, 102, 102, 0.1); - box-shadow: 0px 0px 29px 0px rgba(102, 102, 102, 0.1); - background-color: var(--whiteColor); - padding: 15px; -} - -.widget-area .widget_search form { - position: relative; -} - -.widget-area .widget_search form label { - display: block; - margin-bottom: 0; -} - -.widget-area .widget_search form .screen-reader-text { - display: none; -} - -.widget-area .widget_search form .search-field { - outline: 0; - width: 100%; - height: 50px; - display: block; - padding: 0 0 0 15px; - -webkit-transition: var(--transition); - transition: var(--transition); - border: 1px solid #eeeeee; - background-color: transparent; -} - -.widget-area .widget_search form .search-field:focus { - border-color: var(--startupMainColor); -} - -.widget-area .widget_search form button { - position: absolute; - right: 0; - outline: 0; - bottom: 0; - height: 50px; - width: 50px; - border: none; - color: var(--whiteColor); - background-color: var(--startupMainColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.widget-area .widget_search form button:hover { - color: var(--whiteColor); - background-color: var(--blackColor); -} - -.widget-area .widget_about_author .about-author { - -webkit-transition: var(--transition); - transition: var(--transition); - border: 1px solid #eeeeee; - text-align: center; - padding: 30px; -} - -.widget-area .widget_about_author .about-author .main-image { - width: 100px; -} - -.widget-area .widget_about_author .about-author p { - margin-bottom: 15px; - margin-top: 25px; -} - -.widget-area .widget_about_author .about-author:hover { - border-color: var(--blackColor); -} - -.widget-area .widget_noke_posts_thumb { - position: relative; - overflow: hidden; -} - -.widget-area .widget_noke_posts_thumb .item { - margin-bottom: 20px; - position: relative; - padding-left: 115px; -} - -.widget-area .widget_noke_posts_thumb .item .thumb { - top: 0; - left: 0; - width: 100px; - float: unset; - height: 100%; - display: block; - position: absolute; -} - -.widget-area .widget_noke_posts_thumb .item .thumb .fullimage { - width: 100px; - height: 100%; - display: inline-block; - background-repeat: no-repeat; - background-size: cover !important; - background-position: center center !important; -} - -.widget-area .widget_noke_posts_thumb .item .thumb .fullimage.bg1 { - background-image: url(../../assets/img/blog/blog-img1.jpg); -} - -.widget-area .widget_noke_posts_thumb .item .thumb .fullimage.bg2 { - background-image: url(../../assets/img/blog/blog-img2.jpg); -} - -.widget-area .widget_noke_posts_thumb .item .thumb .fullimage.bg3 { - background-image: url(../../assets/img/blog/blog-img3.jpg); -} - -.widget-area .widget_noke_posts_thumb .item .thumb .fullimage.bg4 { - background-image: url(../../assets/img/blog/blog-img4.jpg); -} - -.widget-area .widget_noke_posts_thumb .item .thumb::before, .widget-area .widget_noke_posts_thumb .item .thumb::after { - content: ''; - z-index: 1; - top: 50%; - left: 50%; - opacity: 0; - position: absolute; - background-color: var(--whiteColor); - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - -webkit-transition: all 0.8s cubic-bezier(0.2, 1, 0.22, 1); - transition: all 0.8s cubic-bezier(0.2, 1, 0.22, 1); -} - -.widget-area .widget_noke_posts_thumb .item .thumb::before { - width: 40px; - height: 1px; - left: 100%; -} - -.widget-area .widget_noke_posts_thumb .item .thumb::after { - height: 40px; - width: 1px; - top: 0; -} - -.widget-area .widget_noke_posts_thumb .item .info .title { - font-size: 16px; - line-height: 1.5; - margin-bottom: 8px; -} - -.widget-area .widget_noke_posts_thumb .item .info .title a { - display: inline-block; -} - -.widget-area .widget_noke_posts_thumb .item .info .date { - padding-left: 20px; - position: relative; - color: var(--optionalColor); -} - -.widget-area .widget_noke_posts_thumb .item .info .date i { - position: absolute; - left: 0; - top: 3px; -} - -.widget-area .widget_noke_posts_thumb .item:last-child { - margin-bottom: 0; -} - -.widget-area .widget_noke_posts_thumb .item:hover .thumb::before, .widget-area .widget_noke_posts_thumb .item:hover .thumb::after { - opacity: 1; - top: 50%; - left: 50%; -} - -.widget-area .widget_follow_us ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.widget-area .widget_follow_us ul li { - margin-bottom: 15px; -} - -.widget-area .widget_follow_us ul li a { - display: block; - position: relative; - color: var(--whiteColor); - background-color: var(--blackColor); - padding-right: 15px; - padding-top: 10px; - padding-bottom: 10px; - padding-left: 55px; -} - -.widget-area .widget_follow_us ul li a i { - position: absolute; - left: 14px; - top: 50%; - font-size: 18px; - line-height: 1; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.widget-area .widget_follow_us ul li a::before { - content: ''; - position: absolute; - left: 42px; - top: 0; - height: 100%; - width: 1px; - opacity: 0.3; - background-color: var(--whiteColor); -} - -.widget-area .widget_follow_us ul li a.facebook { - background-color: #39569e; -} - -.widget-area .widget_follow_us ul li a.twitter { - background-color: #52aaf4; -} - -.widget-area .widget_follow_us ul li a.linkedin { - background-color: #0185ae; -} - -.widget-area .widget_follow_us ul li a.instagram { - background-color: #c8359d; -} - -.widget-area .widget_follow_us ul li:last-child { - margin-bottom: 0; -} - -.widget-area .widget_recent_posts .item { - margin-bottom: 15px; - padding-bottom: 15px; - border-bottom: 1px solid #eeeeee; -} - -.widget-area .widget_recent_posts .item .tag { - position: relative; - margin-bottom: 8px; - display: inline-block; - color: var(--blackColor); -} - -.widget-area .widget_recent_posts .item .title { - font-size: 16px; - line-height: 1.5; - margin-bottom: 10px; -} - -.widget-area .widget_recent_posts .item .title a { - display: inline-block; -} - -.widget-area .widget_recent_posts .item .date { - padding-left: 20px; - position: relative; - color: var(--optionalColor); -} - -.widget-area .widget_recent_posts .item .date i { - position: absolute; - left: 0; - top: 3px; -} - -.widget-area .widget_tag_cloud .tagcloud { - margin-bottom: -8px; -} - -.widget-area .widget_tag_cloud .tagcloud a { - display: inline-block; - border: 1px solid #eeeeee; - padding: 6px 15px; - color: var(--optionalColor); - margin-right: 8px; - margin-bottom: 8px; - font-size: 14px !important; - font-weight: 600; -} - -.widget-area .widget_tag_cloud .tagcloud a:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - border-color: var(--blackColor); -} - -.widget-area .widget_newsletter { - background-color: #f6f7f9; - border: 1px solid #eeeeee; - text-align: center; - padding: 30px; -} - -.widget-area .widget_newsletter h4 { - font-size: 20px; - margin-bottom: 10px; -} - -.widget-area .widget_newsletter .newsletter-form { - margin-top: 20px; -} - -.widget-area .widget_newsletter .newsletter-form .input-newsletter { - display: block; - width: 100%; - background-color: var(--whiteColor); - border: 1px solid #eeeeee; - height: 50px; - padding-left: 15px; - color: var(--blackColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.widget-area .widget_newsletter .newsletter-form .input-newsletter:focus { - border-color: var(--blackColor); -} - -.widget-area .widget_newsletter .newsletter-form button { - display: block; - width: 100%; - text-align: center; - margin-top: 15px; - background-color: var(--blackColor); - color: var(--whiteColor); - border: none; - -webkit-transition: var(--transition); - transition: var(--transition); - padding: 13px 30px; - font-size: var(--fontSize); - font-weight: 700; -} - -.widget-area .widget_newsletter .newsletter-form button:hover { - background-color: var(--optionalColor); -} - -.widget-area .widget_categories ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.widget-area .widget_categories ul li { - margin-bottom: 12px; - position: relative; - padding-left: 15px; -} - -.widget-area .widget_categories ul li::before { - left: 0; - top: 50%; - width: 8px; - height: 8px; - content: ''; - position: absolute; - border-radius: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - background-color: var(--optionalColor); -} - -.widget-area .widget_categories ul li a { - font-weight: 500; - display: inline-block; - color: var(--optionalColor); -} - -.widget-area .widget_categories ul li a:hover { - color: var(--startupMainColor); -} - -.widget-area .widget_categories ul li:last-child { - margin-bottom: 0; -} - -.widget-area .widget_archive ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.widget-area .widget_archive ul li { - margin-bottom: 12px; - position: relative; - padding-left: 15px; -} - -.widget-area .widget_archive ul li::before { - left: 0; - top: 50%; - width: 8px; - height: 8px; - content: ''; - position: absolute; - border-radius: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - background-color: var(--optionalColor); -} - -.widget-area .widget_archive ul li a { - font-weight: 500; - display: inline-block; - color: var(--optionalColor); -} - -.widget-area .widget_archive ul li a:hover { - color: var(--startupMainColor); -} - -.widget-area .widget_archive ul li:last-child { - margin-bottom: 0; -} - -.widget-area .widget_colors ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.widget-area .widget_colors ul li { - display: block; - margin-bottom: 12px; - border-bottom: 1px dashed #eeeeee; - padding-bottom: 12px; -} - -.widget-area .widget_colors ul li a { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; -} - -.widget-area .widget_colors ul li a span { - display: block; - color: var(--optionalColor); -} - -.widget-area .widget_colors ul li a span:nth-child(1) { - width: 17px; - height: 17px; - font-size: 0; - margin-right: 8px; - letter-spacing: 0; - border-radius: 50%; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.widget-area .widget_colors ul li a span:nth-child(1).gray { - background-color: gray; -} - -.widget-area .widget_colors ul li a span:nth-child(1).red { - background-color: red; -} - -.widget-area .widget_colors ul li a span:nth-child(1).green { - background-color: green; -} - -.widget-area .widget_colors ul li a span:nth-child(1).black { - background-color: black; -} - -.widget-area .widget_colors ul li a span:nth-child(1).brown { - background-color: brown; -} - -.widget-area .widget_colors ul li a:hover span:nth-child(1) { - -webkit-transform: scale(1.2); - transform: scale(1.2); -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs-handle { - cursor: pointer; -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-from, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-to, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-single { - background-color: var(--startupMainColor); - padding: 1px 5px 1px 11.5px; -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-from::before, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-to::before, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-single::before { - border-top-color: var(--startupMainColor); -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-from::after, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-to::after, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-single::after { - content: '$'; - position: absolute; - left: 5px; - top: 0.5px; -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-bar { - background-color: var(--startupMainColor); -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-handle > i:first-child { - background-color: var(--startupMainColor); -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-min, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-max { - padding: 1px 3px 1px 9.5px; -} - -.widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-min::before, .widget-area .widget_price_filter .collection_filter_by_price .irs--flat .irs-max::before { - content: '$'; - position: absolute; - left: 3px; - top: 0.5px; -} - -.widget-area .widget_top_rated_products .item { - margin-bottom: 15px; - position: relative; - padding-left: 95px; -} - -.widget-area .widget_top_rated_products .item .thumb { - top: 0; - left: 0; - width: 80px; - float: unset; - height: 100%; - display: block; - position: absolute; -} - -.widget-area .widget_top_rated_products .item .thumb .fullimage { - width: 80px; - height: 100%; - display: inline-block; - border: 1px solid #eeeeee; - background-repeat: no-repeat; - background-size: cover !important; - background-position: center center !important; -} - -.widget-area .widget_top_rated_products .item .thumb .fullimage.bg1 { - background-image: url(../../assets/img/products/products-img1.jpg); -} - -.widget-area .widget_top_rated_products .item .thumb .fullimage.bg2 { - background-image: url(../../assets/img/products/products-img2.jpg); -} - -.widget-area .widget_top_rated_products .item .thumb .fullimage.bg3 { - background-image: url(../../assets/img/products/products-img3.jpg); -} - -.widget-area .widget_top_rated_products .item .thumb .fullimage.bg4 { - background-image: url(../../assets/img/products/products-img4.jpg); -} - -.widget-area .widget_top_rated_products .item .info { - padding-top: 5px; - padding-bottom: 5px; -} - -.widget-area .widget_top_rated_products .item .info .title { - margin-bottom: 8px; - line-height: 1.4; - font-size: var(--fontSize); - font-weight: 600; -} - -.widget-area .widget_top_rated_products .item .info .title a { - display: inline-block; -} - -.widget-area .widget_top_rated_products .item .info .star-rating { - font-size: 14px; -} - -.widget-area .widget_top_rated_products .item .info .star-rating i { - color: #f49f0b; -} - -.widget-area .widget_top_rated_products .item .info .price { - margin-top: 8px; - display: block; - color: var(--optionalColor); -} - -.widget-area .widget_top_rated_products .item:last-child { - margin-bottom: 0; -} - -.widget-area .widget_service_categories ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.widget-area .widget_service_categories ul li { - font-weight: 500; - overflow: hidden; - margin-bottom: 13px; - padding-bottom: 13px; - color: var(--optionalColor); - border-bottom: 1px dashed #eeeeee; -} - -.widget-area .widget_service_categories ul li a { - display: block; - position: relative; - color: var(--optionalColor); -} - -.widget-area .widget_service_categories ul li a i { - position: absolute; - right: 5px; - top: 50%; - font-size: 14px; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.widget-area .widget_service_categories ul li a:hover { - color: var(--startupMainColor); -} - -.widget-area .widget_service_categories ul li a:hover i { - right: 0; - color: var(--startupMainColor); -} - -.widget-area .widget_service_categories ul li:last-child { - margin-bottom: 0; -} - -.widget-area.extra-padding { - padding-left: 15px; -} - -.widget-area.extra-padding.left-sidebar { - padding-left: 0; - padding-right: 15px; -} - -/*================================================ -Profile Authentication Area CSS -=================================================*/ -.profile-authentication-area { - height: 100vh; - position: relative; - background-color: #f3f3f3; -} - -.profile-authentication-area .logo { - left: 0; - right: 0; - top: 20px; - position: absolute; - text-align: center; - display: inline-block; -} - -.signin-form { - max-width: 550px; - border-radius: 5px; - padding: 40px; - background-color: var(--whiteColor); - margin-left: auto; - margin-right: auto; -} - -.signin-form h2 { - font-size: 30px; - margin-bottom: 30px; -} - -.signin-form form .form-group { - margin-bottom: 25px; -} - -.signin-form form .form-group .form-control { - background-color: #f5f5f5 !important; -} - -.signin-form form .remember-me-wrap { - margin-bottom: 0; -} - -.signin-form form .remember-me-wrap [type="checkbox"]:checked, .signin-form form .remember-me-wrap [type="checkbox"]:not(:checked) { - display: none; -} - -.signin-form form .remember-me-wrap [type="checkbox"]:checked + label, .signin-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label { - position: relative; - padding-left: 28px; - cursor: pointer; - line-height: 20px; - display: inline-block; - margin-bottom: 0; - color: var(--optionalColor); -} - -.signin-form form .remember-me-wrap [type="checkbox"]:checked + label:before, .signin-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label:before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 19px; - height: 19px; - -webkit-transition: all 0.2s ease; - transition: all 0.2s ease; - border: 1px solid #f5f5f5; - border-radius: 3px; - background: #f5f5f5; -} - -.signin-form form .remember-me-wrap [type="checkbox"]:checked + label:after, .signin-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label:after { - content: ''; - width: 8px; - height: 8px; - background: var(--startupMainColor); - position: absolute; - top: 5.5px; - left: 6px; - -webkit-transition: all 0.2s ease; - transition: all 0.2s ease; -} - -.signin-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label:after { - opacity: 0; - -webkit-transform: scale(0); - transform: scale(0); -} - -.signin-form form .remember-me-wrap [type="checkbox"]:checked + label:after { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); -} - -.signin-form form .remember-me-wrap [type="checkbox"]:hover + label:before { - border-color: var(--startupMainColor); -} - -.signin-form form .remember-me-wrap [type="checkbox"]:checked + label:before { - border-color: var(--startupMainColor); -} - -.signin-form form .lost-your-password-wrap { - text-align: end; -} - -.signin-form form .lost-your-password-wrap a { - display: inline-block; - position: relative; - line-height: 1.5; - font-weight: 500; -} - -.signin-form form .lost-your-password-wrap a::before { - width: 100%; - height: 1px; - position: absolute; - left: 0; - bottom: 0; - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: #eeeeee; -} - -.signin-form form .lost-your-password-wrap a::after { - width: 0; - height: 1px; - position: absolute; - left: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - bottom: 0; - content: ''; - background-color: var(--startupMainColor); -} - -.signin-form form .lost-your-password-wrap a:hover::before { - width: 0; -} - -.signin-form form .lost-your-password-wrap a:hover::after { - width: 100%; -} - -.signin-form form button { - width: 100%; - border: none; - display: block; - overflow: hidden; - margin-top: 20px; - border-radius: 5px; - text-align: center; - padding: 14px 30px; - color: var(--whiteColor); - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--startupMainColor); - font-size: var(--fontSize); - font-weight: 700; -} - -.signin-form form button:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.signin-form form .dont-account { - display: block; - margin-top: 25px; - color: var(--optionalColor); -} - -.signin-form form .dont-account a { - font-weight: 600; -} - -.signin-form .sign-in-with-button { - margin-top: 25px; -} - -.signin-form .sign-in-with-button button { - width: 100%; - display: block; - color: #248BD2; - border-radius: 5px; - border: 1px solid #D2D2D2; - background-color: transparent; - -webkit-transition: var(--transition); - transition: var(--transition); - padding-top: 13px; - padding-bottom: 13px; - padding-left: 30px; - padding-right: 30px; -} - -.signin-form .sign-in-with-button button svg { - position: absolute; - margin-left: -25px; - margin-left: -25px; - margin-top: 1.5px; -} - -.signin-form .sign-in-with-button button:hover { - color: var(--blackColor); - background-color: #E9E9E9; -} - -.signup-form { - max-width: 550px; - border-radius: 5px; - padding: 40px; - background-color: var(--whiteColor); - margin-left: auto; - margin-right: auto; -} - -.signup-form h2 { - font-size: 30px; - margin-bottom: 30px; -} - -.signup-form form .form-group { - margin-bottom: 25px; -} - -.signup-form form .form-group label { - display: block; - margin-bottom: 13px; - color: var(--blackColor); - font-weight: 600; -} - -.signup-form form .form-group .form-control { - background-color: #f5f5f5 !important; -} - -.signup-form form .remember-me-wrap { - margin-bottom: 0; -} - -.signup-form form .remember-me-wrap [type="checkbox"]:checked, .signup-form form .remember-me-wrap [type="checkbox"]:not(:checked) { - display: none; -} - -.signup-form form .remember-me-wrap [type="checkbox"]:checked + label, .signup-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label { - position: relative; - padding-left: 28px; - cursor: pointer; - line-height: 20px; - display: inline-block; - margin-bottom: 0; - color: var(--optionalColor); -} - -.signup-form form .remember-me-wrap [type="checkbox"]:checked + label:before, .signup-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label:before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 19px; - height: 19px; - -webkit-transition: all 0.2s ease; - transition: all 0.2s ease; - border: 1px solid #f5f5f5; - border-radius: 3px; - background: #f5f5f5; -} - -.signup-form form .remember-me-wrap [type="checkbox"]:checked + label:after, .signup-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label:after { - content: ''; - width: 8px; - height: 8px; - background: var(--startupMainColor); - position: absolute; - top: 5.5px; - left: 6px; - -webkit-transition: all 0.2s ease; - transition: all 0.2s ease; -} - -.signup-form form .remember-me-wrap [type="checkbox"]:not(:checked) + label:after { - opacity: 0; - -webkit-transform: scale(0); - transform: scale(0); -} - -.signup-form form .remember-me-wrap [type="checkbox"]:checked + label:after { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); -} - -.signup-form form .remember-me-wrap [type="checkbox"]:hover + label:before { - border-color: var(--startupMainColor); -} - -.signup-form form .remember-me-wrap [type="checkbox"]:checked + label:before { - border-color: var(--startupMainColor); -} - -.signup-form form .lost-your-password-wrap { - text-align: end; -} - -.signup-form form .lost-your-password-wrap a { - display: inline-block; - position: relative; - line-height: 1.5; - font-weight: 500; -} - -.signup-form form .lost-your-password-wrap a::before { - width: 100%; - height: 1px; - position: absolute; - left: 0; - bottom: 0; - content: ''; - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: #eeeeee; -} - -.signup-form form .lost-your-password-wrap a::after { - width: 0; - height: 1px; - position: absolute; - left: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - bottom: 0; - content: ''; - background-color: var(--startupMainColor); -} - -.signup-form form .lost-your-password-wrap a:hover::before { - width: 0; -} - -.signup-form form .lost-your-password-wrap a:hover::after { - width: 100%; -} - -.signup-form form button { - width: 100%; - border: none; - display: block; - overflow: hidden; - margin-top: 20px; - border-radius: 5px; - text-align: center; - padding: 14px 30px; - color: var(--whiteColor); - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--startupMainColor); - font-size: var(--fontSize); - font-weight: 700; -} - -.signup-form form button:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.signup-form form .dont-account { - display: block; - margin-top: 25px; - color: var(--optionalColor); -} - -.signup-form form .dont-account a { - font-weight: 600; -} - -.signup-form .sign-in-with-button { - margin-top: 25px; -} - -.signup-form .sign-in-with-button button { - width: 100%; - display: block; - color: #248BD2; - border-radius: 5px; - border: 1px solid #D2D2D2; - background-color: transparent; - -webkit-transition: var(--transition); - transition: var(--transition); - padding-top: 13px; - padding-bottom: 13px; - padding-left: 30px; - padding-right: 30px; -} - -.signup-form .sign-in-with-button button svg { - position: absolute; - margin-left: -25px; - margin-left: -25px; - margin-top: 1.5px; -} - -.signup-form .sign-in-with-button button:hover { - color: var(--blackColor); - background-color: #E9E9E9; -} - -/*================================================ -Instagram Area CSS -=================================================*/ -.instagram-area .container-fluid { - padding-left: 0; - padding-right: 0; -} - -.instagram-area.bg-dark .section-title { - border-color: rgba(255, 255, 255, 0.08); -} - -.instagram-area.bg-dark .section-title::before { - background-color: var(--whiteColor); -} - -.single-instagram-item { - position: relative; -} - -.single-instagram-item .link-btn { - display: block; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 2; -} - -.single-instagram-item i { - position: absolute; - left: 50%; - top: 50%; - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); - font-size: 25px; - color: var(--whiteColor); - opacity: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-instagram-item::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: #000000; - opacity: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-instagram-item:hover i { - opacity: 1; - visibility: visible; -} - -.single-instagram-item:hover::before { - opacity: 0.5; - visibility: visible; -} - -/*================================================ -Page Title Area CSS -=================================================*/ -.page-title-area { - background-position: center center; - background-repeat: no-repeat; - background-color: #f9faff; - background-size: cover; - padding-top: 160px; - padding-bottom: 170px; -} - -.page-title-area.transparent-bg1 { - background-image: url(../../assets/img/page-title/page-title-bg1.jpg); -} - -.page-title-area.transparent-bg2 { - background-image: url(../../assets/img/page-title/page-title-bg2.jpg); -} - -.page-title-area.transparent-bg3 { - background-image: url(../../assets/img/page-title/page-title-bg3.jpg); -} - -.page-title-area.transparent-bg4 { - background-image: url(../../assets/img/page-title/page-title-bg4.jpg); -} - -.page-title-area.transparent-bg5 { - background-image: url(../../assets/img/page-title/page-title-bg5.jpg); -} - -.page-title-area.bg-f9faff { - background-color: #f9faff; -} - -.page-title-area.bg-image { - background-color: var(--blackColor); - position: relative; - z-index: 1; -} - -.page-title-area.bg-image .page-title-content { - color: var(--whiteColor); -} - -.page-title-area.bg-image .page-title-content h2 { - color: var(--whiteColor); -} - -.page-title-area.bg-image .page-title-content ul li { - font-weight: 600; -} - -.page-title-area.bg-image .page-title-content ul li::before { - background-color: var(--optionalColor); -} - -.page-title-area.bg-image .page-title-content ul li a { - color: var(--whiteColor); -} - -.page-title-area.bg-image .page-title-content ul li a:hover { - color: var(--startupMainColor); -} - -.page-title-area.bg-image::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: 0.7; -} - -.page-title-area.bg1 { - background-image: url(../../assets/img/page-title/page-title-bg-image1.jpg); -} - -.page-title-area.bg2 { - background-image: url(../../assets/img/page-title/page-title-bg-image2.jpg); -} - -.page-title-area.bg3 { - background-image: url(../../assets/img/page-title/page-title-bg-image3.jpg); -} - -.page-title-area.bg4 { - background-image: url(../../assets/img/page-title/page-title-bg-image4.jpg); -} - -.page-title-area.bg5 { - background-image: url(../../assets/img/page-title/page-title-bg-image5.jpg); -} - -.page-title-area.style-two { - padding-top: 260px; - padding-bottom: 150px; -} - -.page-title-area.style-three { - padding-top: 275px; - padding-bottom: 150px; -} - -.page-title-area.style-four { - position: relative; - z-index: 1; - padding-top: 170px; - padding-bottom: 170px; -} - -.page-title-area.style-four::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-size: cover; - background-repeat: no-repeat; - background-position: center center; - background-image: url(../../assets/img/bg12.png); -} - -.page-title-content { - text-align: center; -} - -.page-title-content h2 { - margin-bottom: 0; - font-size: 45px; -} - -.page-title-content ul { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; - margin-top: 20px; -} - -.page-title-content ul li { - color: var(--startupMainColor); - display: inline-block; - position: relative; - font-weight: 500; - font-size: 16px; - margin-left: 12px; - margin-right: 12px; -} - -.page-title-content ul li::before { - content: ''; - position: absolute; - right: -18px; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - height: 1px; - width: 10px; - background-color: var(--optionalColor); - margin-top: 1px; -} - -.page-title-content ul li a { - color: var(--blackColor); -} - -.page-title-content ul li a:hover { - color: var(--startupMainColor); -} - -.page-title-content ul li:last-child { - margin-right: 0; -} - -.page-title-content ul li:last-child::before { - display: none; -} - -.page-title-content ul li:first-child { - margin-left: 0; -} - -/*================================================ -Pagination Area CSS -=================================================*/ -.pagination-area { - margin-top: 20px; -} - -.pagination-area .page-numbers { - width: 45px; - height: 45px; - line-height: 43px; - position: relative; - text-align: center; - border-radius: 50%; - display: inline-block; - border: 1px solid #eeeeee; - color: var(--blackColor); - background-color: transparent; - font-size: 18px; - font-weight: 600; - margin-left: 3px; - margin-right: 3px; -} - -.pagination-area .page-numbers:hover, .pagination-area .page-numbers.current { - color: var(--whiteColor); - background-color: var(--startupMainColor); - border-color: var(--startupMainColor) !important; -} - -/*================================================ -Gallery Area CSS -=================================================*/ -.single-gallery-item { - margin-bottom: 30px; - position: relative; - text-align: center; - overflow: hidden; -} - -.single-gallery-item img { - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-gallery-item .popup-image { - position: absolute; - left: 0; - -webkit-transform: translateY(-50%) scale(0); - transform: translateY(-50%) scale(0); - top: 50%; - z-index: 2; - right: 0; - width: 45px; - height: 45px; - line-height: 45px; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - border-radius: 3px; - margin-left: auto; - margin-right: auto; -} - -.single-gallery-item .popup-image:hover { - border-radius: 50%; - color: var(--blackColor); - background-color: var(--whiteColor); -} - -.single-gallery-item::before { - z-index: 1; - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: var(--startupMainColor); - opacity: 0; - visibility: hidden; - -webkit-transition: .3s; - transition: .3s; - margin: 10px; -} - -.single-gallery-item:hover img { - -webkit-transform: scale(1.1); - transform: scale(1.1); -} - -.single-gallery-item:hover::before { - opacity: 1; - margin: 20px; - visibility: visible; -} - -.single-gallery-item:hover .popup-image { - -webkit-transform: translateY(-50%) scale(1); - transform: translateY(-50%) scale(1); -} - -.single-gallery-item.left-overlay::before { - left: 0; - top: 0; - margin: 0; - width: 0%; - height: 100%; - right: auto; - bottom: auto; - opacity: .80; -} - -.single-gallery-item.left-overlay:hover::before { - width: 100%; -} - -.single-gallery-item.top-overlay::before { - left: 0; - top: 0; - margin: 0; - width: 100%; - height: 0%; - right: auto; - bottom: auto; - opacity: .80; -} - -.single-gallery-item.top-overlay:hover::before { - height: 100%; -} - -.shorting .mix { - display: none; -} - -.shorting-menu { - text-align: center; - margin-bottom: 40px; -} - -.shorting-menu .filter { - background-color: transparent; - -webkit-transition: var(--transition); - transition: var(--transition); - letter-spacing: 2px; - color: #b0b0b0; - border: none; - padding: 0; - font-size: var(--fontSize); - font-weight: 500; - margin-left: 20px; - margin-right: 20px; -} - -.shorting-menu .filter:last-child { - margin-right: 0; -} - -.shorting-menu .filter:first-child { - margin-left: 0; -} - -.shorting-menu .filter:hover, .shorting-menu .filter.active { - color: var(--blackColor); -} - -/*================================================ -Portfolio Details Area CSS -=================================================*/ -.portfolio-details-image { - position: relative; - margin-bottom: 30px; - text-align: center; -} - -.portfolio-details-desc h3 { - font-size: 25px; - margin-bottom: 15px; - margin-top: 30px; -} - -.portfolio-details-desc h3:first-child { - margin-top: 0; -} - -.portfolio-details-desc .image { - padding-right: 15px; - border-radius: 5px; - margin-top: 15px; - margin-bottom: 30px; -} - -.portfolio-details-desc .content { - margin-top: 15px; - margin-bottom: 30px; -} - -.portfolio-details-desc .content h3 { - margin-top: 0; - margin-bottom: 25px; -} - -.portfolio-details-desc .content ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.portfolio-details-desc .content ul li { - color: var(--optionalColor); - margin-bottom: 12px; - position: relative; - padding-left: 18px; - font-weight: 500; -} - -.portfolio-details-desc .content ul li::before { - left: 0; - top: 50%; - width: 7px; - height: 7px; - content: ''; - position: absolute; - border-radius: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - background-color: var(--startupMainColor); -} - -.portfolio-details-desc .content ul li:last-child { - margin-bottom: 0; -} - -.portfolio-details-info { - -webkit-box-shadow: 0px -10px 30px rgba(0, 0, 0, 0.1); - box-shadow: 0px -10px 30px rgba(0, 0, 0, 0.1); - border-top: 3px solid var(--startupMainColor); - background-color: var(--whiteColor); - border-radius: 5px; - padding: 40px 30px; - margin-left: 15px; -} - -.portfolio-details-info ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.portfolio-details-info ul li { - font-weight: 500; - position: relative; - margin-bottom: 20px; - color: var(--optionalColor); - border-bottom: 1px solid #eeeeee; - padding-left: 35px; - padding-bottom: 20px; -} - -.portfolio-details-info ul li span { - color: var(--blackColor); - margin-bottom: 10px; - display: block; - font-size: 19px; - font-weight: 700; -} - -.portfolio-details-info ul li a { - margin-bottom: 8px; - display: inline-block; - color: var(--optionalColor); -} - -.portfolio-details-info ul li a:last-child { - margin-bottom: 0; -} - -.portfolio-details-info ul li a:hover { - color: var(--startupMainColor); -} - -.portfolio-details-info ul li .icon { - left: 0; - top: 6px; - font-size: 25px; - position: absolute; - color: var(--startupMainColor); -} - -.portfolio-details-info ul li:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; -} - -.portfolio-details-slides.owl-theme .owl-nav { - opacity: 0; - margin-top: 0; - visibility: hidden; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.portfolio-details-slides.owl-theme .owl-nav [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--blackColor); - padding: 0 !important; - border-radius: 0; - -webkit-transition: var(--transition); - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: 25px; - top: 50%; - width: 45px; - height: 45px; - line-height: 45px; - border-radius: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - font-size: 20px; -} - -.portfolio-details-slides.owl-theme .owl-nav [class*=owl-].owl-next { - left: auto; - right: 25px; -} - -.portfolio-details-slides.owl-theme .owl-nav [class*=owl-] i { - line-height: 1; -} - -.portfolio-details-slides.owl-theme .owl-nav [class*=owl-]:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); -} - -.portfolio-details-slides.owl-theme:hover .owl-nav { - opacity: 1; - visibility: visible; -} - -.portfolio-details-slides.owl-theme .owl-dots { - margin-top: 40px; -} - -.portfolio-details-slides.owl-theme .owl-dots .owl-dot span { - width: 18px; - height: 18px; - border-radius: 50%; - -webkit-transition: var(--transition); - transition: var(--transition); - margin: 0 5px; -} - -.portfolio-details-slides.owl-theme .owl-dots .owl-dot:hover span, .portfolio-details-slides.owl-theme .owl-dots .owl-dot.active span { - background-color: var(--startupMainColor); -} - -/*================================================ -Portfolio Details Navigation Area CSS -=================================================*/ -.portfolio-details-navigation { - background-color: #f7f7f7; - padding-top: 40px; - padding-bottom: 40px; -} - -.portfolio-details-left { - text-transform: uppercase; - font-weight: 600; - line-height: 1; -} - -.portfolio-details-left a { - display: inline-block; - color: var(--optionalColor); - position: relative; - padding-left: 25px; -} - -.portfolio-details-left a i { - position: absolute; - left: 0; - top: 0; -} - -.portfolio-details-left a:hover { - color: var(--startupMainColor); -} - -.menu-icon { - line-height: 1; - font-size: 22px; - color: var(--optionalColor); -} - -.menu-icon:hover { - color: var(--startupMainColor); -} - -.portfolio-details-right { - text-transform: uppercase; - font-weight: 600; - line-height: 1; -} - -.portfolio-details-right a { - display: inline-block; - color: var(--optionalColor); - position: relative; - padding-right: 25px; -} - -.portfolio-details-right a i { - position: absolute; - right: 0; - top: 0; -} - -.portfolio-details-right a:hover { - color: var(--startupMainColor); -} - -/*================================================ -Privacy Policy Area CSS -=================================================*/ -.privacy-policy-content h3 { - font-size: 20px; - margin-bottom: 15px; -} - -.privacy-policy-content h3:not(:first-child) { - margin-top: 25px; -} - -.privacy-policy-content strong { - color: var(--blackColor) !important; -} - -.privacy-policy-content ul li { - color: var(--optionalColor); - margin-bottom: 12px; -} - -.privacy-policy-content ul li:last-child { - margin-bottom: 0; -} - -/*================================================ -Terms & Conditions Area CSS -=================================================*/ -.terms-conditions-content h3 { - font-size: 20px; - margin-bottom: 15px; -} - -.terms-conditions-content h3:not(:first-child) { - margin-top: 25px; -} - -.terms-conditions-content strong { - color: var(--blackColor) !important; -} - -.terms-conditions-content ul li { - color: var(--optionalColor); - margin-bottom: 12px; -} - -.terms-conditions-content ul li:last-child { - margin-bottom: 0; -} - -/*================================================ -FAQ Area CSS -=================================================*/ -.faq-accordion { - max-width: 800px; - margin-left: auto; - margin-right: auto; -} - -.faq-accordion .accordion-item { - margin-bottom: 15px; - border: 1px solid #eeeeee; -} - -.faq-accordion .accordion-item .accordion-button { - border: none; - padding: 20px 20px 20px 45px; - position: relative; - border: none; - -webkit-box-shadow: unset !important; - box-shadow: unset !important; - text-align: start; - margin: 0; - font-size: 18px; - font-weight: 700; -} - -.faq-accordion .accordion-item .accordion-button:not(.collapsed) { - color: var(--blackColor); - background-color: transparent; -} - -.faq-accordion .accordion-item .accordion-button:not(.collapsed)::after { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); -} - -.faq-accordion .accordion-item .accordion-button::after { - position: absolute; - left: 20px; - top: 25px; - width: 15px; - height: 15px; - background-size: 15px; -} - -.faq-accordion .accordion-item .accordion-collapse { - border: none; -} - -.faq-accordion .accordion-item .accordion-body { - padding: 0 20px 20px 45px; -} - -.faq-accordion .accordion-item .accordion-body p strong { - color: var(--blackColor); -} - -.faq-accordion .accordion-item .accordion-body ul { - margin-bottom: 15px; -} - -.faq-accordion .accordion-item .accordion-body ul li { - margin-bottom: 12px; - color: var(--optionalColor); -} - -.faq-accordion .accordion-item .accordion-body ul li:last-child { - margin-bottom: 0; -} - -.faq-accordion .accordion-item .accordion-body ul:last-child { - margin-bottom: 0; -} - -.faq-accordion .accordion-item:last-child { - margin-bottom: 0; -} - -.faq-accordion .accordion-item:first-of-type .accordion-button { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -/*================================================ -404 Error Area CSS -=================================================*/ -.error-area { - border-top: 1px dashed #eeeeee; - border-bottom: 1px dashed #eeeeee; -} - -.error-content { - text-align: center; -} - -.error-content h3 { - font-size: 30px; - margin-bottom: 20px; - margin-top: 45px; -} - -.error-content p { - max-width: 550px; - margin-left: auto; - margin-right: auto; - margin-bottom: 0; -} - -.error-content .default-btn { - margin-top: 30px; -} - -/*================================================ -Coming Soon Area CSS -=================================================*/ -.coming-soon-area { - height: 100vh; -} - -.coming-soon-image { - height: 100%; - background-image: url(../../assets/img/coming-soon.jpg); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.coming-soon-image img { - display: none; -} - -.coming-soon-content { - height: 100vh; - text-align: center; - padding-left: 30px; - padding-right: 30px; -} - -.coming-soon-content .logo { - display: inline-block; -} - -.coming-soon-content h2 { - font-size: 40px; - margin-top: 30px; - margin-bottom: 0; -} - -.coming-soon-content #timer { - margin-top: 35px; -} - -.coming-soon-content #timer div { - width: 120px; - height: 120px; - border-radius: 5px; - color: var(--blackColor); - background-color: #f8f8f8; - font-size: 35px; - font-weight: 700; - margin-left: 10px; - margin-right: 10px; -} - -.coming-soon-content #timer div span { - display: block; - margin-top: 2px; - font-size: var(--fontSize); - font-weight: 500; -} - -.coming-soon-content #timer div:first-child { - margin-left: 0; -} - -.coming-soon-content #timer div:last-child { - margin-right: 0; -} - -.coming-soon-content form { - position: relative; - max-width: 530px; - margin-left: auto; - margin-right: auto; - margin-top: 40px; -} - -.coming-soon-content form .input-newsletter { - padding: 0 0 0 15px; - width: 100%; - height: 60px; - border: none; - display: block; - border-radius: 4px; - color: var(--blackColor); - font-size: var(--fontSize); - -webkit-transition: var(--transition); - transition: var(--transition); - border: 1px solid #eeeeee; -} - -.coming-soon-content form .input-newsletter::-webkit-input-placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.coming-soon-content form .input-newsletter:-ms-input-placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.coming-soon-content form .input-newsletter::-ms-input-placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.coming-soon-content form .input-newsletter::placeholder { - color: var(--optionalColor); - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.coming-soon-content form .input-newsletter:focus { - border-color: var(--startupMainColor); -} - -.coming-soon-content form .input-newsletter:focus::-webkit-input-placeholder { - color: transparent; -} - -.coming-soon-content form .input-newsletter:focus:-ms-input-placeholder { - color: transparent; -} - -.coming-soon-content form .input-newsletter:focus::-ms-input-placeholder { - color: transparent; -} - -.coming-soon-content form .input-newsletter:focus::placeholder { - color: transparent; -} - -.coming-soon-content form .default-btn { - position: absolute; - right: 5px; - -webkit-box-shadow: unset; - box-shadow: unset; - bottom: 6px; - padding-left: 35px; - padding-right: 35px; -} - -.coming-soon-content form .default-btn:hover { - background-color: var(--blackColor); - color: var(--whiteColor); -} - -.coming-soon-content form #validator-newsletter { - position: absolute; - bottom: -32px; - left: 0; -} - -.coming-soon-content form #validator-newsletter.validation-danger { - color: red; -} - -/*================================================ -Contact Area CSS -=================================================*/ -.contact-area { - position: relative; - z-index: 1; -} - -.contact-area.bg-image { - background-image: url(../../assets/img/cta-bg1.jpg); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -.contact-area.bg-image::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: 0.8; -} - -.contact-area.bg-image .section-title h2 { - color: var(--whiteColor); -} - -.single-contact-info-box { - padding: 35px; - margin-bottom: 30px; - background-color: #F9FAFB; -} - -.single-contact-info-box .icon { - line-height: 1; - font-size: 40px; - margin-bottom: 25px; - color: var(--startupMainColor); -} - -.single-contact-info-box h3 { - font-size: 20px; - margin-bottom: 15px; -} - -.single-contact-info-box p { - margin-bottom: 2px; -} - -.single-contact-info-box p a { - color: var(--optionalColor); - display: inline-block; -} - -.single-contact-info-box p a:hover { - color: var(--startupMainColor); -} - -.single-contact-info-box p:last-child { - margin-bottom: 0; -} - -.single-contact-info-box.style-two { - padding: 0; - text-align: center; - background-color: transparent; -} - -.single-contact-info-box.style-two .icon { - width: 80px; - color: var(--whiteColor); - background-color: var(--blackColor); - border-radius: 50%; - position: relative; - height: 80px; - font-size: 35px; - -webkit-transition: var(--transition); - transition: var(--transition); - margin-left: auto; - margin-right: auto; - margin-bottom: 25px; -} - -.single-contact-info-box.style-two .icon i { - position: absolute; - left: 0; - right: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.single-contact-info-box.style-two:hover .icon { - background-color: var(--startupMainColor); - color: var(--whiteColor); -} - -.contact-form { - padding: 34px; - margin-bottom: 30px; - background-color: var(--whiteColor); - -webkit-box-shadow: 0 0 1.25rem rgba(108, 118, 134, 0.1); - box-shadow: 0 0 1.25rem rgba(108, 118, 134, 0.1); -} - -.contact-form form .form-group { - margin-bottom: 20px; -} - -.contact-form form .form-group label { - display: block; - margin-bottom: 10px; - color: var(--optionalColor); -} - -.contact-form form .form-group .form-control { - border-radius: 0; - background-color: #f9faff; - border: 1px solid #eeeeee !important; -} - -.contact-form form .form-check { - margin-bottom: 15px; - margin-top: 5px; -} - -.contact-form form .form-check a { - color: var(--startupMainColor); -} - -.contact-form form .form-check a:hover { - color: var(--blackColor); - text-decoration: underline; -} - -.contact-form form .default-btn { - margin-top: 5px; - padding-left: 35px; - padding-right: 35px; -} - -.contact-form form .help-block.with-errors { - color: red; - margin-bottom: 0; - margin-top: 15px; -} - -.contact-form form #msgSubmit { - margin: 0; -} - -.contact-form form #msgSubmit.text-danger { - margin-top: 15px; -} - -#maps iframe { - margin-bottom: -7px; - height: 500px; - border: none; - width: 100%; -} - -/*================================================ -Footer Area CSS -=================================================*/ -.footer-area { - padding-top: 100px; - position: relative; - z-index: 1; -} - -.footer-area.marketing-footer { - border-top: 1px solid #eeeeee; -} - -.footer-area.marketing-footer .footer-top { - border-bottom: none; - margin-bottom: 0; -} - -.footer-area.marketing-footer .copyright-area { - border-top: 1px solid #eeeeee; -} - -.footer-area.marketing-footer .copyright-area p a { - color: var(--marketingMainColor); -} - -.footer-area.marketing-footer .single-footer-widget .footer-contact li i { - color: var(--marketingMainColor); -} - -.footer-area.marketing-footer .single-footer-widget .footer-contact li a:hover { - color: var(--marketingMainColor); -} - -.footer-area.marketing-footer .single-footer-widget .info-links li a:hover { - color: var(--marketingMainColor); -} - -.footer-area.marketing-footer .single-footer-widget .newsletter-form button:hover { - color: var(--whiteColor); - background-color: var(--marketingMainColor); -} - -.footer-area.business-footer { - padding-top: 0; -} - -.footer-area.business-footer .footer-top { - background: var(--businessGradientColor); - margin-bottom: 100px; - border: none; - padding-top: 70px; - padding-bottom: 70px; -} - -.footer-area.business-footer .footer-top .content h3 { - color: var(--whiteColor); -} - -.footer-area.business-footer .footer-top .content .default-btn { - background: transparent; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.footer-area.business-footer .footer-top .content .default-btn i { - color: var(--whiteColor); -} - -.footer-area.business-footer .footer-top .content .default-btn:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.footer-area.business-footer .footer-top .content .default-btn:hover i { - color: var(--blackColor); -} - -.footer-area.business-footer .copyright-area { - border-top: 1px solid #eeeeee; -} - -.footer-area.business-footer .copyright-area p a { - color: var(--businessMainColor); -} - -.footer-area.business-footer .single-footer-widget .footer-contact li i { - color: var(--businessMainColor); -} - -.footer-area.business-footer .single-footer-widget .footer-contact li a:hover { - color: var(--businessMainColor); -} - -.footer-area.business-footer .single-footer-widget .info-links li a:hover { - color: var(--businessMainColor); -} - -.footer-area.business-footer .single-footer-widget .newsletter-form button:hover { - color: var(--whiteColor); - background-color: var(--businessMainColor); -} - -.footer-area.app-footer { - background: var(--appGradientColor); -} - -.footer-area.app-footer .footer-top { - border-color: #0e49b5; -} - -.footer-area.app-footer .footer-top .content h3 { - color: var(--whiteColor); -} - -.footer-area.app-footer .footer-top .content .default-btn { - background: transparent; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.footer-area.app-footer .footer-top .content .default-btn i { - color: var(--whiteColor); -} - -.footer-area.app-footer .footer-top .content .default-btn:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.footer-area.app-footer .footer-top .content .default-btn:hover i { - color: var(--blackColor); -} - -.footer-area.app-footer .copyright-area { - opacity: 0.9; - border-top: 1px solid #2c4687; - background: transparent; -} - -.footer-area.app-footer .copyright-area p { - color: var(--whiteColor); -} - -.footer-area.app-footer .copyright-area p a { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget h3 { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget p { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .footer-contact li { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .footer-contact li i { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .footer-contact li a { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .footer-contact li a:hover { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .info-links li { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .info-links li a { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .info-links li a:hover { - color: var(--whiteColor); -} - -.footer-area.app-footer .single-footer-widget .newsletter-form button { - border-left: 1px solid var(--blackColor); -} - -.footer-area.app-footer .single-footer-widget .newsletter-form button:hover { - color: var(--blackColor); - border-color: #eeeeee; - background-color: var(--whiteColor); -} - -.footer-area.saas-footer { - background: #000000; -} - -.footer-area.saas-footer .footer-top { - border-color: #121419; -} - -.footer-area.saas-footer .footer-top .content h3 { - color: var(--whiteColor); -} - -.footer-area.saas-footer .footer-top .content .default-btn { - background: transparent; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.footer-area.saas-footer .footer-top .content .default-btn i { - color: var(--whiteColor); -} - -.footer-area.saas-footer .footer-top .content .default-btn:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.footer-area.saas-footer .footer-top .content .default-btn:hover i { - color: var(--blackColor); -} - -.footer-area.saas-footer .copyright-area { - border-top: 1px solid #121419; - background: transparent; -} - -.footer-area.saas-footer .copyright-area p { - color: #959595; -} - -.footer-area.saas-footer .copyright-area p a { - color: var(--whiteColor); -} - -.footer-area.saas-footer .single-footer-widget h3 { - color: var(--whiteColor); -} - -.footer-area.saas-footer .single-footer-widget p { - color: var(--whiteColor); -} - -.footer-area.saas-footer .single-footer-widget .footer-contact li { - color: var(--whiteColor); -} - -.footer-area.saas-footer .single-footer-widget .footer-contact li i { - color: var(--saasMainColor); -} - -.footer-area.saas-footer .single-footer-widget .footer-contact li a { - color: var(--whiteColor); -} - -.footer-area.saas-footer .single-footer-widget .footer-contact li a:hover { - color: var(--saasMainColor); -} - -.footer-area.saas-footer .single-footer-widget .info-links li { - color: var(--whiteColor); -} - -.footer-area.saas-footer .single-footer-widget .info-links li a { - color: var(--whiteColor); -} - -.footer-area.saas-footer .single-footer-widget .info-links li a:hover { - color: var(--saasMainColor); -} - -.footer-area.saas-footer .single-footer-widget .newsletter-form button { - background: var(--saasGradientColor); -} - -.footer-area.personal-portfolio-footer { - border-top: 1px solid #eeeeee; -} - -.footer-area.personal-portfolio-footer .single-footer-widget .footer-contact li i { - color: var(--personalPortfolioMainColor); -} - -.footer-area.personal-portfolio-footer .single-footer-widget .footer-contact li a:hover { - color: var(--personalPortfolioMainColor); -} - -.footer-area.personal-portfolio-footer .single-footer-widget .info-links li a:hover { - color: var(--personalPortfolioMainColor); -} - -.footer-area.personal-portfolio-footer .single-footer-widget .newsletter-form button:hover { - color: var(--whiteColor); - background-color: var(--personalPortfolioMainColor); -} - -.footer-area.personal-portfolio-footer .copyright-area { - border-top: none; - background: #06080e; -} - -.footer-area.personal-portfolio-footer .copyright-area p { - color: #959595; -} - -.footer-area.personal-portfolio-footer .copyright-area p a { - color: var(--whiteColor); -} - -.footer-area.consulting-footer { - overflow: hidden; - background-color: #f3f4f7; -} - -.footer-area.consulting-footer .single-footer-widget .footer-contact li i { - color: var(--consultingMainColor); -} - -.footer-area.consulting-footer .single-footer-widget .footer-contact li a:hover { - color: var(--consultingMainColor); -} - -.footer-area.consulting-footer .single-footer-widget .info-links li a:hover { - color: var(--consultingMainColor); -} - -.footer-area.consulting-footer .single-footer-widget .newsletter-form button:hover { - background-color: var(--consultingMainColor); -} - -.footer-area.consulting-footer .copyright-area { - background-color: transparent; - border-top: 1px solid #e4eaf5; -} - -.footer-area.consulting-footer .copyright-area p a { - color: var(--blackColor); -} - -.footer-area.consulting-footer .copyright-area p a:hover { - color: var(--consultingMainColor); -} - -.footer-area.it-agency-footer { - background-color: #111111; -} - -.footer-area.it-agency-footer .footer-top { - border-color: #272727; -} - -.footer-area.it-agency-footer .footer-top .content h3 { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .footer-top .content .default-btn { - border: 1px solid var(--whiteColor); - color: var(--whiteColor); - background: transparent; - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.footer-area.it-agency-footer .footer-top .content .default-btn i { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .footer-top .content .default-btn:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.footer-area.it-agency-footer .footer-top .content .default-btn:hover i { - color: var(--blackColor); -} - -.footer-area.it-agency-footer .single-footer-widget h3 { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .single-footer-widget p { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .single-footer-widget .footer-contact li { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .single-footer-widget .footer-contact li i { - color: var(--itAgencyMainColor); -} - -.footer-area.it-agency-footer .single-footer-widget .footer-contact li a { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .single-footer-widget .footer-contact li a:hover { - color: var(--itAgencyMainColor); -} - -.footer-area.it-agency-footer .single-footer-widget .info-links li { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .single-footer-widget .info-links li a { - color: var(--whiteColor); -} - -.footer-area.it-agency-footer .single-footer-widget .info-links li a:hover { - color: var(--itAgencyMainColor); -} - -.footer-area.it-agency-footer .single-footer-widget .newsletter-form button { - background: var(--itAgencyGradientColor); -} - -.footer-area.it-agency-footer .copyright-area { - border-top: 1px solid #272727; - background-color: transparent; -} - -.footer-area.it-agency-footer .copyright-area p { - color: #c8c5c5; -} - -.footer-area.it-agency-footer .copyright-area p a { - color: var(--whiteColor); -} - -.footer-area.bg-dark .footer-top { - border-color: rgba(255, 255, 255, 0.07); -} - -.footer-area.bg-dark .footer-top .content h3 { - color: var(--whiteColor); -} - -.footer-area.bg-dark .footer-top .content .default-btn { - border: 1px solid var(--whiteColor); - color: var(--whiteColor); - background: transparent; - -webkit-box-shadow: unset; - box-shadow: unset; -} - -.footer-area.bg-dark .footer-top .content .default-btn i { - color: var(--whiteColor); -} - -.footer-area.bg-dark .footer-top .content .default-btn:hover { - background-color: var(--whiteColor); - color: var(--blackColor); -} - -.footer-area.bg-dark .footer-top .content .default-btn:hover i { - color: var(--blackColor); -} - -.footer-area.bg-dark .single-footer-widget { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget h3 { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget p { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .footer-contact li { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .footer-contact li i { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .footer-contact li a { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .footer-contact li a:hover { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .info-links li { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .info-links li a { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .info-links li a:hover { - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .newsletter-form button { - background-color: var(--optionalColor); - color: var(--whiteColor); -} - -.footer-area.bg-dark .single-footer-widget .newsletter-form button:hover { - color: var(--whiteColor); - background-color: var(--blackColor); -} - -.footer-area.bg-dark .copyright-area { - background-color: #06080e; - border-top: 1px solid #292929; -} - -.footer-area.bg-dark .copyright-area p { - color: #c6c6c6; -} - -.footer-area.bg-dark .copyright-area p a { - font-weight: 800; - color: var(--whiteColor); -} - -.footer-area.bg-dark .copyright-area .social-links li a { - color: var(--whiteColor); -} - -.footer-area.minimal-portfolio-color { - padding: 0; -} - -.footer-area.minimal-portfolio-color .copyright-area { - margin: 0; - padding-top: 50px; - padding-bottom: 50px; -} - -.footer-area.minimal-portfolio-color .copyright-area p a { - color: var(--minimalPortfolioColor); -} - -.footer-top { - border-bottom: 1px solid #eeeeee; - padding-bottom: 70px; - margin-bottom: 70px; -} - -.footer-top .logo { - display: inline-block; -} - -.footer-top .content h3 { - font-size: 20px; - margin-bottom: 0; -} - -.single-footer-widget { - margin-bottom: 30px; -} - -.single-footer-widget h3 { - font-size: 20px; - margin-bottom: 20px; -} - -.single-footer-widget .footer-contact { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.single-footer-widget .footer-contact li { - position: relative; - margin-bottom: 12px; - padding-left: 24px; - color: var(--optionalColor); -} - -.single-footer-widget .footer-contact li i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); -} - -.single-footer-widget .footer-contact li a { - display: inline-block; - color: var(--optionalColor); -} - -.single-footer-widget .footer-contact li a:hover { - color: var(--startupMainColor); -} - -.single-footer-widget .footer-contact li:last-child { - margin-bottom: 0; -} - -.single-footer-widget .info-links { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; -} - -.single-footer-widget .info-links li { - margin-bottom: 12px; -} - -.single-footer-widget .info-links li a { - display: inline-block; - color: var(--optionalColor); -} - -.single-footer-widget .info-links li a:hover { - color: var(--startupMainColor); -} - -.single-footer-widget .info-links li:last-child { - margin-bottom: 0; -} - -.single-footer-widget .newsletter-form { - position: relative; -} - -.single-footer-widget .newsletter-form .input-newsletter { - display: block; - width: 100%; - height: 50px; - border: 1px solid #eeeeee; - background-color: var(--whiteColor); - color: var(--blackColor); - padding-left: 15px; -} - -.single-footer-widget .newsletter-form .input-newsletter::-webkit-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.single-footer-widget .newsletter-form .input-newsletter:-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.single-footer-widget .newsletter-form .input-newsletter::-ms-input-placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.single-footer-widget .newsletter-form .input-newsletter::placeholder { - -webkit-transition: var(--transition); - transition: var(--transition); - color: var(--optionalColor); -} - -.single-footer-widget .newsletter-form .input-newsletter:focus::-webkit-input-placeholder { - color: transparent; -} - -.single-footer-widget .newsletter-form .input-newsletter:focus:-ms-input-placeholder { - color: transparent; -} - -.single-footer-widget .newsletter-form .input-newsletter:focus::-ms-input-placeholder { - color: transparent; -} - -.single-footer-widget .newsletter-form .input-newsletter:focus::placeholder { - color: transparent; -} - -.single-footer-widget .newsletter-form button { - position: absolute; - right: 0; - top: 0; - height: 50px; - width: 60px; - background-color: var(--blackColor); - color: var(--whiteColor); - border: none; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.single-footer-widget .newsletter-form button:hover { - color: var(--whiteColor); - background-color: var(--startupMainColor); -} - -.single-footer-widget .newsletter-form #validator-newsletter { - position: absolute; - bottom: -32px; - left: 0; -} - -.single-footer-widget .newsletter-form #validator-newsletter.validation-danger { - color: red; -} - -.single-footer-widget.pl-4 { - padding-left: 4rem; -} - -.single-footer-widget.pl-2 { - padding-left: 2rem; -} - -.single-footer-widget .social-links { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; -} - -.single-footer-widget .social-links li { - display: inline-block; - margin-right: 12px; -} - -.single-footer-widget .social-links li a { - display: inline-block; - font-size: 18px; -} - -.single-footer-widget .social-links li:last-child { - margin-right: 0; -} - -.single-footer-widget .logo { - display: inline-block; - margin-bottom: 15px; -} - -.single-footer-widget.style-two h3 { - position: relative; - padding-left: 55px; -} - -.single-footer-widget.style-two h3::before { - width: 40px; - height: 1px; - background-color: var(--personalPortfolioMainColor); - content: ''; - position: absolute; - left: 0; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.copyright-area { - background-color: var(--whiteColor); - text-align: center; - margin-top: 70px; - padding-top: 25px; - padding-bottom: 25px; -} - -.copyright-area p a { - font-weight: 600; - display: inline-block; - color: var(--startupMainColor); -} - -.copyright-area .social-links { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; - text-align: end; -} - -.copyright-area .social-links li { - display: inline-block; - margin-right: 12px; -} - -.copyright-area .social-links li a { - display: inline-block; - font-size: 18px; -} - -.copyright-area .social-links li:last-child { - margin-right: 0; -} - -.copyright-area.bg-faf7f2 { - background-color: #faf7f2; -} - -/*================================================ -Go Top CSS -=================================================*/ -.go-top { - position: fixed; - cursor: pointer; - bottom: 20px; - right: 20px; - z-index: 4; - width: 43px; - height: 45px; - opacity: 0; - font-size: 20px; - visibility: hidden; - text-align: center; - color: var(--whiteColor); - -webkit-transition: var(--transition); - transition: var(--transition); - background-color: var(--startupMainColor); - -webkit-box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.1); -} - -.go-top i { - right: 0; - left: 0; - top: 50%; - text-align: center; - position: absolute; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - margin-left: auto; - margin-right: auto; -} - -.go-top.active { - opacity: 1; - visibility: visible; - bottom: 20px; -} - -.go-top:hover { - color: var(--whiteColor); - background-color: var(--blackColor) !important; -} - -.go-top.marketing-color { - background-color: var(--marketingMainColor); -} - -.go-top.business-color { - background: var(--businessGradientColor); -} - -.go-top.app-color { - background: var(--appGradientColor); -} - -.go-top.saas-color { - background: var(--saasGradientColor); -} - -.go-top.personal-portfolio-color { - background-color: var(--personalPortfolioMainColor); -} - -.go-top.consulting-color { - background-color: var(--consultingMainColor); -} - -.go-top.it-agency-color { - background: var(--itAgencyGradientColor); -} - -.go-top.shop-color { - background: var(--shopMainColor); -} - -.go-top.minimal-portfolio-color { - background: var(--minimalPortfolioColor); -} - -/*extra-css*/ -.shape1 { - position: absolute; - right: 10%; - top: 5%; - z-index: -1; - -webkit-animation: moveleftbounce 5s linear infinite; - animation: moveleftbounce 5s linear infinite; -} - -.shape2 { - position: absolute; - right: 3%; - bottom: 12%; - z-index: -1; - -webkit-animation-duration: 20s; - animation-duration: 20s; - -webkit-animation-name: rotateme; - animation-name: rotateme; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.shape3 { - position: absolute; - right: 33.6%; - bottom: 7%; - z-index: -1; -} - -.shape4 { - position: absolute; - top: 85px; - left: 20px; - z-index: -1; - -webkit-animation: movebounce 5s linear infinite; - animation: movebounce 5s linear infinite; -} - -.shape5 { - position: absolute; - bottom: 40px; - right: 20px; - z-index: -1; - -webkit-animation: moveleftbounce 5s linear infinite; - animation: moveleftbounce 5s linear infinite; -} - -.shape6 { - position: absolute; - left: 40px; - top: 45px; - -webkit-animation-duration: 20s; - animation-duration: 20s; - -webkit-animation-name: rotateme; - animation-name: rotateme; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.shape7 { - position: absolute; - left: 30px; - top: 20px; - z-index: -1; - -webkit-animation-duration: 20s; - animation-duration: 20s; - -webkit-animation-name: rotateme; - animation-name: rotateme; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.shape8 { - position: absolute; - bottom: 50px; - left: 50px; - z-index: -1; - -webkit-animation-duration: 30s; - animation-duration: 30s; - -webkit-animation-name: rotateme; - animation-name: rotateme; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.shape9 { - position: absolute; - right: 0; - top: 65px; - z-index: -1; - -webkit-animation: moveleftbounce 5s linear infinite; - animation: moveleftbounce 5s linear infinite; -} - -.shape10 { - position: absolute; - left: 0; - top: 0; - z-index: -1; -} - -.shape11 { - position: absolute; - top: 8%; - left: 0; - z-index: -1; - -webkit-animation-duration: 40s; - animation-duration: 40s; - -webkit-animation-name: rotateme2; - animation-name: rotateme2; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.shape12 { - position: absolute; - right: 20px; - bottom: 20px; - z-index: -1; - -webkit-animation-duration: 30s; - animation-duration: 30s; - -webkit-animation-name: rotateme; - animation-name: rotateme; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.shape13 { - position: absolute; - right: 12%; - bottom: 2%; - z-index: -1; - -webkit-animation-duration: 30s; - animation-duration: 30s; - -webkit-animation-name: rotateme; - animation-name: rotateme; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.shape14 { - position: absolute; - left: 0; - bottom: 76px; - z-index: -1; - opacity: 0.23; -} - -.shape15 { - position: absolute; - left: 50%; - top: 50%; - -webkit-transform: translateY(-50%) translateX(-50%); - transform: translateY(-50%) translateX(-50%); - z-index: -1; -} - -.shape15 img { - -webkit-animation: movescale 5s linear infinite; - animation: movescale 5s linear infinite; -} - -.shape16 { - position: absolute; - right: 5px; - top: 0; - z-index: -1; - -webkit-animation: moveleftbounce 5s linear infinite; - animation: moveleftbounce 5s linear infinite; -} - -.shape17 { - left: 0; - bottom: 98px; - z-index: -1; - opacity: .05; - position: absolute; -} - -.shape18 { - position: absolute; - right: 0; - bottom: 0; - z-index: -1; -} - -.shape19 { - position: absolute; - right: 35%; - top: 12%; - z-index: -1; - -webkit-animation-duration: 30s; - animation-duration: 30s; - -webkit-animation-name: rotateme; - animation-name: rotateme; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.divider { - width: 100%; - position: absolute; - height: 100px; - pointer-events: none; - -webkit-mask-image: url("data:image/svg+xml;utf8,%3csvg viewBox='0 0 100 100' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'%3e%3cpath d='M0,0 C16.6666667,66 33.3333333,98 50,98 C66.6666667,98 83.3333333,66 100,0 L100,100 L0,100 L0,0 Z' fill='%23fff'/%3e%3c/svg%3e"); - mask-image: url("data:image/svg+xml;utf8,%3csvg viewBox='0 0 100 100' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'%3e%3cpath d='M0,0 C16.6666667,66 33.3333333,98 50,98 C66.6666667,98 83.3333333,66 100,0 L100,100 L0,100 L0,0 Z' fill='%23fff'/%3e%3c/svg%3e"); - -webkit-mask-size: 100% 101%; - mask-size: 100% 101%; - background-color: var(--whiteColor); - bottom: 0; - left: 0; - z-index: -1; -} - -@-webkit-keyframes border-transform { - 0%, 100% { - border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%; - } - 14% { - border-radius: 40% 60% 54% 46% / 49% 60% 40% 51%; - } - 28% { - border-radius: 54% 46% 38% 62% / 49% 70% 30% 51%; - } - 42% { - border-radius: 61% 39% 55% 45% / 61% 38% 62% 39%; - } - 56% { - border-radius: 61% 39% 67% 33% / 70% 50% 50% 30%; - } - 70% { - border-radius: 50% 50% 34% 66% / 56% 68% 32% 44%; - } - 84% { - border-radius: 46% 54% 50% 50% / 35% 61% 39% 65%; - } -} - -@keyframes border-transform { - 0%, 100% { - border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%; - } - 14% { - border-radius: 40% 60% 54% 46% / 49% 60% 40% 51%; - } - 28% { - border-radius: 54% 46% 38% 62% / 49% 70% 30% 51%; - } - 42% { - border-radius: 61% 39% 55% 45% / 61% 38% 62% 39%; - } - 56% { - border-radius: 61% 39% 67% 33% / 70% 50% 50% 30%; - } - 70% { - border-radius: 50% 50% 34% 66% / 56% 68% 32% 44%; - } - 84% { - border-radius: 46% 54% 50% 50% / 35% 61% 39% 65%; - } -} - -@-webkit-keyframes rotateme { - from { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - to { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} - -@keyframes rotateme { - from { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - to { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} - -@-webkit-keyframes rotateme2 { - from { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - to { - -webkit-transform: rotate(-360deg); - transform: rotate(-360deg); - } -} - -@keyframes rotateme2 { - from { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - to { - -webkit-transform: rotate(-360deg); - transform: rotate(-360deg); - } -} - -@-webkit-keyframes movebounce { - 0% { - -webkit-transform: translateY(0px); - transform: translateY(0px); - } - 50% { - -webkit-transform: translateY(20px); - transform: translateY(20px); - } - 100% { - -webkit-transform: translateY(0px); - transform: translateY(0px); - } -} - -@keyframes movebounce { - 0% { - -webkit-transform: translateY(0px); - transform: translateY(0px); - } - 50% { - -webkit-transform: translateY(20px); - transform: translateY(20px); - } - 100% { - -webkit-transform: translateY(0px); - transform: translateY(0px); - } -} - -@-webkit-keyframes moveleftbounce { - 0% { - -webkit-transform: translateX(0px); - transform: translateX(0px); - } - 50% { - -webkit-transform: translateX(-20px); - transform: translateX(-20px); - } - 100% { - -webkit-transform: translateX(0px); - transform: translateX(0px); - } -} - -@keyframes moveleftbounce { - 0% { - -webkit-transform: translateX(0px); - transform: translateX(0px); - } - 50% { - -webkit-transform: translateX(-20px); - transform: translateX(-20px); - } - 100% { - -webkit-transform: translateX(0px); - transform: translateX(0px); - } -} - -@-webkit-keyframes scroller { - 0% { - bottom: 34px; - } - 5% { - top: 4px; - } - 32% { - bottom: 4px; - } - 66% { - top: 34px; - bottom: 4px; - } - 100% { - top: 4px; - bottom: 34px; - } -} - -@keyframes scroller { - 0% { - bottom: 34px; - } - 5% { - top: 4px; - } - 32% { - bottom: 4px; - } - 66% { - top: 34px; - bottom: 4px; - } - 100% { - top: 4px; - bottom: 34px; - } -} - -@-webkit-keyframes movescale { - 0% { - -webkit-transform: scale(0.9); - transform: scale(0.9); - } - 50% { - -webkit-transform: scale(1); - transform: scale(1); - } - 100% { - -webkit-transform: scale(0.9); - transform: scale(0.9); - } -} - -@keyframes movescale { - 0% { - -webkit-transform: scale(0.9); - transform: scale(0.9); - } - 50% { - -webkit-transform: scale(1); - transform: scale(1); - } - 100% { - -webkit-transform: scale(0.9); - transform: scale(0.9); - } -} - -@-webkit-keyframes rotate3d { - 0% { - -webkit-transform: rotateY(0deg); - transform: rotateY(0deg); - } - 100% { - -webkit-transform: rotateY(360deg); - transform: rotateY(360deg); - } -} - -@keyframes rotate3d { - 0% { - -webkit-transform: rotateY(0deg); - transform: rotateY(0deg); - } - 100% { - -webkit-transform: rotateY(360deg); - transform: rotateY(360deg); - } -} - -@-webkit-keyframes animationFramesOne { - 0% { - -webkit-transform: translate(0px, 0px) rotate(0deg); - transform: translate(0px, 0px) rotate(0deg); - } - 20% { - -webkit-transform: translate(73px, -1px) rotate(36deg); - transform: translate(73px, -1px) rotate(36deg); - } - 40% { - -webkit-transform: translate(141px, 72px) rotate(72deg); - transform: translate(141px, 72px) rotate(72deg); - } - 60% { - -webkit-transform: translate(83px, 122px) rotate(108deg); - transform: translate(83px, 122px) rotate(108deg); - } - 80% { - -webkit-transform: translate(-40px, 72px) rotate(144deg); - transform: translate(-40px, 72px) rotate(144deg); - } - 100% { - -webkit-transform: translate(0px, 0px) rotate(0deg); - transform: translate(0px, 0px) rotate(0deg); - } -} - -@keyframes animationFramesOne { - 0% { - -webkit-transform: translate(0px, 0px) rotate(0deg); - transform: translate(0px, 0px) rotate(0deg); - } - 20% { - -webkit-transform: translate(73px, -1px) rotate(36deg); - transform: translate(73px, -1px) rotate(36deg); - } - 40% { - -webkit-transform: translate(141px, 72px) rotate(72deg); - transform: translate(141px, 72px) rotate(72deg); - } - 60% { - -webkit-transform: translate(83px, 122px) rotate(108deg); - transform: translate(83px, 122px) rotate(108deg); - } - 80% { - -webkit-transform: translate(-40px, 72px) rotate(144deg); - transform: translate(-40px, 72px) rotate(144deg); - } - 100% { - -webkit-transform: translate(0px, 0px) rotate(0deg); - transform: translate(0px, 0px) rotate(0deg); - } -} - -@-webkit-keyframes ripple { - 0% { - -webkit-transform: scale(1); - transform: scale(1); - } - 75% { - -webkit-transform: scale(1.75); - transform: scale(1.75); - opacity: 1; - } - 100% { - -webkit-transform: scale(2); - transform: scale(2); - opacity: 0; - } -} - -@keyframes ripple { - 0% { - -webkit-transform: scale(1); - transform: scale(1); - } - 75% { - -webkit-transform: scale(1.75); - transform: scale(1.75); - opacity: 1; - } - 100% { - -webkit-transform: scale(2); - transform: scale(2); - opacity: 0; - } -} - -.owl-item { - overflow: hidden; -} - -.owl-item.active .startup-works-image { - -webkit-animation-duration: .8s; - animation-duration: .8s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; - -webkit-animation-name: fadeInRight; - animation-name: fadeInRight; - -webkit-animation-delay: .5s; - animation-delay: .5s; -} - -.owl-item.active .startup-works-image img { - display: inline-block !important; - width: auto !important; -} - -.owl-item.active .startup-testimonials-image { - overflow: hidden; -} - -.owl-item.active .startup-testimonials-image .main-image { - -webkit-animation-duration: .8s; - animation-duration: .8s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; - -webkit-animation-name: fadeInRight; - animation-name: fadeInRight; - -webkit-animation-delay: .5s; - animation-delay: .5s; -} - -/*================================================ -Modal CSS -=================================================*/ -.productsQuickView { - padding-right: 0 !important; -} - -.productsQuickView .modal-dialog { - max-width: 900px; - margin: 0 auto; -} - -.productsQuickView .modal-content { - border: none; - padding: 40px; - border-radius: 0; -} - -.productsQuickView .modal-content button.btn-close { - top: 0; - right: 0; - margin: 0; - z-index: 3; - outline: 0; - opacity: 1; - padding: 0; - width: 35px; - height: 35px; - border: none; - font-size: 17px; - line-height: 38px; - -webkit-box-shadow: unset; - box-shadow: unset; - text-shadow: unset; - position: absolute; -} - -.productsQuickView .modal-content .products-content h3 { - margin-bottom: 15px; - font-size: 20px; -} - -.productsQuickView .modal-content .products-content .price { - color: var(--blackColor); - margin-top: 15px; - margin-bottom: 12px; - font-weight: 700; - font-size: 17px; -} - -.productsQuickView .modal-content .products-content .price .old-price { - text-decoration: line-through; - color: #9b9494; - font-weight: normal; -} - -.productsQuickView .modal-content .products-content .products-review .rating { - display: inline-block; - padding-right: 3px; - font-size: 14px; -} - -.productsQuickView .modal-content .products-content .products-review .rating i { - display: inline-block; - color: #f49f0b; - line-height: 1; -} - -.productsQuickView .modal-content .products-content .products-review .rating-count { - display: inline-block; - color: var(--optionalColor); - line-height: initial; - position: relative; - top: 1px; -} - -.productsQuickView .modal-content .products-content .products-review .rating-count:hover { - color: var(--blackColor); -} - -.productsQuickView .modal-content .products-content .products-add-to-cart { - margin-top: 25px; -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .quantities { - position: relative; - padding-left: 45px; -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .quantities .sub-title { - left: 0; - top: 50%; - display: block; - font-weight: 600; - position: absolute; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter { - display: block; - max-width: 120px; - min-width: 120px; - text-align: center; - position: relative; - margin-bottom: 25px; -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter span { - top: 0; - width: 35px; - height: 100%; - cursor: pointer; - font-size: 15px; - line-height: 38px; - position: absolute; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - -webkit-transition: var(--transition); - transition: var(--transition); -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter span i { - position: relative; - top: 1px; -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter span.minus-btn { - left: 0; -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter span.plus-btn { - right: 0; -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter span:hover { - color: var(--optionalColor); -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter input { - height: 40px; - outline: 0; - width: 100%; - display: block; - text-align: center; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - font-size: 17px; - font-weight: 600; -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter input::-webkit-input-placeholder { - color: var(--blackColor); -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter input:-ms-input-placeholder { - color: var(--blackColor); -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter input::-ms-input-placeholder { - color: var(--blackColor); -} - -.productsQuickView .modal-content .products-content .products-add-to-cart .input-counter input::placeholder { - color: var(--blackColor); -} - -.productsQuickView .modal-content .products-content .default-btn { - -webkit-box-shadow: unset !important; - box-shadow: unset !important; -} - -.productsQuickView .modal-content .products-content .social-share { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; - margin-top: 30px; -} - -.productsQuickView .modal-content .products-content .social-share li { - display: inline-block; - margin-right: 12px; -} - -.productsQuickView .modal-content .products-content .social-share li span { - font-weight: 600; - position: relative; - top: -1px; -} - -.productsQuickView .modal-content .products-content .social-share li a { - line-height: 1; - font-size: 18px; - color: var(--optionalColor); -} - -.productsQuickView .modal-content .products-content .social-share li a:hover { - color: var(--blackColor); -} -/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/static/assets/css/style.css.map b/static/assets/css/style.css.map deleted file mode 100644 index e69de29b..00000000 diff --git a/static/assets/css/style.scss b/static/assets/css/style.scss deleted file mode 100644 index d007e8b3..00000000 --- a/static/assets/css/style.scss +++ /dev/null @@ -1,16505 +0,0 @@ -/* -@File: Noke Template Styles - -* This file contains the styling for the actual template, this -is the file you need to edit to change the look of the -template. - -This files table contents are outlined below>>>>> - -******************************************* -******************************************* - -** - Default CSS -** - Top Header Area CSS -** - Navbar Area CSS -** - Search Overlay CSS -** - Startup Banner Area CSS -** - Marketing Banner Area CSS -** - Business Banner Area CSS -** - App Banner Area CSS -** - Saas Banner Area CSS -** - Personal Portfolio Banner Area CSS -** - Consulting Banner Area CSS -** - IT Agency Banner Area CSS -** - Shop Banner Area CSS -** - Blog Banner Area CSS -** - Furniture Banner Area CSS -** - Minimal Portfolio Banner Area CSS -** - Features Area CSS -** - About Area CSS -** - Services Area CSS -** - Services Details Area CSS -** - Works Area CSS -** - What We Do Area CSS -** - Categories Area CSS -** - Who We Are Area CSS -** - Video Area CSS -** - Process Area CSS -** - Funfacts Area CSS -** - Products Area CSS -** - Products Details Area CSS -** - Cart Area CSS -** - Checkout Area CSS -** - Testimonials Area CSS -** - Team Area CSS -** - How It Works Area CSS -** - Skills Area CSS -** - Expertise Area CSS -** - CTA Area CSS -** - Timeline/Short Brief Area CSS -** - App Download Area CSS -** - Trending Style Area CSS -** - Pricing Area CSS -** - App Screenshot Area CSS -** - Facility Area CSS -** - Latest Posts Area CSS -** - Partner Area CSS -** - Clients Area CSS -** - Featured Video Area CSS -** - Blog Area CSS -** - Blog Details Area CSS -** - Widget Area CSS -** - Profile Authentication Area CSS -** - Instagram Area CSS -** - Page Title Area CSS -** - Pagination Area CSS -** - Gallery Area CSS -** - Portfolio Details Area CSS -** - Portfolio Details Navigation Area CSS -** - Privacy Policy Area CSS -** - Terms & Conditions Area CSS -** - FAQ Area CSS -** - 404 Error Area CSS -** - Coming Soon Area CSS -** - Contact Area CSS -** - Footer Area CSS -** - Go Top CSS -** - Modal CSS -*/ - -/*================================================ -Default CSS -=================================================*/ -@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); -:root { - --fontFamily: 'Manrope', sans-serif; - --headingFontFamily: 'Poppins', sans-serif; - --startupMainColor: #fd5444; - --marketingMainColor: #2f4eac; - --businessMainColor: #5d3dec; - --businessGradientColor: linear-gradient(to right, #4651e6, #5749ed, #693ff3, #7c30f7, #8f11fa); - --appMainColor: #0042bb; - --appGradientColor: linear-gradient(to right bottom, #0042bb, #093ea8, #113a96, #163584, #1a3172); - --saasMainColor: #296dfe; - --saasGradientColor: linear-gradient(to right top, #257ef7, #0094fb, #00a7f5, #00b6ea, #17c3dd); - --personalPortfolioMainColor: #111111; - --itAgencyMainColor: #ff4a17; - --itAgencyGradientColor: linear-gradient(to right, #ff4a17, #fd5d15, #fb6d16, #f87b1a, #f68821); - --consultingMainColor: #4eaf4e; - --shopMainColor: #111111; - --minimalPortfolioColor: #4ab19d; - --optionalColor: #6f6f6f; - --whiteColor: #ffffff; - --blackColor: #2d2d37; - --fontSize: 15.5px; - --transition: .5s; -} -body { - margin: 0; - padding: 0; - color: var(--blackColor); - font: { - size: var(--fontSize); - family: var(--fontFamily); - }; -} -.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { - color: var(--blackColor); - font: { - weight: 600; - family: var(--headingFontFamily); - }; -} -a { - transition: var(--transition); - color: var(--blackColor); - text-decoration: none; - outline: 0 !important; - - &:hover { - color: var(--optionalColor); - text-decoration: none; - } -} -:focus { - outline: 0 !important; -} -.d-table { - width: 100%; - height: 100%; - - &-cell { - vertical-align: middle; - } -} -img { - max-width: 100%; - height: auto; -} -p { - color: var(--optionalColor); - margin-bottom: 15px; - line-height: 1.8; - font: { - weight: 500; - size: var(--fontSize); - }; - &:last-child { - margin-bottom: 0; - } -} -.ptb-100 { - padding: { - top: 100px; - bottom: 100px; - }; -} -.pt-100 { - padding-top: 100px; -} -.pb-100 { - padding-bottom: 100px; -} -.ptb-70 { - padding: { - top: 70px; - bottom: 70px; - }; -} -.pt-70 { - padding-top: 70px; -} -.pb-70 { - padding-bottom: 70px; -} -.bg-f9f4f0 { - background-color: #f9f4f0; -} -.bg-faf7f2 { - background-color: #faf7f2; -} -.bg-1b2232 { - background-color: #1b2232; -} -.bg-f3f4f7 { - background-color: #f3f4f7; -} -.bg-f9f9f9 { - background-color: #f9f9f9; -} -.bg-dark { - background-color: #06080e !important; - - .section-title { - color: var(--whiteColor); - - h2 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - } - } -} -.bg-f6f7f9 { - background-color: #f6f7f9; -} -/*section-title*/ -.section-title { - text-align: center; - max-width: 655px; - margin: { - bottom: 60px; - left: auto; - right: auto; - }; - h2 { - margin-bottom: 0; - font-size: 40px; - } - p { - max-width: 540px; - margin: { - top: 15px; - left: auto; - right: auto; - }; - } - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--startupMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - &.marketing-color { - color: var(--marketingMainColor); - } - &.business-color { - color: var(--businessMainColor); - } - &.app-color { - color: var(--appMainColor); - } - &.saas-color { - color: var(--saasMainColor); - } - &.consulting-color { - color: var(--consultingMainColor); - } - &.it-agency-color { - color: var(--itAgencyMainColor); - } - &.minimal-portfolio-color { - color: var(--minimalPortfolioColor); - } - } - &.style-two { - text-align: start; - padding-left: 90px; - margin: { - left: 0; - right: 0; - bottom: 50px; - }; - h2 { - position: relative; - font-size: 30px; - - &::before { - left: -90px; - top: 50%; - width: 70px; - height: 1px; - content: ''; - position: absolute; - transform: translateY(-50%); - background-color: var(--personalPortfolioMainColor); - } - } - p { - margin: { - left: 0; - right: 0; - }; - } - } - &.style-three { - max-width: 100%; - - h2 { - position: relative; - z-index: 1; - - span { - display: inline-block; - background-color: var(--whiteColor); - padding: { - left: 30px; - right: 30px; - }; - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); - background-color: #cfcfcf; - height: 1px; - z-index: -1; - } - } - } - &.style-four { - border-bottom: 1px solid #eeeeee; - padding-bottom: 12px; - position: relative; - text-align: start; - max-width: 100%; - margin: { - left: 0; - right: 0; - bottom: 45px; - }; - h2 { - font-size: 30px; - } - &::before { - content: ''; - position: absolute; - left: 0; - bottom: -1px; - width: 70px; - height: 1px; - background-color: var(--blackColor); - } - } -} -/*default-btn*/ -.default-btn { - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - transition: var(--transition); - display: inline-block; - background-color: var(--startupMainColor); - color: var(--whiteColor); - position: relative; - border-radius: 5px; - border: none; - padding: 13px 50px 13px 35px; - font: { - size: var(--fontSize); - weight: 700; - }; - i { - position: absolute; - right: 35px; - top: 50%; - transform: translateY(-50%); - margin-top: 1px; - font-size: 14px; - transition: var(--transition); - } - &:hover { - color: var(--whiteColor); - background-color: var(--startupMainColor); - box-shadow: 0 0.5rem 1.2rem rgba(0, 0, 0, 0.1), 0 2rem 3rem rgba(0, 0, 0, 0.15); - - i { - right: 30px; - } - } - &.marketing-color { - background-color: var(--marketingMainColor); - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - background-color: var(--marketingMainColor); - } - } - &.business-color { - background: var(--businessGradientColor); - } - &.app-color { - background: var(--appGradientColor); - } - &.saas-color { - background: var(--saasGradientColor); - } - &.personal-portfolio-color { - background: var(--personalPortfolioMainColor); - } - &.consulting-color { - background: var(--consultingMainColor); - } - &.it-agency-color { - background: var(--itAgencyGradientColor); - } - &.shop-color { - background: var(--shopMainColor); - } - &.minimal-portfolio-color { - background: var(--minimalPortfolioColor); - } -} -/*form-control*/ -.form-control { - border-radius: 0; - background-color: #f5f5f5 !important; - box-shadow: unset !important; - transition: var(--transition); - border: none !important; - height: 50px; - padding-left: 15px; - color: var(--blackColor); - font: { - size: var(--fontSize); - weight: 500; - }; - &::placeholder { - color: #999999; - transition: var(--transition); - } - &:focus { - &::placeholder { - color: transparent; - } - } -} -textarea.form-control { - padding-top: 15px; - height: auto; -} - -/*================================================ -Top Header Area CSS -=================================================*/ -.header-area { - &.p-absolute { - position: absolute; - left: 0; - right: 0; - top: 0; - height: auto; - z-index: 3; - - .navbar-area { - background-color: transparent; - } - } -} -.top-header-area { - background-color: var(--startupMainColor); - padding: { - top: 12px; - bottom: 12px; - }; - .container-fluid { - max-width: 1400px; - } - &.business-color { - background: var(--businessGradientColor); - } - &.saas-color { - background: var(--saasGradientColor); - } - &.top-header-style-two { - .container-fluid { - max-width: 1470px; - } - } - &.top-header-style-three { - .container-fluid { - max-width: 1450px; - } - } - &.it-agency-color { - background: var(--itAgencyGradientColor); - } - &.bg-f3f4f7 { - background: #f3f4f7; - - .top-header-social-links { - li { - a { - color: var(--blackColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - } - .top-header-contact-info { - color: var(--blackColor); - - .number { - color: var(--blackColor); - } - .email { - color: var(--blackColor); - } - .lang-switcher { - label { - color: var(--startupMainColor); - } - select { - color: var(--blackColor); - - option { - color: var(--blackColor); - } - } - } - } - &.consulting-color { - .top-header-social-links { - li { - a { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - .top-header-contact-info { - .lang-switcher { - label { - color: var(--consultingMainColor); - } - } - } - .top-header-info { - .number { - i { - color: var(--consultingMainColor); - } - } - .email { - i { - color: var(--consultingMainColor); - } - } - } - } - } - &.shop-color { - background: var(--shopMainColor); - } -} -.top-header-info { - .number { - display: inline-block; - position: relative; - padding-left: 25px; - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); - } - } - .email { - display: inline-block; - position: relative; - padding-left: 25px; - margin-right: 20px; - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); - } - } -} -.top-header-social-links { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - display: inline-block; - margin-right: 15px; - - a { - display: block; - color: var(--whiteColor); - - &:hover { - opacity: 0.8; - } - } - &:last-child { - margin-right: 0; - } - } -} -.top-header-contact-info { - color: var(--whiteColor); - - .number { - display: inline-block; - color: var(--whiteColor); - position: relative; - padding-left: 25px; - - i { - position: absolute; - left: 0; - top: 4px; - } - } - .email { - display: inline-block; - color: var(--whiteColor); - position: relative; - padding-left: 25px; - margin-right: 20px; - - i { - position: absolute; - left: 0; - top: 4px; - } - } - .lang-switcher { - display: inline-block; - position: relative; - margin-left: 20px; - padding-left: 20px; - - label { - display: block; - margin-bottom: 0; - position: absolute; - left: 0; - top: 1px; - } - select { - cursor: pointer; - background-color: transparent; - color: var(--whiteColor); - border: none; - - option { - color: var(--blackColor); - } - } - } -} - -/*================================================ -Navbar Area CSS -=================================================*/ -.navbar-area { - padding: 5px 0; - position: relative; - background-color: var(--whiteColor); - - &.p-absolute { - position: absolute; - left: 0; - right: 0; - top: 0; - height: auto; - z-index: 9; - } - &.is-sticky { - top: 0; - left: 0; - width: 100%; - z-index: 999; - position: fixed; - background-color: var(--whiteColor) !important; - box-shadow: 0 2px 28px 0 rgba(0, 0, 0, 0.09); - animation: 500ms ease-in-out 0s normal none 1 running fadeInDown; - - &.bg-dark { - background-color: #06080e !important; - box-shadow: 0 2px 28px 0 rgba(255, 255, 255, .05); - } - } - &.bg-f9faff { - background-color: #f9faff; - } - &.navbar-style-two { - .noke-nav { - .container-fluid { - max-width: 1400px; - } - } - } - &.navbar-style-three { - .noke-nav { - .container-fluid { - max-width: 1450px; - } - } - } - &.navbar-style-four { - .noke-nav { - .container-fluid { - max-width: 1470px; - } - } - } - &.navbar-style-five { - padding: { - top: 0; - bottom: 0; - }; - .noke-nav { - .container-fluid { - max-width: 1450px; - } - .navbar { - background-color: var(--whiteColor); - padding: { - left: 25px; - right: 25px; - }; - } - } - } - &.bg-dark { - color: var(--whiteColor); - - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - color: var(--whiteColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - &:hover, &.active { - a { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--startupMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--startupMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - color: var(--startupMainColor); - - &:hover { - color: var(--startupMainColor); - } - } - .social-links { - li { - a { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - } - } - .search-box { - .input-search { - color: var(--whiteColor); - - &::placeholder { - color: var(--whiteColor); - } - &:focus { - &::placeholder { - color: transparent; - } - } - } - button { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - .lang-switcher { - color: var(--whiteColor); - - select { - color: var(--whiteColor); - - option { - color: var(--blackColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - .inner { - .circle { - background-color: var(--whiteColor); - } - } - &:hover { - .inner { - .circle { - background-color: var(--startupMainColor); - } - } - } - } - } - } - &.color-white { - &.is-sticky { - background-color: #06080e !important; - box-shadow: 0 2px 28px 0 rgba(255, 255, 255, .05); - } - } -} -.noke-responsive-nav { - display: none; -} -.noke-nav { - .container-fluid { - max-width: 1400px; - } - .navbar { - padding: 0; - position: inherit; - - .navbar-brand { - font-size: inherit; - line-height: 1; - padding: 0; - } - ul { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; - } - .navbar-nav { - margin-left: auto; - position: relative; - - .nav-item { - position: relative; - margin: { - left: 15px; - right: 15px; - }; - a { - position: relative; - color: var(--blackColor); - transition: var(--transition); - font: { - weight: 700; - size: var(--fontSize); - }; - padding: { - left: 0; - right: 0; - top: 30px; - bottom: 30px; - }; - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - .dropdown-toggle { - padding-right: 14px; - - &::after { - display: none; - } - &::before { - -webkit-font-smoothing: antialiased; - text-rendering: auto; - position: absolute; - content: "\f078"; - line-height: 1; - top: 37px; - right: 0; - font: { - family: "Font Awesome 5 Free"; - variant-ligatures: normal; - variant-caps: normal; - style: normal; - weight: 900; - size: 10px; - }; - } - } - &:last-child { - margin-right: 0; - } - &:first-child { - margin-left: 0; - } - &:hover, &.active { - a { - color: var(--startupMainColor); - } - } - .dropdown-menu { - left: 0; - top: 80px; - opacity: 0; - z-index: 99; - border: none; - width: 250px; - margin-top: 0; - display: block; - padding: 10px 0; - border-radius: 0; - position: absolute; - visibility: hidden; - background: var(--whiteColor); - transition: all 0.2s ease-in-out; - box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.1); - - li { - margin: 0; - - a { - display: block; - padding: 9px 20px; - position: relative; - color: var(--blackColor); - font: { - size: 14.5px; - weight: 600; - }; - i { - top: 50%; - margin: 0; - right: 15px; - font-size: 11px; - position: absolute; - transform: translateY(-50%); - } - &:hover, &:focus { - color: var(--startupMainColor); - padding-left: 25px; - } - &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - top: 0; - opacity: 0; - left: auto; - right: 250px; - margin-top: 0; - visibility: hidden; - transform: translateX(0); - - li { - a { - color: var(--blackColor); - padding-left: 20px; - - &:hover, &:focus { - color: var(--startupMainColor); - padding-left: 25px; - } - &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - top: 0; - opacity: 0; - right: 250px; - visibility: hidden; - - li { - a { - color: var(--blackColor); - padding-left: 20px; - - &:hover, &:focus { - padding-left: 25px; - color: var(--startupMainColor); - } - &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - top: 0; - opacity: 0; - right: 250px; - visibility: hidden; - - li { - a { - padding-left: 20px; - color: var(--blackColor); - - &:hover, &:focus { - color: var(--startupMainColor); - padding-left: 25px; - } - &.active { - color: var(--startupMainColor); - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - &:hover { - .dropdown-menu { - opacity: 1; - visibility: visible; - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - &:hover { - .dropdown-menu { - opacity: 1; - visibility: visible; - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - &:hover { - .dropdown-menu { - opacity: 1; - visibility: visible; - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - &:hover { - .dropdown-menu { - opacity: 1; - margin-top: 0; - visibility: visible; - } - } - } - } - &:hover { - .dropdown-menu { - opacity: 1; - visibility: visible; - } - } - &.megamenu { - position: unset; - - .dropdown-menu { - left: 0; - top: auto; - width: 100%; - padding: 25px; - margin-top: 0; - position: absolute; - transform: unset !important; - - .submenu-title { - position: relative; - border-bottom: 1px solid #eeeeee; - padding-bottom: 8px; - text-transform: uppercase; - margin: { - bottom: 25px; - top: 30px; - }; - font: { - size: var(--fontSize); - weight: 600; - }; - &::before { - width: 50px; - height: 1px; - content: ''; - position: absolute; - left: 0; - bottom: -1px; - background-color: var(--startupMainColor); - } - &:first-child { - margin-top: 0; - } - } - a { - border-bottom: none !important; - } - .megamenu-submenu { - li { - a { - padding: 0; - margin-top: 18px; - color: var(--blackColor); - - .count { - float: right; - width: 20px; - height: 20px; - display: block; - font-size: 12px; - line-height: 20px; - text-align: center; - border-radius: 5px; - background-color: #f4f4f4; - transition: var(--transition); - } - &:hover, &.active { - color: var(--startupMainColor); - } - } - } - } - } - } - } - } - .others-option { - margin-left: 30px; - - .option-item { - position: relative; - margin-left: 15px; - top: 1px; - - &:first-child { - margin-left: 0; - } - .search-icon { - line-height: 1; - font-size: 18px; - cursor: pointer; - display: inline-block; - color: var(--blackColor); - transition: var(--transition); - border: { - left: 1px solid rgba(0, 0, 0, 0.20); - right: 1px solid rgba(0, 0, 0, 0.20); - }; - padding: { - left: 15px; - right: 15px; - }; - &:hover { - color: var(--startupMainColor); - } - } - .social-links { - position: relative; - top: -1px; - - span { - display: inline-block; - margin-left: 15px; - line-height: 1; - - a { - font-size: 17px; - display: block; - - &:hover { - color: var(--startupMainColor); - } - } - &:first-child { - margin-left: 0; - } - } - } - .link-btn { - display: inline-block; - font-size: 18px; - line-height: 1; - } - .icon-btn { - position: relative; - font-size: 18px; - line-height: 1; - padding-right: 10px; - margin-left: 5px; - - span { - display: inline-block; - position: absolute; - right: 0; - top: -5px; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: red; - text-align: center; - line-height: 17px; - color: var(--whiteColor); - font: { - weight: 700; - size: 9px; - }; - } - } - } - .search-box { - position: relative; - width: 200px; - - .input-search { - display: block; - width: 100%; - background-color: transparent; - border: none; - border-bottom: 1px solid #eeeeee; - height: auto; - padding-bottom: 8px; - font-size: 14px; - color: var(--blackColor); - transition: var(--transition); - - &::placeholder { - transition: var(--transition); - color: var(--optionalColor); - } - &:focus { - border-color: var(--startupMainColor); - - &::placeholder { - color: transparent; - } - } - } - button { - position: absolute; - right: 0; - top: 0; - background-color: transparent; - border: none; - color: var(--optionalColor); - transition: var(--transition); - - &:hover { - color: var(--startupMainColor); - } - } - } - .lang-switcher { - position: relative; - padding-left: 34px; - position: relative; - top: -2px; - border-left: 1px solid rgba(0, 0, 0, 0.20); - - label { - display: block; - margin-bottom: 0; - position: absolute; - left: 15px; - top: 0; - } - select { - background-color: transparent; - cursor: pointer; - border: none; - margin-top: -3px; - } - } - } - } -} -.others-option-for-responsive { - display: none; - - .dot-menu { - top: -37px; - right: 60px; - height: 30px; - z-index: 999; - padding: 0 10px; - cursor: pointer; - position: absolute; - - .inner { - display: flex; - align-items: center; - height: 30px; - - .circle { - height: 5px; - width: 5px; - border-radius: 100%; - margin: 0 2px; - transition: var(--transition); - background-color: var(--blackColor); - } - } - &:hover { - .inner { - .circle { - background-color: var(--startupMainColor); - } - } - } - } - .container { - position: relative; - - .container { - right: 0; - top: 15px; - opacity: 0; - z-index: 2; - max-width: 300px; - margin-left: auto; - visibility: hidden; - position: absolute; - transform: scaleX(0); - transition: var(--transition); - padding: { - left: 15px; - right: 15px; - }; - &.active { - opacity: 1; - visibility: visible; - transform: scaleX(1); - } - } - } - .option-inner { - padding: 15px 20px; - box-shadow: 0 15px 40px rgba(0,0,0,.09); - background-color: var(--whiteColor); - text-align: center; - - .others-option { - .option-item { - position: relative; - margin-left: 20px; - display: inline-block; - - &:first-child { - margin-left: 0; - } - .search-icon { - line-height: 1; - font-size: 18px; - cursor: pointer; - display: inline-block; - color: var(--blackColor); - transition: var(--transition); - border-right: 1px solid rgba(0, 0, 0, 0.20); - padding-right: 20px; - - &:hover { - color: var(--startupMainColor); - } - } - .social-links { - position: relative; - top: -1px; - - span { - display: inline-block; - margin-left: 15px; - line-height: 1; - - a { - font-size: 17px; - display: block; - - &:hover { - color: var(--startupMainColor); - } - } - &:first-child { - margin-left: 0; - } - } - } - } - .search-box { - position: relative; - width: 100%; - - .input-search { - display: block; - width: 100%; - background-color: transparent; - border: none; - border-bottom: 1px solid #eeeeee; - height: auto; - padding-bottom: 8px; - font-size: 14px; - color: var(--blackColor); - transition: var(--transition); - - &::placeholder { - transition: var(--transition); - color: var(--optionalColor); - } - &:focus { - border-color: var(--startupMainColor); - - &::placeholder { - color: transparent; - } - } - } - button { - position: absolute; - right: 0; - top: 0; - background-color: transparent; - border: none; - color: var(--optionalColor); - transition: var(--transition); - - &:hover { - color: var(--startupMainColor); - } - } - } - .lang-switcher { - top: -2px; - position: relative; - padding-left: 25px; - position: relative; - margin-left: -10px; - border-left: 1px solid rgba(0, 0, 0, 0.20); - - label { - display: block; - margin-bottom: 0; - position: absolute; - left: 10px; - top: 0; - } - select { - background-color: transparent; - cursor: pointer; - border: none; - margin-top: -3px; - } - } - .link-btn { - display: inline-block; - font-size: 18px; - line-height: 1; - } - .icon-btn { - position: relative; - font-size: 18px; - line-height: 1; - padding-right: 10px; - margin-left: 5px; - - span { - display: inline-block; - position: absolute; - right: 0; - top: -5px; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: red; - text-align: center; - line-height: 17px; - color: var(--whiteColor); - font: { - weight: 700; - size: 9px; - }; - } - } - } - } -} -.marketing-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--marketingMainColor); - } - } - &:hover, &.active { - a { - color: var(--marketingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--marketingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--marketingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--marketingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--marketingMainColor); - } - } - &.active { - a { - color: var(--marketingMainColor); - } - } - } - } - &.active { - a { - color: var(--marketingMainColor); - } - } - } - } - &.active { - a { - color: var(--marketingMainColor); - } - } - } - } - &.active { - a { - color: var(--marketingMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--marketingMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--marketingMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--marketingMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--marketingMainColor); - } - } - button { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--marketingMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--marketingMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--marketingMainColor); - } - } - button { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - } - } -} -.business-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--businessMainColor); - } - } - &:hover, &.active { - a { - color: var(--businessMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--businessMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--businessMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--businessMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--businessMainColor); - } - } - &.active { - a { - color: var(--businessMainColor); - } - } - } - } - &.active { - a { - color: var(--businessMainColor); - } - } - } - } - &.active { - a { - color: var(--businessMainColor); - } - } - } - } - &.active { - a { - color: var(--businessMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--businessMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--businessMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--businessMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--businessMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--businessMainColor); - } - } - button { - &:hover { - color: var(--businessMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--businessMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--businessMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--businessMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--businessMainColor); - } - } - button { - &:hover { - color: var(--businessMainColor); - } - } - } - } - } - } -} -.app-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--appMainColor); - } - } - &:hover, &.active { - a { - color: var(--appMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--appMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--appMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--appMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--appMainColor); - } - } - &.active { - a { - color: var(--appMainColor); - } - } - } - } - &.active { - a { - color: var(--appMainColor); - } - } - } - } - &.active { - a { - color: var(--appMainColor); - } - } - } - } - &.active { - a { - color: var(--appMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--appMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--appMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--appMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--appMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--appMainColor); - } - } - button { - &:hover { - color: var(--appMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--appMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--appMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--appMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--appMainColor); - } - } - button { - &:hover { - color: var(--appMainColor); - } - } - } - } - } - } -} -.personal-portfolio-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--personalPortfolioMainColor); - } - } - &:hover, &.active { - a { - color: var(--personalPortfolioMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--personalPortfolioMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--personalPortfolioMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--personalPortfolioMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--personalPortfolioMainColor); - } - } - &.active { - a { - color: var(--personalPortfolioMainColor); - } - } - } - } - &.active { - a { - color: var(--personalPortfolioMainColor); - } - } - } - } - &.active { - a { - color: var(--personalPortfolioMainColor); - } - } - } - } - &.active { - a { - color: var(--personalPortfolioMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--personalPortfolioMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--personalPortfolioMainColor); - } - } - button { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--personalPortfolioMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--personalPortfolioMainColor); - } - } - button { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } -} -.saas-color { - &.bg-dark { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--saasMainColor); - } - } - &:hover, &.active { - a { - color: var(--saasMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--saasMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--saasMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--saasMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--saasMainColor); - } - } - &.active { - a { - color: var(--saasMainColor); - } - } - } - } - &.active { - a { - color: var(--saasMainColor); - } - } - } - } - &.active { - a { - color: var(--saasMainColor); - } - } - } - } - &.active { - a { - color: var(--saasMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--saasMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--saasMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--saasMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--saasMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--saasMainColor); - } - } - button { - &:hover { - color: var(--saasMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--saasMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--saasMainColor); - } - } - .social-links { - li { - a { - &:hover { - color: var(--saasMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--saasMainColor); - } - } - button { - &:hover { - color: var(--saasMainColor); - } - } - } - } - } - } - } -} -.color-white { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - color: var(--whiteColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - &:hover, &.active { - a { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--startupMainColor); - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.active { - a { - color: var(--startupMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--startupMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--startupMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - .social-links { - li { - a { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - } - } - .search-box { - .input-search { - color: var(--whiteColor); - - &::placeholder { - color: var(--whiteColor); - } - &:focus { - &::placeholder { - color: transparent; - } - } - } - button { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - .lang-switcher { - color: var(--whiteColor); - - select { - color: var(--whiteColor); - - option { - color: var(--blackColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - .inner { - .circle { - background-color: var(--whiteColor); - } - } - &:hover { - .inner { - .circle { - background-color: var(--startupMainColor); - } - } - } - } - } -} -.it-agency-color { - &.color-white { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - color: var(--whiteColor); - - &:hover, &:focus, &.active { - color: var(--itAgencyMainColor); - } - } - &:hover, &.active { - a { - color: var(--itAgencyMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--itAgencyMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--itAgencyMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--itAgencyMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--itAgencyMainColor); - } - } - &.active { - a { - color: var(--itAgencyMainColor); - } - } - } - } - &.active { - a { - color: var(--itAgencyMainColor); - } - } - } - } - &.active { - a { - color: var(--itAgencyMainColor); - } - } - } - } - &.active { - a { - color: var(--itAgencyMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--itAgencyMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--itAgencyMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - color: var(--itAgencyMainColor); - - &:hover { - color: var(--itAgencyMainColor); - } - } - .social-links { - li { - a { - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - } - .search-box { - button { - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--itAgencyMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--itAgencyMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--itAgencyMainColor); - } - } - button { - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - } - } - } -} -.consulting-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--consultingMainColor); - } - } - &:hover, &.active { - a { - color: var(--consultingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--consultingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--consultingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--consultingMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--consultingMainColor); - } - } - &.active { - a { - color: var(--consultingMainColor); - } - } - } - } - &.active { - a { - color: var(--consultingMainColor); - } - } - } - } - &.active { - a { - color: var(--consultingMainColor); - } - } - } - } - &.active { - a { - color: var(--consultingMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--consultingMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--consultingMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--consultingMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--consultingMainColor); - } - } - button { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--consultingMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--consultingMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--consultingMainColor); - } - } - button { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - } - } -} -.shop-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--shopMainColor); - } - } - &:hover, &.active { - a { - color: var(--shopMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--shopMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--shopMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--shopMainColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--shopMainColor); - } - } - &.active { - a { - color: var(--shopMainColor); - } - } - } - } - &.active { - a { - color: var(--shopMainColor); - } - } - } - } - &.active { - a { - color: var(--shopMainColor); - } - } - } - } - &.active { - a { - color: var(--shopMainColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--shopMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--shopMainColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--shopMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--shopMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--shopMainColor); - } - } - button { - &:hover { - color: var(--shopMainColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--shopMainColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--shopMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--shopMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--shopMainColor); - } - } - button { - &:hover { - color: var(--shopMainColor); - } - } - } - } - } - } -} -.blog-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--blackColor); - } - } - &:hover, &.active { - a { - color: var(--blackColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--blackColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--blackColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--blackColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--blackColor); - } - } - &.active { - a { - color: var(--blackColor); - } - } - } - } - &.active { - a { - color: var(--blackColor); - } - } - } - } - &.active { - a { - color: var(--blackColor); - } - } - } - } - &.active { - a { - color: var(--blackColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--blackColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--blackColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--blackColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--blackColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--blackColor); - } - } - button { - &:hover { - color: var(--blackColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--blackColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--blackColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--blackColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--blackColor); - } - } - button { - &:hover { - color: var(--blackColor); - } - } - } - } - } - } -} -.minimal-portfolio-color { - .noke-nav { - .navbar { - .navbar-nav { - .nav-item { - a { - &:hover, &:focus, &.active { - color: var(--minimalPortfolioColor); - } - } - &:hover, &.active { - a { - color: var(--minimalPortfolioColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--minimalPortfolioColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--minimalPortfolioColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--minimalPortfolioColor); - } - } - .dropdown-menu { - li { - a { - color: var(--blackColor); - - &:hover, &:focus, &.active { - color: var(--minimalPortfolioColor); - } - } - &.active { - a { - color: var(--minimalPortfolioColor); - } - } - } - } - &.active { - a { - color: var(--minimalPortfolioColor); - } - } - } - } - &.active { - a { - color: var(--minimalPortfolioColor); - } - } - } - } - &.active { - a { - color: var(--minimalPortfolioColor); - } - } - } - } - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--minimalPortfolioColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--minimalPortfolioColor); - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--minimalPortfolioColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--minimalPortfolioColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--minimalPortfolioColor); - } - } - button { - &:hover { - color: var(--minimalPortfolioColor); - } - } - } - } - } - } - .others-option-for-responsive { - .dot-menu { - &:hover { - .inner { - .circle { - background-color: var(--minimalPortfolioColor); - } - } - } - } - .option-inner { - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--minimalPortfolioColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--minimalPortfolioColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--minimalPortfolioColor); - } - } - button { - &:hover { - color: var(--minimalPortfolioColor); - } - } - } - } - } - } -} -// Mobile and iPad Navbar -@media only screen and (max-width: 1199px) { - - .header-area { - &.p-absolute { - .navbar-area { - &.navbar-style-five { - border-bottom: none; - background-color: var(--whiteColor); - } - } - } - } - .navbar-area { - padding: { - top: 15px; - bottom: 15px; - }; - &.is-sticky { - padding: { - top: 15px; - bottom: 15px; - }; - } - &.bg-dark { - border-bottom: 1px solid rgba(255, 255, 255, .07); - - .noke-responsive-nav { - .mean-container { - a { - &.meanmenu-reveal { - color: var(--whiteColor); - - span { - background: var(--whiteColor); - } - } - } - } - .others-option { - .search-box { - .input-search { - border-bottom: 1px solid #6e6a6a; - color: var(--whiteColor); - - &::placeholder { - color: var(--whiteColor); - } - &:focus { - border-color: var(--whiteColor); - } - } - button { - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - } - } - } - } - } - } - &.color-white { - border-bottom: 1px solid rgba(255, 255, 255, .07); - - .noke-responsive-nav { - .mean-container { - a { - &.meanmenu-reveal { - color: var(--whiteColor); - - span { - background: var(--whiteColor); - } - } - } - } - .others-option { - .search-box { - .input-search { - border-bottom: 1px solid #6e6a6a; - color: var(--whiteColor); - - &::placeholder { - color: var(--whiteColor); - } - &:focus { - border-color: var(--whiteColor); - } - } - button { - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - } - } - } - } - } - } - &.navbar-style-five { - border-bottom: 1px solid #eeeeee; - padding: { - top: 15px; - bottom: 15px; - }; - } - } - .noke-responsive-nav { - display: block; - - .noke-responsive-menu { - position: relative; - - &.mean-container { - .mean-nav { - margin-top: 63px; - background-color: var(--whiteColor); - - ul { - font-size: 14px; - border: none !important; - - li { - a { - color: var(--blackColor); - border-top-color: #DBEEFD; - text-transform: unset; - font-weight: 500; - - i { - display: none; - } - &.mean-expand { - width: 50%; - height: 28px; - text-align: end; - padding: 11px!important; - background: transparent !important; - border: { - left: none !important; - bottom: none !important; - }; - } - &.active { - color: var(--startupMainColor); - } - } - li { - a { - font-size: 14px; - } - } - } - } - } - .navbar-nav { - height: 301px; - overflow-y: scroll; - box-shadow: 0 7px 13px 0 rgba(0, 0, 0, .1); - } - } - } - .mean-container { - a { - &.meanmenu-reveal { - top: 0; - padding: 0; - width: 35px; - height: 30px; - padding-top: 13px; - color: var(--blackColor); - font-family: var(--fontFamily); - - span { - background: var(--blackColor); - height: 4px; - margin-top: -6px; - border-radius: 3px; - position: relative; - top: 8px; - } - } - } - .mean-bar { - background: transparent; - position: absolute; - z-index: 999; - padding: 0; - } - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .nav-item { - width: 90%; - overflow: hidden; - padding: 1em 5% 5px; - - .row { - display: block; - flex-wrap: unset; - margin: { - left: 0; - right: 0; - top: -20px; - }; - .col { - flex-basis: unset; - flex-grow: unset; - max-width: 100%; - padding: { - left: 0; - right: 0; - top: 20px; - }; - &:last-child { - margin-bottom: 15px; - } - } - } - a { - width: 100%; - float: unset; - display: block; - - &.p-0 { - border: none !important; - margin-top: 15px; - } - } - } - .submenu-title { - position: relative; - padding-bottom: 8px; - color: var(--blackColor); - text-transform: uppercase; - border-bottom: 1px solid #eeeeee; - margin: { - bottom: 5px; - top: 25px; - left: 0; - right: 0; - }; - font: { - size: 12.5px; - weight: 600; - }; - &::before { - width: 30px; - height: 1px; - content: ''; - position: absolute; - left: 0; - bottom: -1px; - background-color: var(--startupMainColor); - } - &:first-child { - margin-top: 0; - } - } - a { - border-bottom: none !important; - } - .mean-expand { - display: none !important; - } - .megamenu-submenu { - width: auto; - overflow: hidden; - display: block !important; - padding: { - left: 0; - right: 0; - }; - li { - a { - padding: 0; - border: none; - margin-top: 15px; - font-weight: 500; - color: var(--blackColor); - - &:hover, &.active { - color: var(--startupMainColor); - } - } - } - } - } - } - } - } - } - .dropdown-toggle { - &::after { - display: none !important; - } - } - .others-option { - display: none !important; - position: absolute; - right: 60px; - top: 16px; - - .option-item { - position: relative; - margin-left: 20px; - display: inline-block; - - &:first-child { - margin-left: 0; - } - .search-icon { - line-height: 1; - font-size: 18px; - cursor: pointer; - display: inline-block; - color: var(--blackColor); - transition: var(--transition); - border-right: 1px solid rgba(0, 0, 0, 0.20); - padding-right: 20px; - - &:hover { - color: var(--startupMainColor); - } - } - .social-links { - position: relative; - top: -1px; - - span { - display: inline-block; - margin-left: 15px; - line-height: 1; - - a { - font-size: 17px; - display: block; - - &:hover { - color: var(--startupMainColor); - } - } - &:first-child { - margin-left: 0; - } - } - } - } - .search-box { - position: relative; - width: 250px; - top: -7px; - - .input-search { - display: block; - width: 100%; - background-color: transparent; - border: none; - border-bottom: 1px solid #eeeeee; - height: auto; - padding-bottom: 8px; - font-size: 14px; - color: var(--blackColor); - transition: var(--transition); - - &::placeholder { - transition: var(--transition); - color: var(--optionalColor); - } - &:focus { - border-color: var(--startupMainColor); - - &::placeholder { - color: transparent; - } - } - } - button { - position: absolute; - right: 0; - top: 0; - background-color: transparent; - border: none; - color: var(--optionalColor); - transition: var(--transition); - - &:hover { - color: var(--startupMainColor); - } - } - } - .lang-switcher { - top: -2px; - position: relative; - padding-left: 25px; - position: relative; - margin-left: -10px; - border-left: 1px solid rgba(0, 0, 0, 0.20); - - label { - display: block; - margin-bottom: 0; - position: absolute; - left: 10px; - top: 0; - } - select { - background-color: transparent; - cursor: pointer; - border: none; - margin-top: -3px; - } - } - .link-btn { - display: inline-block; - font-size: 18px; - line-height: 1; - } - .icon-btn { - position: relative; - font-size: 18px; - line-height: 1; - padding-right: 10px; - margin-left: 5px; - - span { - display: inline-block; - position: absolute; - right: 0; - top: -5px; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: red; - text-align: center; - line-height: 17px; - color: var(--whiteColor); - font: { - weight: 700; - size: 9px; - }; - } - } - } - .logo { - position: relative; - width: 60%; - z-index: 999; - } - } - .noke-nav { - display: none; - } - .others-option-for-responsive { - display: block; - } - .marketing-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--marketingMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--marketingMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--marketingMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--marketingMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--marketingMainColor); - } - } - button { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - } - } - .business-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--businessMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--businessMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--businessMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--businessMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--businessMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--businessMainColor); - } - } - button { - &:hover { - color: var(--businessMainColor); - } - } - } - } - } - } - .app-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--appMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--appMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--appMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--appMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--appMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--appMainColor); - } - } - button { - &:hover { - color: var(--appMainColor); - } - } - } - } - } - } - .it-agency-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--itAgencyMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--itAgencyMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--itAgencyMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--itAgencyMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--itAgencyMainColor); - } - } - button { - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - } - } - .consulting-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--consultingMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--consultingMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--consultingMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--consultingMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--consultingMainColor); - } - } - button { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - } - } - .saas-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--saasMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--saasMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--saasMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--saasMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--saasMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--saasMainColor); - } - } - button { - &:hover { - color: var(--saasMainColor); - } - } - } - } - } - } - .shop-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--shopMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--shopMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--shopMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--shopMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--shopMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--shopMainColor); - } - } - button { - &:hover { - color: var(--shopMainColor); - } - } - } - } - } - } - .blog-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--personalPortfolioMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--personalPortfolioMainColor); - } - } - button { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - .personal-portfolio-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--personalPortfolioMainColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--personalPortfolioMainColor); - } - } - button { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - } - } - .minimal-portfolio-color { - .noke-responsive-nav { - .noke-responsive-menu { - &.mean-container { - .mean-nav { - ul { - li { - a { - &.active { - color: var(--minimalPortfolioColor); - } - } - } - } - } - } - } - .mean-container { - .navbar-nav { - .nav-item { - &.megamenu { - .dropdown-menu { - .submenu-title { - &::before { - background-color: var(--minimalPortfolioColor); - } - } - .megamenu-submenu { - li { - a { - &:hover, &.active { - color: var(--minimalPortfolioColor); - } - } - } - } - } - } - } - } - } - .others-option { - .option-item { - .search-icon { - &:hover { - color: var(--minimalPortfolioColor); - } - } - .social-links { - span { - a { - &:hover { - color: var(--minimalPortfolioColor); - } - } - } - } - } - .search-box { - .input-search { - &:focus { - border-color: var(--minimalPortfolioColor); - } - } - button { - &:hover { - color: var(--minimalPortfolioColor); - } - } - } - } - } - } - -} - -/*================================================ -Search Overlay CSS -=================================================*/ -.search-overlay { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 99999; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease-in-out; - overflow: hidden; - - .search-overlay-layer { - position: absolute; - top: 0; - right: 0; - bottom: 0; - z-index: 1; - transform: translateX(100%); - - &:nth-child(1) { - left: 0; - background-color: rgba(0, 0, 0, 0.5); - transition: all 0.3s ease-in-out 0s; - } - &:nth-child(2) { - left: 0; - background-color: rgba(0, 0, 0, 0.4); - transition: all 0.3s ease-in-out 0.3s; - } - &:nth-child(3) { - left: 0; - background-color: rgba(0, 0, 0, 0.7); - transition: all 0.9s ease-in-out 0.6s; - } - } - .search-overlay-close { - position: absolute; - top: 40px; - right: 40px; - width: 50px; - z-index: 2; - text-align: center; - cursor: pointer; - padding: 10px; - transition: all 0.9s ease-in-out 1.5s; - opacity: 0; - visibility: hidden; - - .search-overlay-close-line { - width: 100%; - height: 3px; - float: left; - margin-bottom: 5px; - background-color: var(--whiteColor); - transition: all 500ms ease; - - &:nth-child(1) { - transform: rotate(45deg); - } - &:nth-child(2) { - margin-top: -7px; - transform: rotate(-45deg); - } - } - &:hover { - .search-overlay-close-line { - background: red; - transform: rotate(180deg); - } - } - } - .search-overlay-form { - transform: translateY(-50%) translateX(-50%); - transition: all 0.9s ease-in-out 1.4s; - visibility: hidden; - position: absolute; - max-width: 600px; - width: 600px; - opacity: 0; - z-index: 2; - left: 50%; - top: 50%; - - form { - position: relative; - - .input-search { - display: block; - width: 100%; - height: 70px; - border: none; - background-color: transparent; - color: var(--whiteColor); - padding: 0; - transition: var(--transition); - border-bottom: 1px solid rgba(255, 255, 255, .40); - font: { - size: 22px; - weight: 500; - }; - &::placeholder { - transition: var(--transition); - color: rgba(255, 255, 255, .60); - font-weight: 100; - } - &:focus { - border-color: var(--whiteColor); - - &::placeholder { - color: transparent; - } - } - } - button { - position: absolute; - right: 0; - top: 13px; - color: var(--whiteColor); - transition: var(--transition); - background-color: transparent; - border: none; - font-size: 25px; - opacity: 0.7; - - &:hover { - opacity: 1; - } - } - } - } - &.search-overlay-active { - &.search-overlay { - opacity: 1; - visibility: visible; - - .search-overlay-layer { - transform: translateX(0); - } - .search-overlay-close { - opacity: 1; - visibility: visible; - } - .search-overlay-form { - opacity: 1; - visibility: visible; - } - } - } -} - -/*================================================ -Startup Banner Area CSS -=================================================*/ -.startup-banner-area { - .container-fluid { - max-width: 1400px; - } -} -.startup-banner-inner { - position: relative; - z-index: 1; - padding: { - top: 140px; - bottom: 140px; - left: 100px; - right: 100px; - }; -} -.startup-banner-content { - padding-right: 15px; - - .sub-title { - display: block; - margin-bottom: 10px; - color: var(--startupMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h1 { - margin-bottom: 25px; - font-size: 60px; - } - p { - max-width: 580px; - } - .default-btn { - margin-top: 10px; - } -} -.startup-banner-image { - text-align: center; - padding-left: 15px; -} - -/*================================================ -Marketing Banner Area CSS -=================================================*/ -.marketing-banner-area { - z-index: 1; - position: relative; - border: { - top: 1px solid #f0f0f0; - bottom: 1px solid #f0f0f0; - }; - padding: { - top: 80px; - bottom: 80px; - }; -} -.marketing-banner-content { - padding-right: 25px; - - .sub-title { - display: block; - margin-bottom: 10px; - color: var(--marketingMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h1 { - margin-bottom: 25px; - font-size: 60px; - } - p { - max-width: 550px; - } - .default-btn { - margin-top: 10px; - } -} -.marketing-banner-image { - text-align: center; -} - -/*================================================ -Business Banner Area CSS -=================================================*/ -.business-banner-area { - z-index: 1; - position: relative; - padding: { - top: 100px; - bottom: 100px; - }; - &::before { - animation: moveleftbounce 5s linear infinite; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - margin: { - left: 20px; - right: 20px; - }; - background: { - image: url(../../assets/img/bg2.jpg); - position: center center; - repeat: no-repeat; - size: cover; - }; - } - .container { - max-width: 1400px; - } -} -.business-banner-content { - padding-right: 25px; - max-width: 745px; - - .sub-title { - display: block; - margin-bottom: 10px; - color: var(--businessMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h1 { - margin-bottom: 25px; - font-size: 60px; - } - p { - max-width: 550px; - } - .default-btn { - margin-top: 10px; - } -} -.business-banner-image { - text-align: center; - - img { - border-radius: 0 0 0 100px; - } -} - -/*================================================ -App Banner Area CSS -=================================================*/ -.app-banner-area { - z-index: 1; - position: relative; - background: var(--appGradientColor); - background: { - image: url(../../assets/img/bg3.jpg); - position: center center; - repeat: no-repeat; - size: cover; - }; - padding: { - top: 100px; - bottom: 100px; - }; - .container { - max-width: 1450px; - } -} -.app-banner-content { - h1 { - margin-bottom: 25px; - font-size: 60px; - color: var(--whiteColor); - } - p { - max-width: 550px; - color: var(--whiteColor); - } - .btn-box { - margin-top: 35px; - - .store-btn { - display: inline-block; - background-color: var(--whiteColor); - position: relative; - color: var(--optionalColor); - padding: 11px 25px 11px 65px; - margin-right: 15px; - border-radius: 5px; - font: { - size: 13.5px; - weight: 600; - }; - img { - position: absolute; - left: 15px; - top: 50%; - transform: translateY(-50%); - color: var(--blackColor); - font: { - size: 40px; - weight: normal; - }; - } - span { - display: block; - color: var(--blackColor); - margin-top: 2px; - font: { - size: 18px; - weight: 700; - }; - } - &:last-child { - margin-right: 0; - } - } - } -} -.app-banner-image { - text-align: center; - padding-left: 15px; -} - -/*================================================ -Saas Banner Area CSS -=================================================*/ -.saas-banner-area { - background-color: #06080e; - padding-top: 80px; - - .container-fluid { - padding: { - right: 30px; - left: 30px; - }; - } -} -.saas-banner-content { - margin-left: auto; - max-width: 710px; - - h1 { - color: var(--whiteColor); - margin-bottom: 25px; - font-size: 64px; - } - p { - opacity: 0.9; - max-width: 535px; - color: var(--whiteColor); - } - .default-btn { - margin-top: 10px; - } -} -.saas-banner-image { - text-align: center; -} - -/*================================================ -Personal Portfolio Banner Area CSS -=================================================*/ -.personal-portfolio-banner-area { - border-bottom: 1px solid #f0f0f0; - position: relative; - z-index: 1; - padding: { - top: 100px; - bottom: 100px; - }; - .container { - max-width: 1450px; - } -} -.personal-portfolio-banner-content { - .sub-title { - display: block; - position: relative; - padding-left: 65px; - margin-bottom: 12px; - font: { - size: 20px; - weight: 500; - }; - &::before { - left: 0; - top: 50%; - width: 50px; - height: 1px; - content: ''; - position: absolute; - transform: translateY(-50%); - background-color: var(--personalPortfolioMainColor); - } - } - h1 { - margin-bottom: 15px; - font: { - size: 80px; - weight: 700; - }; - } - h4 { - margin-bottom: 20px; - font: { - size: 40px; - weight: 500; - }; - } - p { - max-width: 535px; - } - .default-btn { - margin-top: 10px; - } -} -.personal-portfolio-banner-image { - text-align: center; -} - -/*================================================ -Consulting Banner Area CSS -=================================================*/ -.consulting-banner-area { - position: relative; - z-index: 1; - background: { - size: cover; - repeat: no-repeat; - position: left center; - image: url(../../assets/img/consulting-banner-bg.jpg); - }; - padding: { - top: 300px; - bottom: 180px; - }; - .container { - max-width: 1450px; - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: var(--whiteColor); - opacity: 0.85; - display: none; - } -} -.consulting-banner-content { - max-width: 790px; - - h1 { - margin-bottom: 25px; - font: { - size: 50px; - weight: 700; - }; - } - p { - max-width: 580px; - } - .default-btn { - margin-top: 15px; - } -} - -/*================================================ -IT Agency Banner Area CSS -=================================================*/ -.it-agency-banner-area { - .it-agency-inner-banner { - position: relative; - z-index: 1; - background: { - image: url(../../assets/img/it-agency-banner-bg.jpg); - position: center center; - size: cover; - repeat: no-repeat; - }; - padding: { - top: 280px; - bottom: 330px; - }; - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: 0.8; - } - } -} -.it-agency-banner-content { - max-width: 1000px; - text-align: center; - color: var(--whiteColor); - margin: { - left: auto; - right: auto; - }; - h1 { - color: var(--whiteColor); - margin-bottom: 20px; - line-height: 1.3; - font-size: 60px; - } - p { - color: var(--whiteColor); - max-width: 600px; - opacity: 0.9; - margin: { - left: auto; - right: auto; - }; - } - .default-btn { - margin-top: 15px; - } -} -.banner-video-box { - z-index: 1; - max-width: 1000px; - position: relative; - text-align: center; - border-radius: 10px; - margin: { - left: auto; - right: auto; - top: -130px; - }; - img { - border-radius: 10px; - } - .video-btn { - top: 50%; - left: 50%; - width: 80px; - height: 80px; - font-size: 30px; - border-radius: 50%; - position: absolute; - display: inline-block; - color: var(--whiteColor); - background-color: rgba(255, 74, 23, .75); - transform: translateY(-50%) translateX(-50%); - - i { - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); - } - &::after, &::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 1.5px solid var(--whiteColor); - } - &::before { - animation: ripple 2s linear infinite - } - &::after { - animation: ripple 2s linear 1s infinite - } - } -} - -/*================================================ -Shop Banner Area CSS -=================================================*/ -.shop-banner-area { - padding: { - top: 200px; - bottom: 200px; - }; - background: { - image: url(../../assets/img/shop-banner-bg.jpg); - position: top center; - repeat: no-repeat; - size: cover; - }; -} -.shop-banner-content { - .sub-title { - display: block; - font-size: 30px; - margin-bottom: 8px; - } - h1 { - margin-bottom: 15px; - font: { - size: 60px; - weight: 700; - }; - } - h4 { - margin-bottom: 0; - font-size: 30px; - } - .default-btn { - margin-top: 30px; - } -} - -/*================================================ -Blog Banner Area CSS -=================================================*/ -.blog-banner-area { - background: { - image: url(../../assets/img/bg11.jpg); - position: center center; - repeat: no-repeat; - size: cover; - }; - padding: { - top: 100px; - bottom: 100px; - }; -} -.blog-banner-item { - position: relative; - text-align: center; - overflow: hidden; - - img { - transition: var(--transition); - } - .content { - position: absolute; - top: 50%; - transform: translateY(-50%); - left: 0; - right: 0; - padding: 30px; - background-color: rgba(255, 255, 255, .90); - max-width: 525px; - margin: { - left: auto; - right: auto; - }; - .tag { - display: inline-block; - margin-bottom: 12px; - font-weight: 500; - } - h3 { - font-size: 25px; - line-height: 1.4; - margin-bottom: 12px; - } - p { - margin-bottom: 20px; - } - .meta { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - a { - display: inline-block; - font-weight: 600; - position: relative; - padding-right: 12px; - - i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; - } - &::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--blackColor); - transition: var(--transition); - } - &:hover { - padding-right: 15px; - color: var(--blackColor); - - &::before { - width: 100%; - right: auto; - left: 0; - } - } - } - &.date { - padding-left: 20px; - position: relative; - - i { - position: absolute; - left: 0; - top: 3px; - } - } - } - } - } - &:hover { - img { - transform: scale(1.1); - } - } -} -.blog-banner-item-list { - margin-bottom: -30px; - - .single-blog-banner-item { - margin-bottom: 30px; - - .row { - margin: { - left: 0; - right: 0; - }; - .col-lg-4, .col-lg-8 { - padding: { - left: 0; - right: 0; - }; - } - } - .image { - height: 100%; - position: relative; - background: { - position: center center; - repeat: no-repeat; - size: cover; - }; - &.bg1 { - background-image: url(../../assets/img/blog-banner/blog-banner-list1.jpg); - } - &.bg2 { - background-image: url(../../assets/img/blog-banner/blog-banner-list2.jpg); - } - &.bg3 { - background-image: url(../../assets/img/blog-banner/blog-banner-list3.jpg); - } - &.bg4 { - background-image: url(../../assets/img/blog-banner/blog-banner-list4.jpg); - } - img { - display: none; - } - .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - display: block; - z-index: 1; - } - } - .content { - padding: { - left: 25px; - top: 9.1px; - bottom: 9.1px; - }; - h3 { - margin-bottom: 15px; - color: var(--whiteColor); - line-height: 1.4; - font-size: 18px; - - a { - color: var(--whiteColor); - } - } - .date { - color: var(--whiteColor); - padding-left: 20px; - position: relative; - opacity: 0.9; - - i { - position: absolute; - left: 0; - top: 3px; - } - } - } - } -} - -/*================================================ -Furniture Banner Area CSS -=================================================*/ -.furniture-banner-item { - padding: { - top: 200px; - bottom: 200px; - }; - background: { - position: center center; - repeat: no-repeat; - size: cover; - }; - &.bg-item1 { - background-image: url(../../assets/img/furniture-banner-bg1.jpg); - } - &.bg-item2 { - background-image: url(../../assets/img/furniture-banner-bg2.jpg); - } - &.bg-item3 { - background-image: url(../../assets/img/furniture-banner-bg3.jpg); - } -} -.furniture-banner-content { - padding: 40px; - max-width: 560px; - background-color: var(--whiteColor); - border-left: 5px solid var(--blackColor); - - h1 { - margin-bottom: 15px; - font-size: 45px; - } - .default-btn { - margin-top: 10px; - } -} -.furniture-banner-slides { - &.owl-theme { - .owl-nav.disabled+.owl-dots { - margin-top: 0; - } - .owl-dots { - position: absolute; - right: 15px; - top: 50%; - transform: translateY(-50%); - - .owl-dot { - display: block; - margin: { - top: 10px; - bottom: 10px; - }; - span { - margin: 0; - border-radius: 0; - width: 50px; - height: 6px; - background-color: var(--whiteColor); - transition: var(--transition); - } - &:hover, &.active { - span { - background-color: var(--blackColor); - } - } - } - } - } -} - -/*================================================ -Minimal Portfolio Banner Area CSS -=================================================*/ -.minimal-portfolio-banner-area { - position: relative; - z-index: 1; - padding: { - top: 250px; - bottom: 220px; - }; -} -.minimal-portfolio-banner-content { - text-align: center; - max-width: 770px; - margin: { - left: auto; - right: auto; - }; - .typewrite { - margin-bottom: 30px; - display: inline-block; - color: var(--blackColor); - padding: 8px 20px; - line-height: 1; - letter-spacing: 2px; - background-color: #ebebeb; - font: { - size: 20px; - weight: 300; - }; - } - h1 { - font-size: 60px; - margin-bottom: 40px; - text-shadow: -1px 1px 0 var(--minimalPortfolioColor), 1px 1px 0 var(--startupMainColor), 1px -1px 0 var(--consultingMainColor), -1px -1px 0 var(--itAgencyMainColor); - } -} -.scroll-container { - left: 50%; - bottom: 0; - width: 30px; - height: 60px; - position: absolute; - border-radius: 15px; - border: 3px solid #eeeeee; - transform: translateX(-50%); -} -.scroller { - width: 16px; - border-radius: 8px; - background-color: var(--minimalPortfolioColor); - position: absolute; - top: 4px; - left: 4px; - bottom: 34px; - animation: scroller 1500ms ease-out infinite; -} - -/*================================================ -Features Area CSS -=================================================*/ -.features-area { - position: relative; - z-index: 1; - - &.ptb-70 { - padding-bottom: 40px; - } -} -.single-features-box { - text-align: center; - margin-bottom: 30px; - transition: var(--transition); - position: relative; - padding: { - left: 15px; - right: 15px; - }; - .icon { - line-height: 1; - display: block; - font-size: 50px; - margin-bottom: 22px; - transition: var(--transition); - } - h3 { - font-size: 20px; - margin-bottom: 12px; - transition: var(--transition); - } - p { - margin-bottom: 0; - transition: var(--transition); - } - .link-btn { - display: inline-block; - font-weight: 700; - padding-right: 12px; - position: absolute; - left: 50%; - bottom: 0; - opacity: 0; - visibility: hidden; - transform: translateX(-50%); - - i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; - } - &::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 2px; - background-color: #eeeeee; - transition: var(--transition); - } - &:hover { - color: var(--startupMainColor); - padding-right: 15px; - - &::before { - width: 100%; - right: auto; - left: 0; - } - } - } - &:hover { - .icon { - transform: translateY(-20px); - color: var(--startupMainColor); - } - h3 { - transform: translateY(-20px); - } - p { - transform: translateY(-20px); - } - .link-btn { - opacity: 1; - bottom: -15px; - visibility: visible; - } - } -} -.features-box { - text-align: center; - margin-bottom: 30px; - background-color: var(--whiteColor); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, .1); - padding: 35px 25px; - transition: var(--transition); - - .icon { - display: block; - width: 100px; - height: 100px; - position: relative; - font-size: 55px; - line-height: 100px; - z-index: 1; - color: var(--appMainColor); - border-radius: 10px; - transition: var(--transition); - margin: { - left: auto; - right: auto; - bottom: 25px; - }; - &::after { - content: ''; - position: absolute; - transition: var(--transition); - left: 0; - right: 0; - top: 0; - bottom: 0; - border-radius: 10px; - z-index: -1; - background-color: var(--appMainColor); - opacity: .10; - } - &.bg-7669ff { - color: #7669ff; - - &::after { - background-color: #7669ff; - } - } - &.bg-ff9f07 { - color: #ff9f07; - - &::after { - background-color: #ff9f07; - } - } - &.bg-89c111 { - color: #89c111; - - &::after { - background-color: #89c111; - } - } - &.bg-2ea7ff { - color: #2ea7ff; - - &::after { - background-color: #2ea7ff; - } - } - &.bg-ca83ff { - color: #ca83ff; - - &::after { - background-color: #ca83ff; - } - } - &.bg-ff5f5f { - color: #ff5f5f; - - &::after { - background-color: #ff5f5f; - } - } - } - h3 { - margin-bottom: 12px; - font-size: 20px; - } - &:hover { - transform: translateY(-10px); - - .icon { - color: var(--whiteColor); - - &::after { - opacity: 1; - } - } - } - &.without-box-shadow { - box-shadow: unset; - } -} -.single-features-item { - margin-bottom: 30px; - position: relative; - padding-left: 70px; - - .icon { - position: absolute; - left: 0; - line-height: 1; - top: 0; - font-size: 45px; - color: var(--consultingMainColor); - } - h3 { - margin-bottom: 0; - font-size: 20px; - } - &.color-white { - h3 { - color: var(--whiteColor); - } - } - &.startup-color { - .icon { - color: var(--startupMainColor); - } - } -} -.features-item { - margin-bottom: 30px; - transition: var(--transition); - border-radius: 10px; - position: relative; - padding: 30px; - z-index: 1; - - .icon { - display: block; - line-height: 1; - transition: var(--transition); - font-size: 35px; - color: var(--itAgencyMainColor); - margin-right: 15px; - } - h3 { - transition: var(--transition); - position: relative; - font-size: 20px; - margin-bottom: 0; - } - p { - transition: var(--transition); - margin-top: 15px; - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - border-radius: 10px; - background-color: var(--itAgencyMainColor); - z-index: -1; - transition: var(--transition); - opacity: 0.07; - } - &::after { - content: ''; - position: absolute; - left: 0; - top: 0; - transition: .3s; - border-radius: 10px; - height: 0; - width: 100%; - z-index: -1; - background: var(--itAgencyGradientColor); - } - &.bg-6d5fff { - .icon { - color: #6d5fff; - } - &::before { - background-color: #6d5fff; - } - } - &.bg-ff9f07 { - .icon { - color: #ff9f07; - } - &::before { - background-color: #ff9f07; - } - } - &.bg-2ea7ff { - .icon { - color: #2ea7ff; - } - &::before { - background-color: #2ea7ff; - } - } - &:hover { - &::before { - opacity: 0; - visibility: hidden; - } - &::after { - height: 100%; - } - p { - color: var(--whiteColor); - } - h3 { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - } - } - .icon { - color: var(--whiteColor); - } - } -} - -/*================================================ -About Area CSS -=================================================*/ -.about-area { - &.with-border { - border-bottom: 1px solid #f0f0f0; - } -} -.startup-about-area { - .container-fluid { - max-width: 1400px; - } -} -.startup-about-image { - z-index: 1; - margin-right: 15px; - text-align: center; - position: relative; - background-color: #f9f4f0; - padding: { - top: 80px; - left: 80px; - bottom: 80px; - }; - .video-btn { - position: absolute; - left: 57%; - display: inline-block; - top: 50%; - transform: translateY(-50%) translateX(-57%); - width: 80px; - height: 80px; - background-color: rgba(0, 0, 0, .75); - border-radius: 50%; - color: var(--whiteColor); - font-size: 30px; - - i { - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); - } - &::after, &::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 2px solid var(--whiteColor); - } - &::before { - animation: ripple 2s linear infinite - } - &::after { - animation: ripple 2s linear 1s infinite - } - } -} -.startup-about-content { - padding-left: 15px; - max-width: 600px; - - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--startupMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 15px; - font-size: 40px; - } - p { - strong { - color: var(--blackColor); - } - } - .features-list { - padding-left: 0; - margin: { - left: 16px; - bottom: 0; - }; - li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; - - &:last-child { - margin-bottom: 0; - } - } - } - .default-btn { - margin-top: 5px; - } -} -.marketing-about-area { - border-bottom: 1px solid #f0f0f0; -} -.marketing-about-image { - margin-right: 15px; - text-align: center; -} -.marketing-about-content { - padding-left: 15px; - - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--marketingMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 15px; - font-size: 40px; - } - .default-btn { - margin-top: 10px; - } - .features-list { - padding-left: 0; - margin: { - left: 16px; - bottom: 0; - }; - li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; - - &:last-child { - margin-bottom: 0; - } - } - } -} -.business-about-content { - padding-left: 15px; - - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--businessMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 15px; - font-size: 40px; - } - .default-btn { - margin-top: 10px; - } -} -.business-about-image { - z-index: 1; - text-align: center; - padding-right: 15px; - position: relative; -} -.app-about-content { - padding-right: 15px; - - h3 { - margin-bottom: 16px; - font-size: 24px; - } - .features-list { - padding-left: 0; - margin-bottom: 15px; - list-style-type: none; - - li { - margin-bottom: 12px; - position: relative; - color: var(--optionalColor); - font-weight: 500; - padding-left: 24px; - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--appMainColor); - } - &:last-child { - margin-bottom: 0; - } - } - } - .default-btn { - margin-top: 10px; - } -} -.app-about-image { - padding-left: 15px; - text-align: center; -} -.saas-about-content { - padding-left: 15px; - - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--saasMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 15px; - font-size: 40px; - } - .features-list { - padding-left: 0; - margin-bottom: 15px; - list-style-type: none; - - li { - margin-bottom: 12px; - position: relative; - color: var(--optionalColor); - font-weight: 500; - padding-left: 24px; - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--saasMainColor); - } - &:last-child { - margin-bottom: 0; - } - } - } - .default-btn { - margin-top: 10px; - } - &.white-color { - color: var(--whiteColor); - - h2 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - opacity: 0.9; - } - .features-list { - li { - color: var(--whiteColor); - opacity: 0.9; - - i { - color: var(--whiteColor); - } - } - } - } -} -.saas-about-image { - padding-right: 15px; - text-align: center; -} -.it-agency-about-content { - padding-left: 15px; - - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--itAgencyMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 15px; - font-size: 40px; - } - .default-btn { - margin-top: 10px; - } - .features-list { - padding-left: 0; - margin: { - left: 16px; - bottom: 15px; - }; - li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; - - &:last-child { - margin-bottom: 0; - } - } - } -} -.it-agency-about-image { - text-align: start; - position: relative; - margin-right: 20px; - padding: { - right: 20px; - bottom: 20px; - }; - .main-image { - position: relative; - padding-bottom: 150px; - - img { - border-radius: 10px; - - &:nth-child(2) { - position: absolute; - right: 0; - bottom: 0; - z-index: 1; - } - } - } -} - -/*================================================ -Services Area CSS -=================================================*/ -.services-area { - &.with-black-border { - border-top: 1px solid #12141a; - border-bottom: 1px solid #12141a; - } -} -.single-services-box { - margin-bottom: 30px; - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - .content { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding: 30px; - z-index: 2; - transition: var(--transition); - - .icon { - line-height: 1; - display: block; - font-size: 50px; - margin-bottom: 20px; - transition: var(--transition); - color: var(--startupMainColor); - } - h3 { - font-size: 20px; - margin-bottom: 10px; - color: var(--whiteColor); - transition: var(--transition); - - a { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - p { - transition: var(--transition); - color: var(--whiteColor); - margin-bottom: 0; - opacity: 0.9; - } - .link-btn { - font-weight: 700; - padding-right: 12px; - position: absolute; - left: 30px; - display: inline-block; - bottom: 0; - color: var(--whiteColor); - opacity: 0; - visibility: hidden; - - i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; - } - &::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 2px; - background-color: #eeeeee; - transition: var(--transition); - } - &:hover { - padding-right: 15px; - - &::before { - width: 100%; - right: auto; - left: 0; - } - } - } - } - &::before { - position: absolute; - height: 100%; - content: ""; - width: 100%; - z-index: 1; - left: 0; - top: 0; - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.7) 35%, rgba(22, 22, 23, 0) 60%, rgba(0, 0, 0, 0) 100%); - } - &:hover { - img { - transform: scale(1.05); - } - .content { - .icon { - transform: translateY(-30px); - } - p { - transform: translateY(-30px); - } - h3 { - transform: translateY(-30px); - } - .link-btn { - opacity: 1; - bottom: 25px; - visibility: visible; - } - } - } -} -.single-services-item { - margin-bottom: 30px; - - .content { - padding: { - top: 25px; - left: 20px; - right: 20px; - }; - h3 { - font-size: 20px; - margin-bottom: 10px; - } - } -} -.services-tabs { - .nav { - padding-left: 0; - margin-bottom: 60px; - border: none; - list-style-type: none; - text-align: center; - justify-content: center; - - .nav-item { - margin: { - left: 42px; - right: 42px; - }; - .nav-link { - display: inline-block; - padding: 0; - border: none; - margin: 0; - font: { - size: 19px; - weight: 700; - }; - .icon { - display: block; - width: 80px; - height: 80px; - position: relative; - z-index: 1; - line-height: 75px; - color: var(--businessMainColor); - font-weight: normal; - transition: var(--transition); - font-size: 35px; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - padding-right: 10px; - margin: { - left: auto; - right: auto; - bottom: 15px; - }; - &::after { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - z-index: -1; - bottom: 0; - background: var(--businessGradientColor); - transition: var(--transition); - opacity: 0.10; - } - } - &:hover, &.active { - color: var(--businessMainColor); - - .icon { - color: var(--whiteColor); - - &::after { - opacity: 1; - } - } - } - } - &:last-child { - margin-right: 0; - } - &:first-child { - margin-left: 0; - } - } - } -} -.single-main-services { - margin-bottom: 30px; - - .content { - padding-right: 50px; - - h3 { - font-size: 40px; - margin-bottom: 15px; - line-height: 1.4; - } - p { - max-width: 540px; - } - .default-btn { - margin-top: 10px; - } - } - .image { - text-align: center; - } -} -.services-box { - text-align: center; - margin-bottom: 30px; - border: 1px solid #eeeeee; - background-color: var(--whiteColor); - padding: 35px 30px; - transition: var(--transition); - - .icon { - font-size: 60px; - line-height: 1; - background: var(--saasGradientColor); - -webkit-background-clip: text; - -moz-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - margin-bottom: 22px; - display: block; - } - h3 { - margin-bottom: 12px; - font-size: 20px; - - a { - &:hover { - color: var(--saasMainColor); - } - } - } - &.color-white { - border-color: #272836; - background-color: #040611; - - h3 { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - } - } - p { - opacity: 0.9; - color: var(--whiteColor); - } - } - &:hover { - border-color: var(--saasMainColor) !important; - } - &.startup-color { - .icon { - background: var(--startupMainColor); - -webkit-background-clip: text; - -moz-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - } - &:hover { - border-color: var(--startupMainColor) !important; - } - } -} -.services-item { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - opacity: 0; - visibility: hidden; - z-index: 1; - transition: var(--transition); - background-color: var(--consultingMainColor); - } - h3 { - font-size: 20px; - transition: var(--transition); - margin: { - top: 25px; - bottom: 0; - }; - } - .hover-content { - left: 0; - right: 0; - top: 50%; - padding: 30px; - position: absolute; - transform: translateY(-50%); - z-index: 2; - opacity: 0; - visibility: hidden; - transition: var(--transition); - color: var(--whiteColor); - - h3 { - opacity: 1 !important; - visibility: visible !important; - color: var(--whiteColor); - margin: { - top: 0; - bottom: 12px; - }; - a { - color: var(--whiteColor); - } - } - p { - color: var(--whiteColor); - } - .link-btn { - font-weight: 700; - position: relative; - display: inline-block; - padding-right: 12px; - color: var(--whiteColor); - - i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; - } - &::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--whiteColor); - transition: var(--transition); - } - &:hover { - padding-right: 15px; - - &::before { - width: 100%; - right: auto; - left: 0; - } - } - } - } - &:hover { - img { - transform: scale(1.7); - } - h3 { - opacity: 0; - visibility: hidden; - } - .hover-content { - opacity: 1; - visibility: visible; - } - &::before { - opacity: .90; - visibility: visible; - } - } - &.startup-color { - &::before { - background-color: var(--startupMainColor); - } - } -} -.col-lg-3 { - &:nth-child(2), &:nth-child(4) { - .services-box { - margin-top: 30px; - } - } - &:nth-child(5), &:nth-child(7) { - .services-box { - margin-top: -30px; - } - } -} -.single-it-services-box { - margin-bottom: 30px; - border-radius: 10px; - position: relative; - padding: 30px 30px 30px 100px; - z-index: 1; - - .icon { - position: absolute; - left: 30px; - top: 30px; - line-height: 1; - font-size: 45px; - color: var(--itAgencyMainColor); - transition: var(--transition); - } - h3 { - transition: var(--transition); - margin-bottom: 12px; - font-size: 20px; - - a { - display: inline-block; - - &:hover { - color: var(--itAgencyGradientColor); - } - } - } - p { - transition: var(--transition); - } - &::before { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - opacity: .06; - position: absolute; - border-radius: 10px; - transition: var(--transition); - background-color: var(--itAgencyMainColor); - } - &.bg-6d5fff { - .icon { - color: #6d5fff; - } - &::before { - background-color: #6d5fff; - } - } - &.bg-ff9f07 { - .icon { - color: #ff9f07; - } - &::before { - background-color: #ff9f07; - } - } - &.bg-89c111 { - .icon { - color: #89c111; - } - &::before { - background-color: #89c111; - } - } - &.bg-ca83ff { - .icon { - color: #ca83ff; - } - &::before { - background-color: #ca83ff; - } - } - &.bg-ff5f5f { - .icon { - color: #ff5f5f; - } - &::before { - background-color: #ff5f5f; - } - } - &.bg-2ea7ff { - .icon { - color: #2ea7ff; - } - &::before { - background-color: #2ea7ff; - } - } - &:hover { - .icon { - color: var(--whiteColor) !important; - } - h3 { - color: var(--whiteColor) !important; - - a { - color: var(--whiteColor) !important; - } - } - p { - color: var(--whiteColor); - } - &::before { - opacity: 1; - visibility: visible; - } - } -} - -/*================================================ -Services Details Area CSS -=================================================*/ -.services-details-desc { - h3 { - font-size: 24px; - margin: { - bottom: 15px; - top: 30px; - }; - &:first-child { - margin-top: 0; - } - } - ul { - li { - margin-bottom: 12px; - color: var(--optionalColor); - font-weight: 500; - - &:last-child { - margin-bottom: 0; - } - } - } - p { - strong { - color: var(--blackColor); - } - a { - color: var(--startupMainColor); - - &:hover { - text-decoration: underline; - } - } - } - img { - margin: { - top: 20px; - bottom: 20px; - }; - } -} -.services-details-content { - h2 { - font-size: 40px; - line-height: 1.4; - margin-bottom: 15px; - } -} -.services-details-video { - margin-left: 30px; - text-align: center; - background-color: #f5f5f5; - - iframe { - width: 100%; - border: none; - height: 336px; - display: block; - } - .default-btn { - margin: { - top: 20px; - left: 30px; - right: 30px; - bottom: 20px; - }; - } -} -.services-details-overview { - .overview-content { - h2 { - font-size: 40px; - line-height: 1.4; - margin-bottom: 15px; - } - } -} - -/*================================================ -Works Area CSS -=================================================*/ -.startup-works-area { - .section-title { - max-width: 100%; - text-align: start; - padding-top: 100px; - margin: { - left: 0; - right: 0; - bottom: -220px; - }; - h2 { - font-size: 35px; - } - } -} -.startup-works-content { - padding: { - top: 245px; - right: 15px; - }; - h3 { - font-size: 20px; - margin-bottom: 15px; - - a { - display: inline-block; - - &:hover { - color: var(--startupMainColor); - } - } - } - .custom-owl-nav { - margin-top: 25px; - - [class*=owl-] { - margin: 0 5px; - background-color: var(--startupMainColor); - color: var(--whiteColor); - padding: 8px 25px 7px !important; - border-radius: 0; - transition: var(--transition); - border: none; - cursor: pointer; - font-size: var(--fontSize); - - i { - line-height: 1; - } - &.owl-prev { - margin-left: 0; - } - &.owl-next { - margin-right: 0; - } - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } -} -.startup-works-image { - z-index: 1; - margin-left: 15px; - text-align: center; - position: relative; - background-color: #f9f4f0; - padding: { - top: 80px; - right: 80px; - bottom: 80px; - }; - .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 1; - display: block; - } -} -.single-works-box { - overflow: hidden; - position: relative; - margin-bottom: 30px; - - .works-image { - position: relative; - - img { - transition: var(--transition); - } - .works-overlay { - position: absolute; - overflow: hidden; - top: 10px; - left: 10px; - right: 10px; - bottom: 10px; - - &::before { - content: ''; - transition: var(--transition); - position: absolute; - width: 100%; - bottom: 0; - height: 50%; - background: var(--businessGradientColor); - right: -100%; - } - &::after { - content: ''; - transition: var(--transition); - position: absolute; - background: var(--businessGradientColor); - top: 0; - height: 50%; - width: 100%; - left: -100%; - } - h3 { - color: transparent; - overflow: hidden; - position: absolute; - top: 30px; - text-align: center; - right: 25px; - z-index: 2; - transition: var(--transition); - font-size: 20px; - margin-bottom: 5px; - - &::before { - content: ''; - background: var(--blackColor); - position: absolute; - width: 100%; - bottom: 0; - transition: var(--transition); - left: 100%; - top: 0; - right: auto; - } - } - span { - color: transparent; - overflow: hidden; - z-index: 2; - position: absolute; - top: 62px; - display: block; - right: 25px; - transition: var(--transition); - text-align: center; - - &::before { - content: ''; - background: var(--whiteColor); - width: 100%; - top: 0; - bottom: 0; - position: absolute; - transition: var(--transition); - right: 100%; - } - } - } - } - .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 5; - } - &:hover { - .works-image { - img { - transform: scale(1.3); - } - .works-overlay { - &::before { - right: 0; - opacity: 1; - width: 100%; - height: 50%; - transition: all .8s; - } - &::after { - left: 0; - opacity: 1; - transition: all .8s; - } - span { - color: var(--whiteColor); - - &::before { - right: -100%; - } - } - h3 { - color: var(--whiteColor); - - &::before { - left: -100%; - } - } - } - } - } - &.startup-color { - .works-image { - .works-overlay { - &::before { - background: var(--startupMainColor); - } - &::after { - background: var(--startupMainColor); - } - } - } - } -} -.works-box { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - .link-btn { - display: block; - left: 0; - right: 0; - top: 0; - bottom: 0; - position: absolute; - z-index: 2; - } - h3 { - margin-bottom: 0; - font-size: 20px; - position: absolute; - left: 0; - right: 0; - top: 50%; - opacity: 0; - visibility: hidden; - transition: var(--transition); - transform: translateY(-50%); - color: var(--whiteColor); - background-color: rgba(0, 0, 0, .75); - padding: 15px; - margin: { - left: 25px; - right: 25px; - top: 25px; - }; - } - &:hover { - img { - transform: scale(1.05); - } - h3 { - margin-top: 0; - visibility: visible; - opacity: 1; - } - } -} -.works-box-btn { - text-align: center; - margin-top: 20px; -} -.single-works-item { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: var(--startupMainColor); - z-index: 1; - opacity: 0; - visibility: hidden; - transition: .3s; - } - .content { - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); - margin-top: 15px; - transition: var(--transition); - opacity: 0; - visibility: hidden; - z-index: 2; - padding: { - left: 20px; - right: 20px; - }; - .link-btn { - width: 45px; - height: 45px; - line-height: 45px; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - border-radius: 50%; - margin-bottom: 20px; - display: inline-block; - - &:hover { - color: var(--blackColor); - background-color: var(--whiteColor); - } - } - h3 { - font-size: 22px; - color: var(--whiteColor); - margin-bottom: 12px; - - a { - color: var(--whiteColor); - } - } - span { - display: block; - color: var(--whiteColor); - opacity: 0.9; - } - } - &:hover { - img { - transform: scale(1.1); - } - &::before { - opacity: 1; - margin: 10px; - visibility: visible; - } - .content { - opacity: 1; - visibility: visible; - margin-top: 0; - } - } - &.minimal-portfolio-color { - &::before { - background-color: var(--minimalPortfolioColor); - } - } -} -.showMoreItemsList + .button-box { - margin-bottom: 0; - text-align: start; - - button { - background-color: transparent; - border: 1px solid #d6d9dc; - color: var(--blackColor); - padding: 12px 45px; - transition: var(--transition); - display: inline-block; - width: auto; - margin-top: 10px; - border-radius: 0; - font: { - size: var(--fontSize); - weight: 500; - }; - &:hover { - background-color: var(--startupMainColor); - border-color: var(--startupMainColor); - color: var(--whiteColor); - } - } -} -.works-slides-style-one, .works-slides-style-two { - &.owl-theme { - .single-works-item, .works-box { - margin-bottom: 0; - } - .owl-nav { - opacity: 0; - margin-top: 0; - visibility: hidden; - transition: var(--transition); - - [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--blackColor); - padding: 0 !important; - border-radius: 0; - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: 25px; - top: 50%; - width: 45px; - height: 45px; - line-height: 45px; - border-radius: 50%; - transform: translateY(-50%); - font-size: 20px; - - &.owl-next { - left: auto; - right: 25px; - } - i { - line-height: 1; - } - &:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - &:hover { - .owl-nav { - opacity: 1; - visibility: visible; - } - } - .owl-dots { - margin-top: 40px; - - .owl-dot { - span { - width: 18px; - height: 18px; - border-radius: 50%; - transition: var(--transition); - margin: 0 5px; - } - &:hover, &.active { - span { - background-color: var(--startupMainColor); - } - } - } - } - } -} - -/*================================================ -What We Do Area CSS -=================================================*/ -.what-we-do-content { - .section-title { - text-align: start; - max-width: 100%; - margin: { - left: 0; - right: 0; - bottom: 30px; - }; - p { - max-width: 100%; - margin: { - left: 0; - right: 0; - }; - } - } - .single-what-we-do-box { - margin-bottom: 0; - } -} -.single-what-we-do-box { - margin-bottom: 30px; - - .icon { - display: block; - line-height: 1; - font-size: 45px; - margin-bottom: 25px; - color: var(--consultingMainColor); - } - h3 { - font-size: 20px; - margin-bottom: 12px; - } -} - -/*================================================ -Categories Area CSS -=================================================*/ -.single-categories-box { - margin-bottom: 30px; - overflow: hidden; - position: relative; - - img { - transition: var(--transition); - } - h3 { - margin-bottom: 0; - font-size: 20px; - position: absolute; - left: 25px; - top: 25px; - background-color: var(--whiteColor); - padding: 7px 20px; - clip-path: polygon(5% 0, 100% 0, 95% 100%, 0% 100%); - transition: var(--transition); - } - .link-btn { - display: block; - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - z-index: 2; - } - &:hover { - img { - transform: scale(1.1); - } - h3 { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } -} -.categories-box { - margin-bottom: 30px; - position: relative; - text-align: center; - overflow: hidden; - - img { - transition: var(--transition); - } - h3 { - top: 50%; - left: 50%; - margin-bottom: 0; - position: absolute; - transform: translateX(-50%) translateY(-50%); - font-size: 20px; - z-index: 1; - background-color: rgba(255, 255, 255, .80); - transition: var(--transition); - padding: { - top: 5px; - bottom: 5px; - left: 30px; - right: 30px; - }; - } - .link-btn { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - display: block; - z-index: 2; - } - &:hover { - img { - transform: scale(1.05); - } - h3 { - background-color: var(--whiteColor); - } - } -} -.categories-slides { - &.owl-theme { - .owl-nav { - margin-top: 0; - opacity: 0; - visibility: hidden; - transition: var(--transition); - - [class*=owl-] { - margin: 0; - background-color: transparent; - color: var(--optionalColor); - padding: 0 !important; - border-radius: 0; - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: -45px; - top: 50%; - width: 35px; - height: 35px; - line-height: 35px; - border-radius: 50%; - transform: translateY(-50%); - border: 1px solid var(--optionalColor); - font-size: var(--fontSize); - - &.owl-next { - left: auto; - right: -45px; - } - i { - line-height: 1; - } - &:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - &:hover { - .owl-nav { - opacity: 1; - visibility: visible; - } - } - } -} - -/*================================================ -Who We Are Area CSS -=================================================*/ -.who-we-are-area { - .row { - margin: { - left: 0; - right: 0; - }; - .col-lg-6 { - padding: { - left: 0; - right: 0; - }; - } - } -} -.who-we-are-content { - padding: 40px; - background-color: #f3f4f7; - - .sub-title { - display: block; - margin-bottom: 10px; - color: var(--consultingMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 15px; - line-height: 1.5; - font-size: 21px; - } - .default-btn { - margin-top: 10px; - } -} -.who-we-are-image { - text-align: center; - position: relative; - - .video-btn { - top: 50%; - left: 50%; - width: 80px; - height: 80px; - font-size: 30px; - border-radius: 50%; - position: absolute; - display: inline-block; - color: var(--blackColor); - background-color: var(--whiteColor); - transform: translateY(-50%) translateX(-50%); - - i { - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); - } - &::after, &::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 1.5px solid var(--saasMainColor); - } - &::before { - animation: ripple 2s linear infinite - } - &::after { - animation: ripple 2s linear 1s infinite - } - } -} - -/*================================================ -Video Area CSS -=================================================*/ -.video-box { - text-align: center; - position: relative; - - img { - width: 100%; - } - .video-btn { - position: absolute; - left: 50%; - display: inline-block; - top: 40%; - transform: translateY(-40%) translateX(-50%); - width: 80px; - height: 80px; - background-color: var(--whiteColor); - border-radius: 50%; - color: var(--blackColor); - font-size: 30px; - - i { - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); - } - &::after, &::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - z-index: -1; - bottom: 0; - left: 0; - border-radius: 50%; - border: 2px solid var(--saasMainColor); - } - &::before { - animation: ripple 2s linear infinite - } - &::after { - animation: ripple 2s linear 1s infinite - } - } -} - -/*================================================ -Process Area CSS -=================================================*/ -.single-process-box { - margin-bottom: 30px; - text-align: center; - border: 1px dashed #e1e1e1; - transition: var(--transition); - padding: 40px 25px; - - img { - margin-bottom: 30px; - transition: var(--transition); - } - h3 { - margin-bottom: 12px; - font-size: 20px; - } - &:hover { - box-shadow: 3px 5px 25px 0 rgba(0,0,0,.1); - border-color: var(--whiteColor); - - img { - transform: scale(0.9); - } - } -} - -/*================================================ -Funfacts Area CSS -=================================================*/ -.funfacts-area { - &.bg-business-color { - background: var(--businessGradientColor); - } - &.bg-startup-color { - background: var(--startupMainColor); - - .single-funfacts-box { - &.without-bg-color { - border-color: var(--whiteColor); - } - } - } - &.bg-image { - background: { - color: var(--blackColor); - image: url(../../assets/img/bg8.jpg); - position: center center; - size: cover; - repeat: no-repeat; - }; - } - &.bg-f9f9f9 { - position: relative; - z-index: 1; - - &::before { - left: 0; - right: 0; - bottom: 0; - height: 50%; - z-index: -1; - content: ''; - position: absolute; - background-color: var(--whiteColor); - } - } -} -.single-funfacts-box { - margin-bottom: 30px; - color: var(--whiteColor); - position: relative; - z-index: 1; - overflow: hidden; - background-color: var(--blackColor); - padding: { - top: 40px; - bottom: 40px; - left: 85px; - right: 30px; - }; - .icon { - position: absolute; - left: 25px; - top: 50%; - transform: translateY(-50%); - color: var(--startupMainColor); - font-size: 45px; - } - h3 { - color: var(--whiteColor); - margin-bottom: 4px; - font-size: 40px; - - .odometer-formatting-mark { - display: none; - } - .sign { - position: relative; - top: 1px; - } - } - .sub-title { - display: block; - font-weight: 600; - } - .shape { - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - z-index: -1; - } - &.bg-white { - color: var(--optionalColor); - background-color: var(--whiteColor); - box-shadow: 3px 5px 25px 0 rgba(0,0,0,.1); - - .icon { - &.color-fd5444 { - color: #fd5444; - } - &.color-3251ad { - color: #3251ad; - } - &.color-62bd5e { - color: #62bd5e; - } - &.color-ff6720 { - color: #ff6720; - } - } - h3 { - color: var(--blackColor); - } - .shape { - opacity: .15; - } - } - &.without-bg-color { - background-color: transparent; - border-right: 1px dashed #a684f5; - margin-right: 40px; - padding: { - right: 0; - top: 15px; - bottom: 15px; - left: 65px; - }; - .icon { - left: 0; - color: var(--whiteColor); - } - } - &.text-center { - border-right: 2px dashed rgba(255, 255, 255, .20); - background-color: transparent; - padding: { - left: 0; - top: 20px; - bottom: 20px; - }; - } - &.consulting-color { - h3 { - color: var(--consultingMainColor); - } - } -} -.funfacts-inner { - background-color: var(--blackColor); - padding: { - left: 30px; - right: 30px; - }; - &.bg-app-image { - background: { - image: url(../../assets/img/bg4.jpg); - position: center center; - size: cover; - repeat: no-repeat; - }; - .single-funfacts-box { - &.without-bg-color { - padding: 0; - border: none; - text-align: center; - margin-right: 0; - } - } - } - &.it-agency-color { - background: var(--itAgencyGradientColor); - background: { - size: cover; - repeat: no-repeat; - position: center center; - image: url(../../assets/img/bg9.jpg); - }; - } - &.border-radius { - border-radius: 10px; - - &.ptb-70 { - padding-bottom: 40px; - } - } -} -.row { - .col-lg-3 { - &:last-child { - .single-funfacts-box { - &.without-bg-color, &.text-center { - border-right: none; - } - } - } - } -} - -/*================================================ -Products Area CSS -=================================================*/ -.products-area { - &.with-top-border { - border-top: 1px solid #eeeeee; - } -} -.single-products-box { - margin-bottom: 30px; - text-align: center; - - .image { - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - .new { - position: absolute; - left: 12px; - top: 12px; - background-color: var(--blackColor); - color: var(--whiteColor); - width: 45px; - height: 45px; - text-align: center; - line-height: 45px; - border-radius: 50%; - z-index: 2; - font: { - size: 13px; - weight: 600; - }; - } - .sale { - position: absolute; - z-index: 2; - right: 12px; - top: 12px; - background-color: red; - color: var(--whiteColor); - width: 45px; - height: 45px; - text-align: center; - line-height: 45px; - border-radius: 50%; - font: { - size: 13px; - weight: 600; - }; - } - .buttons-list { - position: absolute; - text-align: center; - z-index: 1; - left: 0; - right: 0; - bottom: 20px; - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - opacity: 0; - visibility: hidden; - display: inline-block; - transform: translateY(30px); - transition: all 0.3s ease-in-out 0s; - margin: { - left: 3px; - right: 3px; - }; - a { - display: inline-block; - font-size: 16px; - color: var(--blackColor); - position: relative; - width: 40px; - border-radius: 50%; - line-height: 39px; - background-color: var(--whiteColor); - height: 40px; - border: 1px solid #eeeeee; - - .tooltip-label { - right: 50%; - top: -15px; - transform: translateX(50%); - position: absolute; - padding: 0 10px; - line-height: 28px; - font-size: 12px; - letter-spacing: .3px; - visibility: hidden; - white-space: nowrap; - opacity: 0; - color: transparent; - background-color: transparent; - transition: all .2s ease-in-out; - background-color: var(--blackColor); - color: var(--whiteColor); - - &::before { - content: "\f0dd"; - color: var(--blackColor); - left: 50%; - position: absolute; - transform: translateX(-50%) rotate(0deg); - transition: all .2s ease-in-out; - bottom: -12px; - font: { - family: "Font Awesome 5 Free"; - variant-ligatures: normal; - variant-caps: normal; - style: normal; - weight: 900; - size: 20px; - }; - } - } - &:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); - - .tooltip-label { - visibility: visible; - opacity: 1; - top: -45px; - } - } - } - &:nth-child(1) { - transition-delay: 0.1s; - } - &:nth-child(2) { - transition-delay: 0.2s; - } - &:nth-child(3) { - transition-delay: 0.3s; - } - } - } - } - .content { - margin-top: 25px; - - .rating { - margin-bottom: 12px; - - i { - color: #fec900; - line-height: 1; - } - } - h3 { - margin-bottom: 13px; - font-size: 20px; - } - .price { - span { - color: var(--blackColor); - font: { - size: 17px; - weight: 700; - }; - &.old-price { - color: var(--optionalColor); - text-decoration: line-through; - } - } - } - } - &:hover { - .image { - img { - transform: scale(1.05); - } - .buttons-list { - li { - transform: translateY(0); - visibility: visible; - opacity: 1; - } - } - } - } -} -.noke-grid-sorting { - margin-bottom: 40px; - - .result-count { - p { - .count { - font-weight: 700; - color: var(--blackColor); - } - } - } - .ordering { - text-align: end; - - label { - display: inline-block; - color: var(--optionalColor); - margin: { - bottom: 0; - right: 5px; - }; - } - select { - width: 215px; - height: 45px; - cursor: pointer; - line-height: 45px; - display: inline-block; - background: #f8f8f8; - border-color: #eeeeee; - color: var(--blackColor); - transition: var(--transition); - padding: { - top: 1px; - left: 12px; - }; - font: { - size: var(--fontSize); - weight: 600; - }; - } - } -} - -/*================================================ -Products Details Area CSS -=================================================*/ -.products-details-desc { - padding-left: 15px; - - h3 { - margin-bottom: 15px; - font-size: 20px; - } - .price { - color: var(--blackColor); - margin: { - top: 15px; - bottom: 12px; - }; - font: { - weight: 700; - size: 17px; - }; - .old-price { - text-decoration: line-through; - color: #9b9494; - font-weight: normal; - } - } - .products-review { - .rating { - display: inline-block; - padding-right: 3px; - font-size: 14px; - - i { - display: inline-block; - color: #f49f0b; - line-height: 1; - } - } - .rating-count { - display: inline-block; - color: var(--optionalColor); - line-height: initial; - position: relative; - top: 1px; - - &:hover { - color: var(--blackColor); - } - } - } - .products-meta { - margin-top: 20px; - - span { - display: block; - font-weight: 500; - margin-bottom: 10px; - color: var(--optionalColor); - - span { - display: inline-block; - margin-bottom: 0; - - &.sku { - color: var(--startupMainColor); - } - } - a { - display: inline-block; - color: var(--blackColor); - - &:hover, &:focus { - color: var(--startupMainColor); - } - } - &.in-stock { - color: green; - } - &:last-child { - margin-bottom: 0; - } - } - } - .products-add-to-cart { - margin-top: 25px; - - .quantities { - position: relative; - padding-left: 45px; - - .sub-title { - left: 0; - top: 50%; - display: block; - font-weight: 600; - position: absolute; - transform: translateY(-50%); - } - } - .input-counter { - display: block; - max-width: 120px; - min-width: 120px; - text-align: center; - position: relative; - margin-bottom: 25px; - - span { - top: 0; - width: 35px; - height: 100%; - cursor: pointer; - font-size: 15px; - line-height: 38px; - position: absolute; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - transition: var(--transition); - - i { - position: relative; - top: 1px; - } - &.minus-btn { - left: 0; - } - &.plus-btn { - right: 0; - } - &:hover { - color: var(--optionalColor); - } - } - input { - height: 40px; - outline: 0; - width: 100%; - display: block; - text-align: center; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - font: { - size: 17px; - weight: 600; - } - &::placeholder { - color: var(--blackColor); - } - } - } - } - .default-btn { - box-shadow: unset !important; - } - .social-share { - padding-left: 0; - list-style-type: none; - margin: { - bottom: 0; - top: 30px; - }; - li { - display: inline-block; - margin-right: 12px; - - span { - font-weight: 600; - position: relative; - top: -1px; - } - a { - line-height: 1; - font-size: 18px; - color: var(--optionalColor); - - &:hover { - color: var(--blackColor); - } - } - } - } -} -.products-details-tabs { - margin-top: 50px; - - .single-tabs-box { - margin-bottom: 40px; - - h2 { - text-align: center; - margin-bottom: 30px; - position: relative; - font-size: 22px; - z-index: 1; - - span { - display: inline-block; - background-color: var(--whiteColor); - padding: { - left: 20px; - right: 20px; - }; - } - &::before { - left: 0; - right: 0; - top: 50%; - z-index: -1; - height: 1px; - content: ''; - margin-top: 1px; - position: absolute; - background-color: #eeeeee; - transform: translateY(-50%); - } - } - &:last-child { - margin-bottom: 0; - } - .inner-box { - max-width: 800px; - margin: { - left: auto; - right: auto; - }; - h3 { - font-size: 20px; - margin-bottom: 15px; - } - ul { - margin: { - top: 20px; - bottom: 0; - }; - li { - font-weight: 500; - margin-bottom: 12px; - color: var(--optionalColor); - - &:last-child { - margin-bottom: 0; - } - } - } - .table-striped { - margin-bottom: 0; - - tbody { - tr { - td { - border: none; - padding: 15px; - color: var(--optionalColor); - font: { - weight: 500; - size: var(--fontSize); - }; - } - } - } - } - .products-review-comments { - margin-top: 40px; - - .user-review { - position: relative; - margin-bottom: 20px; - padding: 0 0 20px 110px; - border-bottom: 1px solid #eeeeee; - - img { - top: 0; - left: 0; - width: 90px; - height: 90px; - border-radius: 5px; - position: absolute; - } - .sub-comment { - margin-bottom: 8px; - font: { - weight: 600; - family: var(--headingFontFamily); - }; - } - .review-rating { - display: block; - margin-bottom: 8px; - - .review-stars { - display: inline-block; - - i { - color: #cecfd2; - display: inline-block; - - &.checked { - color: orange; - } - } - } - } - } - } - .review-form-wrapper { - margin-top: 30px; - - h3 { - margin-bottom: 10px; - } - .comment-notes { - span { - color: red; - } - } - form { - margin-top: 20px; - - .form-group { - margin-bottom: 25px; - text-align: start; - } - .rating { - text-align: start; - overflow: hidden; - max-width: 115px; - margin: { - top: -5px; - bottom: 25px; - }; - label { - float: right; - position: relative; - width: 23px; - height: 23px; - cursor: pointer; - - &:not(:first-of-type) { - padding-right: 5px; - } - &:before { - content: "\2605"; - transition: var(--transition); - font-size: 27px; - color: #CCCCCC; - line-height: 1; - } - } - input { - display: none; - } - input:checked ~ label:before, &:not(:checked) > label:hover:before, &:not(:checked) > label:hover ~ label:before { - color: #f6b500; - } - } - .comment-form-cookies-consent { - text-align: start; - margin-bottom: 0; - - [type="checkbox"]:checked, [type="checkbox"]:not(:checked) { - display: none; - } - [type="checkbox"]:checked + label, [type="checkbox"]:not(:checked) + label { - position: relative; - padding-left: 28px; - cursor: pointer; - line-height: 20px; - display: inline-block; - margin-bottom: 0; - color: var(--optionalColor); - } - [type="checkbox"]:checked + label:before, [type="checkbox"]:not(:checked) + label:before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 19px; - height: 19px; - transition: all 0.2s ease; - border: 1px solid #eeeeee; - border-radius: 3px; - background: var(--whiteColor); - } - [type="checkbox"]:checked + label:after, [type="checkbox"]:not(:checked) + label:after { - content: ''; - width: 8px; - height: 8px; - background: var(--startupMainColor); - position: absolute; - top: 6.5px; - left: 6px; - -webkit-transition: all 0.2s ease; - transition: all 0.2s ease; - } - [type="checkbox"]:not(:checked) + label:after { - opacity: 0; - -webkit-transform: scale(0); - transform: scale(0); - } - [type="checkbox"]:checked + label:after { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); - } - [type="checkbox"]:hover + label:before { - border-color: var(--startupMainColor); - } - [type="checkbox"]:checked + label:before { - border-color: var(--startupMainColor); - } - } - button { - margin-top: 22px; - border: none; - display: inline-block; - text-align: center; - overflow: hidden; - color: var(--whiteColor); - background-color: var(--startupMainColor); - transition: var(--transition); - border-radius: 5px; - padding: 13px 40px; - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - font: { - weight: 700; - size: var(--fontSize); - }; - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } - } - } -} - -/*================================================ -Cart Area CSS -=================================================*/ -.cart-table-desc { - table { - margin-bottom: 0; - - thead { - tr { - th { - border: none; - padding: 13px 20px; - vertical-align: middle; - color: var(--whiteColor); - background-color: var(--blackColor); - font: { - family: var(--headingFontFamily); - size: var(--fontSize); - weight: 600; - }; - } - } - } - tbody { - tr { - td { - color: var(--optionalColor); - vertical-align: middle; - font: { - size: 15px; - weight: 600; - }; - padding: { - left: 0; - right: 0; - top: 15px; - bottom: 15px; - }; - border: { - left: none; - right: none; - color: #ebebeb; - }; - &.product-thumbnail { - a { - display: inline-block; - - img { - width: 80px; - } - } - } - &.product-name { - a { - display: inline-block; - } - } - &.product-subtotal { - .remove { - top: 2px; - color: red; - line-height: 1; - font-size: 20px; - margin-left: 40px; - position: relative; - } - } - &.product-quantity { - .input-counter { - max-width: 120px; - min-width: 120px; - text-align: center; - display: inline-block; - position: relative; - - span { - top: 0; - width: 35px; - height: 100%; - font-size: 14px; - line-height: 37px; - position: absolute; - cursor: pointer; - color: var(--blackColor); - background-color: transparent; - transition: var(--transition); - border: 1px solid #eeeeee; - - i { - position: relative; - top: 1px; - } - &.minus-btn { - left: 0; - } - &.plus-btn { - right: 0; - } - &:hover { - color: var(--mainColor); - } - } - input { - height: 40px; - outline: 0; - width: 100%; - display: block; - text-align: center; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - font: { - size: 17px; - weight: 600; - } - &::placeholder { - color: var(--blackColor); - } - } - } - } - &.product-subtotal { - overflow: hidden; - } - } - &:last-child { - td { - border-bottom: none; - } - } - } - } - } -} -.cart-buttons-desc { - .shopping-coupon-code { - position: relative; - - .form-control { - height: 50px; - } - button { - top: 0; - right: 0; - height: 50px; - border: none; - padding: 0 25px; - position: absolute; - color: var(--whiteColor); - border-radius: 0 5px 5px 0; - transition: var(--transition); - background-color: var(--blackColor); - font: { - size: var(--fontSize); - weight: 700; - }; - &:hover { - background-color: var(--startupMainColor); - } - } - } - .default-btn { - box-shadow: unset; - } -} -.cart-totals-desc { - padding: 30px; - background-color: #f5f5f5; - - h3 { - padding: 16px 20px; - margin-bottom: 30px; - color: var(--whiteColor); - font-size: var(--fontSize); - background-color: var(--blackColor); - margin: { - top: -30px; - left: -30px; - right: -30px; - }; - } - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - overflow: hidden; - display: block; - margin-bottom: 12px; - padding-bottom: 12px; - font-weight: 500; - color: var(--optionalColor); - border-bottom: 1px solid #ebebeb; - - span { - float: right; - } - &:last-child { - font-weight: 700; - color: var(--blackColor); - padding-bottom: 0; - margin-bottom: 0; - border-bottom: none; - } - } - } - .default-btn { - margin-top: 25px; - box-shadow: unset; - } -} - -/*================================================ -Checkout Area CSS -=================================================*/ -.billing-details-desc { - padding: 30px 30px 10px; - background-color: #f5f5f5; - - h3 { - font-size: 20px; - margin-bottom: 30px; - color: var(--whiteColor); - background-color: var(--blackColor); - padding: { - top: 17px; - left: 30px; - right: 30px; - bottom: 17px; - }; - margin: { - right: -30px; - left: -30px; - top: -30px; - }; - } - .form-group { - margin-bottom: 20px; - - label { - display: block; - font-weight: 500; - margin-bottom: 12px; - color: var(--blackColor); - - .required { - color: red; - } - } - .form-control { - background-color: var(--whiteColor) !important; - } - select { - background-color: var(--whiteColor); - height: 50px; - cursor: pointer; - border: none; - display: block; - width: 100%; - padding-left: 15px; - } - } -} -.order-details-desc { - padding: 30px; - background-color: #f5f5f5; - - h3 { - font-size: 20px; - margin-bottom: 30px; - color: var(--whiteColor); - background-color: var(--blackColor); - padding: { - left: 30px; - right: 30px; - top: 17px; - bottom: 17px; - }; - margin: { - left: -30px; - right: -30px; - top: -30px; - }; - } - .order-details { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - display: block; - font-weight: 500; - overflow: hidden; - margin-bottom: 12px; - padding-bottom: 12px; - color: var(--optionalColor); - border-bottom: 1px solid #ebebeb; - - span { - float: right; - } - &:last-child { - font-weight: 700; - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; - color: var(--blackColor); - } - } - } - .payment-method { - margin: { - top: 30px; - bottom: 20px; - }; - p { - margin-bottom: 12px; - - &:last-child { - margin-bottom: 0; - } - [type="radio"] { - &:checked, &:not(:checked) { - display: none; - } - } - [type="radio"] { - &:checked, &:not(:checked) { - + label { - padding-left: 27px; - cursor: pointer; - display: block; - color: var(--blackColor); - position: relative; - margin-bottom: 8px; - font: { - size: var(--fontSize); - weight: 700; - }; - &::before { - content: ''; - position: absolute; - left: 0; - top: 5px; - width: 18px; - height: 18px; - border: 1px solid #dddddd; - border-radius: 50%; - background: var(--whiteColor); - } - &::after { - content: ''; - width: 12px; - height: 12px; - background: var(--startupMainColor); - position: absolute; - top: 8px; - left: 3px; - border-radius: 50%; - transition: var(--transition); - } - } - } - } - [type="radio"] { - &:not(:checked) { - + label { - &::after { - opacity: 0; - visibility: hidden; - transform: scale(0); - } - } - } - } - [type="radio"] { - &:checked { - + label { - &::after { - opacity: 1; - visibility: visible; - transform: scale(1); - } - } - } - } - } - } - .form-check { - color: var(--optionalColor); - font-weight: 500; - - a { - color: var(--startupMainColor); - - &:hover { - text-decoration: underline; - } - } - } - .default-btn { - margin-top: 25px; - display: block; - width: 100%; - box-shadow: unset; - border-radius: 5px; - padding: { - top: 13px; - bottom: 13px; - left: 30px; - right: 30px; - }; - } -} - -/*================================================ -Testimonials Area CSS -=================================================*/ -.testimonials-area { - &.bg-image { - background: { - image: url(../../assets/img/bg10.jpg); - position: center center; - repeat: no-repeat; - size: cover; - }; - .section-title { - &.style-three { - h2 { - span { - background-color: #f7eee8; - } - } - } - } - } -} -.testimonials-inner { - background-color: var(--blackColor); - padding: { - left: 60px; - right: 60px; - }; - &.bg-app-image { - background: { - image: url(../../assets/img/bg5.jpg); - position: center center; - size: cover; - repeat: no-repeat; - }; - } - .testimonials-item { - max-width: 650px; - box-shadow: unset; - background-color: transparent; - padding: 0; - margin: { - left: auto; - right: auto; - }; - .icon { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - } - .info { - .title { - h3 { - color: var(--whiteColor); - } - span { - color: var(--whiteColor); - opacity: 0.9; - } - } - } - } - .testimonials-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--appMainColor); - padding: 0 !important; - border-radius: 50%; - border: none; - font-size: 20px; - width: 50px; - height: 50px; - line-height: 50px; - - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } - } - &.it-agency-color { - background-color: transparent; - - .testimonials-item { - .icon { - color: var(--itAgencyMainColor); - } - p { - color: var(--blackColor); - } - .info { - .title { - h3 { - color: var(--blackColor); - } - span { - color: var(--optionalColor); - opacity: 1; - } - } - } - } - .testimonials-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - background: var(--itAgencyGradientColor); - color: var(--whiteColor); - } - } - } - } - } - &.minimal-portfolio-color { - background-color: transparent; - - .testimonials-item { - .icon { - color: var(--minimalPortfolioColor); - } - p { - color: var(--blackColor); - } - .info { - .title { - h3 { - color: var(--blackColor); - } - span { - color: var(--optionalColor); - opacity: 1; - } - } - } - } - .testimonials-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - background: #f9f9f9; - color: var(--blackColor); - - &:hover { - background-color: var(--minimalPortfolioColor); - color: var(--whiteColor); - } - } - } - } - } - } - &.shop-color { - background-color: transparent; - - .testimonials-item { - .icon { - color: var(--shopMainColor); - } - p { - color: var(--blackColor); - } - .info { - .title { - h3 { - color: var(--blackColor); - } - span { - color: var(--optionalColor); - opacity: 1; - } - } - } - } - .testimonials-slides { - &.owl-theme { - .owl-nav { - [class*=owl-] { - background: var(--whiteColor); - color: var(--blackColor); - - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } - } - } -} -.testimonials-item { - text-align: center; - background-color: var(--whiteColor); - padding: 30px; - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, .1); - - .icon { - line-height: 1; - margin-bottom: 25px; - color: var(--appMainColor); - display: block; - font-size: 45px; - } - p { - color: var(--blackColor); - font: { - size: 20px; - weight: 500; - }; - } - .info { - margin-top: 30px; - text-align: start; - - img { - display: inline-block !important; - width: 80px !important; - height: 80px; - border-radius: 50%; - margin-right: 10px; - } - .title { - margin-left: 10px; - - h3 { - font-size: 18px; - margin-bottom: 8px; - } - span { - display: block; - font-weight: 500; - color: var(--appMainColor); - opacity: .85; - } - } - } -} -.startup-testimonials-area { - .container-fluid { - max-width: 1400px; - } -} -.startup-testimonials-image { - z-index: 1; - text-align: center; - position: relative; - background-color: #2d2d37; - padding: { - top: 80px; - left: 80px; - bottom: 80px; - }; - img { - width: auto !important; - display: inline-block !important; - } -} -.startup-testimonials-content { - text-align: center; - padding: { - left: 40px; - right: 40px; - bottom: 100px; - }; - .icon { - line-height: 1; - margin-bottom: 20px; - color: var(--startupMainColor); - display: block; - font-size: 45px; - } - p { - color: var(--blackColor); - font: { - size: 18px; - weight: 600; - }; - } - .info { - margin-top: 25px; - padding-top: 25px; - border-top: 1px solid #eeeeee; - - h3 { - font-size: 18px; - margin-bottom: 8px; - } - span { - display: block; - font-weight: 500; - } - } -} -.startup-testimonials-slides { - &.owl-theme { - .owl-nav { - margin-top: 0; - position: absolute; - right: 22%; - bottom: 105px; - transform: translateX(22%); - - [class*=owl-] { - margin: 0 5px; - background-color: var(--startupMainColor); - color: var(--whiteColor); - padding: 8px 25px 7px !important; - border-radius: 0; - transition: var(--transition); - border: none; - cursor: pointer; - font-size: var(--fontSize); - - i { - line-height: 1; - } - &.owl-prev { - margin-left: 0; - } - &.owl-next { - margin-right: 0; - } - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } -} -.marketing-testimonials-area { - border-bottom: 1px solid #f0f0f0; -} -.marketing-testimonials-image { - text-align: center; - position: relative; - z-index: 2; - margin: { - right: 50px; - left: 50px; - }; - &::before { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - position: absolute; - border-radius: 50%; - background-color: var(--marketingMainColor); - } - &::after { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -2; - content: ''; - position: absolute; - border-radius: 65% 35% 70% 30% / 64% 31% 69% 36%; - background-color: #ffac00; - } - img { - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - display: inline-block !important; - width: auto !important; - } -} -.marketing-testimonials-content { - text-align: center; - padding-bottom: 60px; - - .icon { - line-height: 1; - margin-bottom: 20px; - color: var(--marketingMainColor); - display: block; - font-size: 45px; - } - p { - color: var(--blackColor); - font: { - size: 18px; - weight: 600; - }; - } - .info { - margin-top: 25px; - - h3 { - font-size: 18px; - margin-bottom: 8px; - } - span { - display: block; - font-weight: 500; - } - } -} -.marketing-testimonials-slides { - &.owl-theme { - .owl-nav { - right: 22%; - margin-top: 0; - bottom: 100px; - position: absolute; - transform: translateX(22%); - - [class*=owl-] { - margin: 0 5px; - background-color: var(--marketingMainColor); - color: var(--whiteColor); - padding: 8px 25px 7px !important; - border-radius: 0; - transition: var(--transition); - border: none; - cursor: pointer; - font-size: var(--fontSize); - - i { - line-height: 1; - } - &.owl-prev { - margin-left: 0; - } - &.owl-next { - margin-right: 0; - } - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } -} -.business-testimonials-area { - background: var(--businessGradientColor); - background: { - image: url(../../assets/img/bg1.jpg); - position: center center; - size: cover; - repeat: no-repeat; - }; -} -.business-testimonials-image { - text-align: center; - position: relative; - margin-right: 30px; - z-index: 2; - - &::before { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - content: ''; - position: absolute; - border-radius: 20% 80% 50% 50% / 20% 50% 50% 80%; - border: 2px solid var(--whiteColor); - } - &::after { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -2; - content: ''; - position: absolute; - border-radius: 65% 35% 70% 30% / 64% 31% 69% 36%; - background-color: #ffac00; - } - img { - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - display: inline-block !important; - width: auto !important; - } -} -.business-testimonials-content { - text-align: center; - - .icon { - line-height: 1; - margin-bottom: 25px; - color: var(--whiteColor); - display: block; - font-size: 45px; - } - p { - color: var(--whiteColor); - font: { - size: 20px; - weight: 500; - }; - } - .info { - margin-top: 30px; - - h3 { - font-size: 18px; - margin-bottom: 8px; - color: var(--whiteColor); - } - span { - display: block; - font-weight: 500; - color: var(--whiteColor); - opacity: .85; - } - } -} -.saas-testimonials-item { - border: 1px solid #242532; - background-color: rgba(4, 6, 17, .90); - padding: 30px; - - .info { - margin-bottom: 25px; - - img { - display: inline-block !important; - width: 80px !important; - height: 80px; - border-radius: 50%; - margin-right: 10px; - } - .title { - margin-left: 10px; - - h3 { - font-size: 18px; - margin-bottom: 8px; - color: var(--whiteColor); - } - span { - display: block; - font-weight: 600; - color: var(--saasMainColor); - } - } - } - p { - font-size: 16px; - color: var(--whiteColor); - } -} -.testimonials-slides { - &.owl-theme { - padding: { - left: 120px; - right: 120px; - }; - .owl-nav { - margin-top: 0; - - [class*=owl-] { - margin: 0; - background-color: transparent; - color: var(--whiteColor); - padding: 8px 25px 6px !important; - border-radius: 0; - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - border: 1px solid var(--whiteColor); - font-size: var(--fontSize); - - &.owl-next { - left: auto; - right: 0; - } - i { - line-height: 1; - } - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - } - } - } - } -} -.saas-testimonials-slides { - &.owl-theme { - position: relative; - margin-top: -100px; - z-index: 1; - padding: { - left: 80px; - right: 80px; - }; - .owl-nav { - margin-top: 0; - - [class*=owl-] { - top: 50%; - margin: 0; - left: 30px; - border: none; - font-size: 20px; - cursor: pointer; - border-radius: 0; - position: absolute; - color: var(--whiteColor); - transform: translateY(-50%); - transition: var(--transition); - background-color: transparent !important; - - &.owl-next { - left: auto; - right: 30px; - } - i { - line-height: 1; - } - &:hover { - color: var(--saasMainColor); - } - } - } - } -} -.testimonials-box { - text-align: center; - border: 1px solid #d5edff; - padding: 30px; - margin-bottom: 30px; - transition: var(--transition); - - .icon { - width: 80px; - height: 80px; - line-height: 82px; - background-color: #d5edff; - border-radius: 50%; - font-size: 35px; - display: block; - color: #2ea7ff; - transition: var(--transition); - margin: { - left: auto; - right: auto; - bottom: 20px; - }; - } - p { - color: var(--blackColor); - font: { - size: 16px; - weight: 500; - }; - } - .info { - margin-top: 25px; - - h3 { - font-size: 18px; - margin-bottom: 8px; - } - span { - display: block; - font-weight: 500; - color: var(--personalPortfolioMainColor); - opacity: .85; - } - } - &:hover { - border-color: var(--personalPortfolioMainColor) !important; - - .icon { - background-color: #2ea7ff; - color: var(--whiteColor); - } - &.bg-ca83ff { - .icon { - color: var(--whiteColor); - background-color: #ca83ff; - } - } - &.bg-ff9f07 { - .icon { - color: var(--whiteColor); - background-color: #ff9f07; - } - } - } - &.bg-ca83ff { - border-color: #faf2ff; - - .icon { - background-color: #faf2ff; - color: #ca83ff; - } - } - &.bg-ff9f07 { - border-color: #fff5e6; - - .icon { - background-color: #fff5e6; - color: #ff9f07; - } - } -} - -/*================================================ -Team Area CSS -=================================================*/ -.team-area { - &.with-border { - border-top: 1px solid #f0f0f0; - } -} -.single-team-member { - background-color: #f9f4f0; - margin-bottom: 30px; - text-align: center; - - .content { - padding: 25px 20px; - z-index: 1; - position: relative; - transition: var(--transition); - - h3 { - transition: var(--transition); - font-size: 20px; - margin-bottom: 10px; - } - span { - display: block; - color: var(--startupMainColor); - transition: var(--transition); - } - .social { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - position: absolute; - left: 0; - right: 0; - bottom: 25px; - opacity: 0; - visibility: hidden; - transition: var(--transition); - - li { - display: inline-block; - margin: { - left: 7px; - right: 7px; - }; - a { - display: block; - font-size: 20px; - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - height: 40px; - background-color: var(--blackColor); - top: -40px; - z-index: -1; - transition: var(--transition); - opacity: 0; - visibility: hidden; - } - } - &:hover, &.active { - .content { - background-color: var(--blackColor); - - h3 { - color: var(--whiteColor); - transform: translateY(-40px); - } - span { - color: var(--whiteColor); - transform: translateY(-40px); - } - .social { - opacity: 1; - visibility: visible; - } - &::before { - opacity: 1; - visibility: visible; - } - } - } - &.marketing-color { - .content { - span { - color: var(--marketingMainColor); - } - .social { - li { - a { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - &::before { - background-color: var(--marketingMainColor); - } - } - &:hover, &.active { - .content { - background-color: var(--marketingMainColor); - - span { - color: var(--whiteColor); - } - } - } - } - &.business-color { - .content { - position: relative; - z-index: 1; - - span { - color: var(--businessMainColor); - } - .social { - li { - a { - &:hover { - color: var(--whiteColor); - } - } - } - } - &::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--businessGradientColor); - content: ''; - transition: var(--transition); - opacity: 0; - visibility: hidden; - } - &::before { - background: var(--businessGradientColor); - } - } - &:hover, &.active { - .content { - background-color: transparent; - - &::after { - opacity: 1; - visibility: visible; - } - span { - color: var(--whiteColor); - } - } - } - } - &.bg-ffefef { - background-color: #ffefef; - } - &.bg-f0efff { - background-color: #f0efff; - } - &.it-agency-color { - .content { - position: relative; - z-index: 1; - - span { - color: var(--itAgencyMainColor); - } - .social { - li { - a { - &:hover { - color: var(--whiteColor); - } - } - } - } - &::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--itAgencyGradientColor); - content: ''; - transition: var(--transition); - opacity: 0; - visibility: hidden; - } - &::before { - background: var(--itAgencyGradientColor); - } - } - &:hover, &.active { - .content { - background-color: transparent; - - &::after { - opacity: 1; - visibility: visible; - } - span { - color: var(--whiteColor); - } - } - } - } - &.border-radius { - border-radius: 10px; - - img { - border-radius: 10px 10px 0 0; - } - } -} -.single-team-box { - text-align: center; - margin-bottom: 30px; - - .image { - position: relative; - - .social { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding-left: 0; - margin-bottom: 15px; - list-style-type: none; - transition: var(--transition); - opacity: 0; - visibility: hidden; - padding: 25px; - - li { - display: inline-block; - margin: { - left: 2px; - right: 2px; - }; - a { - display: block; - width: 35px; - height: 35px; - border-radius: 50%; - line-height: 35px; - background-color: var(--whiteColor); - color: var(--blackColor); - - &:hover { - background-color: var(--startupMainColor); - color: var(--whiteColor); - } - } - } - } - } - .content { - margin-top: 22px; - - h3 { - font-size: 20px; - margin-bottom: 8px; - } - span { - display: block; - color: var(--startupMainColor); - } - } - &:hover { - .image { - .social { - opacity: 1; - margin-bottom: 0; - visibility: visible; - } - } - } - &.consulting-color { - .image { - .social { - li { - a { - &:hover { - background-color: var(--consultingMainColor); - } - } - } - } - } - .content { - span { - color: var(--consultingMainColor); - } - } - } -} -.team-member { - margin-bottom: 30px; - text-align: center; - - .content { - z-index: 1; - padding: 25px 20px; - position: relative; - transition: var(--transition); - background-color: var(--whiteColor); - box-shadow: 0 5px 20px rgba(34, 34, 34, 0.1); - margin: { - left: 15px; - top: -30px; - right: 15px; - }; - h3 { - transition: var(--transition); - font-size: 20px; - margin-bottom: 10px; - } - span { - display: block; - color: var(--startupMainColor); - transition: var(--transition); - } - .social { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - position: absolute; - left: 0; - right: 0; - bottom: 25px; - opacity: 0; - visibility: hidden; - transition: var(--transition); - - li { - display: inline-block; - margin: { - left: 7px; - right: 7px; - }; - a { - display: block; - font-size: 20px; - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - height: 40px; - background-color: var(--blackColor); - top: -40px; - z-index: -1; - transition: var(--transition); - opacity: 0; - visibility: hidden; - } - } - &:hover, &.active { - .content { - background-color: var(--blackColor); - - h3 { - color: var(--whiteColor); - transform: translateY(-40px); - } - span { - color: var(--whiteColor); - transform: translateY(-40px); - } - .social { - opacity: 1; - visibility: visible; - } - &::before { - opacity: 1; - visibility: visible; - } - } - } - &.marketing-color { - .content { - span { - color: var(--marketingMainColor); - } - .social { - li { - a { - &:hover { - color: var(--whiteColor); - } - } - } - } - &::before { - background-color: var(--marketingMainColor); - } - } - &:hover, &.active { - .content { - background-color: var(--marketingMainColor); - - span { - color: var(--whiteColor); - } - } - } - } - &.business-color { - .content { - position: relative; - z-index: 1; - - span { - color: var(--businessMainColor); - } - .social { - li { - a { - &:hover { - color: var(--whiteColor); - } - } - } - } - &::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--businessGradientColor); - content: ''; - transition: var(--transition); - opacity: 0; - visibility: hidden; - } - &::before { - background: var(--businessGradientColor); - } - } - &:hover, &.active { - .content { - background-color: transparent; - - &::after { - opacity: 1; - visibility: visible; - } - span { - color: var(--whiteColor); - } - } - } - } - &.bg-ffefef { - background-color: #ffefef; - } - &.bg-f0efff { - background-color: #f0efff; - } - &.it-agency-color { - .content { - position: relative; - z-index: 1; - - span { - color: var(--itAgencyMainColor); - } - .social { - li { - a { - &:hover { - color: var(--whiteColor); - } - } - } - } - &::after { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--itAgencyGradientColor); - content: ''; - transition: var(--transition); - opacity: 0; - visibility: hidden; - } - &::before { - background: var(--itAgencyGradientColor); - } - } - &:hover, &.active { - .content { - background-color: transparent; - - &::after { - opacity: 1; - visibility: visible; - } - span { - color: var(--whiteColor); - } - } - } - } - &.border-radius { - border-radius: 10px; - - img { - border-radius: 10px 10px 0 0; - } - } -} -.team-box { - margin-bottom: 30px; - text-align: center; - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - .content { - left: 0; - right: 0; - z-index: 1; - bottom: -100px; - padding: 25px; - transition: .3s; - position: absolute; - opacity: 0; - visibility: hidden; - background: var(--startupMainColor); - - h3 { - font-size: 20px; - margin-bottom: 12px; - color: var(--whiteColor); - } - span { - display: block; - color: var(--whiteColor); - margin-bottom: 15px; - font-weight: 600; - } - .social { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - display: inline-block; - margin: { - left: 7px; - right: 7px; - }; - a { - display: block; - font-size: 20px; - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - opacity: 0.85; - } - } - } - } - } - &:hover, &.active { - img { - transform: scale(1.1); - } - .content { - bottom: 0; - opacity: 1; - visibility: visible; - } - } - &.business-color { - .content { - background: var(--businessGradientColor); - } - } -} - -/*================================================ -How It Works Area CSS -=================================================*/ -.how-it-works-area { - .section-title { - max-width: 668px; - } -} -.how-it-works-content { - padding-right: 30px; - - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - margin-bottom: 30px; - position: relative; - padding-left: 65px; - - i { - position: absolute; - left: 0; - top: 0; - font-size: 45px; - background: var(--saasGradientColor); - -webkit-background-clip: text; - -moz-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - } - h3 { - font-size: 20px; - margin-bottom: 12px; - } - &:last-child { - margin-bottom: 0; - } - } - } - &.white-color { - ul { - li { - color: var(--whiteColor); - - h3 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - opacity: 0.9; - } - } - } - } -} -.how-it-works-image { - text-align: center; -} - -/*================================================ -Skills Area CSS -=================================================*/ -.skills-area { - &.with-border { - border-top: 1px solid #f0f0f0; - } -} -.single-skills-box { - margin-bottom: 30px; - text-align: center; - padding: 30px; - background-color: #ffefef; - - &.bg-eaf6ff { - background-color: #eaf6ff; - } - &.bg-fff5e6 { - background-color: #fff5e6; - } - &.bg-f0efff { - background-color: #f0efff; - } -} - -/*================================================ -Expertise Area CSS -=================================================*/ -.expertise-area { - border-bottom: 1px solid #f0f0f0; -} -.expertise-box { - margin-bottom: 30px; - - .icon { - display: block; - width: 80px; - height: 80px; - font-size: 40px; - line-height: 80px; - color: var(--personalPortfolioMainColor); - border-radius: 10px; - transition: var(--transition); - margin-bottom: 25px; - text-align: center; - background-color: #f0efff; - - &.bg-fff5e6 { - background-color: #fff5e6; - } - &.bg-f3f9e7 { - background-color: #f3f9e7; - } - &.bg-ffefef { - background-color: #ffefef; - } - &.bg-eaf6ff { - background-color: #eaf6ff; - } - &.bg-faf2ff { - background-color: #faf2ff; - } - } - h3 { - font-size: 20px; - margin-bottom: 12px; - } - &:hover { - .icon { - background-color: var(--personalPortfolioMainColor); - color: var(--whiteColor); - } - } -} - -/*================================================ -CTA Area CSS -=================================================*/ -.cta-area { - position: relative; - z-index: 1; - background: { - color: var(--blackColor); - position: center center; - repeat: no-repeat; - size: cover; - image: url(../../assets/img/cta-bg1.jpg); - }; - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: .75; - } -} -.cta-content { - text-align: center; - max-width: 720px; - margin: { - left: auto; - right: auto; - }; - h2 { - color: var(--whiteColor); - margin-bottom: 0; - line-height: 1.4; - font-size: 40px; - } - p { - color: var(--whiteColor); - max-width: 600px; - opacity: 0.9; - margin: { - right: auto; - left: auto; - top: 20px; - }; - } - .default-btn { - margin-top: 15px; - - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } -} -.call-to-action-area { - &.bg-image { - background: { - image: url(../../assets/img/bg7.jpg); - position: right center; - repeat: no-repeat; - size: cover; - }; - } -} -.call-to-action-content { - color: var(--whiteColor); - - h2 { - color: var(--whiteColor); - margin-bottom: 0; - line-height: 1.4; - font-size: 37px; - } -} -.call-to-action-btn { - text-align: end; - - .default-btn { - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } -} -.call-to-action-inner { - background-color: var(--blackColor); - border-radius: 10px; - padding: { - left: 50px; - right: 50px; - }; - &.it-agency-color { - background: var(--itAgencyGradientColor); - - .default-btn { - background: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - &:hover { - background: var(--blackColor); - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - } - } - } - &.startup-color { - background: var(--startupMainColor); - - .default-btn { - background: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - &:hover { - background: var(--blackColor); - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - } - } - } -} - -/*================================================ -Timeline/Short Brief Area CSS -=================================================*/ -.short-brief-area { - border-bottom: 1px solid #f0f0f0; -} -.short-brief-content { - margin-bottom: 30px; - - .timeline { - width: 100%; - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - } - .timeline-item { - padding-left: 40px; - position: relative; - - &:last-child { - .timeline-content { - padding-bottom: 0; - } - } - &:not(.period):hover { - .timeline-marker { - &::before { - background: transparent; - border: 3px solid var(--blackColor); - } - } - } - } - .timeline-marker { - position: absolute; - width: 15px; - bottom: 0; - top: 0; - left: 0; - - &::before { - transition: background 0.3s ease-in-out, border 0.3s ease-in-out; - background: var(--blackColor); - border: 3px solid transparent; - border-radius: 100%; - position: absolute; - display: block; - height: 15px; - width: 15px; - content: ""; - top: 4px; - left: 0; - } - &::after { - background: #eeeeee; - position: absolute; - display: block; - content: ""; - top: 24px; - bottom: 0; - left: 6px; - width: 3px; - } - } - .timeline-content { - padding-bottom: 30px; - - h3 { - font-size: 20px; - margin-bottom: 13px; - } - } - .timeline-info { - .sub-title { - color: var(--optionalColor); - margin-bottom: 12px; - font-weight: 500; - display: block; - - span { - display: inline-block; - font: { - size: 18px; - weight: 700; - }; - } - } - } -} - -/*================================================ -App Download Area CSS -=================================================*/ -.app-download-area { - background: { - image: url(../../assets/img/bg6.jpg); - position: center center; - repeat: no-repeat; - size: cover; - }; -} -.app-download-image { - text-align: center; - padding-right: 30px; -} -.app-download-content { - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--businessMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 15px; - font-size: 40px; - } - .btn-box { - margin-top: 25px; - - .store-btn { - display: inline-block; - background-color: var(--whiteColor); - position: relative; - color: var(--optionalColor); - padding: 11px 30px 11px 70px; - margin-right: 15px; - border-radius: 5px; - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.02), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.04); - font: { - size: 13.5px; - weight: 600; - }; - img { - position: absolute; - left: 18px; - top: 50%; - transform: translateY(-50%); - color: var(--blackColor); - font: { - size: 40px; - weight: normal; - }; - } - span { - display: block; - color: var(--blackColor); - margin-top: 2px; - font: { - size: 18px; - weight: 700; - }; - } - &:last-child { - margin-right: 0; - } - } - } -} - -/*================================================ -Trending Style Area CSS -=================================================*/ -.trending-style-image { - position: relative; - margin-right: 15px; - padding: { - bottom: 100px; - right: 150px; - }; - img { - &:nth-child(2) { - position: absolute; - right: 0; - bottom: 0; - width: 315px; - } - } -} -.trending-style-content { - padding-left: 15px; - - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--shopMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - } - h2 { - margin-bottom: 16px; - font-size: 40px; - } - h3 { - font-size: 20px; - line-height: 1.4; - margin-bottom: 15px; - } - .default-btn { - margin-top: 10px; - } -} - -/*================================================ -Pricing Area CSS -=================================================*/ -.pricing-area { - &.with-black-border { - border-top: 1px solid #12141a; - } -} -.single-pricing-box { - padding: 40px; - margin-bottom: 30px; - border: 1px dashed #eeeeee; - background-color: var(--whiteColor); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, .1); - transition: var(--transition); - - .pricing-header { - margin-bottom: 25px; - padding-bottom: 25px; - border-bottom: 1px solid #eeeeee; - - h3 { - color: var(--marketingMainColor); - margin-bottom: 12px; - line-height: 1; - font: { - weight: 700; - size: 45px; - }; - span { - color: var(--optionalColor); - font: { - size: var(--fontSize); - weight: normal; - }; - } - } - .sub-title { - display: block; - font-weight: 700; - color: var(--blackColor); - } - } - .features-list { - margin-bottom: 25px; - padding-left: 0; - list-style-type: none; - - li { - margin-bottom: 12px; - color: var(--optionalColor); - font-family: var(--headingFontFamily); - position: relative; - padding-left: 25px; - - i { - position: absolute; - left: 0; - top: 5px; - color: var(--marketingMainColor); - - &.close { - color: red; - } - } - &:last-child { - margin-bottom: 0; - } - } - } - .default-btn { - display: block; - width: 100%; - text-align: center; - padding: { - left: 35px; - right: 35px; - top: 14px; - bottom: 14px; - }; - &:hover { - color: var(--whiteColor); - background-color: #ffac00; - } - } - &:hover { - border-color: var(--marketingMainColor); - } -} -.pricing-box { - margin-bottom: 30px; - text-align: center; - background-color: var(--whiteColor); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, .1); - padding: 30px; - - .pricing-header { - background: var(--appMainColor); - margin: -30px -30px 30px; - padding: 30px; - position: relative; - z-index: 1; - overflow: hidden; - - h3 { - margin-bottom: 15px; - font-size: 20px; - color: var(--whiteColor); - } - .price { - color: var(--whiteColor); - margin-bottom: 12px; - line-height: 1; - font: { - weight: 700; - size: 45px; - }; - span { - opacity: 0.9; - font: { - size: var(--fontSize); - weight: normal; - }; - } - } - &::before { - width: 400px; - height: 400px; - content: ''; - position: absolute; - left: 50%; - transform: translateX(-50%); - border-radius: 50%; - bottom: 10px; - background-color: #0084ff; - opacity: 0.1; - z-index: -1; - transition: var(--transition); - } - } - .features-list { - padding-left: 0; - margin-bottom: 30px; - list-style-type: none; - - li { - margin-bottom: 12px; - color: var(--optionalColor); - font-family: var(--headingFontFamily); - position: relative; - - &.unactive { - text-decoration: line-through; - color: #9b9dbb; - } - &:last-child { - margin-bottom: 0; - } - } - } - .default-btn { - background: transparent; - color: var(--blackColor); - border: 1px solid var(--appMainColor); - box-shadow: unset; - - i { - color: var(--blackColor); - } - &:hover { - background: var(--appMainColor); - border-color: var(--appMainColor); - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - } - } - &.saas-color { - .pricing-header { - background: var(--saasGradientColor); - - &::before { - background-color: var(--appMainColor); - } - } - .default-btn { - border-color: #4d4d57;; - - &:hover { - background: var(--saasMainColor); - border-color: var(--saasMainColor); - } - } - } - &.bg-black { - box-shadow: unset; - background-color: #040611; - border: 1px solid #272836; - transition: var(--transition); - - .pricing-header { - transition: var(--transition); - background: transparent; - border-bottom: 1px solid #272836; - padding: 0 0 30px; - margin: { - top: 0; - left: 0; - right: 0; - }; - &::before { - display: none; - } - } - .features-list { - color: var(--whiteColor); - - li { - color: var(--whiteColor); - - &.unactive { - color: #5c5656; - } - } - } - .default-btn { - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - } - } - &:hover { - .pricing-header { - &::before { - bottom: -185%; - } - } - } - &.active { - .default-btn { - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05), 0 1.5rem 2.2rem rgba(0, 0, 0, 0.1); - background: var(--appGradientColor); - color: var(--whiteColor); - border: none; - - i { - color: var(--whiteColor); - } - &.saas-color { - background: var(--saasGradientColor); - } - } - } -} -.pricing-tabs { - margin-top: -10px; - - .nav { - padding-left: 0; - margin-bottom: 50px; - list-style-type: none; - justify-content: center; - border-bottom: none; - - .nav-item { - .nav-link { - background-color: var(--whiteColor); - position: relative; - z-index: 1; - color: var(--blackColor); - border-radius: 0; - border: none; - padding: 11px 35px; - font: { - size: var(--fontSize); - weight: 700; - }; - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: var(--saasGradientColor); - transition: var(--transition); - opacity: 0; - visibility: hidden; - } - &:hover, &.active { - color: var(--whiteColor); - - &::before { - opacity: 1; - visibility: visible; - } - } - } - &:first-child { - .nav-link { - border-radius: 5px 0 0 5px; - - &::before { - border-radius: 5px 0 0 5px; - } - } - } - &:last-child { - .nav-link { - border-radius: 0 5px 5px 0; - - &::before { - border-radius: 0 5px 5px 0; - } - } - } - } - } -} -.pricing-item { - padding: 30px; - overflow: hidden; - position: relative; - text-align: center; - margin-bottom: 30px; - background-color: #f3f4f7; - transition: var(--transition); - - .pricing-header { - border-bottom: 1px solid #e5e4e4; - transition: var(--transition); - margin: -30px 0 30px; - padding: 30px; - - h3 { - transition: var(--transition); - margin-bottom: 15px; - font-size: 20px; - } - .price { - color: var(--consultingMainColor); - margin-bottom: 12px; - line-height: 1; - font: { - weight: 700; - size: 45px; - }; - span { - opacity: 0.9; - color: var(--optionalColor); - transition: var(--transition); - font: { - size: var(--fontSize); - weight: normal; - }; - } - } - } - .features-list { - padding-left: 0; - margin-bottom: 30px; - list-style-type: none; - - li { - margin-bottom: 12.5px; - font-family: var(--headingFontFamily); - - &.unactive { - text-decoration: line-through; - color: #b0b1c0; - } - &:last-child { - margin-bottom: 0; - } - } - } - .ribbon { - top: -10px; - right: -25px; - width: 150px; - height: 150px; - overflow: hidden; - position: absolute; - - span { - background-color: var(--whiteColor); - text-transform: uppercase; - color: var(--blackColor); - transform: rotate(45deg); - text-align: center; - position: absolute; - display: block; - width: 225px; - left: -25px; - top: 30px; - padding: { - top: 8px; - bottom: 8px; - }; - font: { - size: var(--fontSize); - weight: 700; - }; - } - } - &.active, &:hover { - color: var(--whiteColor); - background-color: var(--blackColor); - - .pricing-header { - border-bottom-color: #464c59; - - h3 { - color: var(--whiteColor); - } - .price { - span { - color: var(--whiteColor); - } - } - } - .default-btn { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } - &.border-radius { - border-radius: 10px; - background-color: #f9f9f9; - } - &.it-agency-color { - .pricing-header { - border-bottom: none; - background-color: #fff5e6; - margin: { - left: -30px; - right: -30px; - }; - h3 { - color: var(--blackColor); - } - .price { - color: var(--blackColor); - } - } - .features-list { - color: var(--blackColor); - - li { - color: var(--blackColor); - - &.unactive { - color: #b0b1c0; - } - } - } - .default-btn { - background-color: var(--blackColor); - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - } - &:hover { - .pricing-header { - .price { - span { - color: var(--optionalColor); - } - } - } - } - &.active { - background-color: #f9f9f9; - - .pricing-header { - background: var(--itAgencyGradientColor); - - h3 { - color: var(--whiteColor); - } - .price { - color: var(--whiteColor); - - span { - color: var(--whiteColor); - } - } - } - .default-btn { - background: var(--itAgencyGradientColor); - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - } - } - } - &.startup-color { - .pricing-header { - .price { - color: var(--startupMainColor); - } - } - } -} - -/*================================================ -App Screenshot Area CSS -=================================================*/ -.app-screenshot-area { - background: var(--appGradientColor); - position: relative; - overflow: hidden; - z-index: 1; - - .section-title { - h2 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - } - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 200px; - z-index: -1; - background-color: var(--whiteColor); - } - .overlay { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -2; - - &::before { - width: 650px; - height: 650px; - position: absolute; - left: -3%; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - top: -25%; - content: ''; - background-color: #0084ff; - opacity: 0.10; - animation: border-transform 10s linear infinite alternate forwards; - } - &::after { - right: -3%; - bottom: 0; - content: ''; - opacity: 0.10; - width: 600px; - height: 600px; - position: absolute; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; - background-color: #0084ff; - animation: border-transform 10s linear infinite alternate forwards; - } - } -} -.single-sccrenshot-item { - text-align: center; -} -.screenshot-wrap { - position: relative; - - .screenshot-frame { - z-index: 2; - border-radius: 20px; - display: block; - width: 241px; - height: 504px; - position: absolute; - top: 0; - bottom: 0; - margin: 10px 0; - left: 50%; - transform: translateX(-50%); - background: { - image: url(../../assets/img/screenshot/frame.png); - repeat: no-repeat; - size: 100% 100%; - }; - } -} -.screenshot-slides { - margin: { - top: -10px; - bottom: -10px; - }; - &.owl-theme { - .owl-nav { - margin-top: 0; - - [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--appMainColor); - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: -40px; - top: 50%; - width: 40px; - line-height: 40px; - height: 40px; - border-radius: 50%; - transform: translateY(-50%); - font-size: 18px; - - &.owl-next { - left: auto; - right: -40px; - } - i { - line-height: 1; - } - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } -} - -/*================================================ -Facility Area CSS -=================================================*/ -.facility-content { - padding-right: 50px; - - .item { - margin-bottom: 30px; - position: relative; - padding-left: 65px; - - &:last-child { - margin-bottom: 0; - } - i { - position: absolute; - font-size: 45px; - left: 0; - top: 0; - } - h3 { - font-size: 20px; - margin-bottom: 12px; - } - } -} - -/*================================================ -Latest Posts Area CSS -=================================================*/ -.latest-posts-area { - .section-title { - &.style-four { - padding-bottom: 8px; - margin: { - top: -7px; - bottom: 30px; - }; - } - } -} -.single-latest-post-box { - margin-bottom: 30px; - - .post-image { - overflow: hidden; - - img { - transition: var(--transition); - } - } - .post-content { - padding: { - top: 25px; - left: 25px; - right: 25px; - }; - .tag { - margin-bottom: 12px; - display: inline-block; - position: relative; - font-weight: 600; - color: var(--blackColor); - } - .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; - - li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--blackColor); - } - a { - color: var(--optionalColor); - - &:hover { - color: var(--blackColor); - } - } - } - } - h3 { - margin-bottom: 12px; - line-height: 1.4; - font-size: 25px; - } - .link-btn { - font-weight: 600; - position: relative; - display: inline-block; - padding-right: 12px; - - i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; - } - &::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--blackColor); - transition: var(--transition); - } - &:hover { - padding-right: 15px; - color: var(--blackColor); - - &::before { - width: 100%; - right: auto; - left: 0; - } - } - } - } - &:hover { - .post-image { - img { - transform: scale(1.1); - } - } - } -} - -/*================================================ -Partner Area CSS -=================================================*/ -.partner-area { - &.bg-business-color { - background: var(--businessGradientColor); - } - &.with-top-border { - border-top: 1px solid #f0f0f0; - } - &.bg-it-agency-color { - background: var(--itAgencyGradientColor); - } - &.bg-1b2232 { - .section-title { - h2 { - color: var(--whiteColor); - } - } - } -} -.partner-item { - text-align: center; - - a { - img { - width: auto !important; - display: inline-block !important; - } - } -} - -/*================================================ -Clients Area CSS -=================================================*/ -.single-clients-box { - margin-bottom: 30px; - text-align: center; - background-color: var(--whiteColor); - - a { - padding: 30px; - - img { - transition: var(--transition); - } - } - &:hover { - a { - img { - transform: scale(1.05); - } - } - } -} - -/*================================================ -Featured Video Area CSS -=================================================*/ -.single-featured-video-box { - margin-bottom: 30px; - - .image { - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - .video-btn { - position: absolute; - left: 15px; - bottom: 15px; - color: var(--whiteColor); - font-weight: 600; - - i { - display: inline-block; - width: 40px; - height: 40px; - color: var(--blackColor); - background-color: var(--whiteColor); - text-align: center; - border-radius: 50%; - line-height: 41px; - margin-right: 10px; - transition: var(--transition); - } - &:hover { - i { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } - .content { - margin-top: 25px; - - .tag { - margin-bottom: 12px; - display: inline-block; - position: relative; - font-weight: 600; - color: var(--blackColor); - } - .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; - - li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--blackColor); - } - a { - color: var(--optionalColor); - - &:hover { - color: var(--blackColor); - } - } - } - } - h3 { - margin-bottom: 0; - line-height: 1.4; - font-size: 25px; - } - } - &:hover { - img { - transform: scale(1.1); - } - } -} -.featured-video-item-list { - .single-featured-video-item { - margin-bottom: 30px; - - .row { - margin: { - left: 0; - right: 0; - }; - .col-lg-4, .col-lg-8 { - padding: { - left: 0; - right: 0; - }; - } - } - .image { - height: 100%; - position: relative; - background: { - position: center center; - repeat: no-repeat; - size: cover; - }; - &.bg1 { - background-image: url(../../assets/img/featured-video/featured-video2.jpg); - } - &.bg2 { - background-image: url(../../assets/img/featured-video/featured-video3.jpg); - } - &.bg3 { - background-image: url(../../assets/img/featured-video/featured-video4.jpg); - } - &.bg4 { - background-image: url(../../assets/img/featured-video/featured-video5.jpg); - } - img { - display: none; - } - .link-btn { - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 1; - display: block; - position: absolute; - } - .video-btn { - color: var(--whiteColor); - position: absolute; - bottom: 10px; - left: 10px; - z-index: 2; - font: { - size: 14px; - weight: 600; - }; - i { - display: inline-block; - width: 35px; - height: 35px; - color: var(--blackColor); - background-color: var(--whiteColor); - text-align: center; - border-radius: 50%; - line-height: 36px; - margin-right: 5px; - transition: var(--transition); - } - &:hover { - i { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - } - .content { - padding: { - left: 25px; - top: 9.5px; - bottom: 9.5px; - }; - .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; - - li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--blackColor); - } - a { - color: var(--optionalColor); - - &:hover { - color: var(--blackColor); - } - } - } - } - h3 { - margin-bottom: 0; - line-height: 1.4; - font-size: 20px; - } - } - } -} - -/*================================================ -Blog Area CSS -=================================================*/ -.blog-area { - &.with-border { - border-top: 1px solid #f0f0f0; - } - &.with-black-border { - border-top: 1px solid #12141a; - } -} -.single-blog-post { - margin-bottom: 30px; - position: relative; - overflow: hidden; - - img { - transition: var(--transition); - } - .content { - left: 0; - right: 0; - bottom: 0; - z-index: 2; - padding: 25px; - position: absolute; - - .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; - - li { - display: inline-block; - margin-right: 15px; - color: var(--whiteColor); - position: relative; - padding-left: 22px; - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); - } - a { - color: var(--whiteColor); - } - } - } - h3 { - color: var(--whiteColor); - margin-bottom: 13px; - font-size: 20px; - line-height: 1.4; - - a { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - .link-btn { - font-weight: 500; - position: relative; - opacity: 0.85; - color: var(--whiteColor); - padding: { - right: 12px; - bottom: 2px; - }; - i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; - } - &::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: #eeeeee; - transition: var(--transition); - } - &:hover { - opacity: 1; - padding-right: 15px; - - &::before { - width: 100%; - right: auto; - left: 0; - } - } - } - } - &::before { - position: absolute; - height: 100%; - content: ""; - width: 100%; - z-index: 1; - left: 0; - top: 0; - background: linear-gradient(to top, rgba(35, 35, 37, 0.9) 0, rgba(35, 35, 37, 0.5) 50%, rgba(22, 22, 23, 0) 75%, rgba(0, 0, 0, 0) 100%); - } - &:hover { - img { - transform: scale(1.05); - } - } -} -.blog-section-title { - margin-bottom: 30px; - - h2 { - margin-bottom: 0; - font-size: 40px; - } - p { - margin-top: 15px; - } - .sub-title { - display: block; - margin-bottom: 12px; - color: var(--startupMainColor); - font: { - family: var(--headingFontFamily); - weight: 600; - size: 14px; - }; - &.marketing-color { - color: var(--marketingMainColor); - } - } - .default-btn { - margin-top: 10px; - } -} -.single-post-item { - margin-bottom: 30px; - background-color: var(--whiteColor); - box-shadow: 3px 5px 25px 0 rgba(0, 0, 0, .1); - - .post-content { - padding: 25px; - - .tag { - margin-bottom: 14px; - display: inline-block; - position: relative; - padding-left: 24px; - font-weight: 600; - color: var(--appMainColor); - - i { - position: absolute; - left: 0; - top: 4px; - } - } - .meta { - padding-left: 0; - margin-bottom: 13px; - list-style-type: none; - - li { - display: inline-block; - margin-right: 15px; - position: relative; - padding-left: 22px; - color: var(--optionalColor); - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--marketingMainColor); - } - a { - color: var(--optionalColor); - - &:hover { - color: var(--marketingMainColor); - } - } - } - } - h3 { - margin-bottom: 14px; - line-height: 1.4; - font-size: 20px; - } - .link-btn { - font-weight: 600; - position: relative; - display: inline-block; - padding-right: 12px; - - i { - position: absolute; - right: 0; - top: 7px; - font-size: 12px; - } - &::before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 0%; - height: 1px; - background-color: var(--marketingMainColor); - transition: var(--transition); - } - &:hover { - padding-right: 15px; - color: var(--marketingMainColor); - - &::before { - width: 100%; - right: auto; - left: 0; - } - } - } - } - &.border-radius { - border-radius: 10px; - - .post-image { - border-radius: 10px 10px 0 0; - - a { - border-radius: 10px 10px 0 0; - - img { - border-radius: 10px 10px 0 0; - } - } - } - } - &.business-color { - .post-content { - .meta { - li { - i { - color: var(--businessMainColor); - } - a { - &:hover { - color: var(--businessMainColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--businessMainColor); - } - &:hover { - color: var(--businessMainColor); - } - } - } - } - &.app-color { - .post-content { - .meta { - li { - i { - color: var(--appMainColor); - } - a { - &:hover { - color: var(--appMainColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--appMainColor); - } - &:hover { - color: var(--appMainColor); - } - } - } - } - &.saas-color { - .post-content { - .meta { - li { - i { - color: var(--saasMainColor); - } - a { - &:hover { - color: var(--saasMainColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--saasMainColor); - } - &:hover { - color: var(--saasMainColor); - } - } - } - } - &.bg-black { - background-color: #040611; - box-shadow: unset; - border: 1px solid #272836; - transition: var(--transition); - - .post-content { - .tag { - color: var(--saasMainColor); - } - .meta { - color: var(--whiteColor); - - li { - color: var(--whiteColor); - - i { - color: var(--saasMainColor); - } - a { - color: var(--whiteColor); - - &:hover { - color: var(--saasMainColor); - } - } - } - } - h3 { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - - &:hover { - color: var(--saasMainColor); - } - } - } - p { - color: var(--whiteColor); - opacity: 0.9; - } - .link-btn { - color: var(--whiteColor); - - &::before { - background: var(--saasGradientColor); - } - &:hover { - color: var(--whiteColor); - } - } - } - &:hover { - border-color: var(--whiteColor); - } - } - &.bg-faf2ff { - background-color: #faf2ff; - box-shadow: unset; - } - &.bg-fff5e6 { - background-color: #fff5e6; - box-shadow: unset; - } - &.bg-eaf6ff { - background-color: #eaf6ff; - box-shadow: unset; - } - &.personal-portfolio-color { - .post-content { - .meta { - li { - i { - color: var(--personalPortfolioMainColor); - } - a { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--personalPortfolioMainColor); - } - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - &.consulting-color { - .post-content { - .meta { - li { - i { - color: var(--consultingMainColor); - } - a { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--consultingMainColor); - } - &:hover { - color: var(--consultingMainColor); - } - } - } - } - &.it-agency-color { - .post-content { - .tag { - color: var(--optionalColor); - - i { - color: var(--itAgencyMainColor); - } - } - .meta { - li { - i { - color: var(--itAgencyMainColor); - } - a { - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--itAgencyMainColor); - } - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - &.without-box-shadow { - box-shadow: unset; - - .post-content { - padding-bottom: 0; - - .meta { - li { - i { - color: var(--blackColor); - } - a { - &:hover { - color: var(--blackColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--blackColor); - } - &:hover { - color: var(--blackColor); - } - } - } - } - &.startup-color { - .post-content { - .tag { - color: var(--startupMainColor); - } - .meta { - li { - i { - color: var(--startupMainColor); - } - a { - &:hover { - color: var(--startupMainColor); - } - } - } - } - .link-btn { - &::before { - background-color: var(--startupMainColor); - } - &:hover { - color: var(--startupMainColor); - } - } - } - } -} - -/*================================================ -Blog Details Area CSS -=================================================*/ -.blog-details-desc { - .article-content { - margin-top: 30px; - - .entry-meta { - margin-bottom: 15px; - - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - display: inline-block; - margin-right: 20px; - color: var(--optionalColor); - font-weight: 500; - position: relative; - padding-left: 22px; - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); - } - &:last-child { - margin-right: 0; - } - } - } - } - h3 { - font-size: 22px; - margin: { - bottom: 15px; - top: 25px; - }; - } - .wp-block-gallery { - &.columns-3 { - padding-left: 0; - list-style-type: none; - display: flex; - flex-wrap: wrap; - margin: { - right: -10px; - left: -10px; - bottom: 30px; - top: 30px; - } - li { - flex: 0 0 33.3333%; - max-width: 33.3333%; - padding: { - right: 10px; - left: 10px; - } - figure { - margin-bottom: 0; - } - } - } - } - .features-list { - margin: { - top: 25px; - bottom: 30px; - }; - li { - margin-bottom: 15px; - color: var(--optionalColor); - font-weight: 500; - - &:last-child { - margin-bottom: 0; - } - } - } - } - .article-footer { - display: flex; - flex-wrap: wrap; - margin-top: 30px; - padding-top: 30px; - border-top: 1px solid #eeeeee; - - .article-tags { - flex: 0 0 50%; - max-width: 50%; - - p { - line-height: 1; - margin-bottom: 0; - color: var(--blackColor); - font-weight: 600; - text-transform: uppercase; - } - a { - display: inline-block; - margin-left: 10px; - background-color: #f9f9f9; - color: var(--blackColor); - border-radius: 30px; - padding: 7px 20px; - - &:hover { - background-color: var(--startupMainColor); - color: var(--whiteColor); - } - } - } - .article-share { - flex: 0 0 50%; - max-width: 50%; - - .social { - padding-left: 0; - list-style-type: none; - text-align: end; - margin-bottom: 0; - - li { - display: inline-block; - - span { - display: inline-block; - margin-right: 3px; - font-weight: 600; - } - a { - display: block; - width: 32px; - height: 32px; - line-height: 32px; - border-radius: 50%; - background-color: var(--startupMainColor); - color: var(--whiteColor); - border: 1px solid var(--startupMainColor); - text-align: center; - font-size: 14px; - margin-left: 2px; - - &:hover, &:focus { - color: var(--startupMainColor); - background-color: transparent; - } - &.facebook { - background-color: #3b5998; - border-color: #3b5998; - color: var(--whiteColor); - - &:hover, &:focus { - color: #3b5998; - background-color: transparent; - } - } - &.twitter { - background-color: #1da1f2; - border-color: #1da1f2; - color: var(--whiteColor); - - &:hover, &:focus { - color: #1da1f2; - background-color: transparent; - } - } - &.linkedin { - background-color: #007bb5; - border-color: #007bb5; - color: var(--whiteColor); - - &:hover, &:focus { - color: #007bb5; - background-color: transparent; - } - } - &.instagram { - background-color: #c13584; - border-color: #c13584; - color: var(--whiteColor); - - &:hover, &:focus { - color: #c13584; - background-color: transparent; - } - } - } - } - } - } - } -} -blockquote, .blockquote { - z-index: 1; - overflow: hidden; - position: relative; - background-color: #fafafa; - padding: 40px 50px !important; - margin: { - bottom: 20px; - top: 20px; - }; - p { - line-height: 1.7; - margin-bottom: 0; - color: var(--blackColor); - font: { - weight: 700; - style: italic; - size: 17px !important; - }; - } - cite { - display: none; - } - &::after { - top: 0; - left: 0; - bottom: 0; - width: 3px; - content: ''; - position: absolute; - background-color: var(--startupMainColor); - margin: { - top: 20px; - bottom: 20px; - }; - } -} -.comments-area { - margin-top: 30px; - - .comments-title { - font-size: 22px; - margin-bottom: 30px; - line-height: initial; - } - ol, ul { - padding: 0; - margin: 0; - list-style-type: none; - } - .comment-list { - padding: 0; - margin: 0; - list-style-type: none; - } - .children { - margin-left: 20px; - } - .comment-body { - border-bottom: 1px dashed #eeeeee; - padding-left: 70px; - color: var(--blackColor); - margin-bottom: 20px; - padding-bottom: 20px; - - .reply { - margin-top: 15px; - - a { - border: 1px dashed #ded9d9; - color: var(--blackColor); - display: inline-block; - padding: 5px 20px 6px; - border-radius: 30px; - font: { - size: 13px; - weight: 700; - }; - &:hover { - color: var(--whiteColor); - background-color: var(--startupMainColor); - border-color: var(--startupMainColor); - } - } - } - } - .comment-author { - font-size: 16px; - margin-bottom: 6px; - position: relative; - z-index: 2; - - .avatar { - height: 50px; - left: -65px; - position: absolute; - width: 50px; - top: 4px; - } - .says { - display: none; - } - } - .comment-metadata { - margin-bottom: .7em; - color: var(--optionalColor); - letter-spacing: 0.01em; - font: { - size: 13px; - weight: 500; - }; - } - .comment-respond { - margin-top: 30px; - - .comment-reply-title { - margin-bottom: 0; - font-size: 22px; - - #cancel-comment-reply-link { - display: inline-block; - } - } - .comment-form { - overflow: hidden; - } - .comment-notes { - margin: { - bottom: 20px; - top: 10px; - }; - .required { - color: red; - } - } - .comment-form-comment { - float: left; - width: 100%; - } - label { - display: none; - } - input[type="date"], input[type="time"], input[type="datetime-local"], input[type="week"], input[type="month"], input[type="text"], input[type="email"], input[type="url"], input[type="password"], input[type="search"], input[type="tel"], input[type="number"], textarea { - outline: 0; - width: 100%; - border: none; - height: 50px; - display: block; - font-size: 15px; - border-radius: 3px; - padding: 1px 0 0 15px; - color: var(--blackColor); - background-color: #f9f9f9; - - &::placeholder { - color: var(--optionalColor); - transition: var(--transition); - } - &:focus { - &::placeholder { - color: transparent; - } - } - } - textarea { - height: auto !important; - padding-top: 15px; - } - .comment-form-author { - float: left; - width: 50%; - padding-right: 10px; - margin-bottom: 20px; - } - .comment-form-email { - float: left; - width: 50%; - padding-left: 12px; - margin-bottom: 20px; - } - .comment-form-url { - float: left; - width: 100%; - margin-bottom: 20px; - } - .comment-form-cookies-consent { - width: 100%; - float: left; - position: relative; - padding-left: 20px; - margin: { - bottom: 20px; - top: 15px; - }; - input { - position: absolute; - left: 0; - top: 6px; - } - label { - margin: 0; - display: inline-block; - color: var(--optionalColor); - font: { - weight: normal; - size: var(--fontSize); - }; - } - } - .form-submit { - float: left; - width: 100%; - - input { - border: none; - cursor: pointer; - border-radius: 4px; - padding: 10px 30px; - display: inline-block; - color: var(--whiteColor); - background: var(--startupMainColor); - transition: var(--transition); - font: { - weight: 700; - size: var(--fontSize); - }; - &:hover, &:focus { - color: var(--whiteColor); - background-color: var(--blackColor); - } - } - } - } -} - -/*================================================ -Widget Area CSS -=================================================*/ -.widget-area { - .widget { - margin-bottom: 35px; - - .widget-title { - border-bottom: 1px solid #eeeeee; - padding-bottom: 12px; - margin-bottom: 30px; - position: relative; - font-size: 20px; - - &::before { - left: 0; - width: 70px; - height: 1px; - content: ''; - bottom: -1px; - position: absolute; - background-color: var(--blackColor); - } - } - &:last-child { - margin-bottom: 0; - } - } - .widget_search { - box-shadow: 0px 0px 29px 0px rgba(102, 102, 102, 0.1); - background-color: var(--whiteColor); - padding: 15px; - - form { - position: relative; - - label { - display: block; - margin-bottom: 0; - } - .screen-reader-text { - display: none; - } - .search-field { - outline: 0; - width: 100%; - height: 50px; - display: block; - padding: 0 0 0 15px; - transition: var(--transition); - border: 1px solid #eeeeee; - background-color: transparent; - - &:focus { - border-color: var(--startupMainColor); - } - } - button { - position: absolute; - right: 0; - outline: 0; - bottom: 0; - height: 50px; - width: 50px; - border: none; - color: var(--whiteColor); - background-color: var(--startupMainColor); - transition: var(--transition); - - &:hover { - color: var(--whiteColor); - background-color: var(--blackColor); - } - } - } - } - .widget_about_author { - .about-author { - transition: var(--transition); - border: 1px solid #eeeeee; - text-align: center; - padding: 30px; - - .main-image { - width: 100px; - } - p { - margin: { - bottom: 15px; - top: 25px; - }; - } - &:hover { - border-color: var(--blackColor); - } - } - } - .widget_noke_posts_thumb { - position: relative; - overflow: hidden; - - .item { - margin-bottom: 20px; - position: relative; - padding-left: 115px; - - .thumb { - top: 0; - left: 0; - width: 100px; - float: unset; - height: 100%; - display: block; - position: absolute; - - .fullimage { - width: 100px; - height: 100%; - display: inline-block; - background: { - repeat: no-repeat; - size: cover !important; - position: center center !important; - }; - &.bg1 { - background-image: url(../../assets/img/blog/blog-img1.jpg); - } - &.bg2 { - background-image: url(../../assets/img/blog/blog-img2.jpg); - } - &.bg3 { - background-image: url(../../assets/img/blog/blog-img3.jpg); - } - &.bg4 { - background-image: url(../../assets/img/blog/blog-img4.jpg); - } - } - &::before, &::after { - content: ''; - z-index: 1; - top: 50%; - left: 50%; - opacity: 0; - position: absolute; - background-color: var(--whiteColor); - transform: translate(-50%, -50%); - transition: all .8s cubic-bezier(.2, 1, .22, 1); - } - &::before { - width: 40px; - height: 1px; - left: 100%; - } - &::after { - height: 40px; - width: 1px; - top: 0; - } - } - .info { - .title { - font-size: 16px; - line-height: 1.5; - margin-bottom: 8px; - - a { - display: inline-block; - } - } - .date { - padding-left: 20px; - position: relative; - color: var(--optionalColor); - - i { - position: absolute; - left: 0; - top: 3px; - } - } - } - &:last-child { - margin-bottom: 0; - } - &:hover { - .thumb { - &::before, &::after { - opacity: 1; - top: 50%; - left: 50%; - } - } - } - } - } - .widget_follow_us { - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - margin-bottom: 15px; - - a { - display: block; - position: relative; - color: var(--whiteColor); - background-color: var(--blackColor); - padding: { - right: 15px; - top: 10px; - bottom: 10px; - left: 55px; - }; - i { - position: absolute; - left: 14px; - top: 50%; - font-size: 18px; - line-height: 1; - transform: translateY(-50%); - } - &::before { - content: ''; - position: absolute; - left: 42px; - top: 0; - height: 100%; - width: 1px; - opacity: 0.3; - background-color: var(--whiteColor); - } - &.facebook { - background-color: #39569e; - } - &.twitter { - background-color: #52aaf4; - } - &.linkedin { - background-color: #0185ae; - } - &.instagram { - background-color: #c8359d; - } - } - &:last-child { - margin-bottom: 0; - } - } - } - } - .widget_recent_posts { - .item { - margin-bottom: 15px; - padding-bottom: 15px; - border-bottom: 1px solid #eeeeee; - - .tag { - position: relative; - margin-bottom: 8px; - display: inline-block; - color: var(--blackColor); - } - .title { - font-size: 16px; - line-height: 1.5; - margin-bottom: 10px; - - a { - display: inline-block; - } - } - .date { - padding-left: 20px; - position: relative; - color: var(--optionalColor); - - i { - position: absolute; - left: 0; - top: 3px; - } - } - } - } - .widget_tag_cloud { - .tagcloud { - margin-bottom: -8px; - - a { - display: inline-block; - border: 1px solid #eeeeee; - padding: 6px 15px; - color: var(--optionalColor); - margin: { - right: 8px; - bottom: 8px; - }; - font: { - size: 14px !important; - weight: 600; - }; - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - border-color: var(--blackColor); - } - } - } - } - .widget_newsletter { - background-color: #f6f7f9; - border: 1px solid #eeeeee; - text-align: center; - padding: 30px; - - h4 { - font-size: 20px; - margin-bottom: 10px; - } - .newsletter-form { - margin-top: 20px; - - .input-newsletter { - display: block; - width: 100%; - background-color: var(--whiteColor); - border: 1px solid #eeeeee; - height: 50px; - padding-left: 15px; - color: var(--blackColor); - transition: var(--transition); - - &:focus { - border-color: var(--blackColor); - } - } - button { - display: block; - width: 100%; - text-align: center; - margin-top: 15px; - background-color: var(--blackColor); - color: var(--whiteColor); - border: none; - transition: var(--transition); - padding: 13px 30px; - font: { - size: var(--fontSize); - weight: 700; - }; - &:hover { - background-color: var(--optionalColor); - } - } - } - } - .widget_categories { - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - margin-bottom: 12px; - position: relative; - padding-left: 15px; - - &::before { - left: 0; - top: 50%; - width: 8px; - height: 8px; - content: ''; - position: absolute; - border-radius: 50%; - transform: translateY(-50%); - background-color: var(--optionalColor); - } - a { - font-weight: 500; - display: inline-block; - color: var(--optionalColor); - - &:hover { - color: var(--startupMainColor); - } - } - &:last-child { - margin-bottom: 0; - } - } - } - } - .widget_archive { - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - margin-bottom: 12px; - position: relative; - padding-left: 15px; - - &::before { - left: 0; - top: 50%; - width: 8px; - height: 8px; - content: ''; - position: absolute; - border-radius: 50%; - transform: translateY(-50%); - background-color: var(--optionalColor); - } - a { - font-weight: 500; - display: inline-block; - color: var(--optionalColor); - - &:hover { - color: var(--startupMainColor); - } - } - &:last-child { - margin-bottom: 0; - } - } - } - } - .widget_colors { - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - display: block; - margin-bottom: 12px; - border-bottom: 1px dashed #eeeeee; - padding-bottom: 12px; - - a { - display: flex; - align-items: center; - - span { - display: block; - color: var(--optionalColor); - - &:nth-child(1) { - width: 17px; - height: 17px; - font-size: 0; - margin-right: 8px; - letter-spacing: 0; - border-radius: 50%; - transition: var(--transition); - - &.gray { - background-color: gray; - } - &.red { - background-color: red; - } - &.green { - background-color: green; - } - &.black { - background-color: black; - } - &.brown { - background-color: brown - } - } - } - &:hover { - span { - &:nth-child(1) { - transform: scale(1.2); - } - } - } - } - } - } - } - .widget_price_filter { - .collection_filter_by_price { - .irs-handle { - cursor: pointer; - } - .irs--flat .irs-from, .irs--flat .irs-to, .irs--flat .irs-single { - background-color: var(--startupMainColor); - padding: 1px 5px 1px 11.5px; - - &::before { - border-top-color: var(--startupMainColor); - } - &::after { - content: '$'; - position: absolute; - left: 5px; - top: 0.5px; - } - } - .irs--flat .irs-bar { - background-color: var(--startupMainColor); - } - .irs--flat .irs-handle>i:first-child { - background-color: var(--startupMainColor); - } - .irs--flat .irs-min, .irs--flat .irs-max { - padding: 1px 3px 1px 9.5px; - - &::before { - content: '$'; - position: absolute; - left: 3px; - top: 0.5px; - } - } - } - } - .widget_top_rated_products { - .item { - margin-bottom: 15px; - position: relative; - padding-left: 95px; - - .thumb { - top: 0; - left: 0; - width: 80px; - float: unset; - height: 100%; - display: block; - position: absolute; - - .fullimage { - width: 80px; - height: 100%; - display: inline-block; - border: 1px solid #eeeeee; - background: { - repeat: no-repeat; - size: cover !important; - position: center center !important; - }; - &.bg1 { - background-image: url(../../assets/img/products/products-img1.jpg); - } - &.bg2 { - background-image: url(../../assets/img/products/products-img2.jpg); - } - &.bg3 { - background-image: url(../../assets/img/products/products-img3.jpg); - } - &.bg4 { - background-image: url(../../assets/img/products/products-img4.jpg); - } - } - } - .info { - padding: { - top: 5px; - bottom: 5px; - }; - .title { - margin-bottom: 8px; - line-height: 1.4; - font: { - size: var(--fontSize); - weight: 600; - }; - a { - display: inline-block; - } - } - .star-rating { - font-size: 14px; - - i { - color: #f49f0b; - } - } - .price { - margin-top: 8px; - display: block; - color: var(--optionalColor); - } - } - &:last-child { - margin-bottom: 0; - } - } - } - .widget_service_categories { - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - font-weight: 500; - overflow: hidden; - margin-bottom: 13px; - padding-bottom: 13px; - color: var(--optionalColor); - border-bottom: 1px dashed #eeeeee; - - a { - display: block; - position: relative; - color: var(--optionalColor); - - i { - position: absolute; - right: 5px; - top: 50%; - font-size: 14px; - transform: translateY(-50%); - transition: var(--transition); - color: var(--optionalColor); - } - &:hover { - color: var(--startupMainColor); - - i { - right: 0; - color: var(--startupMainColor); - } - } - } - &:last-child { - margin-bottom: 0; - } - } - } - } - &.extra-padding { - padding-left: 15px; - - &.left-sidebar { - padding: { - left: 0; - right: 15px; - }; - } - } -} - -/*================================================ -Profile Authentication Area CSS -=================================================*/ -.profile-authentication-area { - height: 100vh; - position: relative; - background-color: rgb(243, 243, 243); - - .logo { - left: 0; - right: 0; - top: 20px; - position: absolute; - text-align: center; - display: inline-block; - } -} -.signin-form { - max-width: 550px; - border-radius: 5px; - padding: 40px; - background-color: var(--whiteColor); - margin: { - left: auto; - right: auto; - }; - h2 { - font-size: 30px; - margin-bottom: 30px; - } - form { - .form-group { - margin-bottom: 25px; - - .form-control { - background-color: #f5f5f5 !important; - } - } - .remember-me-wrap { - margin-bottom: 0; - - [type="checkbox"]:checked, [type="checkbox"]:not(:checked) { - display: none; - } - [type="checkbox"]:checked + label, [type="checkbox"]:not(:checked) + label { - position: relative; - padding-left: 28px; - cursor: pointer; - line-height: 20px; - display: inline-block; - margin-bottom: 0; - color: var(--optionalColor); - } - [type="checkbox"]:checked + label:before, [type="checkbox"]:not(:checked) + label:before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 19px; - height: 19px; - transition: all 0.2s ease; - border: 1px solid #f5f5f5; - border-radius: 3px; - background: #f5f5f5; - } - [type="checkbox"]:checked + label:after, [type="checkbox"]:not(:checked) + label:after { - content: ''; - width: 8px; - height: 8px; - background: var(--startupMainColor); - position: absolute; - top: 5.5px; - left: 6px; - transition: all 0.2s ease; - } - [type="checkbox"]:not(:checked) + label:after { - opacity: 0; - transform: scale(0); - } - [type="checkbox"]:checked + label:after { - opacity: 1; - transform: scale(1); - } - [type="checkbox"]:hover + label:before { - border-color: var(--startupMainColor); - } - [type="checkbox"]:checked + label:before { - border-color: var(--startupMainColor); - } - } - .lost-your-password-wrap { - text-align: end; - - a { - display: inline-block; - position: relative; - line-height: 1.5; - font-weight: 500; - - &::before { - width: 100%; - height: 1px; - position: absolute; - left: 0; - bottom: 0; - content: ''; - transition: var(--transition); - background-color: #eeeeee; - } - &::after { - width: 0; - height: 1px; - position: absolute; - left: 0; - transition: var(--transition); - bottom: 0; - content: ''; - background-color: var(--startupMainColor); - } - &:hover { - &::before { - width: 0; - } - &::after { - width: 100%; - } - } - } - } - button { - width: 100%; - border: none; - display: block; - overflow: hidden; - margin-top: 20px; - border-radius: 5px; - text-align: center; - padding: 14px 30px; - color: var(--whiteColor); - transition: var(--transition); - background-color: var(--startupMainColor); - font: { - size: var(--fontSize); - weight: 700; - }; - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - .dont-account { - display: block; - margin-top: 25px; - color: var(--optionalColor); - - a { - font-weight: 600; - } - } - } - .sign-in-with-button { - margin-top: 25px; - - button { - width: 100%; - display: block; - color: #248BD2; - border-radius: 5px; - border: 1px solid #D2D2D2; - background-color: transparent; - transition: var(--transition); - padding: { - top: 13px; - bottom: 13px; - left: 30px; - right: 30px; - }; - svg { - position: absolute; - margin-left: -25px; - margin: { - left: -25px; - top: 1.5px; - }; - } - &:hover { - color: var(--blackColor); - background-color: #E9E9E9; - } - } - } -} -.signup-form { - max-width: 550px; - border-radius: 5px; - padding: 40px; - background-color: var(--whiteColor); - margin: { - left: auto; - right: auto; - }; - h2 { - font-size: 30px; - margin-bottom: 30px; - } - form { - .form-group { - margin-bottom: 25px; - - label { - display: block; - margin-bottom: 13px; - color: var(--blackColor); - font-weight: 600; - } - .form-control { - background-color: #f5f5f5 !important; - } - } - .remember-me-wrap { - margin-bottom: 0; - - [type="checkbox"]:checked, [type="checkbox"]:not(:checked) { - display: none; - } - [type="checkbox"]:checked + label, [type="checkbox"]:not(:checked) + label { - position: relative; - padding-left: 28px; - cursor: pointer; - line-height: 20px; - display: inline-block; - margin-bottom: 0; - color: var(--optionalColor); - } - [type="checkbox"]:checked + label:before, [type="checkbox"]:not(:checked) + label:before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 19px; - height: 19px; - transition: all 0.2s ease; - border: 1px solid #f5f5f5; - border-radius: 3px; - background: #f5f5f5; - } - [type="checkbox"]:checked + label:after, [type="checkbox"]:not(:checked) + label:after { - content: ''; - width: 8px; - height: 8px; - background: var(--startupMainColor); - position: absolute; - top: 5.5px; - left: 6px; - transition: all 0.2s ease; - } - [type="checkbox"]:not(:checked) + label:after { - opacity: 0; - transform: scale(0); - } - [type="checkbox"]:checked + label:after { - opacity: 1; - transform: scale(1); - } - [type="checkbox"]:hover + label:before { - border-color: var(--startupMainColor); - } - [type="checkbox"]:checked + label:before { - border-color: var(--startupMainColor); - } - } - .lost-your-password-wrap { - text-align: end; - - a { - display: inline-block; - position: relative; - line-height: 1.5; - font-weight: 500; - - &::before { - width: 100%; - height: 1px; - position: absolute; - left: 0; - bottom: 0; - content: ''; - transition: var(--transition); - background-color: #eeeeee; - } - &::after { - width: 0; - height: 1px; - position: absolute; - left: 0; - transition: var(--transition); - bottom: 0; - content: ''; - background-color: var(--startupMainColor); - } - &:hover { - &::before { - width: 0; - } - &::after { - width: 100%; - } - } - } - } - button { - width: 100%; - border: none; - display: block; - overflow: hidden; - margin-top: 20px; - border-radius: 5px; - text-align: center; - padding: 14px 30px; - color: var(--whiteColor); - transition: var(--transition); - background-color: var(--startupMainColor); - font: { - size: var(--fontSize); - weight: 700; - }; - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - .dont-account { - display: block; - margin-top: 25px; - color: var(--optionalColor); - - a { - font-weight: 600; - } - } - } - .sign-in-with-button { - margin-top: 25px; - - button { - width: 100%; - display: block; - color: #248BD2; - border-radius: 5px; - border: 1px solid #D2D2D2; - background-color: transparent; - transition: var(--transition); - padding: { - top: 13px; - bottom: 13px; - left: 30px; - right: 30px; - }; - svg { - position: absolute; - margin-left: -25px; - margin: { - left: -25px; - top: 1.5px; - }; - } - &:hover { - color: var(--blackColor); - background-color: #E9E9E9; - } - } - } -} - -/*================================================ -Instagram Area CSS -=================================================*/ -.instagram-area { - .container-fluid { - padding: { - left: 0; - right: 0; - }; - } - &.bg-dark { - .section-title { - border-color: rgba(255, 255, 255, .08); - - &::before { - background-color: var(--whiteColor); - } - } - } -} -.single-instagram-item { - position: relative; - - .link-btn { - display: block; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 2; - } - i { - position: absolute; - left: 50%; - top: 50%; - transform: translateY(-50%) translateX(-50%); - font-size: 25px; - color: var(--whiteColor); - opacity: 0; - visibility: hidden; - transition: var(--transition); - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: #000000; - opacity: 0; - visibility: hidden; - transition: var(--transition); - } - &:hover { - i { - opacity: 1; - visibility: visible; - } - &::before { - opacity: 0.5; - visibility: visible; - } - } -} - -/*================================================ -Page Title Area CSS -=================================================*/ -.page-title-area { - background: { - position: center center; - repeat: no-repeat; - color: #f9faff; - size: cover; - }; - padding: { - top: 160px; - bottom: 170px; - }; - &.transparent-bg1 { - background-image: url(../../assets/img/page-title/page-title-bg1.jpg); - } - &.transparent-bg2 { - background-image: url(../../assets/img/page-title/page-title-bg2.jpg); - } - &.transparent-bg3 { - background-image: url(../../assets/img/page-title/page-title-bg3.jpg); - } - &.transparent-bg4 { - background-image: url(../../assets/img/page-title/page-title-bg4.jpg); - } - &.transparent-bg5 { - background-image: url(../../assets/img/page-title/page-title-bg5.jpg); - } - &.bg-f9faff { - background-color: #f9faff; - } - &.bg-image { - background-color: var(--blackColor); - position: relative; - z-index: 1; - - .page-title-content { - color: var(--whiteColor); - - h2 { - color: var(--whiteColor); - } - ul { - li { - font-weight: 600; - - &::before { - background-color: var(--optionalColor); - } - a { - color: var(--whiteColor); - - &:hover { - color: var(--startupMainColor); - } - } - } - } - } - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: 0.7; - } - } - &.bg1 { - background-image: url(../../assets/img/page-title/page-title-bg-image1.jpg); - } - &.bg2 { - background-image: url(../../assets/img/page-title/page-title-bg-image2.jpg); - } - &.bg3 { - background-image: url(../../assets/img/page-title/page-title-bg-image3.jpg); - } - &.bg4 { - background-image: url(../../assets/img/page-title/page-title-bg-image4.jpg); - } - &.bg5 { - background-image: url(../../assets/img/page-title/page-title-bg-image5.jpg); - } - &.style-two { - padding: { - top: 260px; - bottom: 150px; - }; - } - &.style-three { - padding: { - top: 275px; - bottom: 150px; - }; - } - &.style-four { - position: relative; - z-index: 1; - padding: { - top: 170px; - bottom: 170px; - }; - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background: { - size: cover; - repeat: no-repeat; - position: center center; - image: url(../../assets/img/bg12.png); - }; - } - } -} -.page-title-content { - text-align: center; - - h2 { - margin-bottom: 0; - font-size: 45px; - } - ul { - padding-left: 0; - list-style-type: none; - margin: { - bottom: 0; - top: 20px; - }; - li { - color: var(--startupMainColor); - display: inline-block; - position: relative; - font-weight: 500; - font-size: 16px; - margin: { - left: 12px; - right: 12px; - }; - &::before { - content: ''; - position: absolute; - right: -18px; - top: 50%; - transform: translateY(-50%); - height: 1px; - width: 10px; - background-color: var(--optionalColor); - margin-top: 1px; - } - a { - color: var(--blackColor); - - &:hover { - color: var(--startupMainColor); - } - } - &:last-child { - margin-right: 0; - - &::before { - display: none; - } - } - &:first-child { - margin-left: 0; - } - } - } -} - -/*================================================ -Pagination Area CSS -=================================================*/ -.pagination-area { - margin-top: 20px; - - .page-numbers { - width: 45px; - height: 45px; - line-height: 43px; - position: relative; - text-align: center; - border-radius: 50%; - display: inline-block; - border: 1px solid #eeeeee; - color: var(--blackColor); - background-color: transparent; - font: { - size: 18px; - weight: 600; - }; - margin: { - left: 3px; - right: 3px; - }; - &:hover, &.current { - color: var(--whiteColor); - background-color: var(--startupMainColor); - border-color: var(--startupMainColor) !important; - } - } -} - -/*================================================ -Gallery Area CSS -=================================================*/ -.single-gallery-item { - margin-bottom: 30px; - position: relative; - text-align: center; - overflow: hidden; - - img { - transition: var(--transition); - } - .popup-image { - position: absolute; - left: 0; - transform: translateY(-50%) scale(0); - top: 50%; - z-index: 2; - right: 0; - width: 45px; - height: 45px; - line-height: 45px; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - border-radius: 3px; - margin: { - left: auto; - right: auto; - }; - &:hover { - border-radius: 50%; - color: var(--blackColor); - background-color: var(--whiteColor); - } - } - &::before { - z-index: 1; - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: var(--startupMainColor); - opacity: 0; - visibility: hidden; - transition: .3s; - margin: 10px; - } - &:hover { - img { - transform: scale(1.1); - } - &::before { - opacity: 1; - margin: 20px; - visibility: visible; - } - .popup-image { - transform: translateY(-50%) scale(1); - } - } - &.left-overlay { - &::before { - left: 0; - top: 0; - margin: 0; - width: 0%; - height: 100%; - right: auto; - bottom: auto; - opacity: .80; - } - &:hover { - &::before { - width: 100%; - } - } - } - &.top-overlay { - &::before { - left: 0; - top: 0; - margin: 0; - width: 100%; - height: 0%; - right: auto; - bottom: auto; - opacity: .80; - } - &:hover { - &::before { - height: 100%; - } - } - } -} -.shorting { - .mix { - display: none; - } -} -.shorting-menu { - text-align: center; - margin-bottom: 40px; - - .filter { - background-color: transparent; - transition: var(--transition); - letter-spacing: 2px; - color: #b0b0b0; - border: none; - padding: 0; - font: { - size: var(--fontSize); - weight: 500; - }; - margin: { - left: 20px; - right: 20px; - }; - &:last-child { - margin-right: 0; - } - &:first-child { - margin-left: 0; - } - &:hover, &.active { - color: var(--blackColor); - } - } -} - -/*================================================ -Portfolio Details Area CSS -=================================================*/ -.portfolio-details-image { - position: relative; - margin-bottom: 30px; - text-align: center; -} -.portfolio-details-desc { - h3 { - font-size: 25px; - margin: { - bottom: 15px; - top: 30px; - }; - &:first-child { - margin-top: 0; - } - } - .image { - padding-right: 15px; - border-radius: 5px; - margin: { - top: 15px; - bottom: 30px; - }; - } - .content { - margin: { - top: 15px; - bottom: 30px; - }; - h3 { - margin: { - top: 0; - bottom: 25px; - }; - } - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - color: var(--optionalColor); - margin-bottom: 12px; - position: relative; - padding-left: 18px; - font-weight: 500; - - &::before { - left: 0; - top: 50%; - width: 7px; - height: 7px; - content: ''; - position: absolute; - border-radius: 50%; - transform: translateY(-50%); - background-color: var(--startupMainColor); - } - &:last-child { - margin-bottom: 0; - } - } - } - } -} -.portfolio-details-info { - box-shadow: 0px -10px 30px rgba(0, 0, 0, 0.1); - border-top: 3px solid var(--startupMainColor); - background-color: var(--whiteColor); - border-radius: 5px; - padding: 40px 30px; - margin-left: 15px; - - ul { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - font-weight: 500; - position: relative; - margin-bottom: 20px; - color: var(--optionalColor); - border-bottom: 1px solid #eeeeee; - padding: { - left: 35px; - bottom: 20px; - }; - span { - color: var(--blackColor); - margin-bottom: 10px; - display: block; - font: { - size: 19px; - weight: 700; - }; - } - a { - margin-bottom: 8px; - display: inline-block; - color: var(--optionalColor); - - &:last-child { - margin-bottom: 0; - } - &:hover { - color: var(--startupMainColor); - } - } - .icon { - left: 0; - top: 6px; - font-size: 25px; - position: absolute; - color: var(--startupMainColor); - } - &:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; - } - } - } -} -.portfolio-details-slides { - &.owl-theme { - .owl-nav { - opacity: 0; - margin-top: 0; - visibility: hidden; - transition: var(--transition); - - [class*=owl-] { - margin: 0; - background-color: var(--whiteColor); - color: var(--blackColor); - padding: 0 !important; - border-radius: 0; - transition: var(--transition); - border: none; - cursor: pointer; - position: absolute; - left: 25px; - top: 50%; - width: 45px; - height: 45px; - line-height: 45px; - border-radius: 50%; - transform: translateY(-50%); - font-size: 20px; - - &.owl-next { - left: auto; - right: 25px; - } - i { - line-height: 1; - } - &:hover { - background-color: var(--blackColor); - border-color: var(--blackColor); - color: var(--whiteColor); - } - } - } - &:hover { - .owl-nav { - opacity: 1; - visibility: visible; - } - } - .owl-dots { - margin-top: 40px; - - .owl-dot { - span { - width: 18px; - height: 18px; - border-radius: 50%; - transition: var(--transition); - margin: 0 5px; - } - &:hover, &.active { - span { - background-color: var(--startupMainColor); - } - } - } - } - } -} - -/*================================================ -Portfolio Details Navigation Area CSS -=================================================*/ -.portfolio-details-navigation { - background-color: #f7f7f7; - padding: { - top: 40px; - bottom: 40px; - }; -} -.portfolio-details-left { - text-transform: uppercase; - font-weight: 600; - line-height: 1; - - a { - display: inline-block; - color: var(--optionalColor); - position: relative; - padding-left: 25px; - - i { - position: absolute; - left: 0; - top: 0; - } - &:hover { - color: var(--startupMainColor); - } - } -} -.menu-icon { - line-height: 1; - font-size: 22px; - color: var(--optionalColor); - - &:hover { - color: var(--startupMainColor); - } -} -.portfolio-details-right { - text-transform: uppercase; - font-weight: 600; - line-height: 1; - - a { - display: inline-block; - color: var(--optionalColor); - position: relative; - padding-right: 25px; - - i { - position: absolute; - right: 0; - top: 0; - } - &:hover { - color: var(--startupMainColor); - } - } -} - -/*================================================ -Privacy Policy Area CSS -=================================================*/ -.privacy-policy-content { - h3 { - font-size: 20px; - margin-bottom: 15px; - - &:not(:first-child) { - margin-top: 25px; - } - } - strong { - color: var(--blackColor) !important; - } - ul { - li { - color: var(--optionalColor); - margin-bottom: 12px; - - &:last-child { - margin-bottom: 0; - } - } - } -} - -/*================================================ -Terms & Conditions Area CSS -=================================================*/ -.terms-conditions-content { - h3 { - font-size: 20px; - margin-bottom: 15px; - - &:not(:first-child) { - margin-top: 25px; - } - } - strong { - color: var(--blackColor) !important; - } - ul { - li { - color: var(--optionalColor); - margin-bottom: 12px; - - &:last-child { - margin-bottom: 0; - } - } - } -} - -/*================================================ -FAQ Area CSS -=================================================*/ -.faq-accordion { - max-width: 800px; - margin: { - left: auto; - right: auto; - }; - .accordion-item { - margin-bottom: 15px; - border: 1px solid #eeeeee; - - .accordion-button { - border: none; - padding: 20px 20px 20px 45px; - position: relative; - border: none; - box-shadow: unset !important; - text-align: start; - margin: 0; - font: { - size: 18px; - weight: 700; - }; - &:not(.collapsed) { - color: var(--blackColor); - background-color: transparent; - - &::after { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - } - } - &::after { - position: absolute; - left: 20px; - top: 25px; - width: 15px; - height: 15px; - background-size: 15px; - } - } - .accordion-collapse { - border: none; - } - .accordion-body { - padding: 0 20px 20px 45px; - - p { - strong { - color: var(--blackColor); - } - } - ul { - margin-bottom: 15px; - - li { - margin-bottom: 12px; - color: var(--optionalColor); - - &:last-child { - margin-bottom: 0; - } - } - &:last-child { - margin-bottom: 0; - } - } - } - &:last-child { - margin-bottom: 0; - } - &:first-of-type { - .accordion-button { - border: { - top: { - left-radius: 0; - right-radius: 0; - } - }; - } - } - } -} - -/*================================================ -404 Error Area CSS -=================================================*/ -.error-area { - border: { - top: 1px dashed #eeeeee; - bottom: 1px dashed #eeeeee; - }; -} -.error-content { - text-align: center; - - h3 { - font-size: 30px; - margin: { - bottom: 20px; - top: 45px; - }; - } - p { - max-width: 550px; - margin: { - left: auto; - right: auto; - bottom: 0; - }; - } - .default-btn { - margin-top: 30px; - } -} - -/*================================================ -Coming Soon Area CSS -=================================================*/ -.coming-soon-area { - height: 100vh; -} -.coming-soon-image { - height: 100%; - background: { - image: url(../../assets/img/coming-soon.jpg); - position: center center; - repeat: no-repeat; - size: cover; - }; - img { - display: none; - } -} -.coming-soon-content { - height: 100vh; - text-align: center; - padding: { - left: 30px; - right: 30px; - }; - .logo { - display: inline-block; - } - h2 { - font-size: 40px; - margin: { - top: 30px; - bottom: 0; - }; - } - #timer { - margin-top: 35px; - - div { - width: 120px; - height: 120px; - border-radius: 5px; - color: var(--blackColor); - background-color: #f8f8f8; - font: { - size: 35px; - weight: 700; - }; - margin: { - left: 10px; - right: 10px; - }; - span { - display: block; - margin-top: 2px; - font: { - size: var(--fontSize); - weight: 500; - }; - } - &:first-child { - margin-left: 0; - } - &:last-child { - margin-right: 0; - } - } - } - form { - position: relative; - max-width: 530px; - margin: { - left: auto; - right: auto; - top: 40px; - }; - .input-newsletter { - padding: 0 0 0 15px; - width: 100%; - height: 60px; - border: none; - display: block; - border-radius: 4px; - color: var(--blackColor); - font-size: var(--fontSize); - transition: var(--transition); - border: 1px solid #eeeeee; - - &::placeholder { - color: var(--optionalColor); - transition: var(--transition); - } - &:focus { - border-color: var(--startupMainColor); - - &::placeholder { - color: transparent; - } - } - } - .default-btn { - position: absolute; - right: 5px; - box-shadow: unset; - bottom: 6px; - padding: { - left: 35px; - right: 35px; - }; - &:hover { - background-color: var(--blackColor); - color: var(--whiteColor); - } - } - #validator-newsletter { - position: absolute; - bottom: -32px; - left: 0; - - &.validation-danger { - color: red; - } - } - } -} - -/*================================================ -Contact Area CSS -=================================================*/ -.contact-area { - position: relative; - z-index: 1; - - &.bg-image { - background: { - image: url(../../assets/img/cta-bg1.jpg); - position: center center; - repeat: no-repeat; - size: cover; - }; - &::before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - background-color: #000000; - opacity: 0.8; - } - .section-title { - h2 { - color: var(--whiteColor); - } - } - } -} -.single-contact-info-box { - padding: 35px; - margin-bottom: 30px; - background-color: #F9FAFB; - - .icon { - line-height: 1; - font-size: 40px; - margin-bottom: 25px; - color: var(--startupMainColor); - } - h3 { - font-size: 20px; - margin-bottom: 15px; - } - p { - margin-bottom: 2px; - - a { - color: var(--optionalColor); - display: inline-block; - - &:hover { - color: var(--startupMainColor); - } - } - &:last-child { - margin-bottom: 0; - } - } - &.style-two { - padding: 0; - text-align: center; - background-color: transparent; - - .icon { - width: 80px; - color: var(--whiteColor); - background-color: var(--blackColor); - border-radius: 50%; - position: relative; - height: 80px; - font-size: 35px; - transition: var(--transition); - margin: { - left: auto; - right: auto; - bottom: 25px; - }; - i { - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); - } - } - &:hover { - .icon { - background-color: var(--startupMainColor); - color: var(--whiteColor); - } - } - } -} -.contact-form { - padding: 34px; - margin-bottom: 30px; - background-color: var(--whiteColor); - box-shadow: 0 0 1.25rem rgba(108, 118, 134, 0.1); - - form { - .form-group { - margin-bottom: 20px; - - label { - display: block; - margin-bottom: 10px; - color: var(--optionalColor); - } - .form-control { - border-radius: 0; - background-color: #f9faff; - border: 1px solid #eeeeee !important; - } - } - .form-check { - margin: { - bottom: 15px; - top: 5px; - }; - a { - color: var(--startupMainColor); - - &:hover { - color: var(--blackColor); - text-decoration: underline; - } - } - } - .default-btn { - margin-top: 5px; - padding: { - left: 35px; - right: 35px; - }; - } - .help-block { - &.with-errors { - color: red; - margin: { - bottom: 0; - top: 15px; - }; - } - } - #msgSubmit { - margin: 0; - - &.text-danger { - margin-top: 15px; - } - } - } -} -#maps { - iframe { - margin-bottom: -7px; - height: 500px; - border: none; - width: 100%; - } -} - -/*================================================ -Footer Area CSS -=================================================*/ -.footer-area { - padding-top: 100px; - position: relative; - z-index: 1; - - &.marketing-footer { - border-top: 1px solid #eeeeee; - - .footer-top { - border-bottom: none; - margin-bottom: 0; - } - .copyright-area { - border-top: 1px solid #eeeeee; - - p { - a { - color: var(--marketingMainColor); - } - } - } - .single-footer-widget { - .footer-contact { - li { - i { - color: var(--marketingMainColor); - } - a { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - .info-links { - li { - a { - &:hover { - color: var(--marketingMainColor); - } - } - } - } - .newsletter-form { - button { - &:hover { - color: var(--whiteColor); - background-color: var(--marketingMainColor); - } - } - } - } - } - &.business-footer { - padding-top: 0; - - .footer-top { - background: var(--businessGradientColor); - margin-bottom: 100px; - border: none; - padding: { - top: 70px; - bottom: 70px; - }; - .content { - h3 { - color: var(--whiteColor); - } - .default-btn { - background: transparent; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - box-shadow: unset; - - i { - color: var(--whiteColor); - } - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } - } - } - .copyright-area { - border-top: 1px solid #eeeeee; - - p { - a { - color: var(--businessMainColor); - } - } - } - .single-footer-widget { - .footer-contact { - li { - i { - color: var(--businessMainColor); - } - a { - &:hover { - color: var(--businessMainColor); - } - } - } - } - .info-links { - li { - a { - &:hover { - color: var(--businessMainColor); - } - } - } - } - .newsletter-form { - button { - &:hover { - color: var(--whiteColor); - background-color: var(--businessMainColor); - } - } - } - } - } - &.app-footer { - background: var(--appGradientColor); - - .footer-top { - border-color: #0e49b5; - - .content { - h3 { - color: var(--whiteColor); - } - .default-btn { - background: transparent; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - box-shadow: unset; - - i { - color: var(--whiteColor); - } - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } - } - } - .copyright-area { - opacity: 0.9; - border-top: 1px solid #2c4687; - background: transparent; - - p { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - } - } - } - .single-footer-widget { - h3 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - } - .footer-contact { - li { - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - a { - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - } - } - } - } - .info-links { - li { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - } - } - } - } - .newsletter-form { - button { - border-left: 1px solid var(--blackColor); - - &:hover { - color: var(--blackColor); - border-color: #eeeeee; - background-color: var(--whiteColor); - } - } - } - } - } - &.saas-footer { - background: #000000; - - .footer-top { - border-color: #121419; - - .content { - h3 { - color: var(--whiteColor); - } - .default-btn { - background: transparent; - color: var(--whiteColor); - border: 1px solid var(--whiteColor); - box-shadow: unset; - - i { - color: var(--whiteColor); - } - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } - } - } - .copyright-area { - border-top: 1px solid #121419; - background: transparent; - - p { - color: #959595; - - a { - color: var(--whiteColor); - } - } - } - .single-footer-widget { - h3 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - } - .footer-contact { - li { - color: var(--whiteColor); - - i { - color: var(--saasMainColor); - } - a { - color: var(--whiteColor); - - &:hover { - color: var(--saasMainColor); - } - } - } - } - .info-links { - li { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - - &:hover { - color: var(--saasMainColor); - } - } - } - } - .newsletter-form { - button { - background: var(--saasGradientColor); - } - } - } - } - &.personal-portfolio-footer { - border-top: 1px solid #eeeeee; - - .single-footer-widget { - .footer-contact { - li { - i { - color: var(--personalPortfolioMainColor); - } - a { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - .info-links { - li { - a { - &:hover { - color: var(--personalPortfolioMainColor); - } - } - } - } - .newsletter-form { - button { - &:hover { - color: var(--whiteColor); - background-color: var(--personalPortfolioMainColor); - } - } - } - } - .copyright-area { - border-top: none; - background: #06080e; - - p { - color: #959595; - - a { - color: var(--whiteColor); - } - } - } - } - &.consulting-footer { - overflow: hidden; - background-color: #f3f4f7; - - .single-footer-widget { - .footer-contact { - li { - i { - color: var(--consultingMainColor); - } - a { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - .info-links { - li { - a { - &:hover { - color: var(--consultingMainColor); - } - } - } - } - .newsletter-form { - button { - &:hover { - background-color: var(--consultingMainColor); - } - } - } - } - .copyright-area { - background-color: transparent; - border-top: 1px solid #e4eaf5; - - p { - a { - color: var(--blackColor); - - &:hover { - color: var(--consultingMainColor); - } - } - } - } - } - &.it-agency-footer { - background-color: #111111; - - .footer-top { - border-color: #272727; - - .content { - h3 { - color: var(--whiteColor); - } - .default-btn { - border: 1px solid var(--whiteColor); - color: var(--whiteColor); - background: transparent; - box-shadow: unset; - - i { - color: var(--whiteColor); - } - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } - } - } - .single-footer-widget { - h3 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - } - .footer-contact { - li { - color: var(--whiteColor); - - i { - color: var(--itAgencyMainColor); - } - a { - color: var(--whiteColor); - - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - .info-links { - li { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - - &:hover { - color: var(--itAgencyMainColor); - } - } - } - } - .newsletter-form { - button { - background: var(--itAgencyGradientColor); - } - } - } - .copyright-area { - border-top: 1px solid #272727; - background-color: transparent; - - p { - color: #c8c5c5; - - a { - color: var(--whiteColor); - } - } - } - } - &.bg-dark { - .footer-top { - border-color: rgba(255, 255, 255, .07); - - .content { - h3 { - color: var(--whiteColor); - } - .default-btn { - border: 1px solid var(--whiteColor); - color: var(--whiteColor); - background: transparent; - box-shadow: unset; - - i { - color: var(--whiteColor); - } - &:hover { - background-color: var(--whiteColor); - color: var(--blackColor); - - i { - color: var(--blackColor); - } - } - } - } - } - .single-footer-widget { - color: var(--whiteColor); - - h3 { - color: var(--whiteColor); - } - p { - color: var(--whiteColor); - } - .footer-contact { - li { - color: var(--whiteColor); - - i { - color: var(--whiteColor); - } - a { - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - } - } - } - } - .info-links { - li { - color: var(--whiteColor); - - a { - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - } - } - } - } - .newsletter-form { - button { - background-color: var(--optionalColor); - color: var(--whiteColor); - - &:hover { - color: var(--whiteColor); - background-color: var(--blackColor); - } - } - } - } - .copyright-area { - background-color: #06080e; - border-top: 1px solid #292929; - - p { - color: #c6c6c6; - - a { - font-weight: 800; - color: var(--whiteColor); - } - } - .social-links { - li { - a { - color: var(--whiteColor); - } - } - } - } - } - &.minimal-portfolio-color { - padding: 0; - - .copyright-area { - margin: 0; - padding: { - top: 50px; - bottom: 50px; - }; - p { - a { - color: var(--minimalPortfolioColor); - } - } - } - } -} -.footer-top { - border-bottom: 1px solid #eeeeee; - padding-bottom: 70px; - margin-bottom: 70px; - - .logo { - display: inline-block; - } - .content { - h3 { - font-size: 20px; - margin-bottom: 0; - } - } -} -.single-footer-widget { - margin-bottom: 30px; - - h3 { - font-size: 20px; - margin-bottom: 20px; - } - .footer-contact { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - position: relative; - margin-bottom: 12px; - padding-left: 24px; - color: var(--optionalColor); - - i { - position: absolute; - left: 0; - top: 4px; - color: var(--startupMainColor); - } - a { - display: inline-block; - color: var(--optionalColor); - - &:hover { - color: var(--startupMainColor); - } - } - &:last-child { - margin-bottom: 0; - } - } - } - .info-links { - padding-left: 0; - margin-bottom: 0; - list-style-type: none; - - li { - margin-bottom: 12px; - - a { - display: inline-block; - color: var(--optionalColor); - - &:hover { - color: var(--startupMainColor); - } - } - &:last-child { - margin-bottom: 0; - } - } - } - .newsletter-form { - position: relative; - - .input-newsletter { - display: block; - width: 100%; - height: 50px; - border: 1px solid #eeeeee; - background-color: var(--whiteColor); - color: var(--blackColor); - padding-left: 15px; - - &::placeholder { - transition: var(--transition); - color: var(--optionalColor); - } - &:focus { - &::placeholder { - color: transparent; - } - } - } - button { - position: absolute; - right: 0; - top: 0; - height: 50px; - width: 60px; - background-color: var(--blackColor); - color: var(--whiteColor); - border: none; - transition: var(--transition); - - &:hover { - color: var(--whiteColor); - background-color: var(--startupMainColor); - } - } - #validator-newsletter { - position: absolute; - bottom: -32px; - left: 0; - - &.validation-danger { - color: red; - } - } - } - &.pl-4 { - padding-left: 4rem; - } - &.pl-2 { - padding-left: 2rem; - } - .social-links { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; - - li { - display: inline-block; - margin-right: 12px; - - a { - display: inline-block; - font-size: 18px; - } - &:last-child { - margin-right: 0; - } - } - } - .logo { - display: inline-block; - margin-bottom: 15px; - } - &.style-two { - h3 { - position: relative; - padding-left: 55px; - - &::before { - width: 40px; - height: 1px; - background-color: var(--personalPortfolioMainColor); - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - } - } - } -} -.copyright-area { - background-color: var(--whiteColor); - text-align: center; - margin-top: 70px; - padding: { - top: 25px; - bottom: 25px; - }; - p { - a { - font-weight: 600; - display: inline-block; - color: var(--startupMainColor); - } - } - .social-links { - padding-left: 0; - list-style-type: none; - margin-bottom: 0; - text-align: end; - - li { - display: inline-block; - margin-right: 12px; - - a { - display: inline-block; - font-size: 18px; - } - &:last-child { - margin-right: 0; - } - } - } - &.bg-faf7f2 { - background-color: #faf7f2; - } -} - -/*================================================ -Go Top CSS -=================================================*/ -.go-top { - position: fixed; - cursor: pointer; - bottom: 20px; - right: 20px; - z-index: 4; - width: 43px; - height: 45px; - opacity: 0; - font-size: 20px; - visibility: hidden; - text-align: center; - color: var(--whiteColor); - transition: var(--transition); - background-color: var(--startupMainColor); - box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.1); - - i { - right: 0; - left: 0; - top: 50%; - text-align: center; - position: absolute; - transform: translateY(-50%); - margin: { - left: auto; - right: auto; - }; - } - &.active { - opacity: 1; - visibility: visible; - bottom: 20px; - } - &:hover { - color: var(--whiteColor); - background-color: var(--blackColor) !important; - } - &.marketing-color { - background-color: var(--marketingMainColor); - } - &.business-color { - background: var(--businessGradientColor); - } - &.app-color { - background: var(--appGradientColor); - } - &.saas-color { - background: var(--saasGradientColor); - } - &.personal-portfolio-color { - background-color: var(--personalPortfolioMainColor); - } - &.consulting-color { - background-color: var(--consultingMainColor); - } - &.it-agency-color { - background: var(--itAgencyGradientColor); - } - &.shop-color { - background: var(--shopMainColor); - } - &.minimal-portfolio-color { - background: var(--minimalPortfolioColor); - } -} - -/*extra-css*/ -.shape1 { - position: absolute; - right: 10%; - top: 5%; - z-index: -1; - animation: moveleftbounce 5s linear infinite; -} -.shape2 { - position: absolute; - right: 3%; - bottom: 12%; - z-index: -1; - animation: { - duration: 20s; - name: rotateme; - timing-function: linear; - iteration-count: infinite; - }; -} -.shape3 { - position: absolute; - right: 33.6%; - bottom: 7%; - z-index: -1; -} -.shape4 { - position: absolute; - top: 85px; - left: 20px; - z-index: -1; - animation: movebounce 5s linear infinite; -} -.shape5 { - position: absolute; - bottom: 40px; - right: 20px; - z-index: -1; - animation: moveleftbounce 5s linear infinite; -} -.shape6 { - position: absolute; - left: 40px; - top: 45px; - animation: { - duration: 20s; - name: rotateme; - timing-function: linear; - iteration-count: infinite; - }; -} -.shape7 { - position: absolute; - left: 30px; - top: 20px; - z-index: -1; - animation: { - duration: 20s; - name: rotateme; - timing-function: linear; - iteration-count: infinite; - }; -} -.shape8 { - position: absolute; - bottom: 50px; - left: 50px; - z-index: -1; - animation: { - duration: 30s; - name: rotateme; - timing-function: linear; - iteration-count: infinite; - }; -} -.shape9 { - position: absolute; - right: 0; - top: 65px; - z-index: -1; - animation: moveleftbounce 5s linear infinite; -} -.shape10 { - position: absolute; - left: 0; - top: 0; - z-index: -1; -} -.shape11 { - position: absolute; - top: 8%; - left: 0; - z-index: -1; - animation: { - duration: 40s; - name: rotateme2; - timing-function: linear; - iteration-count: infinite; - }; -} -.shape12 { - position: absolute; - right: 20px; - bottom: 20px; - z-index: -1; - animation: { - duration: 30s; - name: rotateme; - timing-function: linear; - iteration-count: infinite; - }; -} -.shape13 { - position: absolute; - right: 12%; - bottom: 2%; - z-index: -1; - animation: { - duration: 30s; - name: rotateme; - timing-function: linear; - iteration-count: infinite; - }; -} -.shape14 { - position: absolute; - left: 0; - bottom: 76px; - z-index: -1; - opacity: 0.23; -} -.shape15 { - position: absolute; - left: 50%; - top: 50%; - transform: translateY(-50%) translateX(-50%); - z-index: -1; - - img { - animation: movescale 5s linear infinite; - } -} -.shape16 { - position: absolute; - right: 5px; - top: 0; - z-index: -1; - animation: moveleftbounce 5s linear infinite; -} -.shape17 { - left: 0; - bottom: 98px; - z-index: -1; - opacity: .05; - position: absolute; -} -.shape18 { - position: absolute; - right: 0; - bottom: 0; - z-index: -1; -} -.shape19 { - position: absolute; - right: 35%; - top: 12%; - z-index: -1; - animation: { - duration: 30s; - name: rotateme; - timing-function: linear; - iteration-count: infinite; - }; -} -.divider { - width: 100%; - position: absolute; - height: 100px; - pointer-events: none; - -webkit-mask-image: url("data:image/svg+xml;utf8,%3csvg viewBox='0 0 100 100' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'%3e%3cpath d='M0,0 C16.6666667,66 33.3333333,98 50,98 C66.6666667,98 83.3333333,66 100,0 L100,100 L0,100 L0,0 Z' fill='%23fff'/%3e%3c/svg%3e"); - mask-image: url("data:image/svg+xml;utf8,%3csvg viewBox='0 0 100 100' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'%3e%3cpath d='M0,0 C16.6666667,66 33.3333333,98 50,98 C66.6666667,98 83.3333333,66 100,0 L100,100 L0,100 L0,0 Z' fill='%23fff'/%3e%3c/svg%3e"); - -webkit-mask-size: 100% 101%; - mask-size: 100% 101%; - background-color: var(--whiteColor); - bottom: 0; - left: 0; - z-index: -1; -} -@keyframes border-transform { - 0%,100% { - border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%; - } - 14% { - border-radius: 40% 60% 54% 46% / 49% 60% 40% 51%; - } - 28% { - border-radius: 54% 46% 38% 62% / 49% 70% 30% 51%; - } - 42% { - border-radius: 61% 39% 55% 45% / 61% 38% 62% 39%; - } - 56% { - border-radius: 61% 39% 67% 33% / 70% 50% 50% 30%; - } - 70% { - border-radius: 50% 50% 34% 66% / 56% 68% 32% 44%; - } - 84% { - border-radius: 46% 54% 50% 50% / 35% 61% 39% 65%; - } -} -@keyframes rotateme { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} -@keyframes rotateme2 { - from { - transform: rotate(0deg); - } - to { - transform: rotate(-360deg); - } -} -@keyframes movebounce { - 0% { - transform: translateY(0px); - } - 50% { - transform: translateY(20px); - } - 100% { - transform: translateY(0px); - } -} -@keyframes moveleftbounce { - 0% { - transform: translateX(0px); - } - 50% { - transform: translateX(-20px); - } - 100% { - transform: translateX(0px); - } -} -@keyframes scroller { - 0% { - bottom: 34px; - } - 5% { - top: 4px; - } - 32% { - bottom: 4px; - } - 66% { - top: 34px; - bottom: 4px; - } - 100% { - top: 4px; - bottom: 34px; - } -} -@keyframes movescale { - 0% { - transform: scale(.9); - } - 50% { - transform: scale(1); - } - 100% { - transform: scale(.9); - } -} -@keyframes rotate3d { - 0% { - -webkit-transform: rotateY(0deg); - transform: rotateY(0deg); - } - 100% { - -webkit-transform: rotateY(360deg); - transform: rotateY(360deg); - } -} -@keyframes animationFramesOne { - 0% { - transform: translate(0px,0px) rotate(0deg) ; - } - 20% { - transform: translate(73px,-1px) rotate(36deg) ; - } - 40% { - transform: translate(141px,72px) rotate(72deg) ; - } - 60% { - transform: translate(83px,122px) rotate(108deg) ; - } - 80% { - transform: translate(-40px,72px) rotate(144deg) ; - } - 100% { - transform: translate(0px,0px) rotate(0deg) ; - } -} -@keyframes ripple { - 0% { - -webkit-transform: scale(1); - transform: scale(1) - } - 75% { - -webkit-transform: scale(1.75); - transform: scale(1.75); - opacity: 1 - } - 100% { - -webkit-transform: scale(2); - transform: scale(2); - opacity: 0 - } -} -.owl-item { - overflow: hidden; - - &.active { - .startup-works-image { - animation: { - duration: .8s; - fill-mode: both; - name: fadeInRight; - delay: .5s; - }; - img { - display: inline-block !important; - width: auto !important; - } - } - .startup-testimonials-image { - overflow: hidden; - - .main-image { - animation: { - duration: .8s; - fill-mode: both; - name: fadeInRight; - delay: .5s; - }; - } - } - } -} - -/*================================================ -Modal CSS -=================================================*/ -.productsQuickView { - padding-right: 0 !important; - - .modal-dialog { - max-width: 900px; - margin: 0 auto; - } - .modal-content { - border: none; - padding: 40px; - border-radius: 0; - - button { - &.btn-close { - top: 0; - right: 0; - margin: 0; - z-index: 3; - outline: 0; - opacity: 1; - padding: 0; - width: 35px; - height: 35px; - border: none; - font-size: 17px; - line-height: 38px; - box-shadow: unset; - text-shadow: unset; - position: absolute; - } - } - .products-content { - h3 { - margin-bottom: 15px; - font-size: 20px; - } - .price { - color: var(--blackColor); - margin: { - top: 15px; - bottom: 12px; - }; - font: { - weight: 700; - size: 17px; - }; - .old-price { - text-decoration: line-through; - color: #9b9494; - font-weight: normal; - } - } - .products-review { - .rating { - display: inline-block; - padding-right: 3px; - font-size: 14px; - - i { - display: inline-block; - color: #f49f0b; - line-height: 1; - } - } - .rating-count { - display: inline-block; - color: var(--optionalColor); - line-height: initial; - position: relative; - top: 1px; - - &:hover { - color: var(--blackColor); - } - } - } - .products-add-to-cart { - margin-top: 25px; - - .quantities { - position: relative; - padding-left: 45px; - - .sub-title { - left: 0; - top: 50%; - display: block; - font-weight: 600; - position: absolute; - transform: translateY(-50%); - } - } - .input-counter { - display: block; - max-width: 120px; - min-width: 120px; - text-align: center; - position: relative; - margin-bottom: 25px; - - span { - top: 0; - width: 35px; - height: 100%; - cursor: pointer; - font-size: 15px; - line-height: 38px; - position: absolute; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - transition: var(--transition); - - i { - position: relative; - top: 1px; - } - &.minus-btn { - left: 0; - } - &.plus-btn { - right: 0; - } - &:hover { - color: var(--optionalColor); - } - } - input { - height: 40px; - outline: 0; - width: 100%; - display: block; - text-align: center; - color: var(--blackColor); - border: 1px solid #eeeeee; - background-color: transparent; - font: { - size: 17px; - weight: 600; - } - &::placeholder { - color: var(--blackColor); - } - } - } - } - .default-btn { - box-shadow: unset !important; - } - .social-share { - padding-left: 0; - list-style-type: none; - margin: { - bottom: 0; - top: 30px; - }; - li { - display: inline-block; - margin-right: 12px; - - span { - font-weight: 600; - position: relative; - top: -1px; - } - a { - line-height: 1; - font-size: 18px; - color: var(--optionalColor); - - &:hover { - color: var(--blackColor); - } - } - } - } - } - } -} \ No newline at end of file diff --git a/static/assets/img/about/about-img1.jpg b/static/assets/img/about/about-img1.jpg deleted file mode 100644 index 2a1c6876..00000000 Binary files a/static/assets/img/about/about-img1.jpg and /dev/null differ diff --git a/static/assets/img/about/about-img2.png b/static/assets/img/about/about-img2.png deleted file mode 100644 index b52b816d..00000000 Binary files a/static/assets/img/about/about-img2.png and /dev/null differ diff --git a/static/assets/img/about/about-img3.png b/static/assets/img/about/about-img3.png deleted file mode 100644 index 4c946021..00000000 Binary files a/static/assets/img/about/about-img3.png and /dev/null differ diff --git a/static/assets/img/about/about-img4.png b/static/assets/img/about/about-img4.png deleted file mode 100644 index eac97843..00000000 Binary files a/static/assets/img/about/about-img4.png and /dev/null differ diff --git a/static/assets/img/about/about-img5.png b/static/assets/img/about/about-img5.png deleted file mode 100644 index 33bfac47..00000000 Binary files a/static/assets/img/about/about-img5.png and /dev/null differ diff --git a/static/assets/img/about/about-img6.jpg b/static/assets/img/about/about-img6.jpg deleted file mode 100644 index 184cd4cf..00000000 Binary files a/static/assets/img/about/about-img6.jpg and /dev/null differ diff --git a/static/assets/img/about/about-img7.jpg b/static/assets/img/about/about-img7.jpg deleted file mode 100644 index eba9c53e..00000000 Binary files a/static/assets/img/about/about-img7.jpg and /dev/null differ diff --git a/static/assets/img/app-banner.png b/static/assets/img/app-banner.png deleted file mode 100644 index 2cc461ae..00000000 Binary files a/static/assets/img/app-banner.png and /dev/null differ diff --git a/static/assets/img/app-download.png b/static/assets/img/app-download.png deleted file mode 100644 index 07b393fb..00000000 Binary files a/static/assets/img/app-download.png and /dev/null differ diff --git a/static/assets/img/apple-store.png b/static/assets/img/apple-store.png deleted file mode 100644 index 3f199736..00000000 Binary files a/static/assets/img/apple-store.png and /dev/null differ diff --git a/static/assets/img/bg1.jpg b/static/assets/img/bg1.jpg deleted file mode 100644 index 5266c858..00000000 Binary files a/static/assets/img/bg1.jpg and /dev/null differ diff --git a/static/assets/img/bg10.jpg b/static/assets/img/bg10.jpg deleted file mode 100644 index f369a610..00000000 Binary files a/static/assets/img/bg10.jpg and /dev/null differ diff --git a/static/assets/img/bg11.jpg b/static/assets/img/bg11.jpg deleted file mode 100644 index b5045be3..00000000 Binary files a/static/assets/img/bg11.jpg and /dev/null differ diff --git a/static/assets/img/bg12.png b/static/assets/img/bg12.png deleted file mode 100644 index d308175e..00000000 Binary files a/static/assets/img/bg12.png and /dev/null differ diff --git a/static/assets/img/bg2.jpg b/static/assets/img/bg2.jpg deleted file mode 100644 index 1f6455a3..00000000 Binary files a/static/assets/img/bg2.jpg and /dev/null differ diff --git a/static/assets/img/bg3.jpg b/static/assets/img/bg3.jpg deleted file mode 100644 index 0a3d317b..00000000 Binary files a/static/assets/img/bg3.jpg and /dev/null differ diff --git a/static/assets/img/bg4.jpg b/static/assets/img/bg4.jpg deleted file mode 100644 index 6756ab9f..00000000 Binary files a/static/assets/img/bg4.jpg and /dev/null differ diff --git a/static/assets/img/bg5.jpg b/static/assets/img/bg5.jpg deleted file mode 100644 index 60024e9d..00000000 Binary files a/static/assets/img/bg5.jpg and /dev/null differ diff --git a/static/assets/img/bg6.jpg b/static/assets/img/bg6.jpg deleted file mode 100644 index ab1c1fab..00000000 Binary files a/static/assets/img/bg6.jpg and /dev/null differ diff --git a/static/assets/img/bg7.jpg b/static/assets/img/bg7.jpg deleted file mode 100644 index d99d2e67..00000000 Binary files a/static/assets/img/bg7.jpg and /dev/null differ diff --git a/static/assets/img/bg8.jpg b/static/assets/img/bg8.jpg deleted file mode 100644 index 12f3d529..00000000 Binary files a/static/assets/img/bg8.jpg and /dev/null differ diff --git a/static/assets/img/bg9.jpg b/static/assets/img/bg9.jpg deleted file mode 100644 index bfe832e5..00000000 Binary files a/static/assets/img/bg9.jpg and /dev/null differ diff --git a/static/assets/img/blog-banner/blog-banner-list1.jpg b/static/assets/img/blog-banner/blog-banner-list1.jpg deleted file mode 100644 index 0eb59141..00000000 Binary files a/static/assets/img/blog-banner/blog-banner-list1.jpg and /dev/null differ diff --git a/static/assets/img/blog-banner/blog-banner-list2.jpg b/static/assets/img/blog-banner/blog-banner-list2.jpg deleted file mode 100644 index 0eb59141..00000000 Binary files a/static/assets/img/blog-banner/blog-banner-list2.jpg and /dev/null differ diff --git a/static/assets/img/blog-banner/blog-banner-list3.jpg b/static/assets/img/blog-banner/blog-banner-list3.jpg deleted file mode 100644 index 0eb59141..00000000 Binary files a/static/assets/img/blog-banner/blog-banner-list3.jpg and /dev/null differ diff --git a/static/assets/img/blog-banner/blog-banner-list4.jpg b/static/assets/img/blog-banner/blog-banner-list4.jpg deleted file mode 100644 index 0eb59141..00000000 Binary files a/static/assets/img/blog-banner/blog-banner-list4.jpg and /dev/null differ diff --git a/static/assets/img/blog-banner/blog-banner.jpg b/static/assets/img/blog-banner/blog-banner.jpg deleted file mode 100644 index 0bbe2ec6..00000000 Binary files a/static/assets/img/blog-banner/blog-banner.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img1.jpg b/static/assets/img/blog/blog-img1.jpg deleted file mode 100644 index 23e57b94..00000000 Binary files a/static/assets/img/blog/blog-img1.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img10.jpg b/static/assets/img/blog/blog-img10.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img10.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img11.jpg b/static/assets/img/blog/blog-img11.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img11.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img12.jpg b/static/assets/img/blog/blog-img12.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img12.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img13.jpg b/static/assets/img/blog/blog-img13.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img13.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img14.jpg b/static/assets/img/blog/blog-img14.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img14.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img15.jpg b/static/assets/img/blog/blog-img15.jpg deleted file mode 100644 index a49fa321..00000000 Binary files a/static/assets/img/blog/blog-img15.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img16.jpg b/static/assets/img/blog/blog-img16.jpg deleted file mode 100644 index 6cb940c2..00000000 Binary files a/static/assets/img/blog/blog-img16.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img17.jpg b/static/assets/img/blog/blog-img17.jpg deleted file mode 100644 index 6cb940c2..00000000 Binary files a/static/assets/img/blog/blog-img17.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img18.jpg b/static/assets/img/blog/blog-img18.jpg deleted file mode 100644 index 6cb940c2..00000000 Binary files a/static/assets/img/blog/blog-img18.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img19.jpg b/static/assets/img/blog/blog-img19.jpg deleted file mode 100644 index 6cb940c2..00000000 Binary files a/static/assets/img/blog/blog-img19.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img2.jpg b/static/assets/img/blog/blog-img2.jpg deleted file mode 100644 index 23e57b94..00000000 Binary files a/static/assets/img/blog/blog-img2.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img20.jpg b/static/assets/img/blog/blog-img20.jpg deleted file mode 100644 index 6cb940c2..00000000 Binary files a/static/assets/img/blog/blog-img20.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img21.jpg b/static/assets/img/blog/blog-img21.jpg deleted file mode 100644 index 6cb940c2..00000000 Binary files a/static/assets/img/blog/blog-img21.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img22.jpg b/static/assets/img/blog/blog-img22.jpg deleted file mode 100644 index 0eb59141..00000000 Binary files a/static/assets/img/blog/blog-img22.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img23.jpg b/static/assets/img/blog/blog-img23.jpg deleted file mode 100644 index 0eb59141..00000000 Binary files a/static/assets/img/blog/blog-img23.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img24.jpg b/static/assets/img/blog/blog-img24.jpg deleted file mode 100644 index 0eb59141..00000000 Binary files a/static/assets/img/blog/blog-img24.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img25.jpg b/static/assets/img/blog/blog-img25.jpg deleted file mode 100644 index 12e04697..00000000 Binary files a/static/assets/img/blog/blog-img25.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img26.jpg b/static/assets/img/blog/blog-img26.jpg deleted file mode 100644 index 12e04697..00000000 Binary files a/static/assets/img/blog/blog-img26.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img27.jpg b/static/assets/img/blog/blog-img27.jpg deleted file mode 100644 index 12e04697..00000000 Binary files a/static/assets/img/blog/blog-img27.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img3.jpg b/static/assets/img/blog/blog-img3.jpg deleted file mode 100644 index 23e57b94..00000000 Binary files a/static/assets/img/blog/blog-img3.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img4.jpg b/static/assets/img/blog/blog-img4.jpg deleted file mode 100644 index b5dfcc29..00000000 Binary files a/static/assets/img/blog/blog-img4.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img5.jpg b/static/assets/img/blog/blog-img5.jpg deleted file mode 100644 index b5dfcc29..00000000 Binary files a/static/assets/img/blog/blog-img5.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img6.jpg b/static/assets/img/blog/blog-img6.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img6.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img7.jpg b/static/assets/img/blog/blog-img7.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img7.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img8.jpg b/static/assets/img/blog/blog-img8.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img8.jpg and /dev/null differ diff --git a/static/assets/img/blog/blog-img9.jpg b/static/assets/img/blog/blog-img9.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/blog/blog-img9.jpg and /dev/null differ diff --git a/static/assets/img/business-banner.jpg b/static/assets/img/business-banner.jpg deleted file mode 100644 index f831b909..00000000 Binary files a/static/assets/img/business-banner.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img1.jpg b/static/assets/img/categories/categories-img1.jpg deleted file mode 100644 index e1f0277c..00000000 Binary files a/static/assets/img/categories/categories-img1.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img10.jpg b/static/assets/img/categories/categories-img10.jpg deleted file mode 100644 index 16c4460c..00000000 Binary files a/static/assets/img/categories/categories-img10.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img11.jpg b/static/assets/img/categories/categories-img11.jpg deleted file mode 100644 index 16c4460c..00000000 Binary files a/static/assets/img/categories/categories-img11.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img2.jpg b/static/assets/img/categories/categories-img2.jpg deleted file mode 100644 index e1f0277c..00000000 Binary files a/static/assets/img/categories/categories-img2.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img3.jpg b/static/assets/img/categories/categories-img3.jpg deleted file mode 100644 index e1f0277c..00000000 Binary files a/static/assets/img/categories/categories-img3.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img4.jpg b/static/assets/img/categories/categories-img4.jpg deleted file mode 100644 index 68e24d5d..00000000 Binary files a/static/assets/img/categories/categories-img4.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img5.jpg b/static/assets/img/categories/categories-img5.jpg deleted file mode 100644 index 68e24d5d..00000000 Binary files a/static/assets/img/categories/categories-img5.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img6.jpg b/static/assets/img/categories/categories-img6.jpg deleted file mode 100644 index 68e24d5d..00000000 Binary files a/static/assets/img/categories/categories-img6.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img7.jpg b/static/assets/img/categories/categories-img7.jpg deleted file mode 100644 index 68e24d5d..00000000 Binary files a/static/assets/img/categories/categories-img7.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img8.jpg b/static/assets/img/categories/categories-img8.jpg deleted file mode 100644 index 68e24d5d..00000000 Binary files a/static/assets/img/categories/categories-img8.jpg and /dev/null differ diff --git a/static/assets/img/categories/categories-img9.jpg b/static/assets/img/categories/categories-img9.jpg deleted file mode 100644 index 16c4460c..00000000 Binary files a/static/assets/img/categories/categories-img9.jpg and /dev/null differ diff --git a/static/assets/img/coming-soon.jpg b/static/assets/img/coming-soon.jpg deleted file mode 100644 index 96929628..00000000 Binary files a/static/assets/img/coming-soon.jpg and /dev/null differ diff --git a/static/assets/img/consulting-banner-bg.jpg b/static/assets/img/consulting-banner-bg.jpg deleted file mode 100644 index d58d1adf..00000000 Binary files a/static/assets/img/consulting-banner-bg.jpg and /dev/null differ diff --git a/static/assets/img/cta-bg1.jpg b/static/assets/img/cta-bg1.jpg deleted file mode 100644 index d58d1adf..00000000 Binary files a/static/assets/img/cta-bg1.jpg and /dev/null differ diff --git a/static/assets/img/custom-ads.jpg b/static/assets/img/custom-ads.jpg deleted file mode 100644 index fc2d96b3..00000000 Binary files a/static/assets/img/custom-ads.jpg and /dev/null differ diff --git a/static/assets/img/error.png b/static/assets/img/error.png deleted file mode 100644 index fa6434db..00000000 Binary files a/static/assets/img/error.png and /dev/null differ diff --git a/static/assets/img/facility-img.png b/static/assets/img/facility-img.png deleted file mode 100644 index 86a022ca..00000000 Binary files a/static/assets/img/facility-img.png and /dev/null differ diff --git a/static/assets/img/favicon.png b/static/assets/img/favicon.png deleted file mode 100644 index ef6e8717..00000000 Binary files a/static/assets/img/favicon.png and /dev/null differ diff --git a/static/assets/img/featured-video/featured-video1.jpg b/static/assets/img/featured-video/featured-video1.jpg deleted file mode 100644 index 1b90434d..00000000 Binary files a/static/assets/img/featured-video/featured-video1.jpg and /dev/null differ diff --git a/static/assets/img/featured-video/featured-video2.jpg b/static/assets/img/featured-video/featured-video2.jpg deleted file mode 100644 index 1b90434d..00000000 Binary files a/static/assets/img/featured-video/featured-video2.jpg and /dev/null differ diff --git a/static/assets/img/featured-video/featured-video3.jpg b/static/assets/img/featured-video/featured-video3.jpg deleted file mode 100644 index 1b90434d..00000000 Binary files a/static/assets/img/featured-video/featured-video3.jpg and /dev/null differ diff --git a/static/assets/img/featured-video/featured-video4.jpg b/static/assets/img/featured-video/featured-video4.jpg deleted file mode 100644 index 1b90434d..00000000 Binary files a/static/assets/img/featured-video/featured-video4.jpg and /dev/null differ diff --git a/static/assets/img/featured-video/featured-video5.jpg b/static/assets/img/featured-video/featured-video5.jpg deleted file mode 100644 index 1b90434d..00000000 Binary files a/static/assets/img/featured-video/featured-video5.jpg and /dev/null differ diff --git a/static/assets/img/furniture-banner-bg1.jpg b/static/assets/img/furniture-banner-bg1.jpg deleted file mode 100644 index d58d1adf..00000000 Binary files a/static/assets/img/furniture-banner-bg1.jpg and /dev/null differ diff --git a/static/assets/img/furniture-banner-bg2.jpg b/static/assets/img/furniture-banner-bg2.jpg deleted file mode 100644 index d58d1adf..00000000 Binary files a/static/assets/img/furniture-banner-bg2.jpg and /dev/null differ diff --git a/static/assets/img/furniture-banner-bg3.jpg b/static/assets/img/furniture-banner-bg3.jpg deleted file mode 100644 index d58d1adf..00000000 Binary files a/static/assets/img/furniture-banner-bg3.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img1.jpg b/static/assets/img/gallery/gallery-img1.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img1.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img2.jpg b/static/assets/img/gallery/gallery-img2.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img2.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img3.jpg b/static/assets/img/gallery/gallery-img3.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img3.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img4.jpg b/static/assets/img/gallery/gallery-img4.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img4.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img5.jpg b/static/assets/img/gallery/gallery-img5.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img5.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img6.jpg b/static/assets/img/gallery/gallery-img6.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img6.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img7.jpg b/static/assets/img/gallery/gallery-img7.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img7.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img8.jpg b/static/assets/img/gallery/gallery-img8.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img8.jpg and /dev/null differ diff --git a/static/assets/img/gallery/gallery-img9.jpg b/static/assets/img/gallery/gallery-img9.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/gallery/gallery-img9.jpg and /dev/null differ diff --git a/static/assets/img/how-it-works.png b/static/assets/img/how-it-works.png deleted file mode 100644 index 7a82eafe..00000000 Binary files a/static/assets/img/how-it-works.png and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img1.jpg b/static/assets/img/instagram/instagram-img1.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img1.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img10.jpg b/static/assets/img/instagram/instagram-img10.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img10.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img11.jpg b/static/assets/img/instagram/instagram-img11.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img11.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img12.jpg b/static/assets/img/instagram/instagram-img12.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img12.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img13.jpg b/static/assets/img/instagram/instagram-img13.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img13.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img14.jpg b/static/assets/img/instagram/instagram-img14.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img14.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img15.jpg b/static/assets/img/instagram/instagram-img15.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img15.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img16.jpg b/static/assets/img/instagram/instagram-img16.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img16.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img2.jpg b/static/assets/img/instagram/instagram-img2.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img2.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img3.jpg b/static/assets/img/instagram/instagram-img3.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img3.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img4.jpg b/static/assets/img/instagram/instagram-img4.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img4.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img5.jpg b/static/assets/img/instagram/instagram-img5.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img5.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img6.jpg b/static/assets/img/instagram/instagram-img6.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img6.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img7.jpg b/static/assets/img/instagram/instagram-img7.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img7.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img8.jpg b/static/assets/img/instagram/instagram-img8.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img8.jpg and /dev/null differ diff --git a/static/assets/img/instagram/instagram-img9.jpg b/static/assets/img/instagram/instagram-img9.jpg deleted file mode 100644 index c0222f75..00000000 Binary files a/static/assets/img/instagram/instagram-img9.jpg and /dev/null differ diff --git a/static/assets/img/it-agency-banner-bg.jpg b/static/assets/img/it-agency-banner-bg.jpg deleted file mode 100644 index d58d1adf..00000000 Binary files a/static/assets/img/it-agency-banner-bg.jpg and /dev/null differ diff --git a/static/assets/img/latest-posts/latest-posts1.jpg b/static/assets/img/latest-posts/latest-posts1.jpg deleted file mode 100644 index af85b9c6..00000000 Binary files a/static/assets/img/latest-posts/latest-posts1.jpg and /dev/null differ diff --git a/static/assets/img/latest-posts/latest-posts2.jpg b/static/assets/img/latest-posts/latest-posts2.jpg deleted file mode 100644 index af85b9c6..00000000 Binary files a/static/assets/img/latest-posts/latest-posts2.jpg and /dev/null differ diff --git a/static/assets/img/latest-posts/latest-posts3.jpg b/static/assets/img/latest-posts/latest-posts3.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/latest-posts/latest-posts3.jpg and /dev/null differ diff --git a/static/assets/img/latest-posts/latest-posts4.jpg b/static/assets/img/latest-posts/latest-posts4.jpg deleted file mode 100644 index f659cb30..00000000 Binary files a/static/assets/img/latest-posts/latest-posts4.jpg and /dev/null differ diff --git a/static/assets/img/latest-posts/latest-posts5.jpg b/static/assets/img/latest-posts/latest-posts5.jpg deleted file mode 100644 index b973f213..00000000 Binary files a/static/assets/img/latest-posts/latest-posts5.jpg and /dev/null differ diff --git a/static/assets/img/logo-app.png b/static/assets/img/logo-app.png deleted file mode 100644 index fd83cb5a..00000000 Binary files a/static/assets/img/logo-app.png and /dev/null differ diff --git a/static/assets/img/logo-blog.png b/static/assets/img/logo-blog.png deleted file mode 100644 index c3204655..00000000 Binary files a/static/assets/img/logo-blog.png and /dev/null differ diff --git a/static/assets/img/logo-business.png b/static/assets/img/logo-business.png deleted file mode 100644 index 0de563f2..00000000 Binary files a/static/assets/img/logo-business.png and /dev/null differ diff --git a/static/assets/img/logo-consulting.png b/static/assets/img/logo-consulting.png deleted file mode 100644 index cdce55ba..00000000 Binary files a/static/assets/img/logo-consulting.png and /dev/null differ diff --git a/static/assets/img/logo-it-agency.png b/static/assets/img/logo-it-agency.png deleted file mode 100644 index 85f0d487..00000000 Binary files a/static/assets/img/logo-it-agency.png and /dev/null differ diff --git a/static/assets/img/logo-marketing.png b/static/assets/img/logo-marketing.png deleted file mode 100644 index dd14b937..00000000 Binary files a/static/assets/img/logo-marketing.png and /dev/null differ diff --git a/static/assets/img/logo-personal-portfolio.png b/static/assets/img/logo-personal-portfolio.png deleted file mode 100644 index 03aa6e25..00000000 Binary files a/static/assets/img/logo-personal-portfolio.png and /dev/null differ diff --git a/static/assets/img/logo-shop.png b/static/assets/img/logo-shop.png deleted file mode 100644 index dc860322..00000000 Binary files a/static/assets/img/logo-shop.png and /dev/null differ diff --git a/static/assets/img/logo-startup.png b/static/assets/img/logo-startup.png deleted file mode 100644 index 658bde79..00000000 Binary files a/static/assets/img/logo-startup.png and /dev/null differ diff --git a/static/assets/img/logo-white-app.png b/static/assets/img/logo-white-app.png deleted file mode 100644 index 2a8b50da..00000000 Binary files a/static/assets/img/logo-white-app.png and /dev/null differ diff --git a/static/assets/img/logo-white-blog.png b/static/assets/img/logo-white-blog.png deleted file mode 100644 index 1c397ab3..00000000 Binary files a/static/assets/img/logo-white-blog.png and /dev/null differ diff --git a/static/assets/img/logo-white-business.png b/static/assets/img/logo-white-business.png deleted file mode 100644 index 112a7ce6..00000000 Binary files a/static/assets/img/logo-white-business.png and /dev/null differ diff --git a/static/assets/img/logo-white-saas.png b/static/assets/img/logo-white-saas.png deleted file mode 100644 index 84913d53..00000000 Binary files a/static/assets/img/logo-white-saas.png and /dev/null differ diff --git a/static/assets/img/logo-white-shop.png b/static/assets/img/logo-white-shop.png deleted file mode 100644 index e1953f30..00000000 Binary files a/static/assets/img/logo-white-shop.png and /dev/null differ diff --git a/static/assets/img/logo-white-startup.png b/static/assets/img/logo-white-startup.png deleted file mode 100644 index 38a6f024..00000000 Binary files a/static/assets/img/logo-white-startup.png and /dev/null differ diff --git a/static/assets/img/marketing-banner.png b/static/assets/img/marketing-banner.png deleted file mode 100644 index 79094fc8..00000000 Binary files a/static/assets/img/marketing-banner.png and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg-image1.jpg b/static/assets/img/page-title/page-title-bg-image1.jpg deleted file mode 100644 index 20789820..00000000 Binary files a/static/assets/img/page-title/page-title-bg-image1.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg-image2.jpg b/static/assets/img/page-title/page-title-bg-image2.jpg deleted file mode 100644 index 20789820..00000000 Binary files a/static/assets/img/page-title/page-title-bg-image2.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg-image3.jpg b/static/assets/img/page-title/page-title-bg-image3.jpg deleted file mode 100644 index 20789820..00000000 Binary files a/static/assets/img/page-title/page-title-bg-image3.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg-image4.jpg b/static/assets/img/page-title/page-title-bg-image4.jpg deleted file mode 100644 index 20789820..00000000 Binary files a/static/assets/img/page-title/page-title-bg-image4.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg-image5.jpg b/static/assets/img/page-title/page-title-bg-image5.jpg deleted file mode 100644 index 20789820..00000000 Binary files a/static/assets/img/page-title/page-title-bg-image5.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg1.jpg b/static/assets/img/page-title/page-title-bg1.jpg deleted file mode 100644 index 9feabdd7..00000000 Binary files a/static/assets/img/page-title/page-title-bg1.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg2.jpg b/static/assets/img/page-title/page-title-bg2.jpg deleted file mode 100644 index 9feabdd7..00000000 Binary files a/static/assets/img/page-title/page-title-bg2.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg3.jpg b/static/assets/img/page-title/page-title-bg3.jpg deleted file mode 100644 index 9feabdd7..00000000 Binary files a/static/assets/img/page-title/page-title-bg3.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg4.jpg b/static/assets/img/page-title/page-title-bg4.jpg deleted file mode 100644 index 9feabdd7..00000000 Binary files a/static/assets/img/page-title/page-title-bg4.jpg and /dev/null differ diff --git a/static/assets/img/page-title/page-title-bg5.jpg b/static/assets/img/page-title/page-title-bg5.jpg deleted file mode 100644 index 9feabdd7..00000000 Binary files a/static/assets/img/page-title/page-title-bg5.jpg and /dev/null differ diff --git a/static/assets/img/partner/partner-white1.png b/static/assets/img/partner/partner-white1.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner-white1.png and /dev/null differ diff --git a/static/assets/img/partner/partner-white2.png b/static/assets/img/partner/partner-white2.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner-white2.png and /dev/null differ diff --git a/static/assets/img/partner/partner-white3.png b/static/assets/img/partner/partner-white3.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner-white3.png and /dev/null differ diff --git a/static/assets/img/partner/partner-white4.png b/static/assets/img/partner/partner-white4.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner-white4.png and /dev/null differ diff --git a/static/assets/img/partner/partner-white5.png b/static/assets/img/partner/partner-white5.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner-white5.png and /dev/null differ diff --git a/static/assets/img/partner/partner-white6.png b/static/assets/img/partner/partner-white6.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner-white6.png and /dev/null differ diff --git a/static/assets/img/partner/partner1.png b/static/assets/img/partner/partner1.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner1.png and /dev/null differ diff --git a/static/assets/img/partner/partner2.png b/static/assets/img/partner/partner2.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner2.png and /dev/null differ diff --git a/static/assets/img/partner/partner3.png b/static/assets/img/partner/partner3.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner3.png and /dev/null differ diff --git a/static/assets/img/partner/partner4.png b/static/assets/img/partner/partner4.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner4.png and /dev/null differ diff --git a/static/assets/img/partner/partner5.png b/static/assets/img/partner/partner5.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner5.png and /dev/null differ diff --git a/static/assets/img/partner/partner6.png b/static/assets/img/partner/partner6.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/partner/partner6.png and /dev/null differ diff --git a/static/assets/img/personal-portfolio-banner.jpg b/static/assets/img/personal-portfolio-banner.jpg deleted file mode 100644 index eefc6edc..00000000 Binary files a/static/assets/img/personal-portfolio-banner.jpg and /dev/null differ diff --git a/static/assets/img/play-store.png b/static/assets/img/play-store.png deleted file mode 100644 index b90a0159..00000000 Binary files a/static/assets/img/play-store.png and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img1.jpg b/static/assets/img/portfolio/portfolio-img1.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img1.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img10.jpg b/static/assets/img/portfolio/portfolio-img10.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img10.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img11.jpg b/static/assets/img/portfolio/portfolio-img11.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img11.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img12.jpg b/static/assets/img/portfolio/portfolio-img12.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img12.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img2.jpg b/static/assets/img/portfolio/portfolio-img2.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img2.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img3.jpg b/static/assets/img/portfolio/portfolio-img3.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img3.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img4.jpg b/static/assets/img/portfolio/portfolio-img4.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img4.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img5.jpg b/static/assets/img/portfolio/portfolio-img5.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img5.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img6.jpg b/static/assets/img/portfolio/portfolio-img6.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img6.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img7.jpg b/static/assets/img/portfolio/portfolio-img7.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img7.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img8.jpg b/static/assets/img/portfolio/portfolio-img8.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img8.jpg and /dev/null differ diff --git a/static/assets/img/portfolio/portfolio-img9.jpg b/static/assets/img/portfolio/portfolio-img9.jpg deleted file mode 100644 index 79a594b0..00000000 Binary files a/static/assets/img/portfolio/portfolio-img9.jpg and /dev/null differ diff --git a/static/assets/img/process/process1.png b/static/assets/img/process/process1.png deleted file mode 100644 index f32f2705..00000000 Binary files a/static/assets/img/process/process1.png and /dev/null differ diff --git a/static/assets/img/process/process2.png b/static/assets/img/process/process2.png deleted file mode 100644 index f32f2705..00000000 Binary files a/static/assets/img/process/process2.png and /dev/null differ diff --git a/static/assets/img/process/process3.png b/static/assets/img/process/process3.png deleted file mode 100644 index f32f2705..00000000 Binary files a/static/assets/img/process/process3.png and /dev/null differ diff --git a/static/assets/img/products/products-img1.jpg b/static/assets/img/products/products-img1.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img1.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img10.jpg b/static/assets/img/products/products-img10.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img10.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img11.jpg b/static/assets/img/products/products-img11.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img11.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img12.jpg b/static/assets/img/products/products-img12.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img12.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img13.jpg b/static/assets/img/products/products-img13.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img13.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img14.jpg b/static/assets/img/products/products-img14.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img14.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img15.jpg b/static/assets/img/products/products-img15.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img15.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img16.jpg b/static/assets/img/products/products-img16.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img16.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img17.jpg b/static/assets/img/products/products-img17.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img17.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img18.jpg b/static/assets/img/products/products-img18.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img18.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img19.jpg b/static/assets/img/products/products-img19.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img19.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img2.jpg b/static/assets/img/products/products-img2.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img2.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img20.jpg b/static/assets/img/products/products-img20.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img20.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img3.jpg b/static/assets/img/products/products-img3.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img3.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img4.jpg b/static/assets/img/products/products-img4.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img4.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img5.jpg b/static/assets/img/products/products-img5.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img5.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img6.jpg b/static/assets/img/products/products-img6.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img6.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img7.jpg b/static/assets/img/products/products-img7.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img7.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img8.jpg b/static/assets/img/products/products-img8.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img8.jpg and /dev/null differ diff --git a/static/assets/img/products/products-img9.jpg b/static/assets/img/products/products-img9.jpg deleted file mode 100644 index f809d752..00000000 Binary files a/static/assets/img/products/products-img9.jpg and /dev/null differ diff --git a/static/assets/img/saas-banner.png b/static/assets/img/saas-banner.png deleted file mode 100644 index eecdf002..00000000 Binary files a/static/assets/img/saas-banner.png and /dev/null differ diff --git a/static/assets/img/screenshot/frame.png b/static/assets/img/screenshot/frame.png deleted file mode 100644 index d06d3ec4..00000000 Binary files a/static/assets/img/screenshot/frame.png and /dev/null differ diff --git a/static/assets/img/screenshot/screenshot1.png b/static/assets/img/screenshot/screenshot1.png deleted file mode 100644 index 0663e70e..00000000 Binary files a/static/assets/img/screenshot/screenshot1.png and /dev/null differ diff --git a/static/assets/img/screenshot/screenshot2.png b/static/assets/img/screenshot/screenshot2.png deleted file mode 100644 index a943387b..00000000 Binary files a/static/assets/img/screenshot/screenshot2.png and /dev/null differ diff --git a/static/assets/img/screenshot/screenshot3.png b/static/assets/img/screenshot/screenshot3.png deleted file mode 100644 index 08818eac..00000000 Binary files a/static/assets/img/screenshot/screenshot3.png and /dev/null differ diff --git a/static/assets/img/screenshot/screenshot4.png b/static/assets/img/screenshot/screenshot4.png deleted file mode 100644 index 770faf2f..00000000 Binary files a/static/assets/img/screenshot/screenshot4.png and /dev/null differ diff --git a/static/assets/img/screenshot/screenshot5.png b/static/assets/img/screenshot/screenshot5.png deleted file mode 100644 index 584e6b22..00000000 Binary files a/static/assets/img/screenshot/screenshot5.png and /dev/null differ diff --git a/static/assets/img/services/services-img1.jpg b/static/assets/img/services/services-img1.jpg deleted file mode 100644 index 3ffcac63..00000000 Binary files a/static/assets/img/services/services-img1.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img10.png b/static/assets/img/services/services-img10.png deleted file mode 100644 index 1c134114..00000000 Binary files a/static/assets/img/services/services-img10.png and /dev/null differ diff --git a/static/assets/img/services/services-img11.png b/static/assets/img/services/services-img11.png deleted file mode 100644 index 1c134114..00000000 Binary files a/static/assets/img/services/services-img11.png and /dev/null differ diff --git a/static/assets/img/services/services-img12.png b/static/assets/img/services/services-img12.png deleted file mode 100644 index 1c134114..00000000 Binary files a/static/assets/img/services/services-img12.png and /dev/null differ diff --git a/static/assets/img/services/services-img13.jpg b/static/assets/img/services/services-img13.jpg deleted file mode 100644 index 81476faf..00000000 Binary files a/static/assets/img/services/services-img13.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img14.jpg b/static/assets/img/services/services-img14.jpg deleted file mode 100644 index 81476faf..00000000 Binary files a/static/assets/img/services/services-img14.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img15.jpg b/static/assets/img/services/services-img15.jpg deleted file mode 100644 index 81476faf..00000000 Binary files a/static/assets/img/services/services-img15.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img16.jpg b/static/assets/img/services/services-img16.jpg deleted file mode 100644 index 81476faf..00000000 Binary files a/static/assets/img/services/services-img16.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img17.jpg b/static/assets/img/services/services-img17.jpg deleted file mode 100644 index 81476faf..00000000 Binary files a/static/assets/img/services/services-img17.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img18.jpg b/static/assets/img/services/services-img18.jpg deleted file mode 100644 index 81476faf..00000000 Binary files a/static/assets/img/services/services-img18.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img2.jpg b/static/assets/img/services/services-img2.jpg deleted file mode 100644 index 3ffcac63..00000000 Binary files a/static/assets/img/services/services-img2.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img3.jpg b/static/assets/img/services/services-img3.jpg deleted file mode 100644 index 3ffcac63..00000000 Binary files a/static/assets/img/services/services-img3.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img4.jpg b/static/assets/img/services/services-img4.jpg deleted file mode 100644 index 3ffcac63..00000000 Binary files a/static/assets/img/services/services-img4.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img5.jpg b/static/assets/img/services/services-img5.jpg deleted file mode 100644 index 3ffcac63..00000000 Binary files a/static/assets/img/services/services-img5.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img6.jpg b/static/assets/img/services/services-img6.jpg deleted file mode 100644 index 3ffcac63..00000000 Binary files a/static/assets/img/services/services-img6.jpg and /dev/null differ diff --git a/static/assets/img/services/services-img7.png b/static/assets/img/services/services-img7.png deleted file mode 100644 index 1c134114..00000000 Binary files a/static/assets/img/services/services-img7.png and /dev/null differ diff --git a/static/assets/img/services/services-img8.png b/static/assets/img/services/services-img8.png deleted file mode 100644 index 1c134114..00000000 Binary files a/static/assets/img/services/services-img8.png and /dev/null differ diff --git a/static/assets/img/services/services-img9.png b/static/assets/img/services/services-img9.png deleted file mode 100644 index 1c134114..00000000 Binary files a/static/assets/img/services/services-img9.png and /dev/null differ diff --git a/static/assets/img/shape/border.png b/static/assets/img/shape/border.png deleted file mode 100644 index d1c0182c..00000000 Binary files a/static/assets/img/shape/border.png and /dev/null differ diff --git a/static/assets/img/shape/border2.png b/static/assets/img/shape/border2.png deleted file mode 100644 index 72b30be2..00000000 Binary files a/static/assets/img/shape/border2.png and /dev/null differ diff --git a/static/assets/img/shape/shape1.png b/static/assets/img/shape/shape1.png deleted file mode 100644 index aad99571..00000000 Binary files a/static/assets/img/shape/shape1.png and /dev/null differ diff --git a/static/assets/img/shape/shape10.png b/static/assets/img/shape/shape10.png deleted file mode 100644 index d1857ce4..00000000 Binary files a/static/assets/img/shape/shape10.png and /dev/null differ diff --git a/static/assets/img/shape/shape11.png b/static/assets/img/shape/shape11.png deleted file mode 100644 index c62a1d30..00000000 Binary files a/static/assets/img/shape/shape11.png and /dev/null differ diff --git a/static/assets/img/shape/shape12.png b/static/assets/img/shape/shape12.png deleted file mode 100644 index d79850a0..00000000 Binary files a/static/assets/img/shape/shape12.png and /dev/null differ diff --git a/static/assets/img/shape/shape13.png b/static/assets/img/shape/shape13.png deleted file mode 100644 index e976c22c..00000000 Binary files a/static/assets/img/shape/shape13.png and /dev/null differ diff --git a/static/assets/img/shape/shape14.png b/static/assets/img/shape/shape14.png deleted file mode 100644 index 3f65f073..00000000 Binary files a/static/assets/img/shape/shape14.png and /dev/null differ diff --git a/static/assets/img/shape/shape15.png b/static/assets/img/shape/shape15.png deleted file mode 100644 index 022cdcbf..00000000 Binary files a/static/assets/img/shape/shape15.png and /dev/null differ diff --git a/static/assets/img/shape/shape16.png b/static/assets/img/shape/shape16.png deleted file mode 100644 index 5d1d44d2..00000000 Binary files a/static/assets/img/shape/shape16.png and /dev/null differ diff --git a/static/assets/img/shape/shape17.png b/static/assets/img/shape/shape17.png deleted file mode 100644 index fa29cdb7..00000000 Binary files a/static/assets/img/shape/shape17.png and /dev/null differ diff --git a/static/assets/img/shape/shape18.png b/static/assets/img/shape/shape18.png deleted file mode 100644 index 416f1289..00000000 Binary files a/static/assets/img/shape/shape18.png and /dev/null differ diff --git a/static/assets/img/shape/shape19.png b/static/assets/img/shape/shape19.png deleted file mode 100644 index 6ef1c4ce..00000000 Binary files a/static/assets/img/shape/shape19.png and /dev/null differ diff --git a/static/assets/img/shape/shape2.png b/static/assets/img/shape/shape2.png deleted file mode 100644 index 75dd2ba1..00000000 Binary files a/static/assets/img/shape/shape2.png and /dev/null differ diff --git a/static/assets/img/shape/shape3.png b/static/assets/img/shape/shape3.png deleted file mode 100644 index 54258619..00000000 Binary files a/static/assets/img/shape/shape3.png and /dev/null differ diff --git a/static/assets/img/shape/shape4.png b/static/assets/img/shape/shape4.png deleted file mode 100644 index 96c818ce..00000000 Binary files a/static/assets/img/shape/shape4.png and /dev/null differ diff --git a/static/assets/img/shape/shape5.png b/static/assets/img/shape/shape5.png deleted file mode 100644 index f4a16afd..00000000 Binary files a/static/assets/img/shape/shape5.png and /dev/null differ diff --git a/static/assets/img/shape/shape6.png b/static/assets/img/shape/shape6.png deleted file mode 100644 index 020f2034..00000000 Binary files a/static/assets/img/shape/shape6.png and /dev/null differ diff --git a/static/assets/img/shape/shape7.png b/static/assets/img/shape/shape7.png deleted file mode 100644 index e8372043..00000000 Binary files a/static/assets/img/shape/shape7.png and /dev/null differ diff --git a/static/assets/img/shape/shape8.png b/static/assets/img/shape/shape8.png deleted file mode 100644 index 7d63d5b4..00000000 Binary files a/static/assets/img/shape/shape8.png and /dev/null differ diff --git a/static/assets/img/shape/shape9.png b/static/assets/img/shape/shape9.png deleted file mode 100644 index d1169fa7..00000000 Binary files a/static/assets/img/shape/shape9.png and /dev/null differ diff --git a/static/assets/img/shop-banner-bg.jpg b/static/assets/img/shop-banner-bg.jpg deleted file mode 100644 index d58d1adf..00000000 Binary files a/static/assets/img/shop-banner-bg.jpg and /dev/null differ diff --git a/static/assets/img/signature.png b/static/assets/img/signature.png deleted file mode 100644 index 36a9150a..00000000 Binary files a/static/assets/img/signature.png and /dev/null differ diff --git a/static/assets/img/skills/skills-img1.png b/static/assets/img/skills/skills-img1.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/skills/skills-img1.png and /dev/null differ diff --git a/static/assets/img/skills/skills-img2.png b/static/assets/img/skills/skills-img2.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/skills/skills-img2.png and /dev/null differ diff --git a/static/assets/img/skills/skills-img3.png b/static/assets/img/skills/skills-img3.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/skills/skills-img3.png and /dev/null differ diff --git a/static/assets/img/skills/skills-img4.png b/static/assets/img/skills/skills-img4.png deleted file mode 100644 index ef412300..00000000 Binary files a/static/assets/img/skills/skills-img4.png and /dev/null differ diff --git a/static/assets/img/startup-banner.jpg b/static/assets/img/startup-banner.jpg deleted file mode 100644 index a03c376d..00000000 Binary files a/static/assets/img/startup-banner.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img1.jpg b/static/assets/img/team/team-img1.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img1.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img2.jpg b/static/assets/img/team/team-img2.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img2.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img3.jpg b/static/assets/img/team/team-img3.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img3.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img4.jpg b/static/assets/img/team/team-img4.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img4.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img5.jpg b/static/assets/img/team/team-img5.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img5.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img6.jpg b/static/assets/img/team/team-img6.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img6.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img7.jpg b/static/assets/img/team/team-img7.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img7.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img8.jpg b/static/assets/img/team/team-img8.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img8.jpg and /dev/null differ diff --git a/static/assets/img/team/team-img9.jpg b/static/assets/img/team/team-img9.jpg deleted file mode 100644 index 4f4fdf96..00000000 Binary files a/static/assets/img/team/team-img9.jpg and /dev/null differ diff --git a/static/assets/img/testimonials/testimonials-img1.jpg b/static/assets/img/testimonials/testimonials-img1.jpg deleted file mode 100644 index 6a4d2543..00000000 Binary files a/static/assets/img/testimonials/testimonials-img1.jpg and /dev/null differ diff --git a/static/assets/img/testimonials/testimonials-img2.jpg b/static/assets/img/testimonials/testimonials-img2.jpg deleted file mode 100644 index 6a4d2543..00000000 Binary files a/static/assets/img/testimonials/testimonials-img2.jpg and /dev/null differ diff --git a/static/assets/img/testimonials/testimonials-img3.jpg b/static/assets/img/testimonials/testimonials-img3.jpg deleted file mode 100644 index 7030ce3d..00000000 Binary files a/static/assets/img/testimonials/testimonials-img3.jpg and /dev/null differ diff --git a/static/assets/img/testimonials/testimonials-img4.jpg b/static/assets/img/testimonials/testimonials-img4.jpg deleted file mode 100644 index 7030ce3d..00000000 Binary files a/static/assets/img/testimonials/testimonials-img4.jpg and /dev/null differ diff --git a/static/assets/img/testimonials/testimonials-img5.jpg b/static/assets/img/testimonials/testimonials-img5.jpg deleted file mode 100644 index 7030ce3d..00000000 Binary files a/static/assets/img/testimonials/testimonials-img5.jpg and /dev/null differ diff --git a/static/assets/img/testimonials/testimonials-img6.jpg b/static/assets/img/testimonials/testimonials-img6.jpg deleted file mode 100644 index 7030ce3d..00000000 Binary files a/static/assets/img/testimonials/testimonials-img6.jpg and /dev/null differ diff --git a/static/assets/img/trending-img1.jpg b/static/assets/img/trending-img1.jpg deleted file mode 100644 index 446911c9..00000000 Binary files a/static/assets/img/trending-img1.jpg and /dev/null differ diff --git a/static/assets/img/trending-img2.jpg b/static/assets/img/trending-img2.jpg deleted file mode 100644 index 973dae15..00000000 Binary files a/static/assets/img/trending-img2.jpg and /dev/null differ diff --git a/static/assets/img/user/user1.jpg b/static/assets/img/user/user1.jpg deleted file mode 100644 index 5ef05485..00000000 Binary files a/static/assets/img/user/user1.jpg and /dev/null differ diff --git a/static/assets/img/user/user2.jpg b/static/assets/img/user/user2.jpg deleted file mode 100644 index 5ef05485..00000000 Binary files a/static/assets/img/user/user2.jpg and /dev/null differ diff --git a/static/assets/img/user/user3.jpg b/static/assets/img/user/user3.jpg deleted file mode 100644 index 5ef05485..00000000 Binary files a/static/assets/img/user/user3.jpg and /dev/null differ diff --git a/static/assets/img/user/user4.jpg b/static/assets/img/user/user4.jpg deleted file mode 100644 index 5ef05485..00000000 Binary files a/static/assets/img/user/user4.jpg and /dev/null differ diff --git a/static/assets/img/user/user5.jpg b/static/assets/img/user/user5.jpg deleted file mode 100644 index 5ef05485..00000000 Binary files a/static/assets/img/user/user5.jpg and /dev/null differ diff --git a/static/assets/img/video-img.jpg b/static/assets/img/video-img.jpg deleted file mode 100644 index 79aa3263..00000000 Binary files a/static/assets/img/video-img.jpg and /dev/null differ diff --git a/static/assets/img/video-img2.jpg b/static/assets/img/video-img2.jpg deleted file mode 100644 index 79aa3263..00000000 Binary files a/static/assets/img/video-img2.jpg and /dev/null differ diff --git a/static/assets/img/what-we-do.jpg b/static/assets/img/what-we-do.jpg deleted file mode 100644 index 1483976c..00000000 Binary files a/static/assets/img/what-we-do.jpg and /dev/null differ diff --git a/static/assets/img/who-we-are.jpg b/static/assets/img/who-we-are.jpg deleted file mode 100644 index 0d2ea392..00000000 Binary files a/static/assets/img/who-we-are.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img1.jpg b/static/assets/img/works/works-img1.jpg deleted file mode 100644 index 2a1c6876..00000000 Binary files a/static/assets/img/works/works-img1.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img10.jpg b/static/assets/img/works/works-img10.jpg deleted file mode 100644 index bf732fb1..00000000 Binary files a/static/assets/img/works/works-img10.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img11.jpg b/static/assets/img/works/works-img11.jpg deleted file mode 100644 index bf732fb1..00000000 Binary files a/static/assets/img/works/works-img11.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img12.jpg b/static/assets/img/works/works-img12.jpg deleted file mode 100644 index bf732fb1..00000000 Binary files a/static/assets/img/works/works-img12.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img13.jpg b/static/assets/img/works/works-img13.jpg deleted file mode 100644 index bf732fb1..00000000 Binary files a/static/assets/img/works/works-img13.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img14.jpg b/static/assets/img/works/works-img14.jpg deleted file mode 100644 index bf732fb1..00000000 Binary files a/static/assets/img/works/works-img14.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img15.jpg b/static/assets/img/works/works-img15.jpg deleted file mode 100644 index de2893d1..00000000 Binary files a/static/assets/img/works/works-img15.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img16.jpg b/static/assets/img/works/works-img16.jpg deleted file mode 100644 index a5c06fd4..00000000 Binary files a/static/assets/img/works/works-img16.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img17.jpg b/static/assets/img/works/works-img17.jpg deleted file mode 100644 index a5c06fd4..00000000 Binary files a/static/assets/img/works/works-img17.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img18.jpg b/static/assets/img/works/works-img18.jpg deleted file mode 100644 index de2893d1..00000000 Binary files a/static/assets/img/works/works-img18.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img2.jpg b/static/assets/img/works/works-img2.jpg deleted file mode 100644 index 2a1c6876..00000000 Binary files a/static/assets/img/works/works-img2.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img3.jpg b/static/assets/img/works/works-img3.jpg deleted file mode 100644 index de2893d1..00000000 Binary files a/static/assets/img/works/works-img3.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img4.jpg b/static/assets/img/works/works-img4.jpg deleted file mode 100644 index a5c06fd4..00000000 Binary files a/static/assets/img/works/works-img4.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img5.jpg b/static/assets/img/works/works-img5.jpg deleted file mode 100644 index a5c06fd4..00000000 Binary files a/static/assets/img/works/works-img5.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img6.jpg b/static/assets/img/works/works-img6.jpg deleted file mode 100644 index de2893d1..00000000 Binary files a/static/assets/img/works/works-img6.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img7.jpg b/static/assets/img/works/works-img7.jpg deleted file mode 100644 index de2893d1..00000000 Binary files a/static/assets/img/works/works-img7.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img8.jpg b/static/assets/img/works/works-img8.jpg deleted file mode 100644 index a5c06fd4..00000000 Binary files a/static/assets/img/works/works-img8.jpg and /dev/null differ diff --git a/static/assets/img/works/works-img9.jpg b/static/assets/img/works/works-img9.jpg deleted file mode 100644 index bf732fb1..00000000 Binary files a/static/assets/img/works/works-img9.jpg and /dev/null differ diff --git a/static/assets/js/ajaxchimp.min.js b/static/assets/js/ajaxchimp.min.js deleted file mode 100644 index 83b637d3..00000000 --- a/static/assets/js/ajaxchimp.min.js +++ /dev/null @@ -1 +0,0 @@ -(function($){"use strict";$.ajaxChimp={responses:{"We have sent you a confirmation email":0,"Please enter a value":1,"An email address must contain a single @":2,"The domain portion of the email address is invalid (the portion after the @: )":3,"The username portion of the email address is invalid (the portion before the @: )":4,"This email address looks fake or invalid. Please enter a real email address":5},translations:{en:null},init:function(selector,options){$(selector).ajaxChimp(options)}};$.fn.ajaxChimp=function(options){$(this).each(function(i,elem){var form=$(elem);var email=form.find("input[type=email]");var label=form.find("label[for="+email.attr("id")+"]");var settings=$.extend({url:form.attr("action"),language:"en"},options);var url=settings.url.replace("/post?","/post-json?").concat("&c=?");form.attr("novalidate","true");email.attr("name","EMAIL");form.submit(function(){var msg;function successCallback(resp){if(resp.result==="success"){msg="We have sent you a confirmation email";label.removeClass("error").addClass("valid");email.removeClass("error").addClass("valid")}else{email.removeClass("valid").addClass("error");label.removeClass("valid").addClass("error");var index=-1;try{var parts=resp.msg.split(" - ",2);if(parts[1]===undefined){msg=resp.msg}else{var i=parseInt(parts[0],10);if(i.toString()===parts[0]){index=parts[0];msg=parts[1]}else{index=-1;msg=resp.msg}}}catch(e){index=-1;msg=resp.msg}}if(settings.language!=="en"&&$.ajaxChimp.responses[msg]!==undefined&&$.ajaxChimp.translations&&$.ajaxChimp.translations[settings.language]&&$.ajaxChimp.translations[settings.language][$.ajaxChimp.responses[msg]]){msg=$.ajaxChimp.translations[settings.language][$.ajaxChimp.responses[msg]]}label.html(msg);label.show(2e3);if(settings.callback){settings.callback(resp)}}var data={};var dataArray=form.serializeArray();$.each(dataArray,function(index,item){data[item.name]=item.value});$.ajax({url:url,data:data,success:successCallback,dataType:"jsonp",error:function(resp,text){console.log("mailchimp ajax submit error: "+text)}});var submitMsg="Submitting...";if(settings.language!=="en"&&$.ajaxChimp.translations&&$.ajaxChimp.translations[settings.language]&&$.ajaxChimp.translations[settings.language]["submit"]){submitMsg=$.ajaxChimp.translations[settings.language]["submit"]}label.html(submitMsg).show(2e3);return false})});return this}})(jQuery); \ No newline at end of file diff --git a/static/assets/js/appear.min.js b/static/assets/js/appear.min.js deleted file mode 100644 index 6e0d23f7..00000000 --- a/static/assets/js/appear.min.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * jQuery.appear - * https://github.com/bas2k/jquery.appear/ - * http://code.google.com/p/jquery-appear/ - * http://bas2k.ru/ - * - * Copyright (c) 2009 Michael Hixson - * Copyright (c) 2012-2014 Alexander Brovikov - * Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php) - */ -(function($){$.fn.appear=function(fn,options){var settings=$.extend({data:undefined,one:!0,accX:0,accY:0},options);return this.each(function(){var t=$(this);t.appeared=!1;if(!fn){t.trigger('appear',settings.data);return} -var w=$(window);var check=function(){if(!t.is(':visible')){t.appeared=!1;return} -var a=w.scrollLeft();var b=w.scrollTop();var o=t.offset();var x=o.left;var y=o.top;var ax=settings.accX;var ay=settings.accY;var th=t.height();var wh=w.height();var tw=t.width();var ww=w.width();if(y+th+ay>=b&&y<=b+wh+ay&&x+tw+ax>=a&&x<=a+ww+ax){if(!t.appeared)t.trigger('appear',settings.data)}else{t.appeared=!1}};var modifiedFn=function(){t.appeared=!0;if(settings.one){w.unbind('scroll',check);var i=$.inArray(check,$.fn.appear.checks);if(i>=0)$.fn.appear.checks.splice(i,1)} -fn.apply(this,arguments)};if(settings.one)t.one('appear',settings.data,modifiedFn);else t.bind('appear',settings.data,modifiedFn);w.scroll(check);$.fn.appear.checks.push(check);(check)()})};$.extend($.fn.appear,{checks:[],timeout:null,checkAll:function(){var length=$.fn.appear.checks.length;if(length>0)while(length--)($.fn.appear.checks[length])();},run:function(){if($.fn.appear.timeout)clearTimeout($.fn.appear.timeout);$.fn.appear.timeout=setTimeout($.fn.appear.checkAll,20)}});$.each(['append','prepend','after','before','attr','removeAttr','addClass','removeClass','toggleClass','remove','css','show','hide'],function(i,n){var old=$.fn[n];if(old){$.fn[n]=function(){var r=old.apply(this,arguments);$.fn.appear.run();return r}}})})(jQuery); \ No newline at end of file diff --git a/static/assets/js/bootstrap.bundle.min.js b/static/assets/js/bootstrap.bundle.min.js deleted file mode 100644 index 2ce34efc..00000000 --- a/static/assets/js/bootstrap.bundle.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v5.0.0-beta1 (https://getbootstrap.com/) - * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";function t(t,e){for(var n=0;n0,i._pointerEvent=Boolean(window.PointerEvent),i._addEventListeners(),i}i(o,t);var r=o.prototype;return r.next=function(){this._isSliding||this._slide("next")},r.nextWhenVisible=function(){!document.hidden&&g(this._element)&&this.next()},r.prev=function(){this._isSliding||this._slide("prev")},r.pause=function(t){t||(this._isPaused=!0),q.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(f(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},r.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},r.to=function(t){var e=this;this._activeElement=q.findOne(".active.carousel-item",this._element);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)H.one(this._element,"slid.bs.carousel",(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var i=t>n?"next":"prev";this._slide(i,this._items[t])}},r.dispose=function(){t.prototype.dispose.call(this),H.off(this._element,V),this._items=null,this._config=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},r._getConfig=function(t){return t=n({},X,t),p(z,t,$),t},r._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},r._addEventListeners=function(){var t=this;this._config.keyboard&&H.on(this._element,"keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&(H.on(this._element,"mouseenter.bs.carousel",(function(e){return t.pause(e)})),H.on(this._element,"mouseleave.bs.carousel",(function(e){return t.cycle(e)}))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()},r._addTouchEventListeners=function(){var t=this,e=function(e){t._pointerEvent&&G[e.pointerType.toUpperCase()]?t.touchStartX=e.clientX:t._pointerEvent||(t.touchStartX=e.touches[0].clientX)},n=function(e){t._pointerEvent&&G[e.pointerType.toUpperCase()]&&(t.touchDeltaX=e.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};q.find(".carousel-item img",this._element).forEach((function(t){H.on(t,"dragstart.bs.carousel",(function(t){return t.preventDefault()}))})),this._pointerEvent?(H.on(this._element,"pointerdown.bs.carousel",(function(t){return e(t)})),H.on(this._element,"pointerup.bs.carousel",(function(t){return n(t)})),this._element.classList.add("pointer-event")):(H.on(this._element,"touchstart.bs.carousel",(function(t){return e(t)})),H.on(this._element,"touchmove.bs.carousel",(function(e){return function(e){e.touches&&e.touches.length>1?t.touchDeltaX=0:t.touchDeltaX=e.touches[0].clientX-t.touchStartX}(e)})),H.on(this._element,"touchend.bs.carousel",(function(t){return n(t)})))},r._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.key){case"ArrowLeft":t.preventDefault(),this.prev();break;case"ArrowRight":t.preventDefault(),this.next()}},r._getItemIndex=function(t){return this._items=t&&t.parentNode?q.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)},r._getItemByDirection=function(t,e){var n="next"===t,i="prev"===t,o=this._getItemIndex(e),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return e;var s=(o+("prev"===t?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},r._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(q.findOne(".active.carousel-item",this._element));return H.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:i,to:n})},r._setActiveIndicatorElement=function(t){if(this._indicatorsElement){for(var e=q.find(".active",this._indicatorsElement),n=0;n0)for(var i=0;i=0}function wt(t){return((ht(t)?t.ownerDocument:t.document)||window.document).documentElement}function Et(t){return"html"===ft(t)?t:t.assignedSlot||t.parentNode||t.host||wt(t)}function Tt(t){if(!pt(t)||"fixed"===bt(t).position)return null;var e=t.offsetParent;if(e){var n=wt(e);if("body"===ft(e)&&"static"===bt(e).position&&"static"!==bt(n).position)return n}return e}function kt(t){for(var e=dt(t),n=Tt(t);n&&yt(n)&&"static"===bt(n).position;)n=Tt(n);return n&&"body"===ft(n)&&"static"===bt(n).position?e:n||function(t){for(var e=Et(t);pt(e)&&["html","body"].indexOf(ft(e))<0;){var n=bt(e);if("none"!==n.transform||"none"!==n.perspective||n.willChange&&"auto"!==n.willChange)return e;e=e.parentNode}return null}(t)||e}function Ot(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Lt(t,e,n){return Math.max(t,Math.min(e,n))}function At(t){return Object.assign(Object.assign({},{top:0,right:0,bottom:0,left:0}),t)}function Ct(t,e){return e.reduce((function(e,n){return e[n]=t,e}),{})}var Dt={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,n=t.state,i=t.name,o=n.elements.arrow,r=n.modifiersData.popperOffsets,s=mt(n.placement),a=Ot(s),l=[st,rt].indexOf(s)>=0?"height":"width";if(o&&r){var c=n.modifiersData[i+"#persistent"].padding,u=vt(o),f="y"===a?it:st,d="y"===a?ot:rt,h=n.rects.reference[l]+n.rects.reference[a]-r[a]-n.rects.popper[l],p=r[a]-n.rects.reference[a],g=kt(o),m=g?"y"===a?g.clientHeight||0:g.clientWidth||0:0,v=h/2-p/2,_=c[f],b=m-u[l]-c[d],y=m/2-u[l]/2+v,w=Lt(_,y,b),E=a;n.modifiersData[i]=((e={})[E]=w,e.centerOffset=w-y,e)}},effect:function(t){var e=t.state,n=t.options,i=t.name,o=n.element,r=void 0===o?"[data-popper-arrow]":o,s=n.padding,a=void 0===s?0:s;null!=r&&("string"!=typeof r||(r=e.elements.popper.querySelector(r)))&&_t(e.elements.popper,r)&&(e.elements.arrow=r,e.modifiersData[i+"#persistent"]={padding:At("number"!=typeof a?a:Ct(a,at))})},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},xt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function St(t){var e,n=t.popper,i=t.popperRect,o=t.placement,r=t.offsets,s=t.position,a=t.gpuAcceleration,l=t.adaptive,c=function(t){var e=t.x,n=t.y,i=window.devicePixelRatio||1;return{x:Math.round(e*i)/i||0,y:Math.round(n*i)/i||0}}(r),u=c.x,f=c.y,d=r.hasOwnProperty("x"),h=r.hasOwnProperty("y"),p=st,g=it,m=window;if(l){var v=kt(n);v===dt(n)&&(v=wt(n)),o===it&&(g=ot,f-=v.clientHeight-i.height,f*=a?1:-1),o===st&&(p=rt,u-=v.clientWidth-i.width,u*=a?1:-1)}var _,b=Object.assign({position:s},l&&xt);return a?Object.assign(Object.assign({},b),{},((_={})[g]=h?"0":"",_[p]=d?"0":"",_.transform=(m.devicePixelRatio||1)<2?"translate("+u+"px, "+f+"px)":"translate3d("+u+"px, "+f+"px, 0)",_)):Object.assign(Object.assign({},b),{},((e={})[g]=h?f+"px":"",e[p]=d?u+"px":"",e.transform="",e))}var jt={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,n=t.options,i=n.gpuAcceleration,o=void 0===i||i,r=n.adaptive,s=void 0===r||r,a={placement:mt(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:o};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign(Object.assign({},e.styles.popper),St(Object.assign(Object.assign({},a),{},{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:s})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign(Object.assign({},e.styles.arrow),St(Object.assign(Object.assign({},a),{},{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1})))),e.attributes.popper=Object.assign(Object.assign({},e.attributes.popper),{},{"data-popper-placement":e.placement})},data:{}},Nt={passive:!0};var It={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,n=t.instance,i=t.options,o=i.scroll,r=void 0===o||o,s=i.resize,a=void 0===s||s,l=dt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return r&&c.forEach((function(t){t.addEventListener("scroll",n.update,Nt)})),a&&l.addEventListener("resize",n.update,Nt),function(){r&&c.forEach((function(t){t.removeEventListener("scroll",n.update,Nt)})),a&&l.removeEventListener("resize",n.update,Nt)}},data:{}},Pt={left:"right",right:"left",bottom:"top",top:"bottom"};function Mt(t){return t.replace(/left|right|bottom|top/g,(function(t){return Pt[t]}))}var Bt={start:"end",end:"start"};function Ht(t){return t.replace(/start|end/g,(function(t){return Bt[t]}))}function Rt(t){var e=t.getBoundingClientRect();return{width:e.width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function Wt(t){var e=dt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Kt(t){return Rt(wt(t)).left+Wt(t).scrollLeft}function Qt(t){var e=bt(t),n=e.overflow,i=e.overflowX,o=e.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+i)}function Ut(t,e){void 0===e&&(e=[]);var n=function t(e){return["html","body","#document"].indexOf(ft(e))>=0?e.ownerDocument.body:pt(e)&&Qt(e)?e:t(Et(e))}(t),i="body"===ft(n),o=dt(n),r=i?[o].concat(o.visualViewport||[],Qt(n)?n:[]):n,s=e.concat(r);return i?s:s.concat(Ut(Et(r)))}function Ft(t){return Object.assign(Object.assign({},t),{},{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Yt(t,e){return"viewport"===e?Ft(function(t){var e=dt(t),n=wt(t),i=e.visualViewport,o=n.clientWidth,r=n.clientHeight,s=0,a=0;return i&&(o=i.width,r=i.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(s=i.offsetLeft,a=i.offsetTop)),{width:o,height:r,x:s+Kt(t),y:a}}(t)):pt(e)?function(t){var e=Rt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Ft(function(t){var e=wt(t),n=Wt(t),i=t.ownerDocument.body,o=Math.max(e.scrollWidth,e.clientWidth,i?i.scrollWidth:0,i?i.clientWidth:0),r=Math.max(e.scrollHeight,e.clientHeight,i?i.scrollHeight:0,i?i.clientHeight:0),s=-n.scrollLeft+Kt(t),a=-n.scrollTop;return"rtl"===bt(i||e).direction&&(s+=Math.max(e.clientWidth,i?i.clientWidth:0)-o),{width:o,height:r,x:s,y:a}}(wt(t)))}function qt(t,e,n){var i="clippingParents"===e?function(t){var e=Ut(Et(t)),n=["absolute","fixed"].indexOf(bt(t).position)>=0&&pt(t)?kt(t):t;return ht(n)?e.filter((function(t){return ht(t)&&_t(t,n)&&"body"!==ft(t)})):[]}(t):[].concat(e),o=[].concat(i,[n]),r=o[0],s=o.reduce((function(e,n){var i=Yt(t,n);return e.top=Math.max(i.top,e.top),e.right=Math.min(i.right,e.right),e.bottom=Math.min(i.bottom,e.bottom),e.left=Math.max(i.left,e.left),e}),Yt(t,r));return s.width=s.right-s.left,s.height=s.bottom-s.top,s.x=s.left,s.y=s.top,s}function zt(t){return t.split("-")[1]}function Vt(t){var e,n=t.reference,i=t.element,o=t.placement,r=o?mt(o):null,s=o?zt(o):null,a=n.x+n.width/2-i.width/2,l=n.y+n.height/2-i.height/2;switch(r){case it:e={x:a,y:n.y-i.height};break;case ot:e={x:a,y:n.y+n.height};break;case rt:e={x:n.x+n.width,y:l};break;case st:e={x:n.x-i.width,y:l};break;default:e={x:n.x,y:n.y}}var c=r?Ot(r):null;if(null!=c){var u="y"===c?"height":"width";switch(s){case"start":e[c]=Math.floor(e[c])-Math.floor(n[u]/2-i[u]/2);break;case"end":e[c]=Math.floor(e[c])+Math.ceil(n[u]/2-i[u]/2)}}return e}function Xt(t,e){void 0===e&&(e={});var n=e,i=n.placement,o=void 0===i?t.placement:i,r=n.boundary,s=void 0===r?"clippingParents":r,a=n.rootBoundary,l=void 0===a?"viewport":a,c=n.elementContext,u=void 0===c?"popper":c,f=n.altBoundary,d=void 0!==f&&f,h=n.padding,p=void 0===h?0:h,g=At("number"!=typeof p?p:Ct(p,at)),m="popper"===u?"reference":"popper",v=t.elements.reference,_=t.rects.popper,b=t.elements[d?m:u],y=qt(ht(b)?b:b.contextElement||wt(t.elements.popper),s,l),w=Rt(v),E=Vt({reference:w,element:_,strategy:"absolute",placement:o}),T=Ft(Object.assign(Object.assign({},_),E)),k="popper"===u?T:w,O={top:y.top-k.top+g.top,bottom:k.bottom-y.bottom+g.bottom,left:y.left-k.left+g.left,right:k.right-y.right+g.right},L=t.modifiersData.offset;if("popper"===u&&L){var A=L[o];Object.keys(O).forEach((function(t){var e=[rt,ot].indexOf(t)>=0?1:-1,n=[it,ot].indexOf(t)>=0?"y":"x";O[t]+=A[n]*e}))}return O}function $t(t,e){void 0===e&&(e={});var n=e,i=n.placement,o=n.boundary,r=n.rootBoundary,s=n.padding,a=n.flipVariations,l=n.allowedAutoPlacements,c=void 0===l?ct:l,u=zt(i),f=u?a?lt:lt.filter((function(t){return zt(t)===u})):at,d=f.filter((function(t){return c.indexOf(t)>=0}));0===d.length&&(d=f);var h=d.reduce((function(e,n){return e[n]=Xt(t,{placement:n,boundary:o,rootBoundary:r,padding:s})[mt(n)],e}),{});return Object.keys(h).sort((function(t,e){return h[t]-h[e]}))}var Gt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,n=t.options,i=t.name;if(!e.modifiersData[i]._skip){for(var o=n.mainAxis,r=void 0===o||o,s=n.altAxis,a=void 0===s||s,l=n.fallbackPlacements,c=n.padding,u=n.boundary,f=n.rootBoundary,d=n.altBoundary,h=n.flipVariations,p=void 0===h||h,g=n.allowedAutoPlacements,m=e.options.placement,v=mt(m),_=l||(v===m||!p?[Mt(m)]:function(t){if("auto"===mt(t))return[];var e=Mt(t);return[Ht(t),e,Ht(e)]}(m)),b=[m].concat(_).reduce((function(t,n){return t.concat("auto"===mt(n)?$t(e,{placement:n,boundary:u,rootBoundary:f,padding:c,flipVariations:p,allowedAutoPlacements:g}):n)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,T=!0,k=b[0],O=0;O=0,x=D?"width":"height",S=Xt(e,{placement:L,boundary:u,rootBoundary:f,altBoundary:d,padding:c}),j=D?C?rt:st:C?ot:it;y[x]>w[x]&&(j=Mt(j));var N=Mt(j),I=[];if(r&&I.push(S[A]<=0),a&&I.push(S[j]<=0,S[N]<=0),I.every((function(t){return t}))){k=L,T=!1;break}E.set(L,I)}if(T)for(var P=function(t){var e=b.find((function(e){var n=E.get(e);if(n)return n.slice(0,t).every((function(t){return t}))}));if(e)return k=e,"break"},M=p?3:1;M>0;M--){if("break"===P(M))break}e.placement!==k&&(e.modifiersData[i]._skip=!0,e.placement=k,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Zt(t,e,n){return void 0===n&&(n={x:0,y:0}),{top:t.top-e.height-n.y,right:t.right-e.width+n.x,bottom:t.bottom-e.height+n.y,left:t.left-e.width-n.x}}function Jt(t){return[it,rt,ot,st].some((function(e){return t[e]>=0}))}var te={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,n=t.name,i=e.rects.reference,o=e.rects.popper,r=e.modifiersData.preventOverflow,s=Xt(e,{elementContext:"reference"}),a=Xt(e,{altBoundary:!0}),l=Zt(s,i),c=Zt(a,o,r),u=Jt(l),f=Jt(c);e.modifiersData[n]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:u,hasPopperEscaped:f},e.attributes.popper=Object.assign(Object.assign({},e.attributes.popper),{},{"data-popper-reference-hidden":u,"data-popper-escaped":f})}};var ee={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,n=t.options,i=t.name,o=n.offset,r=void 0===o?[0,0]:o,s=ct.reduce((function(t,n){return t[n]=function(t,e,n){var i=mt(t),o=[st,it].indexOf(i)>=0?-1:1,r="function"==typeof n?n(Object.assign(Object.assign({},e),{},{placement:t})):n,s=r[0],a=r[1];return s=s||0,a=(a||0)*o,[st,rt].indexOf(i)>=0?{x:a,y:s}:{x:s,y:a}}(n,e.rects,r),t}),{}),a=s[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[i]=s}};var ne={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,n=t.name;e.modifiersData[n]=Vt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}};var ie={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,n=t.options,i=t.name,o=n.mainAxis,r=void 0===o||o,s=n.altAxis,a=void 0!==s&&s,l=n.boundary,c=n.rootBoundary,u=n.altBoundary,f=n.padding,d=n.tether,h=void 0===d||d,p=n.tetherOffset,g=void 0===p?0:p,m=Xt(e,{boundary:l,rootBoundary:c,padding:f,altBoundary:u}),v=mt(e.placement),_=zt(e.placement),b=!_,y=Ot(v),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,T=e.rects.reference,k=e.rects.popper,O="function"==typeof g?g(Object.assign(Object.assign({},e.rects),{},{placement:e.placement})):g,L={x:0,y:0};if(E){if(r){var A="y"===y?it:st,C="y"===y?ot:rt,D="y"===y?"height":"width",x=E[y],S=E[y]+m[A],j=E[y]-m[C],N=h?-k[D]/2:0,I="start"===_?T[D]:k[D],P="start"===_?-k[D]:-T[D],M=e.elements.arrow,B=h&&M?vt(M):{width:0,height:0},H=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=H[A],W=H[C],K=Lt(0,T[D],B[D]),Q=b?T[D]/2-N-K-R-O:I-K-R-O,U=b?-T[D]/2+N+K+W+O:P+K+W+O,F=e.elements.arrow&&kt(e.elements.arrow),Y=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,q=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,z=E[y]+Q-q-Y,V=E[y]+U-q,X=Lt(h?Math.min(S,z):S,x,h?Math.max(j,V):j);E[y]=X,L[y]=X-x}if(a){var $="x"===y?it:st,G="x"===y?ot:rt,Z=E[w],J=Lt(Z+m[$],Z,Z-m[G]);E[w]=J,L[w]=J-Z}e.modifiersData[i]=L}},requiresIfExists:["offset"]};function oe(t,e,n){void 0===n&&(n=!1);var i,o,r=wt(e),s=Rt(t),a=pt(e),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(a||!a&&!n)&&(("body"!==ft(e)||Qt(r))&&(l=(i=e)!==dt(i)&&pt(i)?{scrollLeft:(o=i).scrollLeft,scrollTop:o.scrollTop}:Wt(i)),pt(e)?((c=Rt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=Kt(r))),{x:s.left+l.scrollLeft-c.x,y:s.top+l.scrollTop-c.y,width:s.width,height:s.height}}function re(t){var e=new Map,n=new Set,i=[];return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){n.has(t.name)||function t(o){n.add(o.name),[].concat(o.requires||[],o.requiresIfExists||[]).forEach((function(i){if(!n.has(i)){var o=e.get(i);o&&t(o)}})),i.push(o)}(t)})),i}var se={placement:"bottom",modifiers:[],strategy:"absolute"};function ae(){for(var t=arguments.length,e=new Array(t),n=0;n0&&r--,"ArrowDown"===t.key&&rdocument.documentElement.clientHeight;e||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");var n=u(this._dialog);H.off(this._element,"transitionend"),H.one(this._element,"transitionend",(function(){t._element.classList.remove("modal-static"),e||(H.one(t._element,"transitionend",(function(){t._element.style.overflowY=""})),h(t._element,n))})),h(this._element,n),this._element.focus()}},r._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;(!this._isBodyOverflowing&&t&&!y||this._isBodyOverflowing&&!t&&y)&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),(this._isBodyOverflowing&&!t&&!y||!this._isBodyOverflowing&&t&&y)&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},r._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},r._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",container:!1,fallbackPlacements:null,boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:xe,popperConfig:null},He={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},Re=function(t){function o(e,n){var i;if(void 0===de)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");return(i=t.call(this,e)||this)._isEnabled=!0,i._timeout=0,i._hoverState="",i._activeTrigger={},i._popper=null,i.config=i._getConfig(n),i.tip=null,i._setListeners(),i}i(o,t);var r=o.prototype;return r.enable=function(){this._isEnabled=!0},r.disable=function(){this._isEnabled=!1},r.toggleEnabled=function(){this._isEnabled=!this._isEnabled},r.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=T(t.delegateTarget,e);n||(n=new this.constructor(t.delegateTarget,this._getDelegateConfig()),E(t.delegateTarget,e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}},r.dispose=function(){clearTimeout(this._timeout),H.off(this._element,this.constructor.EVENT_KEY),H.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.parentNode.removeChild(this.tip),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.config=null,this.tip=null,t.prototype.dispose.call(this)},r.show=function(){var t=this;if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(this.isWithContent()&&this._isEnabled){var e=H.trigger(this._element,this.constructor.Event.SHOW),n=function t(e){if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){var n=e.getRootNode();return n instanceof ShadowRoot?n:null}return e instanceof ShadowRoot?e:e.parentNode?t(e.parentNode):null}(this._element),i=null===n?this._element.ownerDocument.documentElement.contains(this._element):n.contains(this._element);if(e.defaultPrevented||!i)return;var o=this.getTipElement(),r=s(this.constructor.NAME);o.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&o.classList.add("fade");var a="function"==typeof this.config.placement?this.config.placement.call(this,o,this._element):this.config.placement,l=this._getAttachment(a);this._addAttachmentClass(l);var c=this._getContainer();E(o,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||c.appendChild(o),H.trigger(this._element,this.constructor.Event.INSERTED),this._popper=fe(this._element,o,this._getPopperConfig(l)),o.classList.add("show");var f,d,p="function"==typeof this.config.customClass?this.config.customClass():this.config.customClass;if(p)(f=o.classList).add.apply(f,p.split(" "));if("ontouchstart"in document.documentElement)(d=[]).concat.apply(d,document.body.children).forEach((function(t){H.on(t,"mouseover",(function(){}))}));var g=function(){var e=t._hoverState;t._hoverState=null,H.trigger(t._element,t.constructor.Event.SHOWN),"out"===e&&t._leave(null,t)};if(this.tip.classList.contains("fade")){var m=u(this.tip);H.one(this.tip,"transitionend",g),h(this.tip,m)}else g()}},r.hide=function(){var t=this;if(this._popper){var e=this.getTipElement(),n=function(){"show"!==t._hoverState&&e.parentNode&&e.parentNode.removeChild(e),t._cleanTipClass(),t._element.removeAttribute("aria-describedby"),H.trigger(t._element,t.constructor.Event.HIDDEN),t._popper&&(t._popper.destroy(),t._popper=null)};if(!H.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented){var i;if(e.classList.remove("show"),"ontouchstart"in document.documentElement)(i=[]).concat.apply(i,document.body.children).forEach((function(t){return H.off(t,"mouseover",m)}));if(this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this.tip.classList.contains("fade")){var o=u(e);H.one(e,"transitionend",n),h(e,o)}else n();this._hoverState=""}}},r.update=function(){null!==this._popper&&this._popper.update()},r.isWithContent=function(){return Boolean(this.getTitle())},r.getTipElement=function(){if(this.tip)return this.tip;var t=document.createElement("div");return t.innerHTML=this.config.template,this.tip=t.children[0],this.tip},r.setContent=function(){var t=this.getTipElement();this.setElementContent(q.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")},r.setElementContent=function(t,e){if(null!==t)return"object"==typeof e&&d(e)?(e.jquery&&(e=e[0]),void(this.config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this.config.html?(this.config.sanitize&&(e=Se(e,this.config.allowList,this.config.sanitizeFn)),t.innerHTML=e):t.textContent=e)},r.getTitle=function(){var t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this._element):this.config.title),t},r.updateAttachment=function(t){return"right"===t?"end":"left"===t?"start":t},r._getPopperConfig=function(t){var e=this,i={name:"flip",options:{altBoundary:!0}};return this.config.fallbackPlacements&&(i.options.fallbackPlacements=this.config.fallbackPlacements),n({},{placement:t,modifiers:[i,{name:"preventOverflow",options:{rootBoundary:this.config.boundary}},{name:"arrow",options:{element:"."+this.constructor.NAME+"-arrow"}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:function(t){return e._handlePopperPlacementChange(t)}}],onFirstUpdate:function(t){t.options.placement!==t.placement&&e._handlePopperPlacementChange(t)}},this.config.popperConfig)},r._addAttachmentClass=function(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))},r._getContainer=function(){return!1===this.config.container?document.body:d(this.config.container)?this.config.container:q.findOne(this.config.container)},r._getAttachment=function(t){return Me[t.toUpperCase()]},r._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)H.on(t._element,t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==e){var n="hover"===e?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i="hover"===e?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;H.on(t._element,n,t.config.selector,(function(e){return t._enter(e)})),H.on(t._element,i,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t._element&&t.hide()},H.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=n({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},r._fixTitle=function(){var t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))},r._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||T(t.delegateTarget,n))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),E(t.delegateTarget,n,e)),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){"show"===e._hoverState&&e.show()}),e.config.delay.show):e.show())},r._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||T(t.delegateTarget,n))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),E(t.delegateTarget,n,e)),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){"out"===e._hoverState&&e.hide()}),e.config.delay.hide):e.hide())},r._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},r._getConfig=function(t){var e=Y.getDataAttributes(this._element);return Object.keys(e).forEach((function(t){Ie.has(t)&&delete e[t]})),t&&"object"==typeof t.container&&t.container.jquery&&(t.container=t.container[0]),"number"==typeof(t=n({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),p(je,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.allowList,t.sanitizeFn)),t},r._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},r._cleanTipClass=function(){var t=this.getTipElement(),e=t.getAttribute("class").match(Ne);null!==e&&e.length>0&&e.map((function(t){return t.trim()})).forEach((function(e){return t.classList.remove(e)}))},r._handlePopperPlacementChange=function(t){var e=t.state;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))},o.jQueryInterface=function(t){return this.each((function(){var e=T(this,"bs.tooltip"),n="object"==typeof t&&t;if((e||!/dispose|hide/.test(t))&&(e||(e=new o(this,n)),"string"==typeof t)){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},e(o,null,[{key:"Default",get:function(){return Be}},{key:"NAME",get:function(){return je}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return He}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Pe}}]),o}(R);b((function(){var t=_();if(t){var e=t.fn[je];t.fn[je]=Re.jQueryInterface,t.fn[je].Constructor=Re,t.fn[je].noConflict=function(){return t.fn[je]=e,Re.jQueryInterface}}}));var We="popover",Ke=new RegExp("(^|\\s)bs-popover\\S+","g"),Qe=n({},Re.Default,{placement:"right",trigger:"click",content:"",template:''}),Ue=n({},Re.DefaultType,{content:"(string|element|function)"}),Fe={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},Ye=function(t){function n(){return t.apply(this,arguments)||this}i(n,t);var o=n.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.setContent=function(){var t=this.getTipElement();this.setElementContent(q.findOne(".popover-header",t),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(q.findOne(".popover-body",t),e),t.classList.remove("fade","show")},o._addAttachmentClass=function(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))},o._getContent=function(){return this._element.getAttribute("data-bs-content")||this.config.content},o._cleanTipClass=function(){var t=this.getTipElement(),e=t.getAttribute("class").match(Ke);null!==e&&e.length>0&&e.map((function(t){return t.trim()})).forEach((function(e){return t.classList.remove(e)}))},n.jQueryInterface=function(t){return this.each((function(){var e=T(this,"bs.popover"),i="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new n(this,i),E(this,"bs.popover",e)),"string"==typeof t)){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},e(n,null,[{key:"Default",get:function(){return Qe}},{key:"NAME",get:function(){return We}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return Fe}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Ue}}]),n}(Re);b((function(){var t=_();if(t){var e=t.fn[We];t.fn[We]=Ye.jQueryInterface,t.fn[We].Constructor=Ye,t.fn[We].noConflict=function(){return t.fn[We]=e,Ye.jQueryInterface}}}));var qe="scrollspy",ze={offset:10,method:"auto",target:""},Ve={offset:"number",method:"string",target:"(string|element)"},Xe=function(t){function o(e,n){var i;return(i=t.call(this,e)||this)._scrollElement="BODY"===e.tagName?window:e,i._config=i._getConfig(n),i._selector=i._config.target+" .nav-link, "+i._config.target+" .list-group-item, "+i._config.target+" .dropdown-item",i._offsets=[],i._targets=[],i._activeTarget=null,i._scrollHeight=0,H.on(i._scrollElement,"scroll.bs.scrollspy",(function(t){return i._process(t)})),i.refresh(),i._process(),i}i(o,t);var r=o.prototype;return r.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?"offset":"position",n="auto"===this._config.method?e:this._config.method,i="position"===n?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),q.find(this._selector).map((function(t){var e=l(t),o=e?q.findOne(e):null;if(o){var r=o.getBoundingClientRect();if(r.width||r.height)return[Y[n](o).top+i,e]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},r.dispose=function(){t.prototype.dispose.call(this),H.off(this._scrollElement,".bs.scrollspy"),this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},r._getConfig=function(t){if("string"!=typeof(t=n({},ze,"object"==typeof t&&t?t:{})).target&&d(t.target)){var e=t.target.id;e||(e=s(qe),t.target.id=e),t.target="#"+e}return p(qe,t,Ve),t},r._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},r._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},r._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},r._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&(void 0===this._offsets[o+1]||t li > .active":".active";e=(e=q.find(o,i))[e.length-1]}var r=null;if(e&&(r=H.trigger(e,"hide.bs.tab",{relatedTarget:this._element})),!(H.trigger(this._element,"show.bs.tab",{relatedTarget:e}).defaultPrevented||null!==r&&r.defaultPrevented)){this._activate(this._element,i);var s=function(){H.trigger(e,"hidden.bs.tab",{relatedTarget:t._element}),H.trigger(t._element,"shown.bs.tab",{relatedTarget:e})};n?this._activate(n,n.parentNode,s):s()}}},o._activate=function(t,e,n){var i=this,o=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?q.children(e,".active"):q.find(":scope > li > .active",e))[0],r=n&&o&&o.classList.contains("fade"),s=function(){return i._transitionComplete(t,o,n)};if(o&&r){var a=u(o);o.classList.remove("show"),H.one(o,"transitionend",s),h(o,a)}else s()},o._transitionComplete=function(t,e,n){if(e){e.classList.remove("active");var i=q.findOne(":scope > .dropdown-menu .active",e.parentNode);i&&i.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}(t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),v(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&t.parentNode.classList.contains("dropdown-menu"))&&(t.closest(".dropdown")&&q.find(".dropdown-toggle").forEach((function(t){return t.classList.add("active")})),t.setAttribute("aria-expanded",!0));n&&n()},n.jQueryInterface=function(t){return this.each((function(){var e=T(this,"bs.tab")||new n(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},e(n,null,[{key:"DATA_KEY",get:function(){return"bs.tab"}}]),n}(R);H.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){t.preventDefault(),(T(this,"bs.tab")||new $e(this)).show()})),b((function(){var t=_();if(t){var e=t.fn.tab;t.fn.tab=$e.jQueryInterface,t.fn.tab.Constructor=$e,t.fn.tab.noConflict=function(){return t.fn.tab=e,$e.jQueryInterface}}}));var Ge={animation:"boolean",autohide:"boolean",delay:"number"},Ze={animation:!0,autohide:!0,delay:5e3},Je=function(t){function o(e,n){var i;return(i=t.call(this,e)||this)._config=i._getConfig(n),i._timeout=null,i._setListeners(),i}i(o,t);var r=o.prototype;return r.show=function(){var t=this;if(!H.trigger(this._element,"show.bs.toast").defaultPrevented){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var e=function(){t._element.classList.remove("showing"),t._element.classList.add("show"),H.trigger(t._element,"shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove("hide"),v(this._element),this._element.classList.add("showing"),this._config.animation){var n=u(this._element);H.one(this._element,"transitionend",e),h(this._element,n)}else e()}},r.hide=function(){var t=this;if(this._element.classList.contains("show")&&!H.trigger(this._element,"hide.bs.toast").defaultPrevented){var e=function(){t._element.classList.add("hide"),H.trigger(t._element,"hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var n=u(this._element);H.one(this._element,"transitionend",e),h(this._element,n)}else e()}},r.dispose=function(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),H.off(this._element,"click.dismiss.bs.toast"),t.prototype.dispose.call(this),this._config=null},r._getConfig=function(t){return t=n({},Ze,Y.getDataAttributes(this._element),"object"==typeof t&&t?t:{}),p("toast",t,this.constructor.DefaultType),t},r._setListeners=function(){var t=this;H.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',(function(){return t.hide()}))},r._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},o.jQueryInterface=function(t){return this.each((function(){var e=T(this,"bs.toast");if(e||(e=new o(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t](this)}}))},e(o,null,[{key:"DefaultType",get:function(){return Ge}},{key:"Default",get:function(){return Ze}},{key:"DATA_KEY",get:function(){return"bs.toast"}}]),o}(R);return b((function(){var t=_();if(t){var e=t.fn.toast;t.fn.toast=Je.jQueryInterface,t.fn.toast.Constructor=Je,t.fn.toast.noConflict=function(){return t.fn.toast=e,Je.jQueryInterface}}})),{Alert:K,Button:Q,Carousel:Z,Collapse:nt,Dropdown:Te,Modal:Le,Popover:Ye,ScrollSpy:Xe,Tab:$e,Toast:Je,Tooltip:Re}})); - //# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/static/assets/js/contact-form-script.js b/static/assets/js/contact-form-script.js deleted file mode 100644 index 8b1b881f..00000000 --- a/static/assets/js/contact-form-script.js +++ /dev/null @@ -1,62 +0,0 @@ -/*==============================================================*/ -// Contact Form JS -/*==============================================================*/ -(function ($) { - "use strict"; // Start of use strict - $("#contactForm").validator().on("submit", function (event) { - if (event.isDefaultPrevented()) { - // handle the invalid form... - formError(); - submitMSG(false, "Did you fill in the form properly?"); - } else { - // everything looks good! - event.preventDefault(); - submitForm(); - } - }); - - - function submitForm(){ - // Initiate Variables With Form Content - var name = $("#name").val(); - var email = $("#email").val(); - var msg_subject = $("#msg_subject").val(); - var phone_number = $("#phone_number").val(); - var message = $("#message").val(); - - - $.ajax({ - type: "POST", - url: "assets/php/form-process.php", - data: "name=" + name + "&email=" + email + "&phone_number=" + phone_number + "&msg_subject=" + msg_subject + "&message=" + message, - success : function(text){ - if (text == "success"){ - formSuccess(); - } else { - formError(); - submitMSG(false,text); - } - } - }); - } - - function formSuccess(){ - $("#contactForm")[0].reset(); - submitMSG(true, "Message Submitted!") - } - - function formError(){ - $("#contactForm").removeClass().addClass('shake animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){ - $(this).removeClass(); - }); - } - - function submitMSG(valid, msg){ - if(valid){ - var msgClasses = "h4 text-start tada animated text-success"; - } else { - var msgClasses = "h4 text-start text-danger"; - } - $("#msgSubmit").removeClass().addClass(msgClasses).text(msg); - } -}(jQuery)); // End of use strict \ No newline at end of file diff --git a/static/assets/js/form-validator.min.js b/static/assets/js/form-validator.min.js deleted file mode 100644 index a48dd8aa..00000000 --- a/static/assets/js/form-validator.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Validator v0.8.1 for Bootstrap 4, by @1000hz - * Copyright 2015 Cina Saffary - * Licensed under http://opensource.org/licenses/MIT - * - * https://github.com/1000hz/bootstrap-validator - */ - -+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b),f=c.data("bs.validator");(f||"destroy"!=b)&&(f||c.data("bs.validator",f=new d(this,e)),"string"==typeof b&&f[b]())})}var c=':input:not([type="submit"], button):enabled:visible',d=function(b,c){this.$element=a(b),this.options=c,c.errors=a.extend({},d.DEFAULTS.errors,c.errors);for(var e in c.custom)if(!c.errors[e])throw new Error("Missing default error message for custom validator: "+e);a.extend(d.VALIDATORS,c.custom),this.$element.attr("novalidate",!0),this.toggleSubmit(),this.$element.on("input.bs.validator change.bs.validator focusout.bs.validator",a.proxy(this.validateInput,this)),this.$element.on("submit.bs.validator",a.proxy(this.onSubmit,this)),this.$element.find("[data-match]").each(function(){var b=a(this),c=b.data("match");a(c).on("input.bs.validator",function(){b.val()&&b.trigger("input.bs.validator")})})};d.DEFAULTS={delay:500,html:!1,disable:!0,custom:{},errors:{match:"Does not match",minlength:"Not long enough"},feedback:{success:"glyphicon-ok",error:"glyphicon-warning-sign"}},d.VALIDATORS={"native":function(a){var b=a[0];return b.checkValidity?b.checkValidity():!0},match:function(b){var c=b.data("match");return!b.val()||b.val()===a(c).val()},minlength:function(a){var b=a.data("minlength");return!a.val()||a.val().length>=b}},d.prototype.validateInput=function(b){var c=a(b.target),d=c.data("bs.validator.errors");if(c.is('[type="radio"]')&&(c=this.$element.find('input[name="'+c.attr("name")+'"]')),this.$element.trigger(b=a.Event("validate.bs.validator",{relatedTarget:c[0]})),!b.isDefaultPrevented()){var e=this;this.runValidators(c).done(function(f){c.data("bs.validator.errors",f),f.length?e.showErrors(c):e.clearErrors(c),d&&f.toString()===d.toString()||(b=f.length?a.Event("invalid.bs.validator",{relatedTarget:c[0],detail:f}):a.Event("valid.bs.validator",{relatedTarget:c[0],detail:d}),e.$element.trigger(b)),e.toggleSubmit(),e.$element.trigger(a.Event("validated.bs.validator",{relatedTarget:c[0]}))})}},d.prototype.runValidators=function(b){function c(a){return b.data(a+"-error")||b.data("error")||"native"==a&&b[0].validationMessage||g.errors[a]}var e=[],f=a.Deferred(),g=this.options;return b.data("bs.validator.deferred")&&b.data("bs.validator.deferred").reject(),b.data("bs.validator.deferred",f),a.each(d.VALIDATORS,a.proxy(function(a,d){if((b.data(a)||"native"==a)&&!d.call(this,b)){var f=c(a);!~e.indexOf(f)&&e.push(f)}},this)),!e.length&&b.val()&&b.data("remote")?this.defer(b,function(){var d={};d[b.attr("name")]=b.val(),a.get(b.data("remote"),d).fail(function(a,b,d){e.push(c("remote")||d)}).always(function(){f.resolve(e)})}):f.resolve(e),f.promise()},d.prototype.validate=function(){var a=this.options.delay;return this.options.delay=0,this.$element.find(c).trigger("input.bs.validator"),this.options.delay=a,this},d.prototype.showErrors=function(b){var c=this.options.html?"html":"text";this.defer(b,function(){var d=b.closest(".form-group"),e=d.find(".help-block.with-errors"),f=d.find(".form-control-feedback"),g=b.data("bs.validator.errors");g.length&&(g=a("
    ").addClass("list-unstyled").append(a.map(g,function(b){return a("
  • ")[c](b)})),void 0===e.data("bs.validator.originalContent")&&e.data("bs.validator.originalContent",e.html()),e.empty().append(g),d.addClass("has-error"),f.length&&f.removeClass(this.options.feedback.success)&&f.addClass(this.options.feedback.error)&&d.removeClass("has-success"))})},d.prototype.clearErrors=function(a){var b=a.closest(".form-group"),c=b.find(".help-block.with-errors"),d=b.find(".form-control-feedback");c.html(c.data("bs.validator.originalContent")),b.removeClass("has-error"),d.length&&d.removeClass(this.options.feedback.error)&&d.addClass(this.options.feedback.success)&&b.addClass("has-success")},d.prototype.hasErrors=function(){function b(){return!!(a(this).data("bs.validator.errors")||[]).length}return!!this.$element.find(c).filter(b).length},d.prototype.isIncomplete=function(){function b(){return"checkbox"===this.type?!this.checked:"radio"===this.type?!a('[name="'+this.name+'"]:checked').length:""===a.trim(this.value)}return!!this.$element.find(c).filter("[required]").filter(b).length},d.prototype.onSubmit=function(a){this.validate(),(this.isIncomplete()||this.hasErrors())&&a.preventDefault()},d.prototype.toggleSubmit=function(){if(this.options.disable){var b=a('button[type="submit"], input[type="submit"]').filter('[form="'+this.$element.attr("id")+'"]').add(this.$element.find('input[type="submit"], button[type="submit"]'));b.toggleClass("disabled",this.isIncomplete()||this.hasErrors()).css({"pointer-events":"all",cursor:"pointer"})}},d.prototype.defer=function(b,c){return c=a.proxy(c,this),this.options.delay?(window.clearTimeout(b.data("bs.validator.timeout")),void b.data("bs.validator.timeout",window.setTimeout(c,this.options.delay))):c()},d.prototype.destroy=function(){return this.$element.removeAttr("novalidate").removeData("bs.validator").off(".bs.validator"),this.$element.find(c).off(".bs.validator").removeData(["bs.validator.errors","bs.validator.deferred"]).each(function(){var b=a(this),c=b.data("bs.validator.timeout");window.clearTimeout(c)&&b.removeData("bs.validator.timeout")}),this.$element.find(".help-block.with-errors").each(function(){var b=a(this),c=b.data("bs.validator.originalContent");b.removeData("bs.validator.originalContent").html(c)}),this.$element.find('input[type="submit"], button[type="submit"]').removeClass("disabled"),this.$element.find(".has-error").removeClass("has-error"),this};var e=a.fn.validator;a.fn.validator=b,a.fn.validator.Constructor=d,a.fn.validator.noConflict=function(){return a.fn.validator=e,this},a(window).on("load",function(){a('form[data-toggle="validator"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery); \ No newline at end of file diff --git a/static/assets/js/jquery.min.js b/static/assets/js/jquery.min.js deleted file mode 100644 index 015256d0..00000000 --- a/static/assets/js/jquery.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0(a||v.height())},_setFocus:function(){(b.st.focus?b.content.find(b.st.focus).eq(0):b.wrap).focus()},_onFocusIn:function(c){return c.target===b.wrap[0]||a.contains(b.wrap[0],c.target)?void 0:(b._setFocus(),!1)},_parseMarkup:function(b,c,d){var e;d.data&&(c=a.extend(d.data,c)),y(l,[b,c,d]),a.each(c,function(c,d){if(void 0===d||d===!1)return!0;if(e=c.split("_"),e.length>1){var f=b.find(p+"-"+e[0]);if(f.length>0){var g=e[1];"replaceWith"===g?f[0]!==d[0]&&f.replaceWith(d):"img"===g?f.is("img")?f.attr("src",d):f.replaceWith(a("").attr("src",d).attr("class",f.attr("class"))):f.attr(e[1],d)}}else b.find(p+"-"+c).html(d)})},_getScrollbarSize:function(){if(void 0===b.scrollbarSize){var a=document.createElement("div");a.style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(a),b.scrollbarSize=a.offsetWidth-a.clientWidth,document.body.removeChild(a)}return b.scrollbarSize}},a.magnificPopup={instance:null,proto:t.prototype,modules:[],open:function(b,c){return A(),b=b?a.extend(!0,{},b):{},b.isObj=!0,b.index=c||0,this.instance.open(b)},close:function(){return a.magnificPopup.instance&&a.magnificPopup.instance.close()},registerModule:function(b,c){c.options&&(a.magnificPopup.defaults[b]=c.options),a.extend(this.proto,c.proto),this.modules.push(b)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'',tClose:"Close (Esc)",tLoading:"Loading...",autoFocusLast:!0}},a.fn.magnificPopup=function(c){A();var d=a(this);if("string"==typeof c)if("open"===c){var e,f=u?d.data("magnificPopup"):d[0].magnificPopup,g=parseInt(arguments[1],10)||0;f.items?e=f.items[g]:(e=d,f.delegate&&(e=e.find(f.delegate)),e=e.eq(g)),b._openClick({mfpEl:e},d,f)}else b.isOpen&&b[c].apply(b,Array.prototype.slice.call(arguments,1));else c=a.extend(!0,{},c),u?d.data("magnificPopup",c):d[0].magnificPopup=c,b.addGroup(d,c);return d};var C,D,E,F="inline",G=function(){E&&(D.after(E.addClass(C)).detach(),E=null)};a.magnificPopup.registerModule(F,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){b.types.push(F),w(h+"."+F,function(){G()})},getInline:function(c,d){if(G(),c.src){var e=b.st.inline,f=a(c.src);if(f.length){var g=f[0].parentNode;g&&g.tagName&&(D||(C=e.hiddenClass,D=x(C),C="mfp-"+C),E=f.after(D).detach().removeClass(C)),b.updateStatus("ready")}else b.updateStatus("error",e.tNotFound),f=a("
    ");return c.inlineElement=f,f}return b.updateStatus("ready"),b._parseMarkup(d,{},c),d}}});var H,I="ajax",J=function(){H&&a(document.body).removeClass(H)},K=function(){J(),b.req&&b.req.abort()};a.magnificPopup.registerModule(I,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'The content could not be loaded.'},proto:{initAjax:function(){b.types.push(I),H=b.st.ajax.cursor,w(h+"."+I,K),w("BeforeChange."+I,K)},getAjax:function(c){H&&a(document.body).addClass(H),b.updateStatus("loading");var d=a.extend({url:c.src,success:function(d,e,f){var g={data:d,xhr:f};y("ParseAjax",g),b.appendContent(a(g.data),I),c.finished=!0,J(),b._setFocus(),setTimeout(function(){b.wrap.addClass(q)},16),b.updateStatus("ready"),y("AjaxContentAdded")},error:function(){J(),c.finished=c.loadError=!0,b.updateStatus("error",b.st.ajax.tError.replace("%url%",c.src))}},b.st.ajax.settings);return b.req=a.ajax(d),""}}});var L,M=function(c){if(c.data&&void 0!==c.data.title)return c.data.title;var d=b.st.image.titleSrc;if(d){if(a.isFunction(d))return d.call(b,c);if(c.el)return c.el.attr(d)||""}return""};a.magnificPopup.registerModule("image",{options:{markup:'
    ',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'The image could not be loaded.'},proto:{initImage:function(){var c=b.st.image,d=".image";b.types.push("image"),w(m+d,function(){"image"===b.currItem.type&&c.cursor&&a(document.body).addClass(c.cursor)}),w(h+d,function(){c.cursor&&a(document.body).removeClass(c.cursor),v.off("resize"+p)}),w("Resize"+d,b.resizeImage),b.isLowIE&&w("AfterChange",b.resizeImage)},resizeImage:function(){var a=b.currItem;if(a&&a.img&&b.st.image.verticalFit){var c=0;b.isLowIE&&(c=parseInt(a.img.css("padding-top"),10)+parseInt(a.img.css("padding-bottom"),10)),a.img.css("max-height",b.wH-c)}},_onImageHasSize:function(a){a.img&&(a.hasSize=!0,L&&clearInterval(L),a.isCheckingImgSize=!1,y("ImageHasSize",a),a.imgHidden&&(b.content&&b.content.removeClass("mfp-loading"),a.imgHidden=!1))},findImageSize:function(a){var c=0,d=a.img[0],e=function(f){L&&clearInterval(L),L=setInterval(function(){return d.naturalWidth>0?void b._onImageHasSize(a):(c>200&&clearInterval(L),c++,void(3===c?e(10):40===c?e(50):100===c&&e(500)))},f)};e(1)},getImage:function(c,d){var e=0,f=function(){c&&(c.img[0].complete?(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("ready")),c.hasSize=!0,c.loaded=!0,y("ImageLoadComplete")):(e++,200>e?setTimeout(f,100):g()))},g=function(){c&&(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("error",h.tError.replace("%url%",c.src))),c.hasSize=!0,c.loaded=!0,c.loadError=!0)},h=b.st.image,i=d.find(".mfp-img");if(i.length){var j=document.createElement("img");j.className="mfp-img",c.el&&c.el.find("img").length&&(j.alt=c.el.find("img").attr("alt")),c.img=a(j).on("load.mfploader",f).on("error.mfploader",g),j.src=c.src,i.is("img")&&(c.img=c.img.clone()),j=c.img[0],j.naturalWidth>0?c.hasSize=!0:j.width||(c.hasSize=!1)}return b._parseMarkup(d,{title:M(c),img_replaceWith:c.img},c),b.resizeImage(),c.hasSize?(L&&clearInterval(L),c.loadError?(d.addClass("mfp-loading"),b.updateStatus("error",h.tError.replace("%url%",c.src))):(d.removeClass("mfp-loading"),b.updateStatus("ready")),d):(b.updateStatus("loading"),c.loading=!0,c.hasSize||(c.imgHidden=!0,d.addClass("mfp-loading"),b.findImageSize(c)),d)}}});var N,O=function(){return void 0===N&&(N=void 0!==document.createElement("p").style.MozTransform),N};a.magnificPopup.registerModule("zoom",{options:{enabled:!1,easing:"ease-in-out",duration:300,opener:function(a){return a.is("img")?a:a.find("img")}},proto:{initZoom:function(){var a,c=b.st.zoom,d=".zoom";if(c.enabled&&b.supportsTransition){var e,f,g=c.duration,j=function(a){var b=a.clone().removeAttr("style").removeAttr("class").addClass("mfp-animated-image"),d="all "+c.duration/1e3+"s "+c.easing,e={position:"fixed",zIndex:9999,left:0,top:0,"-webkit-backface-visibility":"hidden"},f="transition";return e["-webkit-"+f]=e["-moz-"+f]=e["-o-"+f]=e[f]=d,b.css(e),b},k=function(){b.content.css("visibility","visible")};w("BuildControls"+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.content.css("visibility","hidden"),a=b._getItemToZoom(),!a)return void k();f=j(a),f.css(b._getOffset()),b.wrap.append(f),e=setTimeout(function(){f.css(b._getOffset(!0)),e=setTimeout(function(){k(),setTimeout(function(){f.remove(),a=f=null,y("ZoomAnimationEnded")},16)},g)},16)}}),w(i+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.st.removalDelay=g,!a){if(a=b._getItemToZoom(),!a)return;f=j(a)}f.css(b._getOffset(!0)),b.wrap.append(f),b.content.css("visibility","hidden"),setTimeout(function(){f.css(b._getOffset())},16)}}),w(h+d,function(){b._allowZoom()&&(k(),f&&f.remove(),a=null)})}},_allowZoom:function(){return"image"===b.currItem.type},_getItemToZoom:function(){return b.currItem.hasSize?b.currItem.img:!1},_getOffset:function(c){var d;d=c?b.currItem.img:b.st.zoom.opener(b.currItem.el||b.currItem);var e=d.offset(),f=parseInt(d.css("padding-top"),10),g=parseInt(d.css("padding-bottom"),10);e.top-=a(window).scrollTop()-f;var h={width:d.width(),height:(u?d.innerHeight():d[0].offsetHeight)-g-f};return O()?h["-moz-transform"]=h.transform="translate("+e.left+"px,"+e.top+"px)":(h.left=e.left,h.top=e.top),h}}});var P="iframe",Q="//about:blank",R=function(a){if(b.currTemplate[P]){var c=b.currTemplate[P].find("iframe");c.length&&(a||(c[0].src=Q),b.isIE8&&c.css("display",a?"block":"none"))}};a.magnificPopup.registerModule(P,{options:{markup:'
    ',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){b.types.push(P),w("BeforeChange",function(a,b,c){b!==c&&(b===P?R():c===P&&R(!0))}),w(h+"."+P,function(){R()})},getIframe:function(c,d){var e=c.src,f=b.st.iframe;a.each(f.patterns,function(){return e.indexOf(this.index)>-1?(this.id&&(e="string"==typeof this.id?e.substr(e.lastIndexOf(this.id)+this.id.length,e.length):this.id.call(this,e)),e=this.src.replace("%id%",e),!1):void 0});var g={};return f.srcAction&&(g[f.srcAction]=e),b._parseMarkup(d,g,c),b.updateStatus("ready"),d}}});var S=function(a){var c=b.items.length;return a>c-1?a-c:0>a?c+a:a},T=function(a,b,c){return a.replace(/%curr%/gi,b+1).replace(/%total%/gi,c)};a.magnificPopup.registerModule("gallery",{options:{enabled:!1,arrowMarkup:'',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var c=b.st.gallery,e=".mfp-gallery";return b.direction=!0,c&&c.enabled?(f+=" mfp-gallery",w(m+e,function(){c.navigateByImgClick&&b.wrap.on("click"+e,".mfp-img",function(){return b.items.length>1?(b.next(),!1):void 0}),d.on("keydown"+e,function(a){37===a.keyCode?b.prev():39===a.keyCode&&b.next()})}),w("UpdateStatus"+e,function(a,c){c.text&&(c.text=T(c.text,b.currItem.index,b.items.length))}),w(l+e,function(a,d,e,f){var g=b.items.length;e.counter=g>1?T(c.tCounter,f.index,g):""}),w("BuildControls"+e,function(){if(b.items.length>1&&c.arrows&&!b.arrowLeft){var d=c.arrowMarkup,e=b.arrowLeft=a(d.replace(/%title%/gi,c.tPrev).replace(/%dir%/gi,"left")).addClass(s),f=b.arrowRight=a(d.replace(/%title%/gi,c.tNext).replace(/%dir%/gi,"right")).addClass(s);e.click(function(){b.prev()}),f.click(function(){b.next()}),b.container.append(e.add(f))}}),w(n+e,function(){b._preloadTimeout&&clearTimeout(b._preloadTimeout),b._preloadTimeout=setTimeout(function(){b.preloadNearbyImages(),b._preloadTimeout=null},16)}),void w(h+e,function(){d.off(e),b.wrap.off("click"+e),b.arrowRight=b.arrowLeft=null})):!1},next:function(){b.direction=!0,b.index=S(b.index+1),b.updateItemHTML()},prev:function(){b.direction=!1,b.index=S(b.index-1),b.updateItemHTML()},goTo:function(a){b.direction=a>=b.index,b.index=a,b.updateItemHTML()},preloadNearbyImages:function(){var a,c=b.st.gallery.preload,d=Math.min(c[0],b.items.length),e=Math.min(c[1],b.items.length);for(a=1;a<=(b.direction?e:d);a++)b._preloadItem(b.index+a);for(a=1;a<=(b.direction?d:e);a++)b._preloadItem(b.index-a)},_preloadItem:function(c){if(c=S(c),!b.items[c].preloaded){var d=b.items[c];d.parsed||(d=b.parseEl(c)),y("LazyLoad",d),"image"===d.type&&(d.img=a('').on("load.mfploader",function(){d.hasSize=!0}).on("error.mfploader",function(){d.hasSize=!0,d.loadError=!0,y("LazyLoadError",d)}).attr("src",d.src)),d.preloaded=!0}}}});var U="retina";a.magnificPopup.registerModule(U,{options:{replaceSrc:function(a){return a.src.replace(/\.\w+$/,function(a){return"@2x"+a})},ratio:1},proto:{initRetina:function(){if(window.devicePixelRatio>1){var a=b.st.retina,c=a.ratio;c=isNaN(c)?c():c,c>1&&(w("ImageHasSize."+U,function(a,b){b.img.css({"max-width":b.img[0].naturalWidth/c,width:"100%"})}),w("ElementParse."+U,function(b,d){d.src=a.replaceSrc(d,c)}))}}}}),A()}); \ No newline at end of file diff --git a/static/assets/js/main.js b/static/assets/js/main.js deleted file mode 100644 index 041f1417..00000000 --- a/static/assets/js/main.js +++ /dev/null @@ -1,575 +0,0 @@ -(function($){ - "use strict"; - - // Mean Menu - $('.mean-menu').meanmenu({ - meanScreenWidth: "1199" - }); - - // Header Sticky - $(window).on('scroll',function() { - if ($(this).scrollTop() > 120){ - $('.navbar-area').addClass("is-sticky"); - } - else{ - $('.navbar-area').removeClass("is-sticky"); - } - }); - - // Others Option For Responsive JS - $(".others-option-for-responsive .dot-menu").on("click", function(){ - $(".others-option-for-responsive .container .container").toggleClass("active"); - }); - - // Search Menu JS - $(".others-option .search-icon i").on("click", function(){ - $(".search-overlay").toggleClass("search-overlay-active"); - }); - $(".search-overlay-close").on("click", function(){ - $(".search-overlay").removeClass("search-overlay-active"); - }); - - // Tooltip - var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) - var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl) - }); - - // Popup Image - $('.popup-image').magnificPopup({ - type: 'image', - gallery:{ - enabled:true - } - }); - - // Popup Video - $('.popup-video').magnificPopup({ - disableOn: 320, - type: 'iframe', - mainClass: 'mfp-fade', - removalDelay: 160, - preloader: false, - fixedContentPos: false - }); - - // Odometer JS - $('.odometer').appear(function(e) { - var odo = $(".odometer"); - odo.each(function() { - var countNumber = $(this).attr("data-count"); - $(this).html(countNumber); - }); - }); - - // Startup Works Slides - var carouselEl = $('.startup-works-slides'); - carouselEl.owlCarousel({ - items: 1, - loop: true, - margin: 30, - nav: false, - dots: false, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ] - }); - $(".custom-owl-prev").click(function() { - carouselEl.trigger('next.owl.carousel'); - }); - $(".custom-owl-next").click(function() { - carouselEl.trigger('prev.owl.carousel'); - }); - - // Furniture Banner Slides - $('.furniture-banner-slides').owlCarousel({ - items: 1, - nav: false, - loop: true, - margin: 30, - dots: true, - autoplay: true, - animateIn: 'fadeIn', - animateOut: 'fadeOut', - autoplayHoverPause: true, - navText: [ - "", - "" - ] - }); - - // Testimonials Slides - $('.testimonials-slides').owlCarousel({ - items: 1, - nav: true, - loop: true, - margin: 30, - dots: false, - autoplay: true, - animateIn: 'fadeIn', - animateOut: 'fadeOut', - autoplayHoverPause: true, - navText: [ - "", - "" - ] - }); - - // Startup Testimonials Slides - $('.startup-testimonials-slides').owlCarousel({ - items: 1, - nav: true, - loop: true, - margin: 30, - dots: false, - autoplay: true, - animateIn: 'fadeIn', - animateOut: 'fadeOut', - autoplayHoverPause: true, - navText: [ - "", - "" - ] - }); - - // Saas Testimonials Slides - $('.saas-testimonials-slides').owlCarousel({ - nav: true, - loop: true, - margin: 30, - dots: false, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 1 - }, - 576: { - items: 1 - }, - 768: { - items: 2 - }, - 992: { - items: 2 - } - } - }); - - // Partner Slides - $('.partner-slides').owlCarousel({ - nav: false, - loop: true, - margin: 30, - dots: false, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 2 - }, - 576: { - items: 3 - }, - 768: { - items: 4 - }, - 992: { - items: 5 - }, - 1200: { - items: 6 - } - } - }); - - // Screenshot Slides - $('.screenshot-slides').owlCarousel({ - nav: true, - loop: true, - dots: false, - center: true, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 1 - }, - 576: { - items: 2 - }, - 768: { - items: 3 - }, - 992: { - items: 5 - } - } - }); - - // MixItUp Shorting - $(function(){ - $('.shorting').mixItUp(); - }); - - // Marketing Testimonials Slides - $('.marketing-testimonials-slides').owlCarousel({ - items: 1, - nav: true, - loop: true, - margin: 30, - dots: false, - autoplay: true, - animateIn: 'fadeIn', - animateOut: 'fadeOut', - autoplayHoverPause: true, - navText: [ - "", - "" - ] - }); - - // Categories Slides - $('.categories-slides').owlCarousel({ - nav: true, - loop: true, - margin: 30, - dots: false, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 1 - }, - 576: { - items: 2 - }, - 768: { - items: 2 - }, - 992: { - items: 4 - } - } - }); - - // Instagram Slides - $('.instagram-slides').owlCarousel({ - nav: false, - loop: true, - dots: false, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 2 - }, - 576: { - items: 3 - }, - 768: { - items: 5 - }, - 992: { - items: 8 - } - } - }); - - // Work Slides - $('.works-slides-style-one').owlCarousel({ - nav: true, - loop: true, - margin: 30, - dots: false, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 1 - }, - 576: { - items: 2 - }, - 768: { - items: 2 - }, - 992: { - items: 2 - } - } - }); - $('.works-slides-style-two').owlCarousel({ - nav: true, - loop: true, - margin: 30, - dots: true, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 1 - }, - 576: { - items: 2 - }, - 768: { - items: 2 - }, - 992: { - items: 3 - } - } - }); - - // Portfolio Details Slides - $('.portfolio-details-slides').owlCarousel({ - nav: true, - loop: true, - margin: 30, - dots: false, - autoplay: true, - autoplayHoverPause: true, - navText: [ - "", - "" - ], - responsive: { - 0: { - items: 1 - }, - 576: { - items: 2 - }, - 768: { - items: 2 - }, - 992: { - items: 2 - } - } - }); - - // Parallax - $('.jarallax').jarallax({ - speed: 0.5, - imgHeight: 768, - imgWidth: 1366, - }); - - // Input Plus & Minus Number JS - $('.input-counter').each(function() { - var spinner = jQuery(this), - input = spinner.find('input[type="text"]'), - btnUp = spinner.find('.plus-btn'), - btnDown = spinner.find('.minus-btn'), - min = input.attr('min'), - max = input.attr('max'); - - btnUp.on('click', function() { - var oldValue = parseFloat(input.val()); - if (oldValue >= max) { - var newVal = oldValue; - } else { - var newVal = oldValue + 1; - } - spinner.find("input").val(newVal); - spinner.find("input").trigger("change"); - }); - btnDown.on('click', function() { - var oldValue = parseFloat(input.val()); - if (oldValue <= min) { - var newVal = oldValue; - } else { - var newVal = oldValue - 1; - } - spinner.find("input").val(newVal); - spinner.find("input").trigger("change"); - }); - }); - - // Price Range Slider - $(".js-range-of-price").ionRangeSlider({ - type: "double", - drag_interval: true, - min_interval: null, - max_interval: null - }); - - // Show More Item - $('.showmore-box').showMoreItems({ - startNum: 6, - afterNum: 3, - moreText: 'Load More', - noMoreText: 'No More' - }); - $('.showmore-box2').showMoreItems({ - startNum: 6, - afterNum: 4, - moreText: 'Load More', - noMoreText: 'No More' - }); - - // Count Time - function makeTimer() { - var endTime = new Date("September 20, 2021 17:00:00 PDT"); - var endTime = (Date.parse(endTime)) / 1000; - var now = new Date(); - var now = (Date.parse(now) / 1000); - var timeLeft = endTime - now; - var days = Math.floor(timeLeft / 86400); - var hours = Math.floor((timeLeft - (days * 86400)) / 3600); - var minutes = Math.floor((timeLeft - (days * 86400) - (hours * 3600 )) / 60); - var seconds = Math.floor((timeLeft - (days * 86400) - (hours * 3600) - (minutes * 60))); - if (hours < "10") { hours = "0" + hours; } - if (minutes < "10") { minutes = "0" + minutes; } - if (seconds < "10") { seconds = "0" + seconds; } - $("#days").html(days + "Days"); - $("#hours").html(hours + "Hours"); - $("#minutes").html(minutes + "Minutes"); - $("#seconds").html(seconds + "Seconds"); - } - setInterval(function() { makeTimer(); }, 0); - - // Type Write - var TxtType = function(el, toRotate, period) { - this.toRotate = toRotate; - this.el = el; - this.loopNum = 0; - this.period = parseInt(period, 10) || 2000; - this.txt = ''; - this.tick(); - this.isDeleting = false; - }; - TxtType.prototype.tick = function() { - var i = this.loopNum % this.toRotate.length; - var fullTxt = this.toRotate[i]; - if (this.isDeleting) { - this.txt = fullTxt.substring(0, this.txt.length - 1); - } else { - this.txt = fullTxt.substring(0, this.txt.length + 1); - } - this.el.innerHTML = ''+this.txt+''; - var that = this; - var delta = 200 - Math.random() * 100; - if (this.isDeleting) { delta /= 2; } - if (!this.isDeleting && this.txt === fullTxt) { - delta = this.period; - this.isDeleting = true; - } else if (this.isDeleting && this.txt === '') { - this.isDeleting = false; - this.loopNum++; - delta = 500; - } - setTimeout(function() { - that.tick(); - }, delta); - }; - window.onload = function() { - var elements = document.getElementsByClassName('typewrite'); - for (var i=0; i 600) $('.go-top').addClass('active'); - if (scrolled < 600) $('.go-top').removeClass('active'); - }); - // Click Event - $('.go-top').on('click', function() { - $("html, body").animate({ scrollTop: "0" }, 500); - }); - }); - -}(jQuery)); \ No newline at end of file diff --git a/static/assets/js/meanmenu.min.js b/static/assets/js/meanmenu.min.js deleted file mode 100644 index e3cd6705..00000000 --- a/static/assets/js/meanmenu.min.js +++ /dev/null @@ -1 +0,0 @@ -!function($){"use strict";$.fn.meanmenu=function(e){var n={meanMenuTarget:jQuery(this),meanMenuContainer:".noke-responsive-menu",meanMenuClose:"X",meanMenuCloseSize:"18px",meanMenuOpen:"",meanRevealPosition:"right",meanRevealPositionDistance:"0",meanRevealColour:"",meanScreenWidth:"480",meanNavPush:"",meanShowChildren:!0,meanExpandableChildren:!0,meanExpand:"+",meanContract:"-",meanRemoveAttrs:!1,onePage:!1,meanDisplay:"block",removeElements:""};e=$.extend(n,e);var a=window.innerWidth||document.documentElement.clientWidth;return this.each(function(){var n=e.meanMenuTarget,t=e.meanMenuContainer,r=e.meanMenuClose,i=e.meanMenuCloseSize,s=e.meanMenuOpen,u=e.meanRevealPosition,m=e.meanRevealPositionDistance,l=e.meanRevealColour,o=e.meanScreenWidth,c=e.meanNavPush,v=".meanmenu-reveal",h=e.meanShowChildren,d=e.meanExpandableChildren,y=e.meanExpand,j=e.meanContract,Q=e.meanRemoveAttrs,f=e.onePage,g=e.meanDisplay,p=e.removeElements,C=!1;(navigator.userAgent.match(/iPhone/i)||navigator.userAgent.match(/iPod/i)||navigator.userAgent.match(/iPad/i)||navigator.userAgent.match(/Android/i)||navigator.userAgent.match(/Blackberry/i)||navigator.userAgent.match(/Windows Phone/i))&&(C=!0),(navigator.userAgent.match(/MSIE 8/i)||navigator.userAgent.match(/MSIE 7/i))&&jQuery("html").css("overflow-y","scroll");var w="",x=function(){if("center"===u){var e=window.innerWidth||document.documentElement.clientWidth,n=e/2-22+"px";w="left:"+n+";right:auto;",C?jQuery(".meanmenu-reveal").animate({left:n}):jQuery(".meanmenu-reveal").css("left",n)}},A=!1,E=!1;"right"===u&&(w="right:"+m+";left:auto;"),"left"===u&&(w="left:"+m+";right:auto;"),x();var M="",P=function(){M.html(jQuery(M).is(".meanmenu-reveal.meanclose")?r:s)},W=function(){jQuery(".mean-bar,.mean-push").remove(),jQuery(t).removeClass("mean-container"),jQuery(n).css("display",g),A=!1,E=!1,jQuery(p).removeClass("mean-remove")},b=function(){var e="background:"+l+";color:"+l+";"+w;if(o>=a){jQuery(p).addClass("mean-remove"),E=!0,jQuery(t).addClass("mean-container"),jQuery(".mean-container").prepend('');var r=jQuery(n).html();jQuery(".mean-nav").html(r),Q&&jQuery("nav.mean-nav ul, nav.mean-nav ul *").each(function(){jQuery(this).is(".mean-remove")?jQuery(this).attr("class","mean-remove"):jQuery(this).removeAttr("class"),jQuery(this).removeAttr("id")}),jQuery(n).before('
    '),jQuery(".mean-push").css("margin-top",c),jQuery(n).hide(),jQuery(".meanmenu-reveal").show(),jQuery(v).html(s),M=jQuery(v),jQuery(".mean-nav ul").hide(),h?d?(jQuery(".mean-nav ul ul").each(function(){jQuery(this).children().length&&jQuery(this,"li:first").parent().append(''+y+"")}),jQuery(".mean-expand").on("click",function(e){e.preventDefault(),jQuery(this).hasClass("mean-clicked")?(jQuery(this).text(y),jQuery(this).prev("ul").slideUp(300,function(){})):(jQuery(this).text(j),jQuery(this).prev("ul").slideDown(300,function(){})),jQuery(this).toggleClass("mean-clicked")})):jQuery(".mean-nav ul ul").show():jQuery(".mean-nav ul ul").hide(),jQuery(".mean-nav ul li").last().addClass("mean-last"),M.removeClass("meanclose"),jQuery(M).click(function(e){e.preventDefault(),A===!1?(M.css("text-align","center"),M.css("text-indent","0"),M.css("font-size",i),jQuery(".mean-nav ul:first").slideDown(),A=!0):(jQuery(".mean-nav ul:first").slideUp(),A=!1),M.toggleClass("meanclose"),P(),jQuery(p).addClass("mean-remove")}),f&&jQuery(".mean-nav ul > li > a:first-child").on("click",function(){jQuery(".mean-nav ul:first").slideUp(),A=!1,jQuery(M).toggleClass("meanclose").html(s)})}else W()};C||jQuery(window).resize(function(){a=window.innerWidth||document.documentElement.clientWidth,a>o,W(),o>=a?(b(),x()):W()}),jQuery(window).resize(function(){a=window.innerWidth||document.documentElement.clientWidth,C?(x(),o>=a?E===!1&&b():W()):(W(),o>=a&&(b(),x()))}),b()})}}(jQuery); \ No newline at end of file diff --git a/static/assets/js/mixitup.min.js b/static/assets/js/mixitup.min.js deleted file mode 100644 index 93b5ba3e..00000000 --- a/static/assets/js/mixitup.min.js +++ /dev/null @@ -1,14 +0,0 @@ -/**! - * MixItUp v2.1.11 - * - * @copyright Copyright 2015 KunkaLabs Limited. - * @author KunkaLabs Limited. - * @link https://mixitup.kunkalabs.com - * - * @license Commercial use requires a commercial license. - * https://mixitup.kunkalabs.com/licenses/ - * - * Non-commercial use permitted under terms of CC-BY-NC license. - * http://creativecommons.org/licenses/by-nc/3.0/ - */ -!function(a,b){"use strict";a.MixItUp=function(){var b=this;b._execAction("_constructor",0),a.extend(b,{selectors:{target:".mix",filter:".filter",sort:".sort"},animation:{enable:!0,effects:"fade scale",duration:600,easing:"ease",perspectiveDistance:"3000",perspectiveOrigin:"50% 50%",queue:!0,queueLimit:1,animateChangeLayout:!1,animateResizeContainer:!0,animateResizeTargets:!1,staggerSequence:!1,reverseOut:!1},callbacks:{onMixLoad:!1,onMixStart:!1,onMixBusy:!1,onMixEnd:!1,onMixFail:!1,_user:!1},controls:{enable:!0,live:!1,toggleFilterButtons:!1,toggleLogic:"or",activeClass:"active"},layout:{display:"inline-block",containerClass:"",containerClassFail:"fail"},load:{filter:"all",sort:!1},_$body:null,_$container:null,_$targets:null,_$parent:null,_$sortButtons:null,_$filterButtons:null,_suckMode:!1,_mixing:!1,_sorting:!1,_clicking:!1,_loading:!0,_changingLayout:!1,_changingClass:!1,_changingDisplay:!1,_origOrder:[],_startOrder:[],_newOrder:[],_activeFilter:null,_toggleArray:[],_toggleString:"",_activeSort:"default:asc",_newSort:null,_startHeight:null,_newHeight:null,_incPadding:!0,_newDisplay:null,_newClass:null,_targetsBound:0,_targetsDone:0,_queue:[],_$show:a(),_$hide:a()}),b._execAction("_constructor",1)},a.MixItUp.prototype={constructor:a.MixItUp,_instances:{},_handled:{_filter:{},_sort:{}},_bound:{_filter:{},_sort:{}},_actions:{},_filters:{},extend:function(b){for(var c in b)a.MixItUp.prototype[c]=b[c]},addAction:function(b,c,d,e){a.MixItUp.prototype._addHook("_actions",b,c,d,e)},addFilter:function(b,c,d,e){a.MixItUp.prototype._addHook("_filters",b,c,d,e)},_addHook:function(b,c,d,e,f){var g=a.MixItUp.prototype[b],h={};f=1===f||"post"===f?"post":"pre",h[c]={},h[c][f]={},h[c][f][d]=e,a.extend(!0,g,h)},_init:function(b,c){var d=this;if(d._execAction("_init",0,arguments),c&&a.extend(!0,d,c),d._$body=a("body"),d._domNode=b,d._$container=a(b),d._$container.addClass(d.layout.containerClass),d._id=b.id,d._platformDetect(),d._brake=d._getPrefixedCSS("transition","none"),d._refresh(!0),d._$parent=d._$targets.parent().length?d._$targets.parent():d._$container,d.load.sort&&(d._newSort=d._parseSort(d.load.sort),d._newSortString=d.load.sort,d._activeSort=d.load.sort,d._sort(),d._printSort()),d._activeFilter="all"===d.load.filter?d.selectors.target:"none"===d.load.filter?"":d.load.filter,d.controls.enable&&d._bindHandlers(),d.controls.toggleFilterButtons){d._buildToggleArray();for(var e=0;e-1){var k=d._helpers._camelCase(i.substring(5,i.length));f.dataset[k]=j}}}f.mixParent===b&&(f.mixParent=d._id)}if(d._$targets.length&&a||!d._origOrder.length&&d._$targets.length){d._origOrder=[];for(var e=0;e-1)&&(a(e.selectors.sort).removeClass(e.controls.activeClass),f(c,d),e.sort(g))}if("filter"===d){var h,i=c.attr("data-filter"),j="or"===e.controls.toggleLogic?",":"";e.controls.toggleFilterButtons?(e._buildToggleArray(),c.hasClass(e.controls.activeClass)?(f(c,d,!0),h=e._toggleArray.indexOf(i),e._toggleArray.splice(h,1)):(f(c,d),e._toggleArray.push(i)),e._toggleArray=a.grep(e._toggleArray,function(a){return a}),e._toggleString=e._toggleArray.join(j),e.filter(e._toggleString)):c.hasClass(e.controls.activeClass)||(a(e.selectors.filter).removeClass(e.controls.activeClass),f(c,d),e.filter(i))}e._execAction("_processClick",1,arguments)}else"function"==typeof e.callbacks.onMixBusy&&e.callbacks.onMixBusy.call(e._domNode,e._state,e),e._execAction("_processClickBusy",1,arguments)},_buildToggleArray:function(){var a=this,b=a._activeFilter.replace(/\s/g,"");if(a._execAction("_buildToggleArray",0,arguments),"or"===a.controls.toggleLogic)a._toggleArray=b.split(",");else{a._toggleArray=b.split("."),!a._toggleArray[0]&&a._toggleArray.shift();for(var c,d=0;c=a._toggleArray[d];d++)a._toggleArray[d]="."+c}a._execAction("_buildToggleArray",1,arguments)},_updateControls:function(c,d){var e=this,f={filter:c.filter,sort:c.sort},g=function(a,b){try{d&&"filter"===h&&"none"!==f.filter&&""!==f.filter?a.filter(b).addClass(e.controls.activeClass):a.removeClass(e.controls.activeClass).filter(b).addClass(e.controls.activeClass)}catch(c){}},h="filter",i=null;e._execAction("_updateControls",0,arguments),c.filter===b&&(f.filter=e._activeFilter),c.sort===b&&(f.sort=e._activeSort),f.filter===e.selectors.target&&(f.filter="all");for(var j=0;2>j;j++)i=e.controls.live?a(e.selectors[h]):e["_$"+h+"Buttons"],i&&g(i,"[data-"+h+'="'+f[h]+'"]'),h="sort";e._execAction("_updateControls",1,arguments)},_filter:function(){var b=this;b._execAction("_filter",0);for(var c=0;cg?"asc"===e?-1:1:g>h?"asc"===e?1:-1:g===h&&d._newSort.length>c+1?d._compare(a,b,c+1):0},_printSort:function(a){var b=this,c=a?b._startOrder:b._newOrder,d=b._$parent[0].querySelectorAll(b.selectors.target),e=d.length?d[d.length-1].nextElementSibling:null,f=document.createDocumentFragment();b._execAction("_printSort",0,arguments);for(var g=0;g-1){if(c){var e=a.animation.effects.indexOf(b+"(");if(e>-1){var f=a.animation.effects.substring(e),g=/\(([^)]+)\)/.exec(f),h=g[1];return{val:h}}}return!0}return!1},d=function(a,b){return b?"-"===a.charAt(0)?a.substr(1,a.length):"-"+a:a},e=function(a,e){for(var f=[["scale",".01"],["translateX","20px"],["translateY","20px"],["translateZ","20px"],["rotateX","90deg"],["rotateY","90deg"],["rotateZ","180deg"]],g=0;gi;i++){var j=0===i?j=b._prefix:"";b._ff&&b._ff<=20&&(h[j+"transition-property"]="all",h[j+"transition-timing-function"]=b.animation.easing+"ms",h[j+"transition-duration"]=b.animation.duration+"ms"),h[j+"transition-delay"]=g+"ms",h[j+"transform"]="translate("+f.x+"px,"+f.y+"px)"}(b.effects.transform||b.effects.opacity)&&b._bindTargetDone(e),b._ff&&b._ff<=20?e.css(h):e.css(b.effects.transition).css(h)}for(var c=0;ci;i++){var j=0===i?j=b._prefix:"";k[j+"transition-delay"]=g+"ms",k[j+"transform"]=b.effects.transformOut,k.opacity=b.effects.opacity}e.css(b.effects.transition).css(k),(b.effects.transform||b.effects.opacity)&&b._bindTargetDone(e)}b._execAction("_animateTargets",1)},_bindTargetDone:function(b){var c=this,d=b[0];c._execAction("_bindTargetDone",0,arguments),d.dataset.bound||(d.dataset.bound=!0,c._targetsBound++,b.on("webkitTransitionEnd.mixItUp transitionend.mixItUp",function(e){(e.originalEvent.propertyName.indexOf("transform")>-1||e.originalEvent.propertyName.indexOf("opacity")>-1)&&a(e.originalEvent.target).is(c.selectors.target)&&(b.off(".mixItUp"),d.dataset.bound="",c._targetDone())})),c._execAction("_bindTargetDone",1,arguments)},_targetDone:function(){var a=this;a._execAction("_targetDone",0),a._targetsDone++,a._targetsDone===a._targetsBound&&a._cleanUp(),a._execAction("_targetDone",1)},_cleanUp:function(){var b=this,c=b.animation.animateResizeTargets?"transform opacity width height margin-bottom margin-right":"transform opacity",d=function(){b._$targets.removeStyle("transition",b._prefix)};b._execAction("_cleanUp",0),b._changingLayout?b._$show.css("display",b._newDisplay):b._$show.css("display",b.layout.display),b._$targets.css(b._brake),b._$targets.removeStyle(c,b._prefix).removeAttr("data-inter-pos-x data-inter-pos-y data-final-pos-x data-final-pos-y data-orig-pos-x data-orig-pos-y data-orig-height data-orig-width data-final-height data-final-width data-inter-width data-inter-height data-orig-margin-right data-orig-margin-bottom data-inter-margin-right data-inter-margin-bottom data-final-margin-right data-final-margin-bottom"),b._$hide.removeStyle("display"),b._$parent.removeStyle("height transition perspective-distance perspective perspective-origin-x perspective-origin-y perspective-origin perspectiveOrigin",b._prefix),b._sorting&&(b._printSort(),b._activeSort=b._newSortString,b._sorting=!1),b._changingLayout&&(b._changingDisplay&&(b.layout.display=b._newDisplay,b._changingDisplay=!1),b._changingClass&&(b._$parent.removeClass(b.layout.containerClass).addClass(b._newClass),b.layout.containerClass=b._newClass,b._changingClass=!1),b._changingLayout=!1),b._refresh(),b._buildState(),b._state.fail&&b._$container.addClass(b.layout.containerClassFail),b._$show=a(),b._$hide=a(),window.requestAnimationFrame&&requestAnimationFrame(d),b._mixing=!1,"function"==typeof b.callbacks._user&&b.callbacks._user.call(b._domNode,b._state,b),"function"==typeof b.callbacks.onMixEnd&&b.callbacks.onMixEnd.call(b._domNode,b._state,b),b._$container.trigger("mixEnd",[b._state,b]),b._state.fail&&("function"==typeof b.callbacks.onMixFail&&b.callbacks.onMixFail.call(b._domNode,b._state,b),b._$container.trigger("mixFail",[b._state,b])),b._loading&&("function"==typeof b.callbacks.onMixLoad&&b.callbacks.onMixLoad.call(b._domNode,b._state,b),b._$container.trigger("mixLoad",[b._state,b])),b._queue.length&&(b._execAction("_queue",0),b.multiMix(b._queue[0][0],b._queue[0][1],b._queue[0][2]),b._queue.splice(0,1)),b._execAction("_cleanUp",1),b._loading=!1},_getPrefixedCSS:function(a,b,c){var d=this,e={},f="",g=-1;for(g=0;2>g;g++)f=0===g?d._prefix:"",c?e[f+a]=f+b:e[f+a]=b;return d._execFilter("_getPrefixedCSS",e,arguments)},_getDelay:function(a){var b=this,c="function"==typeof b.animation.staggerSequence?b.animation.staggerSequence.call(b._domNode,a,b._state):a,d=b.animation.stagger?c*b.animation.staggerDuration:0;return b._execFilter("_getDelay",d,arguments)},_parseMultiMixArgs:function(a){for(var b=this,c={command:null,animate:b.animation.enable,callback:null},d=0;dg;)h=d[g],g++;return a!==b?a[e]!==b?a[e]:a:void 0};return a?c._execFilter("getOption",d(c,a),arguments):c},setOptions:function(b){var c=this;c._execAction("setOptions",0,arguments),"object"==typeof b&&a.extend(!0,c,b),c._execAction("setOptions",1,arguments)},getState:function(){var a=this;return a._execFilter("getState",a._state,a)},forceRefresh:function(){var a=this;a._refresh(!1,!0)},destroy:function(b){var c=this,d=a.MixItUp.prototype._bound._filter,e=a.MixItUp.prototype._bound._sort;c._execAction("destroy",0,arguments),c._$body.add(a(c.selectors.sort)).add(a(c.selectors.filter)).off(".mixItUp");for(var f=0;f1?d[c.selectors.filter]--:1===d[c.selectors.filter]&&delete d[c.selectors.filter],e[c.selectors.sort]&&e[c.selectors.sort]>1?e[c.selectors.sort]--:1===e[c.selectors.sort]&&delete e[c.selectors.sort],delete a.MixItUp.prototype._instances[c._id]}},a.fn.mixItUp=function(){var c,d=arguments,e=[],f=function(b,c){var d=new a.MixItUp,e=function(){return("00000"+(16777216*Math.random()<<0).toString(16)).substr(-6).toUpperCase()};d._execAction("_instantiate",0,arguments),b.id=b.id?b.id:"MixItUp"+e(),d._instances[b.id]||(d._instances[b.id]=d,d._init(b,c)),d._execAction("_instantiate",1,arguments)};return c=this.each(function(){if(d&&"string"==typeof d[0]){var c=a.MixItUp.prototype._instances[this.id];if("isLoaded"===d[0])e.push(c?!0:!1);else{var g=c[d[0]](d[1],d[2],d[3]);g!==b&&e.push(g)}}else f(this,d[0])}),e.length?e.length>1?e:e[0]:c},a.fn.removeStyle=function(c,d){return d=d?d:"",this.each(function(){for(var e=this,f=c.split(" "),g=0;gh;h++){switch(h){case 0:var i=f[g];break;case 1:var i=a.MixItUp.prototype._helpers._camelCase(i);break;case 2:var i=d+f[g];break;case 3:var i=a.MixItUp.prototype._helpers._camelCase(d+f[g])}if(e.style[i]!==b&&"unknown"!=typeof e.style[i]&&e.style[i].length>0&&(e.style[i]=""),!d&&1===h)break}e.attributes&&e.attributes.style&&e.attributes.style!==b&&""===e.attributes.style.value&&e.attributes.removeNamedItem("style")})}}(jQuery); \ No newline at end of file diff --git a/static/assets/js/odometer.min.js b/static/assets/js/odometer.min.js deleted file mode 100644 index ed039bcb..00000000 --- a/static/assets/js/odometer.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! odometer 0.4.8 */ -(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G=[].slice;q='',n=''+q+"",d='8'+n+"",g='',c="(,ddd).dd",h=/^\(?([^)]*)\)?(?:(.)(d+))?$/,i=30,f=2e3,a=20,j=2,e=.5,k=1e3/i,b=1e3/a,o="transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd",y=document.createElement("div").style,p=null!=y.transition||null!=y.webkitTransition||null!=y.mozTransition||null!=y.oTransition,w=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,l=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver,s=function(a){var b;return b=document.createElement("div"),b.innerHTML=a,b.children[0]},v=function(a,b){return a.className=a.className.replace(new RegExp("(^| )"+b.split(" ").join("|")+"( |$)","gi")," ")},r=function(a,b){return v(a,b),a.className+=" "+b},z=function(a,b){var c;return null!=document.createEvent?(c=document.createEvent("HTMLEvents"),c.initEvent(b,!0,!0),a.dispatchEvent(c)):void 0},u=function(){var a,b;return null!=(a=null!=(b=window.performance)&&"function"==typeof b.now?b.now():void 0)?a:+new Date},x=function(a,b){return null==b&&(b=0),b?(a*=Math.pow(10,b),a+=.5,a=Math.floor(a),a/=Math.pow(10,b)):Math.round(a)},A=function(a){return 0>a?Math.ceil(a):Math.floor(a)},t=function(a){return a-x(a)},C=!1,(B=function(){var a,b,c,d,e;if(!C&&null!=window.jQuery){for(C=!0,d=["html","text"],e=[],b=0,c=d.length;c>b;b++)a=d[b],e.push(function(a){var b;return b=window.jQuery.fn[a],window.jQuery.fn[a]=function(a){var c;return null==a||null==(null!=(c=this[0])?c.odometer:void 0)?b.apply(this,arguments):this[0].odometer.update(a)}}(a));return e}})(),setTimeout(B,0),m=function(){function a(b){var c,d,e,g,h,i,l,m,n,o,p=this;if(this.options=b,this.el=this.options.el,null!=this.el.odometer)return this.el.odometer;this.el.odometer=this,m=a.options;for(d in m)g=m[d],null==this.options[d]&&(this.options[d]=g);null==(h=this.options).duration&&(h.duration=f),this.MAX_VALUES=this.options.duration/k/j|0,this.resetFormat(),this.value=this.cleanValue(null!=(n=this.options.value)?n:""),this.renderInside(),this.render();try{for(o=["innerHTML","innerText","textContent"],i=0,l=o.length;l>i;i++)e=o[i],null!=this.el[e]&&!function(a){return Object.defineProperty(p.el,a,{get:function(){var b;return"innerHTML"===a?p.inside.outerHTML:null!=(b=p.inside.innerText)?b:p.inside.textContent},set:function(a){return p.update(a)}})}(e)}catch(q){c=q,this.watchForMutations()}}return a.prototype.renderInside=function(){return this.inside=document.createElement("div"),this.inside.className="odometer-inside",this.el.innerHTML="",this.el.appendChild(this.inside)},a.prototype.watchForMutations=function(){var a,b=this;if(null!=l)try{return null==this.observer&&(this.observer=new l(function(a){var c;return c=b.el.innerText,b.renderInside(),b.render(b.value),b.update(c)})),this.watchMutations=!0,this.startWatchingMutations()}catch(c){a=c}},a.prototype.startWatchingMutations=function(){return this.watchMutations?this.observer.observe(this.el,{childList:!0}):void 0},a.prototype.stopWatchingMutations=function(){var a;return null!=(a=this.observer)?a.disconnect():void 0},a.prototype.cleanValue=function(a){var b;return"string"==typeof a&&(a=a.replace(null!=(b=this.format.radix)?b:".",""),a=a.replace(/[.,]/g,""),a=a.replace("","."),a=parseFloat(a,10)||0),x(a,this.format.precision)},a.prototype.bindTransitionEnd=function(){var a,b,c,d,e,f,g=this;if(!this.transitionEndBound){for(this.transitionEndBound=!0,b=!1,e=o.split(" "),f=[],c=0,d=e.length;d>c;c++)a=e[c],f.push(this.el.addEventListener(a,function(){return b?!0:(b=!0,setTimeout(function(){return g.render(),b=!1,z(g.el,"odometerdone")},0),!0)},!1));return f}},a.prototype.resetFormat=function(){var a,b,d,e,f,g,i,j;if(a=null!=(i=this.options.format)?i:c,a||(a="d"),d=h.exec(a),!d)throw new Error("Odometer: Unparsable digit format");return j=d.slice(1,4),g=j[0],f=j[1],b=j[2],e=(null!=b?b.length:void 0)||0,this.format={repeating:g,radix:f,precision:e}},a.prototype.render=function(a){var b,c,d,e,f,g,h;for(null==a&&(a=this.value),this.stopWatchingMutations(),this.resetFormat(),this.inside.innerHTML="",f=this.options.theme,b=this.el.className.split(" "),e=[],g=0,h=b.length;h>g;g++)c=b[g],c.length&&((d=/^odometer-theme-(.+)$/.exec(c))?f=d[1]:/^odometer(-|$)/.test(c)||e.push(c));return e.push("odometer"),p||e.push("odometer-no-transitions"),f?e.push("odometer-theme-"+f):e.push("odometer-auto-theme"),this.el.className=e.join(" "),this.ribbons={},this.formatDigits(a),this.startWatchingMutations()},a.prototype.formatDigits=function(a){var b,c,d,e,f,g,h,i,j,k;if(this.digits=[],this.options.formatFunction)for(d=this.options.formatFunction(a),j=d.split("").reverse(),f=0,h=j.length;h>f;f++)c=j[f],c.match(/0-9/)?(b=this.renderDigit(),b.querySelector(".odometer-value").innerHTML=c,this.digits.push(b),this.insertDigit(b)):this.addSpacer(c);else for(e=!this.format.precision||!t(a)||!1,k=a.toString().split("").reverse(),g=0,i=k.length;i>g;g++)b=k[g],"."===b&&(e=!0),this.addDigit(b,e)},a.prototype.update=function(a){var b,c=this;return a=this.cleanValue(a),(b=a-this.value)?(v(this.el,"odometer-animating-up odometer-animating-down odometer-animating"),b>0?r(this.el,"odometer-animating-up"):r(this.el,"odometer-animating-down"),this.stopWatchingMutations(),this.animate(a),this.startWatchingMutations(),setTimeout(function(){return c.el.offsetHeight,r(c.el,"odometer-animating")},0),this.value=a):void 0},a.prototype.renderDigit=function(){return s(d)},a.prototype.insertDigit=function(a,b){return null!=b?this.inside.insertBefore(a,b):this.inside.children.length?this.inside.insertBefore(a,this.inside.children[0]):this.inside.appendChild(a)},a.prototype.addSpacer=function(a,b,c){var d;return d=s(g),d.innerHTML=a,c&&r(d,c),this.insertDigit(d,b)},a.prototype.addDigit=function(a,b){var c,d,e,f;if(null==b&&(b=!0),"-"===a)return this.addSpacer(a,null,"odometer-negation-mark");if("."===a)return this.addSpacer(null!=(f=this.format.radix)?f:".",null,"odometer-radix-mark");if(b)for(e=!1;;){if(!this.format.repeating.length){if(e)throw new Error("Bad odometer format without digits");this.resetFormat(),e=!0}if(c=this.format.repeating[this.format.repeating.length-1],this.format.repeating=this.format.repeating.substring(0,this.format.repeating.length-1),"d"===c)break;this.addSpacer(c)}return d=this.renderDigit(),d.querySelector(".odometer-value").innerHTML=a,this.digits.push(d),this.insertDigit(d)},a.prototype.animate=function(a){return p&&"count"!==this.options.animation?this.animateSlide(a):this.animateCount(a)},a.prototype.animateCount=function(a){var c,d,e,f,g,h=this;if(d=+a-this.value)return f=e=u(),c=this.value,(g=function(){var i,j,k;return u()-f>h.options.duration?(h.value=a,h.render(),void z(h.el,"odometerdone")):(i=u()-e,i>b&&(e=u(),k=i/h.options.duration,j=d*k,c+=j,h.render(Math.round(c))),null!=w?w(g):setTimeout(g,b))})()},a.prototype.getDigitCount=function(){var a,b,c,d,e,f;for(d=1<=arguments.length?G.call(arguments,0):[],a=e=0,f=d.length;f>e;a=++e)c=d[a],d[a]=Math.abs(c);return b=Math.max.apply(Math,d),Math.ceil(Math.log(b+1)/Math.log(10))},a.prototype.getFractionalDigitCount=function(){var a,b,c,d,e,f,g;for(e=1<=arguments.length?G.call(arguments,0):[],b=/^\-?\d*\.(\d*?)0*$/,a=f=0,g=e.length;g>f;a=++f)d=e[a],e[a]=d.toString(),c=b.exec(e[a]),null==c?e[a]=0:e[a]=c[1].length;return Math.max.apply(Math,e)},a.prototype.resetDigits=function(){return this.digits=[],this.ribbons=[],this.inside.innerHTML="",this.resetFormat()},a.prototype.animateSlide=function(a){var b,c,d,f,g,h,i,j,k,l,m,n,o,p,q,s,t,u,v,w,x,y,z,B,C,D,E;if(s=this.value,j=this.getFractionalDigitCount(s,a),j&&(a*=Math.pow(10,j),s*=Math.pow(10,j)),d=a-s){for(this.bindTransitionEnd(),f=this.getDigitCount(s,a),g=[],b=0,m=v=0;f>=0?f>v:v>f;m=f>=0?++v:--v){if(t=A(s/Math.pow(10,f-m-1)),i=A(a/Math.pow(10,f-m-1)),h=i-t,Math.abs(h)>this.MAX_VALUES){for(l=[],n=h/(this.MAX_VALUES+this.MAX_VALUES*b*e),c=t;h>0&&i>c||0>h&&c>i;)l.push(Math.round(c)),c+=n;l[l.length-1]!==i&&l.push(i),b++}else l=function(){E=[];for(var a=t;i>=t?i>=a:a>=i;i>=t?a++:a--)E.push(a);return E}.apply(this);for(m=w=0,y=l.length;y>w;m=++w)k=l[m],l[m]=Math.abs(k%10);g.push(l)}for(this.resetDigits(),D=g.reverse(),m=x=0,z=D.length;z>x;m=++x)for(l=D[m],this.digits[m]||this.addDigit(" ",m>=j),null==(u=this.ribbons)[m]&&(u[m]=this.digits[m].querySelector(".odometer-ribbon-inner")),this.ribbons[m].innerHTML="",0>d&&(l=l.reverse()),o=C=0,B=l.length;B>C;o=++C)k=l[o],q=document.createElement("div"),q.className="odometer-value",q.innerHTML=k,this.ribbons[m].appendChild(q),o===l.length-1&&r(q,"odometer-last-value"),0===o&&r(q,"odometer-first-value");return 0>t&&this.addDigit("-"),p=this.inside.querySelector(".odometer-radix-mark"),null!=p&&p.parent.removeChild(p),j?this.addSpacer(this.format.radix,this.digits[j-1],"odometer-radix-mark"):void 0}},a}(),m.options=null!=(E=window.odometerOptions)?E:{},setTimeout(function(){var a,b,c,d,e;if(window.odometerOptions){d=window.odometerOptions,e=[];for(a in d)b=d[a],e.push(null!=(c=m.options)[a]?(c=m.options)[a]:c[a]=b);return e}},0),m.init=function(){var a,b,c,d,e,f;if(null!=document.querySelectorAll){for(b=document.querySelectorAll(m.options.selector||".odometer"),f=[],c=0,d=b.length;d>c;c++)a=b[c],f.push(a.odometer=new m({el:a,value:null!=(e=a.innerText)?e:a.textContent}));return f}},null!=(null!=(F=document.documentElement)?F.doScroll:void 0)&&null!=document.createEventObject?(D=document.onreadystatechange,document.onreadystatechange=function(){return"complete"===document.readyState&&m.options.auto!==!1&&m.init(),null!=D?D.apply(this,arguments):void 0}):document.addEventListener("DOMContentLoaded",function(){return m.options.auto!==!1?m.init():void 0},!1),"function"==typeof define&&define.amd?define([],function(){return m}):"undefined"!=typeof exports&&null!==exports?module.exports=m:window.Odometer=m}).call(this); \ No newline at end of file diff --git a/static/assets/js/owl.carousel.min.js b/static/assets/js/owl.carousel.min.js deleted file mode 100644 index 6a8bfa92..00000000 --- a/static/assets/js/owl.carousel.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Owl Carousel v2.3.4 - * Copyright 2013-2018 David Deutsch - * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE - */ -!function(a,b,c,d){function e(b,c){this.settings=null,this.options=a.extend({},e.Defaults,c),this.$element=a(b),this._handlers={},this._plugins={},this._supress={},this._current=null,this._speed=null,this._coordinates=[],this._breakpoint=null,this._width=null,this._items=[],this._clones=[],this._mergers=[],this._widths=[],this._invalidated={},this._pipe=[],this._drag={time:null,target:null,pointer:null,stage:{start:null,current:null},direction:null},this._states={current:{},tags:{initializing:["busy"],animating:["busy"],dragging:["interacting"]}},a.each(["onResize","onThrottledResize"],a.proxy(function(b,c){this._handlers[c]=a.proxy(this[c],this)},this)),a.each(e.Plugins,a.proxy(function(a,b){this._plugins[a.charAt(0).toLowerCase()+a.slice(1)]=new b(this)},this)),a.each(e.Workers,a.proxy(function(b,c){this._pipe.push({filter:c.filter,run:a.proxy(c.run,this)})},this)),this.setup(),this.initialize()}e.Defaults={items:3,loop:!1,center:!1,rewind:!1,checkVisibility:!0,mouseDrag:!0,touchDrag:!0,pullDrag:!0,freeDrag:!1,margin:0,stagePadding:0,merge:!1,mergeFit:!0,autoWidth:!1,startPosition:0,rtl:!1,smartSpeed:250,fluidSpeed:!1,dragEndSpeed:!1,responsive:{},responsiveRefreshRate:200,responsiveBaseElement:b,fallbackEasing:"swing",slideTransition:"",info:!1,nestedItemSelector:!1,itemElement:"div",stageElement:"div",refreshClass:"owl-refresh",loadedClass:"owl-loaded",loadingClass:"owl-loading",rtlClass:"owl-rtl",responsiveClass:"owl-responsive",dragClass:"owl-drag",itemClass:"owl-item",stageClass:"owl-stage",stageOuterClass:"owl-stage-outer",grabClass:"owl-grab"},e.Width={Default:"default",Inner:"inner",Outer:"outer"},e.Type={Event:"event",State:"state"},e.Plugins={},e.Workers=[{filter:["width","settings"],run:function(){this._width=this.$element.width()}},{filter:["width","items","settings"],run:function(a){a.current=this._items&&this._items[this.relative(this._current)]}},{filter:["items","settings"],run:function(){this.$stage.children(".cloned").remove()}},{filter:["width","items","settings"],run:function(a){var b=this.settings.margin||"",c=!this.settings.autoWidth,d=this.settings.rtl,e={width:"auto","margin-left":d?b:"","margin-right":d?"":b};!c&&this.$stage.children().css(e),a.css=e}},{filter:["width","items","settings"],run:function(a){var b=(this.width()/this.settings.items).toFixed(3)-this.settings.margin,c=null,d=this._items.length,e=!this.settings.autoWidth,f=[];for(a.items={merge:!1,width:b};d--;)c=this._mergers[d],c=this.settings.mergeFit&&Math.min(c,this.settings.items)||c,a.items.merge=c>1||a.items.merge,f[d]=e?b*c:this._items[d].width();this._widths=f}},{filter:["items","settings"],run:function(){var b=[],c=this._items,d=this.settings,e=Math.max(2*d.items,4),f=2*Math.ceil(c.length/2),g=d.loop&&c.length?d.rewind?e:Math.max(e,f):0,h="",i="";for(g/=2;g>0;)b.push(this.normalize(b.length/2,!0)),h+=c[b[b.length-1]][0].outerHTML,b.push(this.normalize(c.length-1-(b.length-1)/2,!0)),i=c[b[b.length-1]][0].outerHTML+i,g-=1;this._clones=b,a(h).addClass("cloned").appendTo(this.$stage),a(i).addClass("cloned").prependTo(this.$stage)}},{filter:["width","items","settings"],run:function(){for(var a=this.settings.rtl?1:-1,b=this._clones.length+this._items.length,c=-1,d=0,e=0,f=[];++c",h)||this.op(b,"<",g)&&this.op(b,">",h))&&i.push(c);this.$stage.children(".active").removeClass("active"),this.$stage.children(":eq("+i.join("), :eq(")+")").addClass("active"),this.$stage.children(".center").removeClass("center"),this.settings.center&&this.$stage.children().eq(this.current()).addClass("center")}}],e.prototype.initializeStage=function(){this.$stage=this.$element.find("."+this.settings.stageClass),this.$stage.length||(this.$element.addClass(this.options.loadingClass),this.$stage=a("<"+this.settings.stageElement+">",{class:this.settings.stageClass}).wrap(a("
    ",{class:this.settings.stageOuterClass})),this.$element.append(this.$stage.parent()))},e.prototype.initializeItems=function(){var b=this.$element.find(".owl-item");if(b.length)return this._items=b.get().map(function(b){return a(b)}),this._mergers=this._items.map(function(){return 1}),void this.refresh();this.replace(this.$element.children().not(this.$stage.parent())),this.isVisible()?this.refresh():this.invalidate("width"),this.$element.removeClass(this.options.loadingClass).addClass(this.options.loadedClass)},e.prototype.initialize=function(){if(this.enter("initializing"),this.trigger("initialize"),this.$element.toggleClass(this.settings.rtlClass,this.settings.rtl),this.settings.autoWidth&&!this.is("pre-loading")){var a,b,c;a=this.$element.find("img"),b=this.settings.nestedItemSelector?"."+this.settings.nestedItemSelector:d,c=this.$element.children(b).width(),a.length&&c<=0&&this.preloadAutoWidthImages(a)}this.initializeStage(),this.initializeItems(),this.registerEventHandlers(),this.leave("initializing"),this.trigger("initialized")},e.prototype.isVisible=function(){return!this.settings.checkVisibility||this.$element.is(":visible")},e.prototype.setup=function(){var b=this.viewport(),c=this.options.responsive,d=-1,e=null;c?(a.each(c,function(a){a<=b&&a>d&&(d=Number(a))}),e=a.extend({},this.options,c[d]),"function"==typeof e.stagePadding&&(e.stagePadding=e.stagePadding()),delete e.responsive,e.responsiveClass&&this.$element.attr("class",this.$element.attr("class").replace(new RegExp("("+this.options.responsiveClass+"-)\\S+\\s","g"),"$1"+d))):e=a.extend({},this.options),this.trigger("change",{property:{name:"settings",value:e}}),this._breakpoint=d,this.settings=e,this.invalidate("settings"),this.trigger("changed",{property:{name:"settings",value:this.settings}})},e.prototype.optionsLogic=function(){this.settings.autoWidth&&(this.settings.stagePadding=!1,this.settings.merge=!1)},e.prototype.prepare=function(b){var c=this.trigger("prepare",{content:b});return c.data||(c.data=a("<"+this.settings.itemElement+"/>").addClass(this.options.itemClass).append(b)),this.trigger("prepared",{content:c.data}),c.data},e.prototype.update=function(){for(var b=0,c=this._pipe.length,d=a.proxy(function(a){return this[a]},this._invalidated),e={};b0)&&this._pipe[b].run(e),b++;this._invalidated={},!this.is("valid")&&this.enter("valid")},e.prototype.width=function(a){switch(a=a||e.Width.Default){case e.Width.Inner:case e.Width.Outer:return this._width;default:return this._width-2*this.settings.stagePadding+this.settings.margin}},e.prototype.refresh=function(){this.enter("refreshing"),this.trigger("refresh"),this.setup(),this.optionsLogic(),this.$element.addClass(this.options.refreshClass),this.update(),this.$element.removeClass(this.options.refreshClass),this.leave("refreshing"),this.trigger("refreshed")},e.prototype.onThrottledResize=function(){b.clearTimeout(this.resizeTimer),this.resizeTimer=b.setTimeout(this._handlers.onResize,this.settings.responsiveRefreshRate)},e.prototype.onResize=function(){return!!this._items.length&&(this._width!==this.$element.width()&&(!!this.isVisible()&&(this.enter("resizing"),this.trigger("resize").isDefaultPrevented()?(this.leave("resizing"),!1):(this.invalidate("width"),this.refresh(),this.leave("resizing"),void this.trigger("resized")))))},e.prototype.registerEventHandlers=function(){a.support.transition&&this.$stage.on(a.support.transition.end+".owl.core",a.proxy(this.onTransitionEnd,this)),!1!==this.settings.responsive&&this.on(b,"resize",this._handlers.onThrottledResize),this.settings.mouseDrag&&(this.$element.addClass(this.options.dragClass),this.$stage.on("mousedown.owl.core",a.proxy(this.onDragStart,this)),this.$stage.on("dragstart.owl.core selectstart.owl.core",function(){return!1})),this.settings.touchDrag&&(this.$stage.on("touchstart.owl.core",a.proxy(this.onDragStart,this)),this.$stage.on("touchcancel.owl.core",a.proxy(this.onDragEnd,this)))},e.prototype.onDragStart=function(b){var d=null;3!==b.which&&(a.support.transform?(d=this.$stage.css("transform").replace(/.*\(|\)| /g,"").split(","),d={x:d[16===d.length?12:4],y:d[16===d.length?13:5]}):(d=this.$stage.position(),d={x:this.settings.rtl?d.left+this.$stage.width()-this.width()+this.settings.margin:d.left,y:d.top}),this.is("animating")&&(a.support.transform?this.animate(d.x):this.$stage.stop(),this.invalidate("position")),this.$element.toggleClass(this.options.grabClass,"mousedown"===b.type),this.speed(0),this._drag.time=(new Date).getTime(),this._drag.target=a(b.target),this._drag.stage.start=d,this._drag.stage.current=d,this._drag.pointer=this.pointer(b),a(c).on("mouseup.owl.core touchend.owl.core",a.proxy(this.onDragEnd,this)),a(c).one("mousemove.owl.core touchmove.owl.core",a.proxy(function(b){var d=this.difference(this._drag.pointer,this.pointer(b));a(c).on("mousemove.owl.core touchmove.owl.core",a.proxy(this.onDragMove,this)),Math.abs(d.x)0^this.settings.rtl?"left":"right";a(c).off(".owl.core"),this.$element.removeClass(this.options.grabClass),(0!==d.x&&this.is("dragging")||!this.is("valid"))&&(this.speed(this.settings.dragEndSpeed||this.settings.smartSpeed),this.current(this.closest(e.x,0!==d.x?f:this._drag.direction)),this.invalidate("position"),this.update(),this._drag.direction=f,(Math.abs(d.x)>3||(new Date).getTime()-this._drag.time>300)&&this._drag.target.one("click.owl.core",function(){return!1})),this.is("dragging")&&(this.leave("dragging"),this.trigger("dragged"))},e.prototype.closest=function(b,c){var e=-1,f=30,g=this.width(),h=this.coordinates();return this.settings.freeDrag||a.each(h,a.proxy(function(a,i){return"left"===c&&b>i-f&&bi-g-f&&b",h[a+1]!==d?h[a+1]:i-g)&&(e="left"===c?a+1:a),-1===e},this)),this.settings.loop||(this.op(b,">",h[this.minimum()])?e=b=this.minimum():this.op(b,"<",h[this.maximum()])&&(e=b=this.maximum())),e},e.prototype.animate=function(b){var c=this.speed()>0;this.is("animating")&&this.onTransitionEnd(),c&&(this.enter("animating"),this.trigger("translate")),a.support.transform3d&&a.support.transition?this.$stage.css({transform:"translate3d("+b+"px,0px,0px)",transition:this.speed()/1e3+"s"+(this.settings.slideTransition?" "+this.settings.slideTransition:"")}):c?this.$stage.animate({left:b+"px"},this.speed(),this.settings.fallbackEasing,a.proxy(this.onTransitionEnd,this)):this.$stage.css({left:b+"px"})},e.prototype.is=function(a){return this._states.current[a]&&this._states.current[a]>0},e.prototype.current=function(a){if(a===d)return this._current;if(0===this._items.length)return d;if(a=this.normalize(a),this._current!==a){var b=this.trigger("change",{property:{name:"position",value:a}});b.data!==d&&(a=this.normalize(b.data)),this._current=a,this.invalidate("position"),this.trigger("changed",{property:{name:"position",value:this._current}})}return this._current},e.prototype.invalidate=function(b){return"string"===a.type(b)&&(this._invalidated[b]=!0,this.is("valid")&&this.leave("valid")),a.map(this._invalidated,function(a,b){return b})},e.prototype.reset=function(a){(a=this.normalize(a))!==d&&(this._speed=0,this._current=a,this.suppress(["translate","translated"]),this.animate(this.coordinates(a)),this.release(["translate","translated"]))},e.prototype.normalize=function(a,b){var c=this._items.length,e=b?0:this._clones.length;return!this.isNumeric(a)||c<1?a=d:(a<0||a>=c+e)&&(a=((a-e/2)%c+c)%c+e/2),a},e.prototype.relative=function(a){return a-=this._clones.length/2,this.normalize(a,!0)},e.prototype.maximum=function(a){var b,c,d,e=this.settings,f=this._coordinates.length;if(e.loop)f=this._clones.length/2+this._items.length-1;else if(e.autoWidth||e.merge){if(b=this._items.length)for(c=this._items[--b].width(),d=this.$element.width();b--&&!((c+=this._items[b].width()+this.settings.margin)>d););f=b+1}else f=e.center?this._items.length-1:this._items.length-e.items;return a&&(f-=this._clones.length/2),Math.max(f,0)},e.prototype.minimum=function(a){return a?0:this._clones.length/2},e.prototype.items=function(a){return a===d?this._items.slice():(a=this.normalize(a,!0),this._items[a])},e.prototype.mergers=function(a){return a===d?this._mergers.slice():(a=this.normalize(a,!0),this._mergers[a])},e.prototype.clones=function(b){var c=this._clones.length/2,e=c+this._items.length,f=function(a){return a%2==0?e+a/2:c-(a+1)/2};return b===d?a.map(this._clones,function(a,b){return f(b)}):a.map(this._clones,function(a,c){return a===b?f(c):null})},e.prototype.speed=function(a){return a!==d&&(this._speed=a),this._speed},e.prototype.coordinates=function(b){var c,e=1,f=b-1;return b===d?a.map(this._coordinates,a.proxy(function(a,b){return this.coordinates(b)},this)):(this.settings.center?(this.settings.rtl&&(e=-1,f=b+1),c=this._coordinates[b],c+=(this.width()-c+(this._coordinates[f]||0))/2*e):c=this._coordinates[f]||0,c=Math.ceil(c))},e.prototype.duration=function(a,b,c){return 0===c?0:Math.min(Math.max(Math.abs(b-a),1),6)*Math.abs(c||this.settings.smartSpeed)},e.prototype.to=function(a,b){var c=this.current(),d=null,e=a-this.relative(c),f=(e>0)-(e<0),g=this._items.length,h=this.minimum(),i=this.maximum();this.settings.loop?(!this.settings.rewind&&Math.abs(e)>g/2&&(e+=-1*f*g),a=c+e,(d=((a-h)%g+g)%g+h)!==a&&d-e<=i&&d-e>0&&(c=d-e,a=d,this.reset(c))):this.settings.rewind?(i+=1,a=(a%i+i)%i):a=Math.max(h,Math.min(i,a)),this.speed(this.duration(c,a,b)),this.current(a),this.isVisible()&&this.update()},e.prototype.next=function(a){a=a||!1,this.to(this.relative(this.current())+1,a)},e.prototype.prev=function(a){a=a||!1,this.to(this.relative(this.current())-1,a)},e.prototype.onTransitionEnd=function(a){if(a!==d&&(a.stopPropagation(),(a.target||a.srcElement||a.originalTarget)!==this.$stage.get(0)))return!1;this.leave("animating"),this.trigger("translated")},e.prototype.viewport=function(){var d;return this.options.responsiveBaseElement!==b?d=a(this.options.responsiveBaseElement).width():b.innerWidth?d=b.innerWidth:c.documentElement&&c.documentElement.clientWidth?d=c.documentElement.clientWidth:console.warn("Can not detect viewport width."),d},e.prototype.replace=function(b){this.$stage.empty(),this._items=[],b&&(b=b instanceof jQuery?b:a(b)),this.settings.nestedItemSelector&&(b=b.find("."+this.settings.nestedItemSelector)),b.filter(function(){return 1===this.nodeType}).each(a.proxy(function(a,b){b=this.prepare(b),this.$stage.append(b),this._items.push(b),this._mergers.push(1*b.find("[data-merge]").addBack("[data-merge]").attr("data-merge")||1)},this)),this.reset(this.isNumeric(this.settings.startPosition)?this.settings.startPosition:0),this.invalidate("items")},e.prototype.add=function(b,c){var e=this.relative(this._current);c=c===d?this._items.length:this.normalize(c,!0),b=b instanceof jQuery?b:a(b),this.trigger("add",{content:b,position:c}),b=this.prepare(b),0===this._items.length||c===this._items.length?(0===this._items.length&&this.$stage.append(b),0!==this._items.length&&this._items[c-1].after(b),this._items.push(b),this._mergers.push(1*b.find("[data-merge]").addBack("[data-merge]").attr("data-merge")||1)):(this._items[c].before(b),this._items.splice(c,0,b),this._mergers.splice(c,0,1*b.find("[data-merge]").addBack("[data-merge]").attr("data-merge")||1)),this._items[e]&&this.reset(this._items[e].index()),this.invalidate("items"),this.trigger("added",{content:b,position:c})},e.prototype.remove=function(a){(a=this.normalize(a,!0))!==d&&(this.trigger("remove",{content:this._items[a],position:a}),this._items[a].remove(),this._items.splice(a,1),this._mergers.splice(a,1),this.invalidate("items"),this.trigger("removed",{content:null,position:a}))},e.prototype.preloadAutoWidthImages=function(b){b.each(a.proxy(function(b,c){this.enter("pre-loading"),c=a(c),a(new Image).one("load",a.proxy(function(a){c.attr("src",a.target.src),c.css("opacity",1),this.leave("pre-loading"),!this.is("pre-loading")&&!this.is("initializing")&&this.refresh()},this)).attr("src",c.attr("src")||c.attr("data-src")||c.attr("data-src-retina"))},this))},e.prototype.destroy=function(){this.$element.off(".owl.core"),this.$stage.off(".owl.core"),a(c).off(".owl.core"),!1!==this.settings.responsive&&(b.clearTimeout(this.resizeTimer),this.off(b,"resize",this._handlers.onThrottledResize));for(var d in this._plugins)this._plugins[d].destroy();this.$stage.children(".cloned").remove(),this.$stage.unwrap(),this.$stage.children().contents().unwrap(),this.$stage.children().unwrap(),this.$stage.remove(),this.$element.removeClass(this.options.refreshClass).removeClass(this.options.loadingClass).removeClass(this.options.loadedClass).removeClass(this.options.rtlClass).removeClass(this.options.dragClass).removeClass(this.options.grabClass).attr("class",this.$element.attr("class").replace(new RegExp(this.options.responsiveClass+"-\\S+\\s","g"),"")).removeData("owl.carousel")},e.prototype.op=function(a,b,c){var d=this.settings.rtl;switch(b){case"<":return d?a>c:a":return d?ac;case">=":return d?a<=c:a>=c;case"<=":return d?a>=c:a<=c}},e.prototype.on=function(a,b,c,d){a.addEventListener?a.addEventListener(b,c,d):a.attachEvent&&a.attachEvent("on"+b,c)},e.prototype.off=function(a,b,c,d){a.removeEventListener?a.removeEventListener(b,c,d):a.detachEvent&&a.detachEvent("on"+b,c)},e.prototype.trigger=function(b,c,d,f,g){var h={item:{count:this._items.length,index:this.current()}},i=a.camelCase(a.grep(["on",b,d],function(a){return a}).join("-").toLowerCase()),j=a.Event([b,"owl",d||"carousel"].join(".").toLowerCase(),a.extend({relatedTarget:this},h,c));return this._supress[b]||(a.each(this._plugins,function(a,b){b.onTrigger&&b.onTrigger(j)}),this.register({type:e.Type.Event,name:b}),this.$element.trigger(j),this.settings&&"function"==typeof this.settings[i]&&this.settings[i].call(this,j)),j},e.prototype.enter=function(b){a.each([b].concat(this._states.tags[b]||[]),a.proxy(function(a,b){this._states.current[b]===d&&(this._states.current[b]=0),this._states.current[b]++},this))},e.prototype.leave=function(b){a.each([b].concat(this._states.tags[b]||[]),a.proxy(function(a,b){this._states.current[b]--},this))},e.prototype.register=function(b){if(b.type===e.Type.Event){if(a.event.special[b.name]||(a.event.special[b.name]={}),!a.event.special[b.name].owl){var c=a.event.special[b.name]._default;a.event.special[b.name]._default=function(a){return!c||!c.apply||a.namespace&&-1!==a.namespace.indexOf("owl")?a.namespace&&a.namespace.indexOf("owl")>-1:c.apply(this,arguments)},a.event.special[b.name].owl=!0}}else b.type===e.Type.State&&(this._states.tags[b.name]?this._states.tags[b.name]=this._states.tags[b.name].concat(b.tags):this._states.tags[b.name]=b.tags,this._states.tags[b.name]=a.grep(this._states.tags[b.name],a.proxy(function(c,d){return a.inArray(c,this._states.tags[b.name])===d},this)))},e.prototype.suppress=function(b){a.each(b,a.proxy(function(a,b){this._supress[b]=!0},this))},e.prototype.release=function(b){a.each(b,a.proxy(function(a,b){delete this._supress[b]},this))},e.prototype.pointer=function(a){var c={x:null,y:null};return a=a.originalEvent||a||b.event,a=a.touches&&a.touches.length?a.touches[0]:a.changedTouches&&a.changedTouches.length?a.changedTouches[0]:a,a.pageX?(c.x=a.pageX,c.y=a.pageY):(c.x=a.clientX,c.y=a.clientY),c},e.prototype.isNumeric=function(a){return!isNaN(parseFloat(a))},e.prototype.difference=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},a.fn.owlCarousel=function(b){var c=Array.prototype.slice.call(arguments,1);return this.each(function(){var d=a(this),f=d.data("owl.carousel");f||(f=new e(this,"object"==typeof b&&b),d.data("owl.carousel",f),a.each(["next","prev","to","destroy","refresh","replace","add","remove"],function(b,c){f.register({type:e.Type.Event,name:c}),f.$element.on(c+".owl.carousel.core",a.proxy(function(a){a.namespace&&a.relatedTarget!==this&&(this.suppress([c]),f[c].apply(this,[].slice.call(arguments,1)),this.release([c]))},f))})),"string"==typeof b&&"_"!==b.charAt(0)&&f[b].apply(f,c)})},a.fn.owlCarousel.Constructor=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._interval=null,this._visible=null,this._handlers={"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoRefresh&&this.watch()},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers)};e.Defaults={autoRefresh:!0,autoRefreshInterval:500},e.prototype.watch=function(){this._interval||(this._visible=this._core.isVisible(),this._interval=b.setInterval(a.proxy(this.refresh,this),this._core.settings.autoRefreshInterval))},e.prototype.refresh=function(){this._core.isVisible()!==this._visible&&(this._visible=!this._visible,this._core.$element.toggleClass("owl-hidden",!this._visible),this._visible&&this._core.invalidate("width")&&this._core.refresh())},e.prototype.destroy=function(){var a,c;b.clearInterval(this._interval);for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(c in Object.getOwnPropertyNames(this))"function"!=typeof this[c]&&(this[c]=null)},a.fn.owlCarousel.Constructor.Plugins.AutoRefresh=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._loaded=[],this._handlers={"initialized.owl.carousel change.owl.carousel resized.owl.carousel":a.proxy(function(b){if(b.namespace&&this._core.settings&&this._core.settings.lazyLoad&&(b.property&&"position"==b.property.name||"initialized"==b.type)){var c=this._core.settings,e=c.center&&Math.ceil(c.items/2)||c.items,f=c.center&&-1*e||0,g=(b.property&&b.property.value!==d?b.property.value:this._core.current())+f,h=this._core.clones().length,i=a.proxy(function(a,b){this.load(b)},this);for(c.lazyLoadEager>0&&(e+=c.lazyLoadEager,c.loop&&(g-=c.lazyLoadEager,e++));f++-1||(e.each(a.proxy(function(c,d){var e,f=a(d),g=b.devicePixelRatio>1&&f.attr("data-src-retina")||f.attr("data-src")||f.attr("data-srcset");this._core.trigger("load",{element:f,url:g},"lazy"),f.is("img")?f.one("load.owl.lazy",a.proxy(function(){f.css("opacity",1),this._core.trigger("loaded",{element:f,url:g},"lazy")},this)).attr("src",g):f.is("source")?f.one("load.owl.lazy",a.proxy(function(){this._core.trigger("loaded",{element:f,url:g},"lazy")},this)).attr("srcset",g):(e=new Image,e.onload=a.proxy(function(){f.css({"background-image":'url("'+g+'")',opacity:"1"}),this._core.trigger("loaded",{element:f,url:g},"lazy")},this),e.src=g)},this)),this._loaded.push(d.get(0)))},e.prototype.destroy=function(){var a,b;for(a in this.handlers)this._core.$element.off(a,this.handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Lazy=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(c){this._core=c,this._previousHeight=null,this._handlers={"initialized.owl.carousel refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&this.update()},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&"position"===a.property.name&&this.update()},this),"loaded.owl.lazy":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&a.element.closest("."+this._core.settings.itemClass).index()===this._core.current()&&this.update()},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers),this._intervalId=null;var d=this;a(b).on("load",function(){d._core.settings.autoHeight&&d.update()}),a(b).resize(function(){d._core.settings.autoHeight&&(null!=d._intervalId&&clearTimeout(d._intervalId),d._intervalId=setTimeout(function(){d.update()},250))})};e.Defaults={autoHeight:!1,autoHeightClass:"owl-height"},e.prototype.update=function(){var b=this._core._current,c=b+this._core.settings.items,d=this._core.settings.lazyLoad,e=this._core.$stage.children().toArray().slice(b,c),f=[],g=0;a.each(e,function(b,c){f.push(a(c).height())}),g=Math.max.apply(null,f),g<=1&&d&&this._previousHeight&&(g=this._previousHeight),this._previousHeight=g,this._core.$stage.parent().height(g).addClass(this._core.settings.autoHeightClass)},e.prototype.destroy=function(){var a,b;for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.AutoHeight=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._videos={},this._playing=null,this._handlers={"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.register({type:"state",name:"playing",tags:["interacting"]})},this),"resize.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.video&&this.isInFullScreen()&&a.preventDefault()},this),"refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.is("resizing")&&this._core.$stage.find(".cloned .owl-video-frame").remove()},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&"position"===a.property.name&&this._playing&&this.stop()},this),"prepared.owl.carousel":a.proxy(function(b){if(b.namespace){var c=a(b.content).find(".owl-video");c.length&&(c.css("display","none"),this.fetch(c,a(b.content)))}},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers),this._core.$element.on("click.owl.video",".owl-video-play-icon",a.proxy(function(a){this.play(a)},this))};e.Defaults={video:!1,videoHeight:!1,videoWidth:!1},e.prototype.fetch=function(a,b){var c=function(){return a.attr("data-vimeo-id")?"vimeo":a.attr("data-vzaar-id")?"vzaar":"youtube"}(),d=a.attr("data-vimeo-id")||a.attr("data-youtube-id")||a.attr("data-vzaar-id"),e=a.attr("data-width")||this._core.settings.videoWidth,f=a.attr("data-height")||this._core.settings.videoHeight,g=a.attr("href");if(!g)throw new Error("Missing video URL.");if(d=g.match(/(http:|https:|)\/\/(player.|www.|app.)?(vimeo\.com|youtu(be\.com|\.be|be\.googleapis\.com|be\-nocookie\.com)|vzaar\.com)\/(video\/|videos\/|embed\/|channels\/.+\/|groups\/.+\/|watch\?v=|v\/)?([A-Za-z0-9._%-]*)(\&\S+)?/),d[3].indexOf("youtu")>-1)c="youtube";else if(d[3].indexOf("vimeo")>-1)c="vimeo";else{if(!(d[3].indexOf("vzaar")>-1))throw new Error("Video URL not supported.");c="vzaar"}d=d[6],this._videos[g]={type:c,id:d,width:e,height:f},b.attr("data-video",g),this.thumbnail(a,this._videos[g])},e.prototype.thumbnail=function(b,c){var d,e,f,g=c.width&&c.height?"width:"+c.width+"px;height:"+c.height+"px;":"",h=b.find("img"),i="src",j="",k=this._core.settings,l=function(c){e='
    ',d=k.lazyLoad?a("
    ",{class:"owl-video-tn "+j,srcType:c}):a("
    ",{class:"owl-video-tn",style:"opacity:1;background-image:url("+c+")"}),b.after(d),b.after(e)};if(b.wrap(a("
    ",{class:"owl-video-wrapper",style:g})),this._core.settings.lazyLoad&&(i="data-src",j="owl-lazy"),h.length)return l(h.attr(i)),h.remove(),!1;"youtube"===c.type?(f="//img.youtube.com/vi/"+c.id+"/hqdefault.jpg",l(f)):"vimeo"===c.type?a.ajax({type:"GET",url:"//vimeo.com/api/v2/video/"+c.id+".json",jsonp:"callback",dataType:"jsonp",success:function(a){f=a[0].thumbnail_large,l(f)}}):"vzaar"===c.type&&a.ajax({type:"GET",url:"//vzaar.com/api/videos/"+c.id+".json",jsonp:"callback",dataType:"jsonp",success:function(a){f=a.framegrab_url,l(f)}})},e.prototype.stop=function(){this._core.trigger("stop",null,"video"),this._playing.find(".owl-video-frame").remove(),this._playing.removeClass("owl-video-playing"),this._playing=null,this._core.leave("playing"),this._core.trigger("stopped",null,"video")},e.prototype.play=function(b){var c,d=a(b.target),e=d.closest("."+this._core.settings.itemClass),f=this._videos[e.attr("data-video")],g=f.width||"100%",h=f.height||this._core.$stage.height();this._playing||(this._core.enter("playing"),this._core.trigger("play",null,"video"),e=this._core.items(this._core.relative(e.index())),this._core.reset(e.index()),c=a(''),c.attr("height",h),c.attr("width",g),"youtube"===f.type?c.attr("src","//www.youtube.com/embed/"+f.id+"?autoplay=1&rel=0&v="+f.id):"vimeo"===f.type?c.attr("src","//player.vimeo.com/video/"+f.id+"?autoplay=1"):"vzaar"===f.type&&c.attr("src","//view.vzaar.com/"+f.id+"/player?autoplay=true"),a(c).wrap('
    ').insertAfter(e.find(".owl-video")),this._playing=e.addClass("owl-video-playing"))},e.prototype.isInFullScreen=function(){var b=c.fullscreenElement||c.mozFullScreenElement||c.webkitFullscreenElement;return b&&a(b).parent().hasClass("owl-video-frame")},e.prototype.destroy=function(){var a,b;this._core.$element.off("click.owl.video");for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Video=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this.core=b,this.core.options=a.extend({},e.Defaults,this.core.options),this.swapping=!0,this.previous=d,this.next=d,this.handlers={"change.owl.carousel":a.proxy(function(a){a.namespace&&"position"==a.property.name&&(this.previous=this.core.current(),this.next=a.property.value)},this),"drag.owl.carousel dragged.owl.carousel translated.owl.carousel":a.proxy(function(a){a.namespace&&(this.swapping="translated"==a.type)},this),"translate.owl.carousel":a.proxy(function(a){a.namespace&&this.swapping&&(this.core.options.animateOut||this.core.options.animateIn)&&this.swap()},this)},this.core.$element.on(this.handlers)};e.Defaults={animateOut:!1, -animateIn:!1},e.prototype.swap=function(){if(1===this.core.settings.items&&a.support.animation&&a.support.transition){this.core.speed(0);var b,c=a.proxy(this.clear,this),d=this.core.$stage.children().eq(this.previous),e=this.core.$stage.children().eq(this.next),f=this.core.settings.animateIn,g=this.core.settings.animateOut;this.core.current()!==this.previous&&(g&&(b=this.core.coordinates(this.previous)-this.core.coordinates(this.next),d.one(a.support.animation.end,c).css({left:b+"px"}).addClass("animated owl-animated-out").addClass(g)),f&&e.one(a.support.animation.end,c).addClass("animated owl-animated-in").addClass(f))}},e.prototype.clear=function(b){a(b.target).css({left:""}).removeClass("animated owl-animated-out owl-animated-in").removeClass(this.core.settings.animateIn).removeClass(this.core.settings.animateOut),this.core.onTransitionEnd()},e.prototype.destroy=function(){var a,b;for(a in this.handlers)this.core.$element.off(a,this.handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Animate=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._call=null,this._time=0,this._timeout=0,this._paused=!0,this._handlers={"changed.owl.carousel":a.proxy(function(a){a.namespace&&"settings"===a.property.name?this._core.settings.autoplay?this.play():this.stop():a.namespace&&"position"===a.property.name&&this._paused&&(this._time=0)},this),"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoplay&&this.play()},this),"play.owl.autoplay":a.proxy(function(a,b,c){a.namespace&&this.play(b,c)},this),"stop.owl.autoplay":a.proxy(function(a){a.namespace&&this.stop()},this),"mouseover.owl.autoplay":a.proxy(function(){this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.pause()},this),"mouseleave.owl.autoplay":a.proxy(function(){this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.play()},this),"touchstart.owl.core":a.proxy(function(){this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.pause()},this),"touchend.owl.core":a.proxy(function(){this._core.settings.autoplayHoverPause&&this.play()},this)},this._core.$element.on(this._handlers),this._core.options=a.extend({},e.Defaults,this._core.options)};e.Defaults={autoplay:!1,autoplayTimeout:5e3,autoplayHoverPause:!1,autoplaySpeed:!1},e.prototype._next=function(d){this._call=b.setTimeout(a.proxy(this._next,this,d),this._timeout*(Math.round(this.read()/this._timeout)+1)-this.read()),this._core.is("interacting")||c.hidden||this._core.next(d||this._core.settings.autoplaySpeed)},e.prototype.read=function(){return(new Date).getTime()-this._time},e.prototype.play=function(c,d){var e;this._core.is("rotating")||this._core.enter("rotating"),c=c||this._core.settings.autoplayTimeout,e=Math.min(this._time%(this._timeout||c),c),this._paused?(this._time=this.read(),this._paused=!1):b.clearTimeout(this._call),this._time+=this.read()%c-e,this._timeout=c,this._call=b.setTimeout(a.proxy(this._next,this,d),c-e)},e.prototype.stop=function(){this._core.is("rotating")&&(this._time=0,this._paused=!0,b.clearTimeout(this._call),this._core.leave("rotating"))},e.prototype.pause=function(){this._core.is("rotating")&&!this._paused&&(this._time=this.read(),this._paused=!0,b.clearTimeout(this._call))},e.prototype.destroy=function(){var a,b;this.stop();for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.autoplay=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){"use strict";var e=function(b){this._core=b,this._initialized=!1,this._pages=[],this._controls={},this._templates=[],this.$element=this._core.$element,this._overrides={next:this._core.next,prev:this._core.prev,to:this._core.to},this._handlers={"prepared.owl.carousel":a.proxy(function(b){b.namespace&&this._core.settings.dotsData&&this._templates.push('
    '+a(b.content).find("[data-dot]").addBack("[data-dot]").attr("data-dot")+"
    ")},this),"added.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.dotsData&&this._templates.splice(a.position,0,this._templates.pop())},this),"remove.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.dotsData&&this._templates.splice(a.position,1)},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&"position"==a.property.name&&this.draw()},this),"initialized.owl.carousel":a.proxy(function(a){a.namespace&&!this._initialized&&(this._core.trigger("initialize",null,"navigation"),this.initialize(),this.update(),this.draw(),this._initialized=!0,this._core.trigger("initialized",null,"navigation"))},this),"refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._initialized&&(this._core.trigger("refresh",null,"navigation"),this.update(),this.draw(),this._core.trigger("refreshed",null,"navigation"))},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this.$element.on(this._handlers)};e.Defaults={nav:!1,navText:['‹','›'],navSpeed:!1,navElement:'button type="button" role="presentation"',navContainer:!1,navContainerClass:"owl-nav",navClass:["owl-prev","owl-next"],slideBy:1,dotClass:"owl-dot",dotsClass:"owl-dots",dots:!0,dotsEach:!1,dotsData:!1,dotsSpeed:!1,dotsContainer:!1},e.prototype.initialize=function(){var b,c=this._core.settings;this._controls.$relative=(c.navContainer?a(c.navContainer):a("
    ").addClass(c.navContainerClass).appendTo(this.$element)).addClass("disabled"),this._controls.$previous=a("<"+c.navElement+">").addClass(c.navClass[0]).html(c.navText[0]).prependTo(this._controls.$relative).on("click",a.proxy(function(a){this.prev(c.navSpeed)},this)),this._controls.$next=a("<"+c.navElement+">").addClass(c.navClass[1]).html(c.navText[1]).appendTo(this._controls.$relative).on("click",a.proxy(function(a){this.next(c.navSpeed)},this)),c.dotsData||(this._templates=[a('
    "),$(o).next().on("click",".addListData",function(t){n.goNum=n.nowNum+r.options.afterNum,n.sum<=n.goNum&&(n.goNum=n.sum,n.goOut=!0);for(var e=n.nowNum;ee.breakpoint?-1:1}),o.options.responsive=ResponsiveArr,o.Oindex=-1,o.Owidth=$(window).width(),$.each(o.options.responsive,function(t,e){$(window).width()<=e.breakpoint&&(o.Oindex=t,e=e.settings,o.options=$.extend({},o.options,e))}),$(window).resize(function(){return run=!1,$(window).width()o.Owidth&&(o.Owidth=$(window).width(),$.each(ResponsiveArr,function(t,e){if(o.Owidth>e.breakpoint&&o.Oindex>t-1)return o.Oindex=t-1,run=(-1!=o.Oindex?(e=ResponsiveArr[t-1].settings,o.options=$.extend({},o.options,o.defaults),o.options=$.extend({},o.options,e)):o.options=$.extend({},o.options,o.defaults),!0),o.Oindex})),1==run&&o.runData(t,o),!1}))},$.fn.showMoreItems=function(){for(var t,e=this,o=arguments[0],r=Array.prototype.slice.call(arguments,1),n=e.length,s=0;s=p.animationTime,c=i?1:l/p.animationTime;p.pulseAlgorithm&&(c=F(c));var s=a.x*c-a.lastX>>0,u=a.y*c-a.lastY>>0;o+=s,n+=u,a.lastX+=s,a.lastY+=u,i&&(b.splice(r,1),r--)}h?window.scrollBy(o,n):(o&&(d.scrollLeft+=o),n&&(d.scrollTop+=n)),f||m||(b=[]),b.length?j(w,d,1e3/p.frameRate+1):(g=!1,null!=d.$scrollBehavior&&(d.style.scrollBehavior=d.$scrollBehavior,d.$scrollBehavior=null))};j(w,d,0),g=!0}}function e(e){f||y();var t=e.target;if(e.defaultPrevented||e.ctrlKey)return!0;if(N(s,"embed")||N(t,"embed")&&/\.pdf/i.test(t.src)||N(s,"object")||t.shadowRoot)return!0;var o=-e.wheelDeltaX||e.deltaX||0,n=-e.wheelDeltaY||e.deltaY||0;h&&(e.wheelDeltaX&&K(e.wheelDeltaX,120)&&(o=e.wheelDeltaX/Math.abs(e.wheelDeltaX)*-120),e.wheelDeltaY&&K(e.wheelDeltaY,120)&&(n=e.wheelDeltaY/Math.abs(e.wheelDeltaY)*-120)),o||n||(n=-e.wheelDelta||0),1===e.deltaMode&&(o*=40,n*=40);var r=z(t);return r?!!function(e){if(!e)return;l.length||(l=[e,e,e]);e=Math.abs(e),l.push(e),l.shift(),clearTimeout(a),a=setTimeout(function(){try{localStorage.SS_deltaBuffer=l.join(",")}catch(e){}},1e3);var t=120 - - - - -Created by FontForge 20170731 at Thu Dec 24 05:29:26 2020 - By root - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/assets/webfonts/Flaticon.ttf b/static/assets/webfonts/Flaticon.ttf deleted file mode 100644 index 8f1869d1..00000000 Binary files a/static/assets/webfonts/Flaticon.ttf and /dev/null differ diff --git a/static/assets/webfonts/Flaticon.woff b/static/assets/webfonts/Flaticon.woff deleted file mode 100644 index 0edc28af..00000000 Binary files a/static/assets/webfonts/Flaticon.woff and /dev/null differ diff --git a/static/assets/webfonts/Flaticon.woff2 b/static/assets/webfonts/Flaticon.woff2 deleted file mode 100644 index 6dbae672..00000000 Binary files a/static/assets/webfonts/Flaticon.woff2 and /dev/null differ diff --git a/static/assets/webfonts/fa-brands-400.eot b/static/assets/webfonts/fa-brands-400.eot deleted file mode 100644 index 958684e2..00000000 Binary files a/static/assets/webfonts/fa-brands-400.eot and /dev/null differ diff --git a/static/assets/webfonts/fa-brands-400.svg b/static/assets/webfonts/fa-brands-400.svg deleted file mode 100644 index 2b7cf17b..00000000 --- a/static/assets/webfonts/fa-brands-400.svg +++ /dev/null @@ -1,3717 +0,0 @@ - - - - -Created by FontForge 20200314 at Mon Oct 5 09:50:45 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/assets/webfonts/fa-brands-400.ttf b/static/assets/webfonts/fa-brands-400.ttf deleted file mode 100644 index f0718251..00000000 Binary files a/static/assets/webfonts/fa-brands-400.ttf and /dev/null differ diff --git a/static/assets/webfonts/fa-brands-400.woff b/static/assets/webfonts/fa-brands-400.woff deleted file mode 100644 index 277ab65b..00000000 Binary files a/static/assets/webfonts/fa-brands-400.woff and /dev/null differ diff --git a/static/assets/webfonts/fa-brands-400.woff2 b/static/assets/webfonts/fa-brands-400.woff2 deleted file mode 100644 index 47805d47..00000000 Binary files a/static/assets/webfonts/fa-brands-400.woff2 and /dev/null differ diff --git a/static/assets/webfonts/fa-regular-400.eot b/static/assets/webfonts/fa-regular-400.eot deleted file mode 100644 index bef9f722..00000000 Binary files a/static/assets/webfonts/fa-regular-400.eot and /dev/null differ diff --git a/static/assets/webfonts/fa-regular-400.svg b/static/assets/webfonts/fa-regular-400.svg deleted file mode 100644 index bccc256b..00000000 --- a/static/assets/webfonts/fa-regular-400.svg +++ /dev/null @@ -1,801 +0,0 @@ - - - - -Created by FontForge 20200314 at Mon Oct 5 09:50:45 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/assets/webfonts/fa-regular-400.ttf b/static/assets/webfonts/fa-regular-400.ttf deleted file mode 100644 index 659527a6..00000000 Binary files a/static/assets/webfonts/fa-regular-400.ttf and /dev/null differ diff --git a/static/assets/webfonts/fa-regular-400.woff b/static/assets/webfonts/fa-regular-400.woff deleted file mode 100644 index 31f44b2d..00000000 Binary files a/static/assets/webfonts/fa-regular-400.woff and /dev/null differ diff --git a/static/assets/webfonts/fa-regular-400.woff2 b/static/assets/webfonts/fa-regular-400.woff2 deleted file mode 100644 index 0332a9bf..00000000 Binary files a/static/assets/webfonts/fa-regular-400.woff2 and /dev/null differ diff --git a/static/assets/webfonts/fa-solid-900.eot b/static/assets/webfonts/fa-solid-900.eot deleted file mode 100644 index 5da4fa00..00000000 Binary files a/static/assets/webfonts/fa-solid-900.eot and /dev/null differ diff --git a/static/assets/webfonts/fa-solid-900.svg b/static/assets/webfonts/fa-solid-900.svg deleted file mode 100644 index 313b3118..00000000 --- a/static/assets/webfonts/fa-solid-900.svg +++ /dev/null @@ -1,5028 +0,0 @@ - - - - -Created by FontForge 20200314 at Mon Oct 5 09:50:45 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/assets/webfonts/fa-solid-900.ttf b/static/assets/webfonts/fa-solid-900.ttf deleted file mode 100644 index e0746084..00000000 Binary files a/static/assets/webfonts/fa-solid-900.ttf and /dev/null differ diff --git a/static/assets/webfonts/fa-solid-900.woff b/static/assets/webfonts/fa-solid-900.woff deleted file mode 100644 index ef6b4474..00000000 Binary files a/static/assets/webfonts/fa-solid-900.woff and /dev/null differ diff --git a/static/assets/webfonts/fa-solid-900.woff2 b/static/assets/webfonts/fa-solid-900.woff2 deleted file mode 100644 index 120b3007..00000000 Binary files a/static/assets/webfonts/fa-solid-900.woff2 and /dev/null differ diff --git a/system_requirements.txt b/system_requirements.txt deleted file mode 100644 index f0b1ccae..00000000 --- a/system_requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -sudo apt-get update && apt-get upgrade -y - -sudo apt-get install -y curl wget libpq-dev python3-dev gem ruby ruby-dev build-essential libssl-dev libffi-dev python-dev python-virtualenv python-pip git redis-server libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev tcl8.6-dev tk8.6-dev python-tk - -sudo gem install sass - -sudo apt-get install wkhtmltopdf - -sudo apt-get install postfix \ No newline at end of file diff --git a/tasks/__init__.py b/tasks/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tasks/admin.py b/tasks/admin.py deleted file mode 100644 index 3ade634f..00000000 --- a/tasks/admin.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.contrib import admin - -from tasks.models import Task - -# Register your models here. - -admin.site.register(Task) diff --git a/tasks/migrations/0001_initial.py b/tasks/migrations/0001_initial.py deleted file mode 100644 index 1929aff3..00000000 --- a/tasks/migrations/0001_initial.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('teams', '0001_initial'), - ('accounts', '0001_initial'), - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contacts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Task', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('title', models.CharField(max_length=200, verbose_name='title')), - ('status', models.CharField(choices=[('New', 'New'), ('In Progress', 'In Progress'), ('Completed', 'Completed')], max_length=50, verbose_name='status')), - ('priority', models.CharField(choices=[('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High')], max_length=50, verbose_name='priority')), - ('due_date', models.DateField(blank=True, null=True)), - ('account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts_tasks', to='accounts.account')), - ('assigned_to', models.ManyToManyField(related_name='users_tasks', to='common.profile')), - ('contacts', models.ManyToManyField(related_name='contacts_tasks', to='contacts.contact')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_created', to='common.profile')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_org', to='common.org')), - ('teams', models.ManyToManyField(related_name='tasks_teams', to='teams.teams')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ], - options={ - 'verbose_name': 'Task', - 'verbose_name_plural': 'Tasks', - 'db_table': 'task', - 'ordering': ('-due_date',), - }, - ), - ] diff --git a/tasks/migrations/0002_alter_task_created_by.py b/tasks/migrations/0002_alter_task_created_by.py deleted file mode 100644 index 3460261f..00000000 --- a/tasks/migrations/0002_alter_task_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-31 12:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('tasks', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='task', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/tasks/migrations/__init__.py b/tasks/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tasks/models.py b/tasks/models.py deleted file mode 100644 index 1fa891b9..00000000 --- a/tasks/models.py +++ /dev/null @@ -1,73 +0,0 @@ -import arrow -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from accounts.models import Account -from common.base import BaseModel -from common.models import Org, Profile -from contacts.models import Contact -from teams.models import Teams - - -class Task(BaseModel): - - STATUS_CHOICES = ( - ("New", "New"), - ("In Progress", "In Progress"), - ("Completed", "Completed"), - ) - - PRIORITY_CHOICES = (("Low", "Low"), ("Medium", "Medium"), ("High", "High")) - - title = models.CharField(_("title"), max_length=200) - status = models.CharField(_("status"), max_length=50, choices=STATUS_CHOICES) - priority = models.CharField(_("priority"), max_length=50, choices=PRIORITY_CHOICES) - due_date = models.DateField(blank=True, null=True) - account = models.ForeignKey( - Account, - related_name="accounts_tasks", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - - contacts = models.ManyToManyField(Contact, related_name="contacts_tasks") - - assigned_to = models.ManyToManyField(Profile, related_name="users_tasks") - - # created_by = models.ForeignKey( - # Profile, - # related_name="task_created", - # blank=True, - # null=True, - # on_delete=models.SET_NULL, - # ) - teams = models.ManyToManyField(Teams, related_name="tasks_teams") - org = models.ForeignKey( - Org, on_delete=models.SET_NULL, null=True, blank=True, related_name="task_org" - ) - - class Meta: - verbose_name = "Task" - verbose_name_plural = "Tasks" - db_table = "task" - ordering = ("-due_date",) - - def __str__(self): - return f"{self.title}" - - @property - def created_on_arrow(self): - return arrow.get(self.created_at).humanize() - - @property - def get_team_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - return Profile.objects.filter(id__in=team_user_ids) - - @property - def get_team_and_assigned_users(self): - team_user_ids = list(self.teams.values_list("users__id", flat=True)) - assigned_user_ids = list(self.assigned_to.values_list("id", flat=True)) - user_ids = team_user_ids + assigned_user_ids - return Profile.objects.filter(id__in=user_ids) diff --git a/tasks/serializer.py b/tasks/serializer.py deleted file mode 100644 index cf4b6acb..00000000 --- a/tasks/serializer.py +++ /dev/null @@ -1,95 +0,0 @@ -from rest_framework import serializers - -from common.serializer import ( - AttachmentsSerializer, - CommentSerializer, - ProfileSerializer, - UserSerializer -) -from contacts.serializer import ContactSerializer -from tasks.models import Task -from teams.serializer import TeamsSerializer - - -class TaskSerializer(serializers.ModelSerializer): - created_by = UserSerializer() - assigned_to = ProfileSerializer(read_only=True, many=True) - contacts = ContactSerializer(read_only=True, many=True) - teams = TeamsSerializer(read_only=True, many=True) - task_attachment = AttachmentsSerializer(read_only=True, many=True) - task_comments = CommentSerializer(read_only=True, many=True) - - class Meta: - model = Task - fields = ( - "id", - "title", - "status", - "priority", - "due_date", - "account", - "created_by", - "created_at", - "contacts", - "teams", - "assigned_to", - "task_attachment", - "task_comments", - ) - - -class TaskCreateSerializer(serializers.ModelSerializer): - def __init__(self, *args, **kwargs): - request_obj = kwargs.pop("request_obj", None) - super().__init__(*args, **kwargs) - self.org = request_obj.profile.org - - self.fields["title"].required = True - - def validate_title(self, title): - if self.instance: - if ( - Task.objects.filter(title__iexact=title, org=self.org) - .exclude(id=self.instance.id) - .exists() - ): - raise serializers.ValidationError("Task already exists with this title") - else: - if Task.objects.filter(title__iexact=title, org=self.org).exists(): - raise serializers.ValidationError("Task already exists with this title") - return title - - class Meta: - model = Task - fields = ( - "id", - "title", - "status", - "priority", - "due_date", - "account", - "created_by", - "created_at", - ) - -class TaskDetailEditSwaggerSerializer(serializers.Serializer): - comment = serializers.CharField() - task_attachment = serializers.FileField() - -class TaskCommentEditSwaggerSerializer(serializers.Serializer): - comment = serializers.CharField() - -class TaskCreateSwaggerSerializer(serializers.ModelSerializer): - class Meta: - model = Task - fields = ( - "title", - "status", - "priority", - "due_date", - "account", - "contacts", - "teams", - "assigned_to" - ) - diff --git a/tasks/urls.py b/tasks/urls.py deleted file mode 100644 index 4367773c..00000000 --- a/tasks/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path - -from tasks import views - -app_name = "api_tasks" - -urlpatterns = [ - path("", views.TaskListView.as_view()), - path("/", views.TaskDetailView.as_view()), - path("comment//", views.TaskCommentView.as_view()), - path("attachment//", views.TaskAttachmentView.as_view()), -] diff --git a/tasks/views.py b/tasks/views.py deleted file mode 100644 index ade998a9..00000000 --- a/tasks/views.py +++ /dev/null @@ -1,424 +0,0 @@ -import json - -from django.db.models import Q -from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema -from rest_framework import status -from rest_framework.pagination import LimitOffsetPagination -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView - -from accounts.models import Account -from accounts.serializer import AccountSerializer -from common.models import Attachments, Comment, Profile - -#from common.external_auth import CustomDualAuthentication -from common.serializer import ( - AttachmentsSerializer, - CommentSerializer, - ProfileSerializer, -) -from contacts.models import Contact -from contacts.serializer import ContactSerializer -from tasks import swagger_params1 -from tasks.models import Task -from tasks.serializer import * -from tasks.utils import PRIORITY_CHOICES, STATUS_CHOICES -from teams.models import Teams -from teams.serializer import TeamsSerializer - - -class TaskListView(APIView, LimitOffsetPagination): - model = Task - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_context_data(self, **kwargs): - params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") - accounts = Account.objects.filter(org=self.request.profile.org) - contacts = Contact.objects.filter(org=self.request.profile.org) - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - queryset = queryset.filter( - Q(assigned_to__in=[self.request.profile]) - | Q(created_by=self.request.profile.user) - ) - accounts = accounts.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) - ).distinct() - contacts = contacts.filter( - Q(created_by=self.request.profile.user) | Q(assigned_to=self.request.profile) - ).distinct() - - if params: - if params.get("title"): - queryset = queryset.filter(title__icontains=params.get("title")) - if params.get("status"): - queryset = queryset.filter(status=params.get("status")) - if params.get("priority"): - queryset = queryset.filter(priority=params.get("priority")) - context = {} - results_tasks = self.paginate_queryset( - queryset.distinct(), self.request, view=self - ) - tasks = TaskSerializer(results_tasks, many=True).data - if results_tasks: - offset = queryset.filter(id__gte=results_tasks[-1].id).count() - if offset == queryset.count(): - offset = None - else: - offset = 0 - context.update( - { - "tasks_count": self.count, - "offset": offset, - } - ) - context["tasks"] = tasks - context["status"] = STATUS_CHOICES - context["priority"] = PRIORITY_CHOICES - context["accounts_list"] = AccountSerializer(accounts, many=True).data - context["contacts_list"] = ContactSerializer(contacts, many=True).data - return context - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.task_list_get_params - ) - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) - return Response(context) - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params,request=TaskCreateSwaggerSerializer - ) - def post(self, request, *args, **kwargs): - params = request.data - serializer = TaskCreateSerializer(data=params, request_obj=request) - if serializer.is_valid(): - task_obj = serializer.save( - created_by=request.profile.user, - due_date=params.get("due_date"), - org=request.profile.org, - ) - if params.get("contacts"): - contacts_list = params.get("contacts") - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) - task_obj.contacts.add(*contacts) - - if params.get("teams"): - teams_list = params.get("teams") - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - task_obj.teams.add(*teams) - - if params.get("assigned_to"): - assinged_to_list = params.get("assigned_to") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org, is_active=True - ) - task_obj.assigned_to.add(*profiles) - - return Response( - {"error": False, "message": "Task Created Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class TaskDetailView(APIView): - model = Task - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_object(self, pk): - return Task.objects.get(pk=pk) - - def get_context_data(self, **kwargs): - context = {} - user_assgn_list = [ - assigned_to.id for assigned_to in self.task_obj.assigned_to.all() - ] - if self.request.profile == self.task_obj.created_by: - user_assgn_list.append(self.request.profile.id) - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - if self.request.profile.id not in user_assgn_list: - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - comments = Comment.objects.filter(task=self.task_obj).order_by("-id") - attachments = Attachments.objects.filter(task=self.task_obj).order_by("-id") - - assigned_data = self.task_obj.assigned_to.values("id", "user__email") - - if self.request.profile.is_admin or self.request.profile.role == "ADMIN": - users_mention = list( - Profile.objects.filter(is_active=True, org=self.request.profile.org).values( - "user__email" - ) - ) - elif self.request.profile != self.task_obj.created_by: - users_mention = [{"username": self.task_obj.created_by.user.email}] - else: - users_mention = list( - self.task_obj.assigned_to.all().values("user__email") - ) - if self.request.profile.role == "ADMIN" or self.request.profile.is_admin: - users = Profile.objects.filter( - is_active=True, org=self.request.profile.org - ).order_by("user__email") - else: - users = Profile.objects.filter(role="ADMIN", org=self.request.profile.org).order_by( - "user__email" - ) - - if self.request.profile == self.task_obj.created_by: - user_assgn_list.append(self.request.profile.id) - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - if self.request.profile.id not in user_assgn_list: - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - team_ids = [user.id for user in self.task_obj.get_team_users] - all_user_ids = users.values_list("id", flat=True) - users_excluding_team_id = set(all_user_ids) - set(team_ids) - users_excluding_team = Profile.objects.filter(id__in=users_excluding_team_id) - context.update( - { - "task_obj": TaskSerializer(self.task_obj).data, - "attachments": AttachmentsSerializer(attachments, many=True).data, - "comments": CommentSerializer(comments, many=True).data, - "users_mention": users_mention, - "assigned_data": assigned_data, - } - ) - context["users"] = ProfileSerializer(users, many=True).data - context["users_excluding_team"] = ProfileSerializer( - users_excluding_team, many=True - ).data - context["teams"] = TeamsSerializer(Teams.objects.all(), many=True).data - return context - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params - ) - def get(self, request, pk, **kwargs): - self.task_obj = self.get_object(pk) - context = self.get_context_data(**kwargs) - return Response(context) - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params,request=TaskDetailEditSwaggerSerializer - ) - def post(self, request, pk, **kwargs): - params = request.data - context = {} - self.task_obj = Task.objects.get(pk=pk) - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - if not ( - (self.request.profile == self.task_obj.created_by) - or (self.request.profile in self.task_obj.assigned_to.all()) - ): - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - comment_serializer = CommentSerializer(data=params) - if comment_serializer.is_valid(): - if params.get("comment"): - comment_serializer.save( - task_id=self.task_obj.id, - commented_by_id=self.request.profile.id, - ) - - if self.request.FILES.get("task_attachment"): - attachment = Attachments() - attachment.created_by = self.request.profile.user - attachment.file_name = self.request.FILES.get("task_attachment").name - attachment.task = self.task_obj - attachment.attachment = self.request.FILES.get("task_attachment") - attachment.save() - - comments = Comment.objects.filter(task__id=self.task_obj.id).order_by("-id") - attachments = Attachments.objects.filter(task__id=self.task_obj.id).order_by( - "-id" - ) - context.update( - { - "task_obj": TaskSerializer(self.task_obj).data, - "attachments": AttachmentsSerializer(attachments, many=True).data, - "comments": CommentSerializer(comments, many=True).data, - } - ) - return Response(context) - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params,request=TaskCreateSwaggerSerializer - ) - def put(self, request, pk, **kwargs): - params = request.data - self.task_obj = self.get_object(pk) - serializer = TaskCreateSerializer( - data=params, - instance=self.task_obj, - request_obj=request, - ) - if serializer.is_valid(): - task_obj = serializer.save() - previous_assigned_to_users = list( - task_obj.assigned_to.all().values_list("id", flat=True) - ) - task_obj.contacts.clear() - if params.get("contacts"): - contacts_list = params.get("contacts") - contacts = Contact.objects.filter(id__in=contacts_list, org=request.profile.org) - task_obj.contacts.add(*contacts) - - task_obj.teams.clear() - if params.get("teams"): - teams_list = params.get("teams") - teams = Teams.objects.filter(id__in=teams_list, org=request.profile.org) - task_obj.teams.add(*teams) - - task_obj.assigned_to.clear() - if params.get("assigned_to"): - assinged_to_list = params.get("assigned_to") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org, is_active=True - ) - task_obj.assigned_to.add(*profiles) - - return Response( - {"error": False, "message": "Task updated Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, **kwargs): - self.object = self.get_object(pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == self.object.created_by - ): - self.object.delete() - return Response( - {"error": False, "message": "Task deleted Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": "you don't have permission to delete this task"}, - status=status.HTTP_403_FORBIDDEN, - ) - - -class TaskCommentView(APIView): - model = Comment - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_object(self, pk): - return self.model.objects.get(pk=pk) - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params,request=TaskCommentEditSwaggerSerializer - ) - def put(self, request, pk, format=None): - params = request.data - obj = self.get_object(pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == obj.commented_by - ): - serializer = CommentSerializer(obj, data=params) - if params.get("comment"): - if serializer.is_valid(): - serializer.save() - return Response( - {"error": False, "message": "Comment Submitted"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, format=None): - self.object = self.get_object(pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == self.object.commented_by - ): - self.object.delete() - return Response( - {"error": False, "message": "Comment Deleted Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) - - -class TaskAttachmentView(APIView): - model = Attachments - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - @extend_schema( - tags=["Tasks"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, format=None): - self.object = self.model.objects.get(pk=pk) - if ( - request.profile.role == "ADMIN" - or request.profile.is_admin - or request.profile == self.object.created_by - ): - self.object.delete() - return Response( - {"error": False, "message": "Attachment Deleted Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - { - "error": True, - "errors": "You don't have Permission to perform this action", - }, - status=status.HTTP_403_FORBIDDEN, - ) diff --git a/teams/__init__.py b/teams/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/teams/admin.py b/teams/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/teams/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/teams/apps.py b/teams/apps.py deleted file mode 100644 index 2cbcff72..00000000 --- a/teams/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class TeamsConfig(AppConfig): - name = "teams" diff --git a/teams/migrations/0001_initial.py b/teams/migrations/0001_initial.py deleted file mode 100644 index 37a5f807..00000000 --- a/teams/migrations/0001_initial.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Teams', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=100)), - ('description', models.TextField()), - ('created_on', models.DateTimeField(auto_now_add=True, verbose_name='Created on')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teams_created', to='common.profile')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.org')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('users', models.ManyToManyField(related_name='user_teams', to='common.profile')), - ], - options={ - 'verbose_name': 'Team', - 'verbose_name_plural': 'Teams', - 'db_table': 'eams', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/teams/migrations/0002_alter_teams_table.py b/teams/migrations/0002_alter_teams_table.py deleted file mode 100644 index e4fe82d4..00000000 --- a/teams/migrations/0002_alter_teams_table.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-12 09:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('teams', '0001_initial'), - ] - - operations = [ - migrations.AlterModelTable( - name='teams', - table='teams', - ), - ] diff --git a/teams/migrations/0003_alter_teams_created_by.py b/teams/migrations/0003_alter_teams_created_by.py deleted file mode 100644 index 8acdbecc..00000000 --- a/teams/migrations/0003_alter_teams_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-24 08:19 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('teams', '0002_alter_teams_table'), - ] - - operations = [ - migrations.AlterField( - model_name='teams', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/teams/migrations/__init__.py b/teams/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/teams/models.py b/teams/models.py deleted file mode 100644 index 41dc6f31..00000000 --- a/teams/models.py +++ /dev/null @@ -1,34 +0,0 @@ -import arrow -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from common.models import Org, Profile -from common.base import BaseModel - - - -class Teams(BaseModel): - name = models.CharField(max_length=100) - description = models.TextField() - users = models.ManyToManyField(Profile, related_name="user_teams") - created_on = models.DateTimeField(_("Created on"), auto_now_add=True) - org = models.ForeignKey(Org, on_delete=models.SET_NULL, null=True, blank=True) - - class Meta: - verbose_name = "Team" - verbose_name_plural = "Teams" - db_table = "teams" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.name}" - - @property - def created_on_arrow(self): - return arrow.get(self.created_on).humanize() - - def get_users(self): - return ",".join( - [str(_id) for _id in list(self.users.values_list("id", flat=True))] - ) - # return ','.join(list(self.users.values_list('id', flat=True))) diff --git a/teams/serializer.py b/teams/serializer.py deleted file mode 100644 index fc6a8b63..00000000 --- a/teams/serializer.py +++ /dev/null @@ -1,67 +0,0 @@ -from rest_framework import serializers - -from common.serializer import ProfileSerializer,UserSerializer -from teams.models import Teams - - -class TeamsSerializer(serializers.ModelSerializer): - users = ProfileSerializer(read_only=True, many=True) - created_by = UserSerializer() - - class Meta: - model = Teams - fields = ( - "id", - "name", - "description", - "users", - "created_at", - "created_by", - "created_on_arrow", - ) - - -class TeamCreateSerializer(serializers.ModelSerializer): - - def __init__(self, *args, **kwargs): - request_obj = kwargs.pop("request_obj", None) - super().__init__(*args, **kwargs) - self.org = request_obj.profile.org - - self.fields["name"].required = True - self.fields["description"].required = False - - def validate_name(self, name): - if self.instance: - if ( - Teams.objects.filter(name__iexact=name, org=self.org) - .exclude(id=self.instance.id) - .exists() - ): - raise serializers.ValidationError("Team already exists with this name") - else: - if Teams.objects.filter(name__iexact=name).exists(): - raise serializers.ValidationError("Team already exists with this name") - return name - - class Meta: - model = Teams - fields = ( - "name", - "description", - "created_at", - "created_by", - "created_on_arrow", - "org", - ) - -class TeamswaggerCreateSerializer(serializers.ModelSerializer): - - - class Meta: - model = Teams - fields = ( - "name", - "description", - "users", - ) \ No newline at end of file diff --git a/teams/swagger_params1.py b/teams/swagger_params1.py deleted file mode 100644 index 7f146a42..00000000 --- a/teams/swagger_params1.py +++ /dev/null @@ -1,17 +0,0 @@ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter - -organization_params_in_header = organization_params_in_header = OpenApiParameter( - "org", OpenApiTypes.STR, OpenApiParameter.HEADER -) - -organization_params = [ - organization_params_in_header, -] - -teams_list_get_params = [ - organization_params_in_header, - OpenApiParameter("team_name", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("created_by", OpenApiTypes.STR,OpenApiParameter.QUERY), - OpenApiParameter("assigned_users", OpenApiTypes.STR,OpenApiParameter.QUERY), -] diff --git a/teams/tasks.py b/teams/tasks.py deleted file mode 100644 index f7623e85..00000000 --- a/teams/tasks.py +++ /dev/null @@ -1,146 +0,0 @@ -from celery import Celery - -from common.models import Profile -from teams.models import Teams - -app = Celery("redis://") - - -@app.task -def remove_users(removed_users_list, team_id): - removed_users_list = [i for i in removed_users_list if i.isdigit()] - users_list = Profile.objects.filter(id__in=removed_users_list) - if users_list.exists(): - team = Teams.objects.filter(id=team_id).first() - if team: - accounts = team.account_teams.all() - for account in accounts: - for user in users_list: - account.assigned_to.remove(user) - - # for contacts - contacts = team.contact_teams.all() - for contact in contacts: - for user in users_list: - contact.assigned_to.remove(user) - - # for leads - leads = team.lead_teams.all() - for lead in leads: - for user in users_list: - lead.assigned_to.remove(user) - - # for opportunities - opportunities = team.oppurtunity_teams.all() - for opportunity in opportunities: - for user in users_list: - opportunity.assigned_to.remove(user) - - # for cases - cases = team.cases_teams.all() - for case in cases: - for user in users_list: - case.assigned_to.remove(user) - - # for documents - docs = team.document_teams.all() - for doc in docs: - for user in users_list: - doc.shared_to.remove(user) - - # for tasks - tasks = team.tasks_teams.all() - for task in tasks: - for user in users_list: - task.assigned_to.remove(user) - - # for invoices - invoices = team.invoices_teams.all() - for invoice in invoices: - for user in users_list: - invoice.assigned_to.remove(user) - - # for events - events = team.event_teams.all() - for event in events: - for user in users_list: - event.assigned_to.remove(user) - - -@app.task -def update_team_users(team_id): - """this function updates assigned_to field on all models when a team is updated""" - team = Teams.objects.filter(id=team_id).first() - if team: - teams_members = team.users.all() - # for accounts - accounts = team.account_teams.all() - for account in accounts: - account_assigned_to_users = account.assigned_to.all() - for team_member in teams_members: - if team_member not in account_assigned_to_users: - account.assigned_to.add(team_member) - - # for contacts - contacts = team.contact_teams.all() - for contact in contacts: - contact_assigned_to_users = contact.assigned_to.all() - for team_member in teams_members: - if team_member not in contact_assigned_to_users: - contact.assigned_to.add(team_member) - - # for leads - leads = team.lead_teams.all() - for lead in leads: - lead_assigned_to_users = lead.assigned_to.all() - for team_member in teams_members: - if team_member not in lead_assigned_to_users: - lead.assigned_to.add(team_member) - - # for opportunities - opportunities = team.oppurtunity_teams.all() - for opportunity in opportunities: - opportunity_assigned_to_users = opportunity.assigned_to.all() - for team_member in teams_members: - if team_member not in opportunity_assigned_to_users: - opportunity.assigned_to.add(team_member) - - # for cases - cases = team.cases_teams.all() - for case in cases: - case_assigned_to_users = case.assigned_to.all() - for team_member in teams_members: - if team_member not in case_assigned_to_users: - case.assigned_to.add(team_member) - - # for documents - docs = team.document_teams.all() - for doc in docs: - doc_assigned_to_users = doc.shared_to.all() - for team_member in teams_members: - if team_member not in doc_assigned_to_users: - doc.shared_to.add(team_member) - - # for tasks - tasks = team.tasks_teams.all() - for task in tasks: - task_assigned_to_users = task.assigned_to.all() - for team_member in teams_members: - if team_member not in task_assigned_to_users: - task.assigned_to.add(team_member) - - # for invoices - invoices = team.invoices_teams.all() - for invoice in invoices: - invoice_assigned_to_users = invoice.assigned_to.all() - for team_member in teams_members: - if team_member not in invoice_assigned_to_users: - invoice.assigned_to.add(team_member) - - # for events - events = team.event_teams.all() - for event in events: - event_assigned_to_users = event.assigned_to.all() - for team_member in teams_members: - if team_member not in event_assigned_to_users: - event.assigned_to.add(team_member) diff --git a/teams/urls.py b/teams/urls.py deleted file mode 100644 index 1a23bbea..00000000 --- a/teams/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path - -from teams import views - -app_name = "api_leads" - -urlpatterns = [ - path("", views.TeamsListView.as_view()), - path("/", views.TeamsDetailView.as_view()), -] diff --git a/teams/views.py b/teams/views.py deleted file mode 100644 index b182adec..00000000 --- a/teams/views.py +++ /dev/null @@ -1,191 +0,0 @@ -import json -from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema - -#from common.external_auth import CustomDualAuthentication -from rest_framework import status -from rest_framework.pagination import LimitOffsetPagination -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView - -from common.models import Profile -from teams import swagger_params1 -from teams.models import Teams -from teams.serializer import TeamCreateSerializer, TeamsSerializer,TeamswaggerCreateSerializer -from teams.tasks import remove_users, update_team_users - - -class TeamsListView(APIView, LimitOffsetPagination): - model = Teams - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_context_data(self, **kwargs): - params = self.request.query_params - queryset = self.model.objects.filter(org=self.request.profile.org).order_by("-id") - if params: - if params.get("team_name"): - queryset = queryset.filter(name__icontains=params.get("team_name")) - if params.get("created_by"): - queryset = queryset.filter(created_by=params.get("created_by")) - if params.get("assigned_users"): - queryset = queryset.filter( - users__id__in=params.get("assigned_users") - ) - - context = {} - results_teams = self.paginate_queryset( - queryset.distinct(), self.request, view=self - ) - teams = TeamsSerializer(results_teams, many=True).data - if results_teams: - offset = queryset.filter(id__gte=results_teams[-1].id).count() - if offset == queryset.count(): - offset = None - else: - offset = 0 - context["per_page"] = 10 - page_number = (int(self.offset / 10) + 1,) - context["page_number"] = page_number - context.update({"teams_count": self.count, "offset": offset}) - context["teams"] = teams - return context - - @extend_schema( - tags=["Teams"], parameters=swagger_params1.teams_list_get_params - ) - def get(self, *args, **kwargs): - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - return Response( - { - "error": True, - "errors": "You don't have permission to perform this action.", - }, - status=status.HTTP_403_FORBIDDEN, - ) - context = self.get_context_data(**kwargs) - return Response(context) - - @extend_schema( - tags=["Teams"], request=TeamswaggerCreateSerializer,parameters=swagger_params1.organization_params - ) - def post(self, request, *args, **kwargs): - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - return Response( - { - "error": True, - "errors": "You don't have permission to perform this action.", - }, - status=status.HTTP_403_FORBIDDEN, - ) - params = self.request.data - print(request,self.request) - serializer = TeamCreateSerializer(data=params, request_obj=request) - if serializer.is_valid(): - team_obj = serializer.save(org=request.profile.org) - - if params.get("assign_users"): - assinged_to_list = params.get("users") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org - ) - if profiles: - team_obj.users.add(*profiles) - return Response( - {"error": False, "message": "Team Created Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class TeamsDetailView(APIView): - model = Teams - #authentication_classes = (CustomDualAuthentication,) - permission_classes = (IsAuthenticated,) - - def get_object(self, pk): - return self.model.objects.get(pk=pk, org=self.request.profile.org) - - @extend_schema( - tags=["Teams"], parameters=swagger_params1.organization_params - ) - def get(self, request, pk, **kwargs): - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - return Response( - { - "error": True, - "errors": "You don't have permission to perform this action.", - }, - status=status.HTTP_403_FORBIDDEN, - ) - self.team_obj = self.get_object(pk) - context = {} - context["team"] = TeamsSerializer(self.team_obj).data - return Response(context) - - @extend_schema( - tags=["Teams"], request=TeamswaggerCreateSerializer,parameters=swagger_params1.organization_params - ) - def put(self, request, pk, *args, **kwargs): - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - return Response( - { - "error": True, - "errors": "You don't have permission to perform this action.", - }, - status=status.HTTP_403_FORBIDDEN, - ) - params = request.data - self.team = self.get_object(pk) - actual_users = self.team.get_users() - removed_users = [] - serializer = TeamCreateSerializer( - data=params, instance=self.team, request_obj=request - ) - if serializer.is_valid(): - team_obj = serializer.save() - - team_obj.users.clear() - if params.get("assign_users"): - assinged_to_list = params.get("assign_users") - profiles = Profile.objects.filter( - id__in=assinged_to_list, org=request.profile.org - ) - if profiles: - team_obj.users.add(*profiles) - update_team_users.delay(pk) - latest_users = team_obj.get_users() - for user in actual_users: - if user not in latest_users: - removed_users.append(user) - remove_users.delay(removed_users, pk) - return Response( - {"error": False, "message": "Team Updated Successfully"}, - status=status.HTTP_200_OK, - ) - return Response( - {"error": True, "errors": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - @extend_schema( - tags=["Teams"], parameters=swagger_params1.organization_params - ) - def delete(self, request, pk, **kwargs): - if self.request.profile.role != "ADMIN" and not self.request.profile.is_admin: - return Response( - { - "error": True, - "errors": "You don't have permission to perform this action.", - }, - status=status.HTTP_403_FORBIDDEN, - ) - self.team_obj = self.get_object(pk) - self.team_obj.delete() - return Response( - {"error": False, "message": "Team Deleted Successfully"}, - status=status.HTTP_200_OK, - ) diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index 76d85892..00000000 --- a/templates/base.html +++ /dev/null @@ -1,99 +0,0 @@ - -{% load static %} -{% load compress %} -{% load thumbnail %} - - - - - - - {{APPLICATION_NAME}} - - - - - {% compress css %} - - - {% endcompress %} - {% block extralinks %} {% endblock %} - - -
    - -
    - -
    - -
    - - {% block breadcrumb %} - {% endblock %} - - {% block content %} - {% endblock %} -
    - -
    - - {% block js_block %} - {% endblock js_block %} - - - diff --git a/templates/cms/blog/blog_detail_page.html b/templates/cms/blog/blog_detail_page.html deleted file mode 100644 index 3959083a..00000000 --- a/templates/cms/blog/blog_detail_page.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends 'cms_base.html' %} -{% load wagtailcore_tags %} -{% block content %} - - - - -
    -
    -

    {{ page.title }}

    -
      -
    • Posted on: {{ self.first_published_at }}
    • -
    -
    -
    - - - -
    -
    -
    -
    -
    - {% comment %}
    -
    - image -
    -

    Quuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quia non numquam eius modi tempora incidunt ut labore et dolore magnam dolor sit amet, consectetur adipisicing.

    -

    Four major elements that we offer:

    -
    {% endcomment %} - {% for block in page.content %} - {% include_block block %} - {% endfor %} -
    -
    -
    -
    -
    - - - -
    - -{% endblock content %} \ No newline at end of file diff --git a/templates/cms/blog/blog_list_page.html b/templates/cms/blog/blog_list_page.html deleted file mode 100644 index 6f24f2f4..00000000 --- a/templates/cms/blog/blog_list_page.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends 'cms_base.html' %} -{% load wagtailcore_tags %} -{% block content %} - - -
    -
    -
    - NEWS & ARTICLES -

    Learn More From Our Latest Blog

    -
    - -
    - {% for post in posts %} -
    -
    - {% comment %}
    - - image - -
    {% endcomment %} - -
    -
      -
    • {{ self.first_published_at }}
    • - {% comment %}
    • Shelley Percy
    • {% endcomment %} -
    -

    {{ post.title }}

    -

    {{ post.excerpt|safe }}

    - Read More -
    - -
    -
    - {% empty %} -

    No blog posts

    postpage - - {% endfor %} - -
    -
    - {% if posts.has_previous %} - - {% endif %} - - {% for page_num in posts.paginator.page_range %} - {{ page_num }} - {% endfor %} - - {% if posts.has_next %} - - {% endif %} -
    -
    - - -
    -
    -
    - - - - -
    -{% endblock content %} \ No newline at end of file diff --git a/templates/cms/home_page.html b/templates/cms/home_page.html deleted file mode 100644 index 16910488..00000000 --- a/templates/cms/home_page.html +++ /dev/null @@ -1,14 +0,0 @@ - -{% extends 'cms_base.html' %} -{% load wagtailcore_tags static %} - -{% block content %} - - {% for block in page.content %} - -
    {% include_block block %}
    - - {% endfor %} - -{% endblock content %} - \ No newline at end of file diff --git a/templates/cms/streams/aboutapp.html b/templates/cms/streams/aboutapp.html deleted file mode 100644 index dab21582..00000000 --- a/templates/cms/streams/aboutapp.html +++ /dev/null @@ -1,39 +0,0 @@ -{% load wagtailimages_tags static %} - - -
    -
    -
    -

    {{ self.heading }}

    -

    {{ self.paragraph }}

    -
    - -
    -
    -
    -

    {{ self.sub_heading }}

    -

    {{ self.sub_paragraph }}

    - -
      - {% for link in self.links %} -
    • {{ link.text }}
    • - {% endfor %} -
    - {% if self.cta_url %} - {{ self.cta_text }} - {% else %} - {{ self.cta_text }} - {% endif %} - -
    -
    - -
    -
    - image -
    -
    -
    -
    -
    - \ No newline at end of file diff --git a/templates/cms/streams/app_features.html b/templates/cms/streams/app_features.html deleted file mode 100644 index 96634689..00000000 --- a/templates/cms/streams/app_features.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load wagtailcore_tags static %} - - -
    -
    -
    -

    {{ self.heading }}

    -

    {{ self.description }}

    -
    - -
    - {% for card in self.cards %} -
    -
    - -

    {{ card.card_heading }}s

    -

    {{ card.card_description }}

    -
    -
    - {% endfor %} -
    -
    -
    image
    -
    - diff --git a/templates/cms/streams/blog/code_block.html b/templates/cms/streams/blog/code_block.html deleted file mode 100644 index 163867a1..00000000 --- a/templates/cms/streams/blog/code_block.html +++ /dev/null @@ -1,2 +0,0 @@ - -
    {{ self }}
    \ No newline at end of file diff --git a/templates/cms/streams/blog/content_title.html b/templates/cms/streams/blog/content_title.html deleted file mode 100644 index 2fdc8309..00000000 --- a/templates/cms/streams/blog/content_title.html +++ /dev/null @@ -1 +0,0 @@ -

    {{ self }}

    \ No newline at end of file diff --git a/templates/cms/streams/blog/image.html b/templates/cms/streams/blog/image.html deleted file mode 100644 index dc3ee941..00000000 --- a/templates/cms/streams/blog/image.html +++ /dev/null @@ -1,4 +0,0 @@ -{% load wagtailimages_tags %} - -{% image self scale-100 as tmp_photo %} -
    {{ tmp_photo.alt }}
    \ No newline at end of file diff --git a/templates/cms/streams/blog/rich_text.html b/templates/cms/streams/blog/rich_text.html deleted file mode 100644 index 4874df5d..00000000 --- a/templates/cms/streams/blog/rich_text.html +++ /dev/null @@ -1 +0,0 @@ -

    {{ self }}

    \ No newline at end of file diff --git a/templates/cms/streams/carousel.html b/templates/cms/streams/carousel.html deleted file mode 100644 index 9f1dfa4d..00000000 --- a/templates/cms/streams/carousel.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load wagtailcore_tags wagtailimages_tags static %} - -
    -
    -
    - -
    -
    -
    - \ No newline at end of file diff --git a/templates/cms/streams/count.html b/templates/cms/streams/count.html deleted file mode 100644 index 4a4cf205..00000000 --- a/templates/cms/streams/count.html +++ /dev/null @@ -1,20 +0,0 @@ - - -
    -
    -
    -
    - {% for link in self.links %} -
    -
    -

    {{ link.count_value }}

    - {{ link.text }} -
    -
    - {% endfor %} - -
    -
    -
    -
    - \ No newline at end of file diff --git a/templates/cms/streams/home_blog.html b/templates/cms/streams/home_blog.html deleted file mode 100644 index bf1287cc..00000000 --- a/templates/cms/streams/home_blog.html +++ /dev/null @@ -1,38 +0,0 @@ -{% load wagtailimages_tags static %} - - -
    -
    -
    -

    {{ self.heading }}

    -

    {{ self.description }}

    -
    - -
    - {% for card in self.cards %} - - {% endfor %} -
    -
    -
    - - - diff --git a/templates/cms/streams/left_image_right_content.html b/templates/cms/streams/left_image_right_content.html deleted file mode 100644 index 5909f1f8..00000000 --- a/templates/cms/streams/left_image_right_content.html +++ /dev/null @@ -1,44 +0,0 @@ -{% load wagtailimages_tags static %} - - -
    -
    -
    -
    -
    - {% image self.image scale-100 as photo %} - image -
    -
    - -
    -
    - DOWNLOAD APP -

    {{ self.heading }}

    -

    {{ self.paragraph }}

    - - -
    -
    -
    -
    -
    - \ No newline at end of file diff --git a/templates/cms/streams/pricing.html b/templates/cms/streams/pricing.html deleted file mode 100644 index c168e844..00000000 --- a/templates/cms/streams/pricing.html +++ /dev/null @@ -1,36 +0,0 @@ - - - -
    -
    -
    -

    {{ self.heading }}

    -

    {{ self.description }}

    -
    - -
    - {% for card in self.cards %} -
    -
    -
    -

    {{ card.card_heading }}

    - {{ card.card_price }}/year -
    -
      -
    • {{ card.card_description }}
    • - -
    - {% if card.cta_url %} - {{ card.cta_text }} - {% else %} - {{ card.cta_text }} - {% endif %} -
    -
    - {% endfor %} - - -
    -
    -
    - \ No newline at end of file diff --git a/templates/cms/streams/right_image_left_content.html b/templates/cms/streams/right_image_left_content.html deleted file mode 100644 index e8394fc6..00000000 --- a/templates/cms/streams/right_image_left_content.html +++ /dev/null @@ -1,42 +0,0 @@ -{% load wagtailimages_tags static %} - - -
    -
    -
    -
    -
    -

    {{ self.heading }}

    -

    {{ self.paragraph }}

    - -
    -
    - -
    -
    - {% image self.image scale-100 as photo %} - image -
    -
    -
    -
    -
    - \ No newline at end of file diff --git a/templates/cms_base.html b/templates/cms_base.html deleted file mode 100644 index 49b3d265..00000000 --- a/templates/cms_base.html +++ /dev/null @@ -1,325 +0,0 @@ - -{% load static menus_tags wagtailimages_tags %} -{% get_menu "navbar" as media %} -{% get_menu "footer" as footer %} - - - - - - - - - - - - - - - - - - - - - - - - Noke - Creative Multipurpose HTML Template - - - - - - - - - - {% block content %} - - {% endblock content %} - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/templates/root.html b/templates/root.html deleted file mode 100644 index ed4b8cbf..00000000 --- a/templates/root.html +++ /dev/null @@ -1,208 +0,0 @@ -{% load static %} -{% load compress %} -{% load thumbnail %} - - - - - - - - - {{ settings.APPLICATION_NAME }} - - - - - - - - - - - - - - - - - - - {% compress css %} - - - - {% endcompress %} - - {% block extralinks %} {% endblock %} - - - -
    - {% block header %} -
    - -
    - {% endblock %} - -
    - {% block breadcrumb %} - {% endblock %} - - {% block content %} - {% endblock %} -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% block js_block %}{% endblock js_block %} - - - -