| @ -0,0 +1,3 @@ | |||
| Dockerfile | |||
| Makefile | |||
| volcadossql/ | |||
| @ -0,0 +1,73 @@ | |||
| # syntax=docker/dockerfile:1 | |||
| ################## | |||
| # BUILDER # | |||
| ################## | |||
| FROM python:3.11.4-slim-buster AS builder | |||
| # set work directory | |||
| WORKDIR /app | |||
| # set environment variables | |||
| ENV PYTHONDONTWRITEBYTECODE=1 | |||
| ENV PYTHONUNBUFFERED=1 | |||
| # install system dependencies | |||
| RUN apt-get update && \ | |||
| apt-get install -y --no-install-recommends gcc | |||
| # lint | |||
| RUN pip install --upgrade pip | |||
| RUN pip install flake8==6.0.0 | |||
| COPY . /app/ | |||
| RUN flake8 --ignore=E501,F401,E126 . | |||
| COPY ./requirements.txt . | |||
| RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt | |||
| ################## | |||
| # FINAL # | |||
| ################## | |||
| FROM python:3.11.4-slim-buster | |||
| # create directory for the app user | |||
| RUN mkdir -p /app | |||
| # create the app user | |||
| #RUN addgroup --system app && adduser --system --group app | |||
| # create the appropriate directories | |||
| ENV APP_HOME=/app | |||
| RUN mkdir -p $APP_HOME | |||
| #RUN mkdir -p $APP_HOME/staticfiles | |||
| #RUN mkdir -p $APP_HOME/mediafiles | |||
| WORKDIR $APP_HOME | |||
| # install system dependencies | |||
| RUN apt-get update && apt-get install -y sqlite3 netcat | |||
| COPY --from=builder /app/wheels /wheels | |||
| COPY --from=builder /app/requirements.txt . | |||
| RUN pip install --upgrade pip | |||
| RUN pip install --no-cache /wheels/* | |||
| # copy entrypoint.sh | |||
| COPY ./entrypoint.sh . | |||
| # copy project | |||
| COPY . $APP_HOME | |||
| # chown all the files to the app user | |||
| #RUN chown -R app:app $APP_HOME | |||
| # change to the app user | |||
| #USER app | |||
| WORKDIR $APP_HOME/biblioteca | |||
| # run entrypoint.sh | |||
| ENTRYPOINT ["/app/entrypoint.sh"] | |||
| @ -0,0 +1,48 @@ | |||
| export ARQUITECTURA := $(shell lscpu |grep itectur | tr -d ' '| cut -f2 -d':') | |||
| export IMG_VERSION = 1.43 | |||
| export IMG_NGINX_VERSION = 1.17 | |||
| # limpia todo | |||
| all: imagen nginx clean install | |||
| imagen: | |||
| cd ../; make | |||
| install: | |||
| -kubectl create -f namespace.yaml | |||
| -kubectl create -f reg-secret.yaml | |||
| -kubectl create -f env-prod-configmap.yaml | |||
| -kubectl create -f env-prod-db-configmap.yaml | |||
| -kubectl create -f pv-local-libros.yaml | |||
| -kubectl create -f libros-prod-persistentvolumeclaim.yaml | |||
| -kubectl create -f static-volume-persistentvolumeclaim.yaml | |||
| -kubectl create -f postgres-data-persistentvolumeclaim.yaml | |||
| -kubectl create -f db-deployment.yaml | |||
| -kubectl create -f db-service.yaml | |||
| -envsubst < libros-deployment.yaml |kubectl create -f - | |||
| -envsubst < nginx-deployment.yaml |kubectl create -f - | |||
| -kubectl create -f nginx-service.yaml | |||
| clean: | |||
| -envsubst < nginx-deployment.yaml |kubectl delete -f - | |||
| -kubectl delete -f nginx-service.yaml | |||
| -envsubst < libros-deployment.yaml |kubectl delete -f - | |||
| -kubectl delete -f db-deployment.yaml | |||
| -kubectl delete -f db-service.yaml | |||
| -kubectl delete -f env-prod-configmap.yaml | |||
| -kubectl delete -f env-prod-db-configmap.yaml | |||
| -kubectl delete -f postgres-data-persistentvolumeclaim.yaml | |||
| -kubectl delete -f static-volume-persistentvolumeclaim.yaml | |||
| -kubectl delete -f libros-prod-persistentvolumeclaim.yaml | |||
| -kubectl delete -f pv-local-libros.yaml | |||
| -kubectl delete -f reg-secret.yaml | |||
| -kubectl delete -f namespace.yaml | |||
| nginx: | |||
| cd ../nginx; make | |||
| @ -0,0 +1,52 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| annotations: | |||
| kompose.cmd: kompose convert | |||
| kompose.version: 1.34.0 (cbf2835db) | |||
| labels: | |||
| io.kompose.service: db | |||
| name: db | |||
| namespace: libros | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| io.kompose.service: db | |||
| strategy: | |||
| type: Recreate | |||
| template: | |||
| metadata: | |||
| annotations: | |||
| kompose.cmd: kompose convert | |||
| kompose.version: 1.34.0 (cbf2835db) | |||
| labels: | |||
| io.kompose.service: db | |||
| spec: | |||
| containers: | |||
| - env: | |||
| - name: POSTGRES_DB | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: POSTGRES_DB | |||
| name: env-prod-db | |||
| - name: POSTGRES_PASSWORD | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: POSTGRES_PASSWORD | |||
| name: env-prod-db | |||
| - name: POSTGRES_USER | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: POSTGRES_USER | |||
| name: env-prod-db | |||
| image: postgres:15 | |||
| name: db | |||
| volumeMounts: | |||
| - mountPath: /var/lib/postgresql/data | |||
| name: postgres-data | |||
| restartPolicy: Always | |||
| volumes: | |||
| - name: postgres-data | |||
| persistentVolumeClaim: | |||
| claimName: postgres-data | |||
| @ -0,0 +1,17 @@ | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| annotations: | |||
| kompose.cmd: kompose convert | |||
| kompose.version: 1.34.0 (cbf2835db) | |||
| labels: | |||
| io.kompose.service: db | |||
| name: db | |||
| namespace: libros | |||
| spec: | |||
| ports: | |||
| - name: "5432" | |||
| port: 5432 | |||
| targetPort: 5432 | |||
| selector: | |||
| io.kompose.service: db | |||
| @ -0,0 +1 @@ | |||
| kubectl -n libros exec -ti deployment.apps/libros -- /bin/bash | |||
| @ -0,0 +1 @@ | |||
| kubectl -n libros exec -ti deployment.apps/db -- psql --username=creylopez --dbname=libros | |||
| @ -0,0 +1,19 @@ | |||
| apiVersion: v1 | |||
| data: | |||
| DEBUG: "0" | |||
| DJANGO_ALLOWED_HOSTS: "libros.reymota.es k8s-server localhost 127.0.0.1 [::1]" | |||
| CSRF_TRUSTED_ORIGINS: "https://libros.reymota.es" | |||
| SECRET_KEY: change_me | |||
| SQL_DATABASE: libros | |||
| SQL_ENGINE: django.db.backends.postgresql | |||
| SQL_HOST: db | |||
| SQL_PASSWORD: Dsa-0213 | |||
| SQL_PORT: "5432" | |||
| SQL_USER: creylopez | |||
| DATABASE: postgres | |||
| kind: ConfigMap | |||
| metadata: | |||
| labels: | |||
| io.kompose.service: web-env-prod | |||
| name: env-prod | |||
| namespace: libros | |||
| @ -0,0 +1,11 @@ | |||
| apiVersion: v1 | |||
| data: | |||
| POSTGRES_DB: libros | |||
| POSTGRES_PASSWORD: Dsa-0213 | |||
| POSTGRES_USER: creylopez | |||
| kind: ConfigMap | |||
| metadata: | |||
| labels: | |||
| io.kompose.service: db-env-prod-db | |||
| name: env-prod-db | |||
| namespace: libros | |||
| @ -0,0 +1,124 @@ | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: libros | |||
| namespace: libros | |||
| spec: | |||
| ports: | |||
| - name: "8000" | |||
| port: 8000 | |||
| targetPort: 8000 | |||
| selector: | |||
| app: libros | |||
| --- | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: libros | |||
| namespace: libros | |||
| labels: | |||
| app: libros | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: libros | |||
| strategy: | |||
| type: Recreate | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: libros | |||
| spec: | |||
| containers: | |||
| - args: | |||
| - gunicorn | |||
| - biblioteca.wsgi:application | |||
| - --bind | |||
| - 0.0.0.0:8000 | |||
| name: libros | |||
| image: registry.reymota.es/libros-$ARQUITECTURA:$IMG_VERSION | |||
| env: | |||
| - name: VERSION | |||
| value: "$IMG_VERSION" | |||
| - name: ENVIRONMENT | |||
| value: "Producción" | |||
| - name: DEBUG | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: DEBUG | |||
| name: env-prod | |||
| - name: DJANGO_ALLOWED_HOSTS | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: DJANGO_ALLOWED_HOSTS | |||
| name: env-prod | |||
| - name: CSRF_TRUSTED_ORIGINS | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: CSRF_TRUSTED_ORIGINS | |||
| name: env-prod | |||
| - name: SECRET_KEY | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: SECRET_KEY | |||
| name: env-prod | |||
| - name: DATABASE | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: DATABASE | |||
| name: env-prod | |||
| - name: SQL_HOST | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: SQL_HOST | |||
| name: env-prod | |||
| - name: SQL_PORT | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: SQL_PORT | |||
| name: env-prod | |||
| - name: SQL_ENGINE | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: SQL_ENGINE | |||
| name: env-prod | |||
| - name: SQL_DATABASE | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: SQL_DATABASE | |||
| name: env-prod | |||
| - name: SQL_USER | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: SQL_USER | |||
| name: env-prod | |||
| - name: SQL_PASSWORD | |||
| valueFrom: | |||
| configMapKeyRef: | |||
| key: SQL_PASSWORD | |||
| name: env-prod | |||
| ports: | |||
| - containerPort: 8000 | |||
| protocol: TCP | |||
| volumeMounts: | |||
| - mountPath: /app/biblioteca/mediafiles | |||
| name: libros-media | |||
| - mountPath: /app/gestion/migrations | |||
| name: libros-migrations | |||
| - mountPath: /app/biblioteca/staticfiles | |||
| name: static-volume | |||
| imagePullSecrets: | |||
| - name: myregistrykey | |||
| restartPolicy: Always | |||
| volumes: | |||
| - name: libros-media | |||
| persistentVolumeClaim: | |||
| claimName: libros-media | |||
| - name: libros-migrations | |||
| persistentVolumeClaim: | |||
| claimName: libros-migrations | |||
| - name: static-volume | |||
| persistentVolumeClaim: | |||
| claimName: static-volume | |||
| status: {} | |||
| @ -0,0 +1,32 @@ | |||
| apiVersion: v1 | |||
| kind: PersistentVolumeClaim | |||
| metadata: | |||
| creationTimestamp: null | |||
| labels: | |||
| io.kompose.service: libros-media | |||
| name: libros-media | |||
| namespace: libros | |||
| spec: | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| resources: | |||
| requests: | |||
| storage: 100Mi | |||
| status: {} | |||
| --- | |||
| apiVersion: v1 | |||
| kind: PersistentVolumeClaim | |||
| metadata: | |||
| creationTimestamp: null | |||
| labels: | |||
| io.kompose.service: libros-migrations | |||
| name: libros-migrations | |||
| namespace: libros | |||
| spec: | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| resources: | |||
| requests: | |||
| storage: 50Mi | |||
| status: {} | |||
| @ -0,0 +1,7 @@ | |||
| ################################################### | |||
| # Namespace Libros | |||
| ################################################### | |||
| apiVersion: v1 | |||
| kind: Namespace | |||
| metadata: | |||
| name: libros | |||
| @ -0,0 +1,46 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| annotations: | |||
| kompose.cmd: kompose convert | |||
| kompose.version: 1.34.0 (cbf2835db) | |||
| labels: | |||
| io.kompose.service: nginx | |||
| name: nginx | |||
| namespace: libros | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| io.kompose.service: nginx | |||
| strategy: | |||
| type: Recreate | |||
| template: | |||
| metadata: | |||
| annotations: | |||
| kompose.cmd: kompose convert | |||
| kompose.version: 1.34.0 (cbf2835db) | |||
| labels: | |||
| io.kompose.service: nginx | |||
| spec: | |||
| containers: | |||
| - image: registry.reymota.es/nginx-libros-$ARQUITECTURA:$IMG_NGINX_VERSION | |||
| name: nginx | |||
| ports: | |||
| - containerPort: 80 | |||
| protocol: TCP | |||
| volumeMounts: | |||
| - mountPath: /app/biblioteca/staticfiles | |||
| name: static-volume | |||
| - mountPath: /app/biblioteca/mediafiles | |||
| name: libros-media | |||
| imagePullSecrets: | |||
| - name: myregistrykey | |||
| restartPolicy: Always | |||
| volumes: | |||
| - name: static-volume | |||
| persistentVolumeClaim: | |||
| claimName: static-volume | |||
| - name: libros-media | |||
| persistentVolumeClaim: | |||
| claimName: libros-media | |||
| @ -0,0 +1,20 @@ | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| annotations: | |||
| kompose.cmd: kompose convert | |||
| kompose.version: 1.34.0 (cbf2835db) | |||
| labels: | |||
| io.kompose.service: nginx | |||
| name: nginx | |||
| namespace: libros | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - name: "1337" | |||
| port: 1337 | |||
| nodePort: 30343 | |||
| targetPort: 80 | |||
| selector: | |||
| io.kompose.service: nginx | |||
| @ -0,0 +1,13 @@ | |||
| apiVersion: v1 | |||
| kind: PersistentVolumeClaim | |||
| metadata: | |||
| labels: | |||
| io.kompose.service: postgres-data | |||
| name: postgres-data | |||
| namespace: libros | |||
| spec: | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| resources: | |||
| requests: | |||
| storage: 100Mi | |||
| @ -0,0 +1,59 @@ | |||
| apiVersion: v1 | |||
| kind: PersistentVolume | |||
| metadata: | |||
| name: libros-media-folder | |||
| namespace: libros | |||
| labels: | |||
| app: libros | |||
| spec: | |||
| capacity: | |||
| storage: 100Mi | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| hostPath: | |||
| path: "/mnt/Externo/libros/media" | |||
| --- | |||
| apiVersion: v1 | |||
| kind: PersistentVolume | |||
| metadata: | |||
| name: libros-migrations-folder | |||
| namespace: libros | |||
| labels: | |||
| app: libros | |||
| spec: | |||
| capacity: | |||
| storage: 50Mi | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| hostPath: | |||
| path: "/mnt/Externo/libros/migrations" | |||
| --- | |||
| apiVersion: v1 | |||
| kind: PersistentVolume | |||
| metadata: | |||
| name: libros-static-folder | |||
| namespace: libros | |||
| labels: | |||
| app: libros | |||
| spec: | |||
| capacity: | |||
| storage: 70Mi | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| hostPath: | |||
| path: "/mnt/Externo/libros/static" | |||
| --- | |||
| apiVersion: v1 | |||
| kind: PersistentVolume | |||
| metadata: | |||
| name: libros-pg-folder | |||
| namespace: libros | |||
| labels: | |||
| app: libros | |||
| spec: | |||
| capacity: | |||
| storage: 200Mi | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| hostPath: | |||
| path: "/mnt/Externo/libros/pg" | |||
| @ -0,0 +1,8 @@ | |||
| apiVersion: v1 | |||
| kind: Secret | |||
| metadata: | |||
| name: myregistrykey | |||
| namespace: libros | |||
| data: | |||
| .dockerconfigjson: ewoJImF1dGhzIjogewoJCSJyZWdpc3RyeS5yZXltb3RhLmVzIjogewoJCQkiYXV0aCI6ICJZM0psZVd4dmNHVjZPbEpsZVMweE1UYzIiCgkJfQoJfQp9 | |||
| type: kubernetes.io/dockerconfigjson | |||
| @ -0,0 +1,13 @@ | |||
| apiVersion: v1 | |||
| kind: PersistentVolumeClaim | |||
| metadata: | |||
| labels: | |||
| io.kompose.service: static-volume | |||
| name: static-volume | |||
| namespace: libros | |||
| spec: | |||
| accessModes: | |||
| - ReadWriteOnce | |||
| resources: | |||
| requests: | |||
| storage: 70Mi | |||
| @ -0,0 +1 @@ | |||
| docker run -it registry.reymota.es/libros:1.19 bash | |||
| @ -0,0 +1,8 @@ | |||
| install: | |||
| echo "Creando imagen con version ${IMG_VERSION}" | |||
| docker build --no-cache -t registry.reymota.es/libros-${ARQUITECTURA}:${IMG_VERSION} . | |||
| docker push registry.reymota.es/libros-${ARQUITECTURA}:${IMG_VERSION} | |||
| @ -0,0 +1,16 @@ | |||
| https://learndjango.com/tutorials/django-login-and-logout-tutorial | |||
| Username: {{ user.username }} | |||
| User Full name: {{ user.get_full_name }} | |||
| User Group: {{ user.groups.all.0 }} | |||
| Email: {{ user.email }} | |||
| Session Started at: {{ user.last_login }} | |||
| ## Para funcionar con gunicorn y nginx | |||
| https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/ | |||
| @ -0,0 +1,19 @@ | |||
| #!/bin/bash | |||
| if [ "$DATABASE" = "postgres" ] | |||
| then | |||
| echo "Waiting for postgres..." | |||
| while ! nc -z $SQL_HOST $SQL_PORT; do | |||
| sleep 0.1 | |||
| done | |||
| echo "PostgreSQL started" | |||
| else | |||
| echo "la base de datos no es postgres: '$DATABASE'" | |||
| fi | |||
| #python manage.py flush --no-input | |||
| #python manage.py migrate | |||
| exec "$@" | |||
| @ -0,0 +1,4 @@ | |||
| FROM nginx:1.25 | |||
| RUN rm /etc/nginx/conf.d/default.conf | |||
| COPY nginx.conf /etc/nginx/conf.d | |||
| @ -0,0 +1,8 @@ | |||
| install: | |||
| echo "Creando imagen con version ${IMG_NGINX_VERSION} para la arquitectura ${ARQUITECTURA}" | |||
| docker build --no-cache -t registry.reymota.es/nginx-libros-${ARQUITECTURA}:${IMG_NGINX_VERSION} . | |||
| docker push registry.reymota.es/nginx-libros-${ARQUITECTURA}:${IMG_NGINX_VERSION} | |||
| @ -0,0 +1,25 @@ | |||
| upstream libros { | |||
| server libros:8000; | |||
| } | |||
| server { | |||
| listen 80; | |||
| location / { | |||
| proxy_pass http://libros; | |||
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |||
| proxy_set_header Host $http_host; | |||
| proxy_redirect off; | |||
| client_max_body_size 100M; | |||
| } | |||
| location /static/ { | |||
| alias /app/biblioteca/staticfiles/; | |||
| } | |||
| location /media/ { | |||
| alias /app/biblioteca/mediafiles/; | |||
| } | |||
| } | |||
| @ -0,0 +1,4 @@ | |||
| asgiref==3.8.1 | |||
| Django==5.1 | |||
| sqlparse==0.5.1 | |||
| typing_extensions==4.12.2 | |||
| @ -0,0 +1,3 @@ | |||
| from django.contrib import admin | |||
| # Register your models here. | |||
| @ -0,0 +1,6 @@ | |||
| from django.apps import AppConfig | |||
| class AccountsConfig(AppConfig): | |||
| default_auto_field = 'django.db.models.BigAutoField' | |||
| name = 'accounts' | |||
| @ -0,0 +1,3 @@ | |||
| from django.db import models | |||
| # Create your models here. | |||
| @ -0,0 +1,3 @@ | |||
| from django.test import TestCase | |||
| # Create your tests here. | |||
| @ -0,0 +1,8 @@ | |||
| # accounts/urls.py | |||
| from django.urls import path | |||
| from django.contrib.auth import views as auth_views | |||
| urlpatterns = [ | |||
| path('login/', auth_views.LoginView.as_view(), name='login'), | |||
| path('logout/', auth_views.LogoutView.as_view(), name='logout'), | |||
| ] | |||
| @ -0,0 +1 @@ | |||
| from django.shortcuts import render | |||
| @ -0,0 +1,22 @@ | |||
| #!/usr/bin/env python | |||
| """Django's command-line utility for administrative tasks.""" | |||
| import os | |||
| import sys | |||
| def main(): | |||
| """Run administrative tasks.""" | |||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vehiculos.settings') | |||
| try: | |||
| from django.core.management import execute_from_command_line | |||
| except ImportError as exc: | |||
| raise ImportError( | |||
| "Couldn't import Django. Are you sure it's installed and " | |||
| "available on your PYTHONPATH environment variable? Did you " | |||
| "forget to activate a virtual environment?" | |||
| ) from exc | |||
| execute_from_command_line(sys.argv) | |||
| if __name__ == '__main__': | |||
| main() | |||
| @ -0,0 +1,45 @@ | |||
| from django.contrib import admin | |||
| from django.contrib.auth.admin import UserAdmin | |||
| # Register your models here. | |||
| from .models import Vehiculos, Repostajes, ReyMotaUser | |||
| from .forms import ReyMotaUserCreationForm, ReyMotaUserChangeForm | |||
| admin.site.register(Vehiculos) | |||
| admin.site.register(Repostajes) | |||
| class ReyMotaUserAdmin(UserAdmin): | |||
| add_form = ReyMotaUserCreationForm | |||
| form = ReyMotaUserChangeForm | |||
| model = ReyMotaUser | |||
| list_display = ("email", "nombre", "is_staff", "is_active", "foto") | |||
| list_filter = ("email", "nombre", "is_staff", "is_active",) | |||
| fieldsets = ( | |||
| (None, {"fields": ("email", "password")}), | |||
| ("Personal", {"fields": ("nombre",)}), | |||
| ("Permissions", {"fields": ("is_staff", "is_active", "groups", | |||
| "user_permissions")}), | |||
| ("Varios", {"fields": ("foto",)}), | |||
| ) | |||
| add_fieldsets = ( | |||
| ( | |||
| None, | |||
| { | |||
| "classes": ("wide",), | |||
| "fields": ( | |||
| "email", "password1", "password2", "is_staff", | |||
| "is_active", "groups", "user_permissions" | |||
| ) | |||
| } | |||
| ), | |||
| ("Personal", {"fields": ("nombre",)}), | |||
| ("Varios", {"fields": ("foto",)}), | |||
| ) | |||
| search_fields = ("email",) | |||
| ordering = ("email",) | |||
| admin.site.register(ReyMotaUser, ReyMotaUserAdmin) | |||
| @ -0,0 +1,6 @@ | |||
| from django.apps import AppConfig | |||
| class RepostajesConfig(AppConfig): | |||
| default_auto_field = 'django.db.models.BigAutoField' | |||
| name = 'repostajes' | |||
| @ -0,0 +1,47 @@ | |||
| from django import forms | |||
| from django.contrib.auth.forms import UserCreationForm, UserChangeForm | |||
| from .models import Vehiculo, Repostaje, ReyMotaUser | |||
| class VehiculoForm(forms.ModelForm): | |||
| class Meta: | |||
| model = Vehiculo | |||
| fields = ['marca', 'modelo', 'matricula', 'foto'] | |||
| marca = forms.CharField( | |||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||
| modelo = forms.CharField( | |||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||
| matricula = forms.CharField( | |||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||
| class RepostajeForm(forms.ModelForm): | |||
| class Meta: | |||
| model = Repostaje | |||
| fields = ['fecha', 'kms', 'litros', 'descuento'] | |||
| fecha = forms.DateFieldField( | |||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||
| kms = forms.DecimalField( | |||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||
| Vehiculo = forms.ModelChoiceField( | |||
| queryset=Vehiculo.objects.all(), | |||
| widget=forms.Select(attrs={'class': 'form-control'})) | |||
| class ReyMotaUserCreationForm(UserCreationForm): | |||
| class Meta: | |||
| model = ReyMotaUser | |||
| fields = ("email", "nombre", "foto") | |||
| labels = {'email': 'Dirección de correo'} | |||
| class ReyMotaUserChangeForm(UserChangeForm): | |||
| class Meta: | |||
| model = ReyMotaUser | |||
| fields = ("email", "foto") | |||
| @ -0,0 +1,34 @@ | |||
| from django.contrib.auth.base_user import BaseUserManager | |||
| from django.utils.translation import gettext_lazy as _ | |||
| class ReyMotaUserManager(BaseUserManager): | |||
| """ | |||
| ReyMota user model manager where email is the unique identifiers | |||
| for authentication instead of usernames. | |||
| """ | |||
| def create_user(self, email, password, **extra_fields): | |||
| """ | |||
| Create and save a user with the given email and password. | |||
| """ | |||
| if not email: | |||
| raise ValueError(_("The Email must be set")) | |||
| email = self.normalize_email(email) | |||
| user = self.model(email=email, **extra_fields) | |||
| user.set_password(password) | |||
| user.save() | |||
| return user | |||
| def create_superuser(self, email, password, **extra_fields): | |||
| """ | |||
| Create and save a SuperUser with the given email and password. | |||
| """ | |||
| extra_fields.setdefault("is_staff", True) | |||
| extra_fields.setdefault("is_superuser", True) | |||
| extra_fields.setdefault("is_active", 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.")) | |||
| return self.create_user(email, password, **extra_fields) | |||
| @ -0,0 +1,55 @@ | |||
| from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin | |||
| from django.db import models | |||
| import datetime | |||
| from django.core.validators import MaxValueValidator, MinValueValidator | |||
| from django.utils.translation import gettext_lazy as _ | |||
| from .managers import ReyMotaUserManager | |||
| def current_year(): | |||
| return datetime.date.today().year | |||
| def max_value_current_year(value): | |||
| return MaxValueValidator(current_year())(value) | |||
| class Vehiculo(models.Model): | |||
| marca = models.CharField(max_length=200) | |||
| modelo = models.CharField(max_length=200) | |||
| matricula = models.CharField(max_length=200) | |||
| foto = models.ImageField(upload_to='vehiculos/', blank=True, null=True) # Nuevo campo | |||
| def __str__(self): | |||
| return self.marca | |||
| class Repostaje(models.Model): | |||
| vehiculo = models.ForeignKey(Vehiculo, on_delete=models.CASCADE) | |||
| fecha = models.DateField() | |||
| kms = models.DecimalField(blank=True, null=True) | |||
| litros = models.DecimalField(blank=True, null=True) | |||
| descuento = models.DecimalField(blank=True, null=True) | |||
| precioxlitro = models.DecimalField(blank=True, null=True) | |||
| kmsrecorridos = models.DecimalField(blank=True, null=True) | |||
| consumo = models.DecimalField(blank=True, null=True) | |||
| def __str__(self): | |||
| return self.fecha | |||
| class ReyMotaUser(AbstractBaseUser, PermissionsMixin): | |||
| email = models.EmailField(_("email address"), unique=True) | |||
| foto = models.ImageField(upload_to="profile_images", blank=True) | |||
| is_staff = models.BooleanField(default=False) | |||
| is_active = models.BooleanField(default=True) | |||
| nombre = models.CharField(max_length=200, blank=True, null=True) | |||
| USERNAME_FIELD = "email" | |||
| REQUIRED_FIELDS = [] | |||
| objects = ReyMotaUserManager() | |||
| def __str__(self): | |||
| return self.email | |||
| @ -0,0 +1,21 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg width="215px" height="215px" viewBox="0 0 215 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch --> | |||
| <title>portal-logo</title> | |||
| <desc>Created with Sketch.</desc> | |||
| <defs> | |||
| <path d="M51.165,8.742 C54.505,12.619 56.876,17.365 57.892,22.588 C60.148,17.225 65.452,13.46 71.636,13.46 C79.867,13.46 86.541,20.134 86.541,28.365 C86.541,36.597 79.867,43.269 71.636,43.269 C63.404,43.269 56.728,36.597 56.728,28.365 C56.728,12.7 44.03,0 28.365,0 C12.7,0 0,12.7 0,28.365 C0,44.031 12.7,56.731 28.365,56.731 C36.419,56.731 43.695,53.393 48.858,48.003 C45.501,44.117 43.128,39.383 42.108,34.14 C39.852,39.504 34.548,43.269 28.365,43.269 C20.133,43.269 13.46,36.597 13.46,28.365 C13.46,20.134 20.133,13.46 28.365,13.46 C36.966,13.46 43.27,20.577 43.27,28.365 C43.27,44.031 55.97,56.731 71.636,56.731 C87.3,56.731 100,44.031 100,28.365 C100,12.7 87.3,0 71.636,0 C63.589,0 56.327,3.358 51.165,8.742 Z" id="path-1"></path> | |||
| </defs> | |||
| <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="portal-logo"> | |||
| <circle id="Oval" fill="#51B37F" fill-rule="nonzero" cx="107.5" cy="107.5" r="107.5"></circle> | |||
| <g id="logo" transform="translate(58.000000, 79.000000)"> | |||
| <mask id="mask-2" fill="white"> | |||
| <use xlink:href="#path-1"></use> | |||
| </mask> | |||
| <g id="Clip-2"></g> | |||
| <polygon id="Fill-1" fill="#FFFFFE" mask="url(#mask-2)" points="-5 61.73 105 61.73 105 -5 -5 -5"></polygon> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @ -0,0 +1,21 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg width="215px" height="215px" viewBox="0 0 215 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch --> | |||
| <title>portal-logo</title> | |||
| <desc>Created with Sketch.</desc> | |||
| <defs> | |||
| <path d="M51.165,8.742 C54.505,12.619 56.876,17.365 57.892,22.588 C60.148,17.225 65.452,13.46 71.636,13.46 C79.867,13.46 86.541,20.134 86.541,28.365 C86.541,36.597 79.867,43.269 71.636,43.269 C63.404,43.269 56.728,36.597 56.728,28.365 C56.728,12.7 44.03,0 28.365,0 C12.7,0 0,12.7 0,28.365 C0,44.031 12.7,56.731 28.365,56.731 C36.419,56.731 43.695,53.393 48.858,48.003 C45.501,44.117 43.128,39.383 42.108,34.14 C39.852,39.504 34.548,43.269 28.365,43.269 C20.133,43.269 13.46,36.597 13.46,28.365 C13.46,20.134 20.133,13.46 28.365,13.46 C36.966,13.46 43.27,20.577 43.27,28.365 C43.27,44.031 55.97,56.731 71.636,56.731 C87.3,56.731 100,44.031 100,28.365 C100,12.7 87.3,0 71.636,0 C63.589,0 56.327,3.358 51.165,8.742 Z" id="path-1"></path> | |||
| </defs> | |||
| <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="portal-logo"> | |||
| <circle id="Oval" fill="#09B6CA" fill-rule="nonzero" cx="107.5" cy="107.5" r="107.5"></circle> | |||
| <g id="logo" transform="translate(58.000000, 79.000000)"> | |||
| <mask id="mask-2" fill="white"> | |||
| <use xlink:href="#path-1"></use> | |||
| </mask> | |||
| <g id="Clip-2"></g> | |||
| <polygon id="Fill-1" fill="#FFFFFE" mask="url(#mask-2)" points="-5 61.73 105 61.73 105 -5 -5 -5"></polygon> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @ -0,0 +1,18 @@ | |||
| <svg width="400" height="400" xmlns="http://www.w3.org/2000/svg"> | |||
| <!-- Fondo --> | |||
| <rect width="400" height="400" fill="#ffffff" /> | |||
| <!-- Corona --> | |||
| <g transform="translate(100, 100) scale(2)"> | |||
| <polygon points="50,150 75,50 100,150" fill="#FFD700" /> | |||
| <polygon points="0,150 50,0 100,150" fill="#FFD700" /> | |||
| <polygon points="100,150 125,50 150,150" fill="#FFD700" /> | |||
| </g> | |||
| <!-- Letra R --> | |||
| <!-- | |||
| <text x="100" y="360" font-family="Arial, sans-serif" font-size="400" fill="#000000" font-weight="bold">R</text> | |||
| --> | |||
| <text x="100" y="360" font-family="Open Sans" font-size="400" fill="#000000" font-weight="bold">R</text> | |||
| </svg> | |||
| @ -0,0 +1,96 @@ | |||
| 'use strict'; | |||
| /* ===== Enable Bootstrap Popover (on element ====== */ | |||
| const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]') | |||
| const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)) | |||
| /* ==== Enable Bootstrap Alert ====== */ | |||
| //var alertList = document.querySelectorAll('.alert') | |||
| //alertList.forEach(function (alert) { | |||
| // new bootstrap.Alert(alert) | |||
| //}); | |||
| const alertList = document.querySelectorAll('.alert') | |||
| const alerts = [...alertList].map(element => new bootstrap.Alert(element)) | |||
| /* ===== Responsive Sidepanel ====== */ | |||
| const sidePanelToggler = document.getElementById('sidepanel-toggler'); | |||
| const sidePanel = document.getElementById('app-sidepanel'); | |||
| const sidePanelDrop = document.getElementById('sidepanel-drop'); | |||
| const sidePanelClose = document.getElementById('sidepanel-close'); | |||
| window.addEventListener('load', function(){ | |||
| responsiveSidePanel(); | |||
| }); | |||
| window.addEventListener('resize', function(){ | |||
| responsiveSidePanel(); | |||
| }); | |||
| function responsiveSidePanel() { | |||
| let w = window.innerWidth; | |||
| if(w >= 1200) { | |||
| // if larger | |||
| //console.log('larger'); | |||
| sidePanel.classList.remove('sidepanel-hidden'); | |||
| sidePanel.classList.add('sidepanel-visible'); | |||
| } else { | |||
| // if smaller | |||
| //console.log('smaller'); | |||
| sidePanel.classList.remove('sidepanel-visible'); | |||
| sidePanel.classList.add('sidepanel-hidden'); | |||
| } | |||
| }; | |||
| sidePanelToggler.addEventListener('click', () => { | |||
| if (sidePanel.classList.contains('sidepanel-visible')) { | |||
| console.log('visible'); | |||
| sidePanel.classList.remove('sidepanel-visible'); | |||
| sidePanel.classList.add('sidepanel-hidden'); | |||
| } else { | |||
| console.log('hidden'); | |||
| sidePanel.classList.remove('sidepanel-hidden'); | |||
| sidePanel.classList.add('sidepanel-visible'); | |||
| } | |||
| }); | |||
| sidePanelClose.addEventListener('click', (e) => { | |||
| e.preventDefault(); | |||
| sidePanelToggler.click(); | |||
| }); | |||
| sidePanelDrop.addEventListener('click', (e) => { | |||
| sidePanelToggler.click(); | |||
| }); | |||
| /* ====== Mobile search ======= */ | |||
| const searchMobileTrigger = document.querySelector('.search-mobile-trigger'); | |||
| const searchBox = document.querySelector('.app-search-box'); | |||
| searchMobileTrigger.addEventListener('click', () => { | |||
| searchBox.classList.toggle('is-visible'); | |||
| let searchMobileTriggerIcon = document.querySelector('.search-mobile-trigger-icon'); | |||
| if(searchMobileTriggerIcon.classList.contains('fa-magnifying-glass')) { | |||
| searchMobileTriggerIcon.classList.remove('fa-magnifying-glass'); | |||
| searchMobileTriggerIcon.classList.add('fa-xmark'); | |||
| } else { | |||
| searchMobileTriggerIcon.classList.remove('fa-xmark'); | |||
| searchMobileTriggerIcon.classList.add('fa-magnifying-glass'); | |||
| } | |||
| }); | |||
| @ -0,0 +1,366 @@ | |||
| 'use strict'; | |||
| /* Chart.js docs: https://www.chartjs.org/ */ | |||
| window.chartColors = { | |||
| green: '#75c181', // rgba(117,193,129, 1) | |||
| blue: '#5b99ea', // rgba(91,153,234, 1) | |||
| gray: '#a9b5c9', | |||
| text: '#252930', | |||
| border: '#e7e9ed' | |||
| }; | |||
| /* Random number generator for demo purpose */ | |||
| var randomDataPoint = function(){ return Math.round(Math.random()*100)}; | |||
| //Area line Chart Demo | |||
| var lineChartConfig = { | |||
| type: 'line', | |||
| data: { | |||
| labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'], | |||
| datasets: [{ | |||
| label: 'Dataset', | |||
| backgroundColor: "rgba(117,193,129,0.2)", | |||
| borderColor: "rgba(117,193,129, 0.8)", | |||
| data: [ | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint() | |||
| ], | |||
| }] | |||
| }, | |||
| options: { | |||
| responsive: true, | |||
| legend: { | |||
| display: true, | |||
| position: 'bottom', | |||
| align: 'end', | |||
| }, | |||
| tooltips: { | |||
| mode: 'index', | |||
| intersect: false, | |||
| titleMarginBottom: 10, | |||
| bodySpacing: 10, | |||
| xPadding: 16, | |||
| yPadding: 16, | |||
| borderColor: window.chartColors.border, | |||
| borderWidth: 1, | |||
| backgroundColor: '#fff', | |||
| bodyFontColor: window.chartColors.text, | |||
| titleFontColor: window.chartColors.text, | |||
| callbacks: { | |||
| label: function(tooltipItem, data) { | |||
| return tooltipItem.value + '%'; | |||
| } | |||
| }, | |||
| }, | |||
| hover: { | |||
| mode: 'nearest', | |||
| intersect: true | |||
| }, | |||
| scales: { | |||
| xAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.border, | |||
| }, | |||
| scaleLabel: { | |||
| display: false, | |||
| } | |||
| }], | |||
| yAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.border, | |||
| }, | |||
| scaleLabel: { | |||
| display: false, | |||
| }, | |||
| ticks: { | |||
| beginAtZero: true, | |||
| userCallback: function(value, index, values) { | |||
| return value.toLocaleString() + '%'; | |||
| } | |||
| }, | |||
| }] | |||
| } | |||
| } | |||
| }; | |||
| //Bar Chart Demo | |||
| var barChartConfig = { | |||
| type: 'bar', | |||
| data: { | |||
| labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'], | |||
| datasets: [{ | |||
| label: 'Dataset 1', | |||
| backgroundColor: "rgba(117,193,129,0.8)", | |||
| hoverBackgroundColor: "rgba(117,193,129,1)", | |||
| data: [ | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint() | |||
| ] | |||
| }, | |||
| { | |||
| label: 'Dataset 2', | |||
| backgroundColor: "rgba(91,153,234,0.8)", | |||
| hoverBackgroundColor: "rgba(91,153,234,1)", | |||
| data: [ | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint() | |||
| ] | |||
| } | |||
| ] | |||
| }, | |||
| options: { | |||
| responsive: true, | |||
| legend: { | |||
| position: 'bottom', | |||
| align: 'end', | |||
| }, | |||
| tooltips: { | |||
| mode: 'index', | |||
| intersect: false, | |||
| titleMarginBottom: 10, | |||
| bodySpacing: 10, | |||
| xPadding: 16, | |||
| yPadding: 16, | |||
| borderColor: window.chartColors.border, | |||
| borderWidth: 1, | |||
| backgroundColor: '#fff', | |||
| bodyFontColor: window.chartColors.text, | |||
| titleFontColor: window.chartColors.text, | |||
| callbacks: { | |||
| label: function(tooltipItem, data) { | |||
| return tooltipItem.value + '%'; | |||
| } | |||
| }, | |||
| }, | |||
| scales: { | |||
| xAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.border, | |||
| }, | |||
| }], | |||
| yAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.borders, | |||
| }, | |||
| ticks: { | |||
| beginAtZero: true, | |||
| userCallback: function(value, index, values) { | |||
| return value + '%'; | |||
| } | |||
| }, | |||
| }] | |||
| } | |||
| } | |||
| } | |||
| // Pie Chart Demo | |||
| var pieChartConfig = { | |||
| type: 'pie', | |||
| data: { | |||
| datasets: [{ | |||
| data: [ | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| ], | |||
| backgroundColor: [ | |||
| window.chartColors.green, | |||
| window.chartColors.blue, | |||
| window.chartColors.gray, | |||
| ], | |||
| label: 'Dataset 1' | |||
| }], | |||
| labels: [ | |||
| 'Green', | |||
| 'Blue', | |||
| 'Gray', | |||
| ] | |||
| }, | |||
| options: { | |||
| responsive: true, | |||
| legend: { | |||
| display: true, | |||
| position: 'bottom', | |||
| align: 'center', | |||
| }, | |||
| tooltips: { | |||
| titleMarginBottom: 10, | |||
| bodySpacing: 10, | |||
| xPadding: 16, | |||
| yPadding: 16, | |||
| borderColor: window.chartColors.border, | |||
| borderWidth: 1, | |||
| backgroundColor: '#fff', | |||
| bodyFontColor: window.chartColors.text, | |||
| titleFontColor: window.chartColors.text, | |||
| /* Display % in tooltip - https://stackoverflow.com/questions/37257034/chart-js-2-0-doughnut-tooltip-percentages */ | |||
| callbacks: { | |||
| label: function(tooltipItem, data) { | |||
| //get the concerned dataset | |||
| var dataset = data.datasets[tooltipItem.datasetIndex]; | |||
| //calculate the total of this data set | |||
| var total = dataset.data.reduce(function(previousValue, currentValue, currentIndex, array) { | |||
| return previousValue + currentValue; | |||
| }); | |||
| //get the current items value | |||
| var currentValue = dataset.data[tooltipItem.index]; | |||
| //calculate the precentage based on the total and current item, also this does a rough rounding to give a whole number | |||
| var percentage = Math.floor(((currentValue/total) * 100)+0.5); | |||
| return percentage + "%"; | |||
| }, | |||
| }, | |||
| }, | |||
| } | |||
| }; | |||
| // Doughnut Chart Demo | |||
| var doughnutChartConfig = { | |||
| type: 'doughnut', | |||
| data: { | |||
| datasets: [{ | |||
| data: [ | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| ], | |||
| backgroundColor: [ | |||
| window.chartColors.green, | |||
| window.chartColors.blue, | |||
| window.chartColors.gray, | |||
| ], | |||
| label: 'Dataset 1' | |||
| }], | |||
| labels: [ | |||
| 'Green', | |||
| 'Blue', | |||
| 'Gray', | |||
| ] | |||
| }, | |||
| options: { | |||
| responsive: true, | |||
| legend: { | |||
| display: true, | |||
| position: 'bottom', | |||
| align: 'center', | |||
| }, | |||
| tooltips: { | |||
| titleMarginBottom: 10, | |||
| bodySpacing: 10, | |||
| xPadding: 16, | |||
| yPadding: 16, | |||
| borderColor: window.chartColors.border, | |||
| borderWidth: 1, | |||
| backgroundColor: '#fff', | |||
| bodyFontColor: window.chartColors.text, | |||
| titleFontColor: window.chartColors.text, | |||
| animation: { | |||
| animateScale: true, | |||
| animateRotate: true | |||
| }, | |||
| /* Display % in tooltip - https://stackoverflow.com/questions/37257034/chart-js-2-0-doughnut-tooltip-percentages */ | |||
| callbacks: { | |||
| label: function(tooltipItem, data) { | |||
| //get the concerned dataset | |||
| var dataset = data.datasets[tooltipItem.datasetIndex]; | |||
| //calculate the total of this data set | |||
| var total = dataset.data.reduce(function(previousValue, currentValue, currentIndex, array) { | |||
| return previousValue + currentValue; | |||
| }); | |||
| //get the current items value | |||
| var currentValue = dataset.data[tooltipItem.index]; | |||
| //calculate the precentage based on the total and current item, also this does a rough rounding to give a whole number | |||
| var percentage = Math.floor(((currentValue/total) * 100)+0.5); | |||
| return percentage + "%"; | |||
| }, | |||
| }, | |||
| }, | |||
| } | |||
| }; | |||
| // Generate charts on load | |||
| window.addEventListener('load', function(){ | |||
| var lineChart = document.getElementById('chart-line').getContext('2d'); | |||
| window.myLine = new Chart(lineChart, lineChartConfig); | |||
| var barChart = document.getElementById('chart-bar').getContext('2d'); | |||
| window.myBar = new Chart(barChart, barChartConfig); | |||
| var pieChart = document.getElementById('chart-pie').getContext('2d'); | |||
| window.myPie = new Chart(pieChart, pieChartConfig); | |||
| var doughnutChart = document.getElementById('chart-doughnut').getContext('2d'); | |||
| window.myDoughnut = new Chart(doughnutChart, doughnutChartConfig); | |||
| }); | |||
| @ -0,0 +1,224 @@ | |||
| 'use strict'; | |||
| /* Chart.js docs: https://www.chartjs.org/ */ | |||
| window.chartColors = { | |||
| green: '#75c181', | |||
| gray: '#a9b5c9', | |||
| text: '#252930', | |||
| border: '#e7e9ed' | |||
| }; | |||
| /* Random number generator for demo purpose */ | |||
| var randomDataPoint = function(){ return Math.round(Math.random()*10000)}; | |||
| //Chart.js Line Chart Example | |||
| var lineChartConfig = { | |||
| type: 'line', | |||
| data: { | |||
| labels: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7'], | |||
| datasets: [{ | |||
| label: 'Current week', | |||
| fill: false, | |||
| backgroundColor: window.chartColors.green, | |||
| borderColor: window.chartColors.green, | |||
| data: [ | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint() | |||
| ], | |||
| }, { | |||
| label: 'Previous week', | |||
| borderDash: [3, 5], | |||
| backgroundColor: window.chartColors.gray, | |||
| borderColor: window.chartColors.gray, | |||
| data: [ | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint(), | |||
| randomDataPoint() | |||
| ], | |||
| fill: false, | |||
| }] | |||
| }, | |||
| options: { | |||
| responsive: true, | |||
| aspectRatio: 1.5, | |||
| legend: { | |||
| display: true, | |||
| position: 'bottom', | |||
| align: 'end', | |||
| }, | |||
| title: { | |||
| display: true, | |||
| text: 'Chart.js Line Chart Example', | |||
| }, | |||
| tooltips: { | |||
| mode: 'index', | |||
| intersect: false, | |||
| titleMarginBottom: 10, | |||
| bodySpacing: 10, | |||
| xPadding: 16, | |||
| yPadding: 16, | |||
| borderColor: window.chartColors.border, | |||
| borderWidth: 1, | |||
| backgroundColor: '#fff', | |||
| bodyFontColor: window.chartColors.text, | |||
| titleFontColor: window.chartColors.text, | |||
| callbacks: { | |||
| //Ref: https://stackoverflow.com/questions/38800226/chart-js-add-commas-to-tooltip-and-y-axis | |||
| label: function(tooltipItem, data) { | |||
| if (parseInt(tooltipItem.value) >= 1000) { | |||
| return "$" + tooltipItem.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |||
| } else { | |||
| return '$' + tooltipItem.value; | |||
| } | |||
| } | |||
| }, | |||
| }, | |||
| hover: { | |||
| mode: 'nearest', | |||
| intersect: true | |||
| }, | |||
| scales: { | |||
| xAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.border, | |||
| }, | |||
| scaleLabel: { | |||
| display: false, | |||
| } | |||
| }], | |||
| yAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.border, | |||
| }, | |||
| scaleLabel: { | |||
| display: false, | |||
| }, | |||
| ticks: { | |||
| beginAtZero: true, | |||
| userCallback: function(value, index, values) { | |||
| return '$' + value.toLocaleString(); //Ref: https://stackoverflow.com/questions/38800226/chart-js-add-commas-to-tooltip-and-y-axis | |||
| } | |||
| }, | |||
| }] | |||
| } | |||
| } | |||
| }; | |||
| // Chart.js Bar Chart Example | |||
| var barChartConfig = { | |||
| type: 'bar', | |||
| data: { | |||
| labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], | |||
| datasets: [{ | |||
| label: 'Orders', | |||
| backgroundColor: window.chartColors.green, | |||
| borderColor: window.chartColors.green, | |||
| borderWidth: 1, | |||
| maxBarThickness: 16, | |||
| data: [ | |||
| 23, | |||
| 45, | |||
| 76, | |||
| 75, | |||
| 62, | |||
| 37, | |||
| 83 | |||
| ] | |||
| }] | |||
| }, | |||
| options: { | |||
| responsive: true, | |||
| aspectRatio: 1.5, | |||
| legend: { | |||
| position: 'bottom', | |||
| align: 'end', | |||
| }, | |||
| title: { | |||
| display: true, | |||
| text: 'Chart.js Bar Chart Example' | |||
| }, | |||
| tooltips: { | |||
| mode: 'index', | |||
| intersect: false, | |||
| titleMarginBottom: 10, | |||
| bodySpacing: 10, | |||
| xPadding: 16, | |||
| yPadding: 16, | |||
| borderColor: window.chartColors.border, | |||
| borderWidth: 1, | |||
| backgroundColor: '#fff', | |||
| bodyFontColor: window.chartColors.text, | |||
| titleFontColor: window.chartColors.text, | |||
| }, | |||
| scales: { | |||
| xAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.border, | |||
| }, | |||
| }], | |||
| yAxes: [{ | |||
| display: true, | |||
| gridLines: { | |||
| drawBorder: false, | |||
| color: window.chartColors.borders, | |||
| }, | |||
| }] | |||
| } | |||
| } | |||
| } | |||
| // Generate charts on load | |||
| window.addEventListener('load', function(){ | |||
| var lineChart = document.getElementById('canvas-linechart').getContext('2d'); | |||
| window.myLine = new Chart(lineChart, lineChartConfig); | |||
| var barChart = document.getElementById('canvas-barchart').getContext('2d'); | |||
| window.myBar = new Chart(barChart, barChartConfig); | |||
| }); | |||
| @ -0,0 +1,593 @@ | |||
| /*! | |||
| * Bootstrap Reboot v5.3.0 (https://getbootstrap.com/) | |||
| * Copyright 2011-2023 The Bootstrap Authors | |||
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) | |||
| */ | |||
| :root, | |||
| [data-bs-theme=light] { | |||
| --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-black: #000; | |||
| --bs-white: #fff; | |||
| --bs-gray: #6c757d; | |||
| --bs-gray-dark: #343a40; | |||
| --bs-gray-100: #f8f9fa; | |||
| --bs-gray-200: #e9ecef; | |||
| --bs-gray-300: #dee2e6; | |||
| --bs-gray-400: #ced4da; | |||
| --bs-gray-500: #adb5bd; | |||
| --bs-gray-600: #6c757d; | |||
| --bs-gray-700: #495057; | |||
| --bs-gray-800: #343a40; | |||
| --bs-gray-900: #212529; | |||
| --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-primary-rgb: 13, 110, 253; | |||
| --bs-secondary-rgb: 108, 117, 125; | |||
| --bs-success-rgb: 25, 135, 84; | |||
| --bs-info-rgb: 13, 202, 240; | |||
| --bs-warning-rgb: 255, 193, 7; | |||
| --bs-danger-rgb: 220, 53, 69; | |||
| --bs-light-rgb: 248, 249, 250; | |||
| --bs-dark-rgb: 33, 37, 41; | |||
| --bs-primary-text-emphasis: #052c65; | |||
| --bs-secondary-text-emphasis: #2b2f32; | |||
| --bs-success-text-emphasis: #0a3622; | |||
| --bs-info-text-emphasis: #055160; | |||
| --bs-warning-text-emphasis: #664d03; | |||
| --bs-danger-text-emphasis: #58151c; | |||
| --bs-light-text-emphasis: #495057; | |||
| --bs-dark-text-emphasis: #495057; | |||
| --bs-primary-bg-subtle: #cfe2ff; | |||
| --bs-secondary-bg-subtle: #e2e3e5; | |||
| --bs-success-bg-subtle: #d1e7dd; | |||
| --bs-info-bg-subtle: #cff4fc; | |||
| --bs-warning-bg-subtle: #fff3cd; | |||
| --bs-danger-bg-subtle: #f8d7da; | |||
| --bs-light-bg-subtle: #fcfcfd; | |||
| --bs-dark-bg-subtle: #ced4da; | |||
| --bs-primary-border-subtle: #9ec5fe; | |||
| --bs-secondary-border-subtle: #c4c8cb; | |||
| --bs-success-border-subtle: #a3cfbb; | |||
| --bs-info-border-subtle: #9eeaf9; | |||
| --bs-warning-border-subtle: #ffe69c; | |||
| --bs-danger-border-subtle: #f1aeb5; | |||
| --bs-light-border-subtle: #e9ecef; | |||
| --bs-dark-border-subtle: #adb5bd; | |||
| --bs-white-rgb: 255, 255, 255; | |||
| --bs-black-rgb: 0, 0, 0; | |||
| --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, 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)); | |||
| --bs-body-font-family: var(--bs-font-sans-serif); | |||
| --bs-body-font-size: 1rem; | |||
| --bs-body-font-weight: 400; | |||
| --bs-body-line-height: 1.5; | |||
| --bs-body-color: #212529; | |||
| --bs-body-color-rgb: 33, 37, 41; | |||
| --bs-body-bg: #fff; | |||
| --bs-body-bg-rgb: 255, 255, 255; | |||
| --bs-emphasis-color: #000; | |||
| --bs-emphasis-color-rgb: 0, 0, 0; | |||
| --bs-secondary-color: rgba(33, 37, 41, 0.75); | |||
| --bs-secondary-color-rgb: 33, 37, 41; | |||
| --bs-secondary-bg: #e9ecef; | |||
| --bs-secondary-bg-rgb: 233, 236, 239; | |||
| --bs-tertiary-color: rgba(33, 37, 41, 0.5); | |||
| --bs-tertiary-color-rgb: 33, 37, 41; | |||
| --bs-tertiary-bg: #f8f9fa; | |||
| --bs-tertiary-bg-rgb: 248, 249, 250; | |||
| --bs-heading-color: inherit; | |||
| --bs-link-color: #0d6efd; | |||
| --bs-link-color-rgb: 13, 110, 253; | |||
| --bs-link-decoration: underline; | |||
| --bs-link-hover-color: #0a58ca; | |||
| --bs-link-hover-color-rgb: 10, 88, 202; | |||
| --bs-code-color: #d63384; | |||
| --bs-highlight-bg: #fff3cd; | |||
| --bs-border-width: 1px; | |||
| --bs-border-style: solid; | |||
| --bs-border-color: #dee2e6; | |||
| --bs-border-color-translucent: rgba(0, 0, 0, 0.175); | |||
| --bs-border-radius: 0.375rem; | |||
| --bs-border-radius-sm: 0.25rem; | |||
| --bs-border-radius-lg: 0.5rem; | |||
| --bs-border-radius-xl: 1rem; | |||
| --bs-border-radius-xxl: 2rem; | |||
| --bs-border-radius-2xl: var(--bs-border-radius-xxl); | |||
| --bs-border-radius-pill: 50rem; | |||
| --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); | |||
| --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); | |||
| --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); | |||
| --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); | |||
| --bs-focus-ring-width: 0.25rem; | |||
| --bs-focus-ring-opacity: 0.25; | |||
| --bs-focus-ring-color: rgba(13, 110, 253, 0.25); | |||
| --bs-form-valid-color: #198754; | |||
| --bs-form-valid-border-color: #198754; | |||
| --bs-form-invalid-color: #dc3545; | |||
| --bs-form-invalid-border-color: #dc3545; | |||
| } | |||
| [data-bs-theme=dark] { | |||
| color-scheme: dark; | |||
| --bs-body-color: #adb5bd; | |||
| --bs-body-color-rgb: 173, 181, 189; | |||
| --bs-body-bg: #212529; | |||
| --bs-body-bg-rgb: 33, 37, 41; | |||
| --bs-emphasis-color: #fff; | |||
| --bs-emphasis-color-rgb: 255, 255, 255; | |||
| --bs-secondary-color: rgba(173, 181, 189, 0.75); | |||
| --bs-secondary-color-rgb: 173, 181, 189; | |||
| --bs-secondary-bg: #343a40; | |||
| --bs-secondary-bg-rgb: 52, 58, 64; | |||
| --bs-tertiary-color: rgba(173, 181, 189, 0.5); | |||
| --bs-tertiary-color-rgb: 173, 181, 189; | |||
| --bs-tertiary-bg: #2b3035; | |||
| --bs-tertiary-bg-rgb: 43, 48, 53; | |||
| --bs-primary-text-emphasis: #6ea8fe; | |||
| --bs-secondary-text-emphasis: #a7acb1; | |||
| --bs-success-text-emphasis: #75b798; | |||
| --bs-info-text-emphasis: #6edff6; | |||
| --bs-warning-text-emphasis: #ffda6a; | |||
| --bs-danger-text-emphasis: #ea868f; | |||
| --bs-light-text-emphasis: #f8f9fa; | |||
| --bs-dark-text-emphasis: #dee2e6; | |||
| --bs-primary-bg-subtle: #031633; | |||
| --bs-secondary-bg-subtle: #161719; | |||
| --bs-success-bg-subtle: #051b11; | |||
| --bs-info-bg-subtle: #032830; | |||
| --bs-warning-bg-subtle: #332701; | |||
| --bs-danger-bg-subtle: #2c0b0e; | |||
| --bs-light-bg-subtle: #343a40; | |||
| --bs-dark-bg-subtle: #1a1d20; | |||
| --bs-primary-border-subtle: #084298; | |||
| --bs-secondary-border-subtle: #41464b; | |||
| --bs-success-border-subtle: #0f5132; | |||
| --bs-info-border-subtle: #087990; | |||
| --bs-warning-border-subtle: #997404; | |||
| --bs-danger-border-subtle: #842029; | |||
| --bs-light-border-subtle: #495057; | |||
| --bs-dark-border-subtle: #343a40; | |||
| --bs-heading-color: inherit; | |||
| --bs-link-color: #6ea8fe; | |||
| --bs-link-hover-color: #8bb9fe; | |||
| --bs-link-color-rgb: 110, 168, 254; | |||
| --bs-link-hover-color-rgb: 139, 185, 254; | |||
| --bs-code-color: #e685b5; | |||
| --bs-border-color: #495057; | |||
| --bs-border-color-translucent: rgba(255, 255, 255, 0.15); | |||
| --bs-form-valid-color: #75b798; | |||
| --bs-form-valid-border-color: #75b798; | |||
| --bs-form-invalid-color: #ea868f; | |||
| --bs-form-invalid-border-color: #ea868f; | |||
| } | |||
| *, | |||
| *::before, | |||
| *::after { | |||
| box-sizing: border-box; | |||
| } | |||
| @media (prefers-reduced-motion: no-preference) { | |||
| :root { | |||
| scroll-behavior: smooth; | |||
| } | |||
| } | |||
| body { | |||
| margin: 0; | |||
| font-family: var(--bs-body-font-family); | |||
| font-size: var(--bs-body-font-size); | |||
| font-weight: var(--bs-body-font-weight); | |||
| line-height: var(--bs-body-line-height); | |||
| color: var(--bs-body-color); | |||
| text-align: var(--bs-body-text-align); | |||
| background-color: var(--bs-body-bg); | |||
| -webkit-text-size-adjust: 100%; | |||
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |||
| } | |||
| hr { | |||
| margin: 1rem 0; | |||
| color: inherit; | |||
| border: 0; | |||
| border-top: var(--bs-border-width) solid; | |||
| opacity: 0.25; | |||
| } | |||
| h6, h5, h4, h3, h2, h1 { | |||
| margin-top: 0; | |||
| margin-bottom: 0.5rem; | |||
| font-weight: 500; | |||
| line-height: 1.2; | |||
| color: var(--bs-heading-color); | |||
| } | |||
| h1 { | |||
| font-size: calc(1.375rem + 1.5vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h1 { | |||
| font-size: 2.5rem; | |||
| } | |||
| } | |||
| h2 { | |||
| font-size: calc(1.325rem + 0.9vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h2 { | |||
| font-size: 2rem; | |||
| } | |||
| } | |||
| h3 { | |||
| font-size: calc(1.3rem + 0.6vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h3 { | |||
| font-size: 1.75rem; | |||
| } | |||
| } | |||
| h4 { | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h4 { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| h5 { | |||
| font-size: 1.25rem; | |||
| } | |||
| h6 { | |||
| font-size: 1rem; | |||
| } | |||
| p { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| abbr[title] { | |||
| -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; | |||
| } | |||
| ol, | |||
| ul, | |||
| dl { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| ol ol, | |||
| ul ul, | |||
| ol ul, | |||
| ul ol { | |||
| margin-bottom: 0; | |||
| } | |||
| dt { | |||
| font-weight: 700; | |||
| } | |||
| dd { | |||
| margin-bottom: 0.5rem; | |||
| margin-left: 0; | |||
| } | |||
| blockquote { | |||
| margin: 0 0 1rem; | |||
| } | |||
| b, | |||
| strong { | |||
| font-weight: bolder; | |||
| } | |||
| small { | |||
| font-size: 0.875em; | |||
| } | |||
| mark { | |||
| padding: 0.1875em; | |||
| background-color: var(--bs-highlight-bg); | |||
| } | |||
| sub, | |||
| sup { | |||
| position: relative; | |||
| font-size: 0.75em; | |||
| line-height: 0; | |||
| vertical-align: baseline; | |||
| } | |||
| sub { | |||
| bottom: -0.25em; | |||
| } | |||
| sup { | |||
| top: -0.5em; | |||
| } | |||
| a { | |||
| color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); | |||
| text-decoration: underline; | |||
| } | |||
| a:hover { | |||
| --bs-link-color-rgb: var(--bs-link-hover-color-rgb); | |||
| } | |||
| a:not([href]):not([class]), a:not([href]):not([class]):hover { | |||
| color: inherit; | |||
| text-decoration: none; | |||
| } | |||
| pre, | |||
| code, | |||
| kbd, | |||
| samp { | |||
| font-family: var(--bs-font-monospace); | |||
| font-size: 1em; | |||
| } | |||
| pre { | |||
| display: block; | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| overflow: auto; | |||
| font-size: 0.875em; | |||
| } | |||
| pre code { | |||
| font-size: inherit; | |||
| color: inherit; | |||
| word-break: normal; | |||
| } | |||
| code { | |||
| font-size: 0.875em; | |||
| color: var(--bs-code-color); | |||
| word-wrap: break-word; | |||
| } | |||
| a > code { | |||
| color: inherit; | |||
| } | |||
| kbd { | |||
| padding: 0.1875rem 0.375rem; | |||
| font-size: 0.875em; | |||
| color: var(--bs-body-bg); | |||
| background-color: var(--bs-body-color); | |||
| border-radius: 0.25rem; | |||
| } | |||
| kbd kbd { | |||
| padding: 0; | |||
| font-size: 1em; | |||
| } | |||
| figure { | |||
| margin: 0 0 1rem; | |||
| } | |||
| img, | |||
| svg { | |||
| vertical-align: middle; | |||
| } | |||
| table { | |||
| caption-side: bottom; | |||
| border-collapse: collapse; | |||
| } | |||
| caption { | |||
| padding-top: 0.5rem; | |||
| padding-bottom: 0.5rem; | |||
| color: var(--bs-secondary-color); | |||
| text-align: left; | |||
| } | |||
| th { | |||
| text-align: inherit; | |||
| text-align: -webkit-match-parent; | |||
| } | |||
| thead, | |||
| tbody, | |||
| tfoot, | |||
| tr, | |||
| td, | |||
| th { | |||
| border-color: inherit; | |||
| border-style: solid; | |||
| border-width: 0; | |||
| } | |||
| label { | |||
| display: inline-block; | |||
| } | |||
| button { | |||
| border-radius: 0; | |||
| } | |||
| button:focus:not(:focus-visible) { | |||
| outline: 0; | |||
| } | |||
| input, | |||
| button, | |||
| select, | |||
| optgroup, | |||
| 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; | |||
| } | |||
| select:disabled { | |||
| opacity: 1; | |||
| } | |||
| [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { | |||
| display: none !important; | |||
| } | |||
| button, | |||
| [type=button], | |||
| [type=reset], | |||
| [type=submit] { | |||
| -webkit-appearance: button; | |||
| } | |||
| button:not(:disabled), | |||
| [type=button]:not(:disabled), | |||
| [type=reset]:not(:disabled), | |||
| [type=submit]: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: 0.5rem; | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| line-height: inherit; | |||
| } | |||
| @media (min-width: 1200px) { | |||
| legend { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| legend + * { | |||
| clear: left; | |||
| } | |||
| ::-webkit-datetime-edit-fields-wrapper, | |||
| ::-webkit-datetime-edit-text, | |||
| ::-webkit-datetime-edit-minute, | |||
| ::-webkit-datetime-edit-hour-field, | |||
| ::-webkit-datetime-edit-day-field, | |||
| ::-webkit-datetime-edit-month-field, | |||
| ::-webkit-datetime-edit-year-field { | |||
| padding: 0; | |||
| } | |||
| ::-webkit-inner-spin-button { | |||
| height: auto; | |||
| } | |||
| [type=search] { | |||
| outline-offset: -2px; | |||
| -webkit-appearance: textfield; | |||
| } | |||
| /* rtl:raw: | |||
| [type="tel"], | |||
| [type="url"], | |||
| [type="email"], | |||
| [type="number"] { | |||
| direction: ltr; | |||
| } | |||
| */ | |||
| ::-webkit-search-decoration { | |||
| -webkit-appearance: none; | |||
| } | |||
| ::-webkit-color-swatch-wrapper { | |||
| padding: 0; | |||
| } | |||
| ::-webkit-file-upload-button { | |||
| font: inherit; | |||
| -webkit-appearance: button; | |||
| } | |||
| ::file-selector-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; | |||
| } | |||
| /*# sourceMappingURL=bootstrap-reboot.css.map */ | |||
| @ -0,0 +1,590 @@ | |||
| /*! | |||
| * Bootstrap Reboot v5.3.0 (https://getbootstrap.com/) | |||
| * Copyright 2011-2023 The Bootstrap Authors | |||
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) | |||
| */ | |||
| :root, | |||
| [data-bs-theme=light] { | |||
| --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-black: #000; | |||
| --bs-white: #fff; | |||
| --bs-gray: #6c757d; | |||
| --bs-gray-dark: #343a40; | |||
| --bs-gray-100: #f8f9fa; | |||
| --bs-gray-200: #e9ecef; | |||
| --bs-gray-300: #dee2e6; | |||
| --bs-gray-400: #ced4da; | |||
| --bs-gray-500: #adb5bd; | |||
| --bs-gray-600: #6c757d; | |||
| --bs-gray-700: #495057; | |||
| --bs-gray-800: #343a40; | |||
| --bs-gray-900: #212529; | |||
| --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-primary-rgb: 13, 110, 253; | |||
| --bs-secondary-rgb: 108, 117, 125; | |||
| --bs-success-rgb: 25, 135, 84; | |||
| --bs-info-rgb: 13, 202, 240; | |||
| --bs-warning-rgb: 255, 193, 7; | |||
| --bs-danger-rgb: 220, 53, 69; | |||
| --bs-light-rgb: 248, 249, 250; | |||
| --bs-dark-rgb: 33, 37, 41; | |||
| --bs-primary-text-emphasis: #052c65; | |||
| --bs-secondary-text-emphasis: #2b2f32; | |||
| --bs-success-text-emphasis: #0a3622; | |||
| --bs-info-text-emphasis: #055160; | |||
| --bs-warning-text-emphasis: #664d03; | |||
| --bs-danger-text-emphasis: #58151c; | |||
| --bs-light-text-emphasis: #495057; | |||
| --bs-dark-text-emphasis: #495057; | |||
| --bs-primary-bg-subtle: #cfe2ff; | |||
| --bs-secondary-bg-subtle: #e2e3e5; | |||
| --bs-success-bg-subtle: #d1e7dd; | |||
| --bs-info-bg-subtle: #cff4fc; | |||
| --bs-warning-bg-subtle: #fff3cd; | |||
| --bs-danger-bg-subtle: #f8d7da; | |||
| --bs-light-bg-subtle: #fcfcfd; | |||
| --bs-dark-bg-subtle: #ced4da; | |||
| --bs-primary-border-subtle: #9ec5fe; | |||
| --bs-secondary-border-subtle: #c4c8cb; | |||
| --bs-success-border-subtle: #a3cfbb; | |||
| --bs-info-border-subtle: #9eeaf9; | |||
| --bs-warning-border-subtle: #ffe69c; | |||
| --bs-danger-border-subtle: #f1aeb5; | |||
| --bs-light-border-subtle: #e9ecef; | |||
| --bs-dark-border-subtle: #adb5bd; | |||
| --bs-white-rgb: 255, 255, 255; | |||
| --bs-black-rgb: 0, 0, 0; | |||
| --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, 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)); | |||
| --bs-body-font-family: var(--bs-font-sans-serif); | |||
| --bs-body-font-size: 1rem; | |||
| --bs-body-font-weight: 400; | |||
| --bs-body-line-height: 1.5; | |||
| --bs-body-color: #212529; | |||
| --bs-body-color-rgb: 33, 37, 41; | |||
| --bs-body-bg: #fff; | |||
| --bs-body-bg-rgb: 255, 255, 255; | |||
| --bs-emphasis-color: #000; | |||
| --bs-emphasis-color-rgb: 0, 0, 0; | |||
| --bs-secondary-color: rgba(33, 37, 41, 0.75); | |||
| --bs-secondary-color-rgb: 33, 37, 41; | |||
| --bs-secondary-bg: #e9ecef; | |||
| --bs-secondary-bg-rgb: 233, 236, 239; | |||
| --bs-tertiary-color: rgba(33, 37, 41, 0.5); | |||
| --bs-tertiary-color-rgb: 33, 37, 41; | |||
| --bs-tertiary-bg: #f8f9fa; | |||
| --bs-tertiary-bg-rgb: 248, 249, 250; | |||
| --bs-heading-color: inherit; | |||
| --bs-link-color: #0d6efd; | |||
| --bs-link-color-rgb: 13, 110, 253; | |||
| --bs-link-decoration: underline; | |||
| --bs-link-hover-color: #0a58ca; | |||
| --bs-link-hover-color-rgb: 10, 88, 202; | |||
| --bs-code-color: #d63384; | |||
| --bs-highlight-bg: #fff3cd; | |||
| --bs-border-width: 1px; | |||
| --bs-border-style: solid; | |||
| --bs-border-color: #dee2e6; | |||
| --bs-border-color-translucent: rgba(0, 0, 0, 0.175); | |||
| --bs-border-radius: 0.375rem; | |||
| --bs-border-radius-sm: 0.25rem; | |||
| --bs-border-radius-lg: 0.5rem; | |||
| --bs-border-radius-xl: 1rem; | |||
| --bs-border-radius-xxl: 2rem; | |||
| --bs-border-radius-2xl: var(--bs-border-radius-xxl); | |||
| --bs-border-radius-pill: 50rem; | |||
| --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); | |||
| --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); | |||
| --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); | |||
| --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); | |||
| --bs-focus-ring-width: 0.25rem; | |||
| --bs-focus-ring-opacity: 0.25; | |||
| --bs-focus-ring-color: rgba(13, 110, 253, 0.25); | |||
| --bs-form-valid-color: #198754; | |||
| --bs-form-valid-border-color: #198754; | |||
| --bs-form-invalid-color: #dc3545; | |||
| --bs-form-invalid-border-color: #dc3545; | |||
| } | |||
| [data-bs-theme=dark] { | |||
| color-scheme: dark; | |||
| --bs-body-color: #adb5bd; | |||
| --bs-body-color-rgb: 173, 181, 189; | |||
| --bs-body-bg: #212529; | |||
| --bs-body-bg-rgb: 33, 37, 41; | |||
| --bs-emphasis-color: #fff; | |||
| --bs-emphasis-color-rgb: 255, 255, 255; | |||
| --bs-secondary-color: rgba(173, 181, 189, 0.75); | |||
| --bs-secondary-color-rgb: 173, 181, 189; | |||
| --bs-secondary-bg: #343a40; | |||
| --bs-secondary-bg-rgb: 52, 58, 64; | |||
| --bs-tertiary-color: rgba(173, 181, 189, 0.5); | |||
| --bs-tertiary-color-rgb: 173, 181, 189; | |||
| --bs-tertiary-bg: #2b3035; | |||
| --bs-tertiary-bg-rgb: 43, 48, 53; | |||
| --bs-primary-text-emphasis: #6ea8fe; | |||
| --bs-secondary-text-emphasis: #a7acb1; | |||
| --bs-success-text-emphasis: #75b798; | |||
| --bs-info-text-emphasis: #6edff6; | |||
| --bs-warning-text-emphasis: #ffda6a; | |||
| --bs-danger-text-emphasis: #ea868f; | |||
| --bs-light-text-emphasis: #f8f9fa; | |||
| --bs-dark-text-emphasis: #dee2e6; | |||
| --bs-primary-bg-subtle: #031633; | |||
| --bs-secondary-bg-subtle: #161719; | |||
| --bs-success-bg-subtle: #051b11; | |||
| --bs-info-bg-subtle: #032830; | |||
| --bs-warning-bg-subtle: #332701; | |||
| --bs-danger-bg-subtle: #2c0b0e; | |||
| --bs-light-bg-subtle: #343a40; | |||
| --bs-dark-bg-subtle: #1a1d20; | |||
| --bs-primary-border-subtle: #084298; | |||
| --bs-secondary-border-subtle: #41464b; | |||
| --bs-success-border-subtle: #0f5132; | |||
| --bs-info-border-subtle: #087990; | |||
| --bs-warning-border-subtle: #997404; | |||
| --bs-danger-border-subtle: #842029; | |||
| --bs-light-border-subtle: #495057; | |||
| --bs-dark-border-subtle: #343a40; | |||
| --bs-heading-color: inherit; | |||
| --bs-link-color: #6ea8fe; | |||
| --bs-link-hover-color: #8bb9fe; | |||
| --bs-link-color-rgb: 110, 168, 254; | |||
| --bs-link-hover-color-rgb: 139, 185, 254; | |||
| --bs-code-color: #e685b5; | |||
| --bs-border-color: #495057; | |||
| --bs-border-color-translucent: rgba(255, 255, 255, 0.15); | |||
| --bs-form-valid-color: #75b798; | |||
| --bs-form-valid-border-color: #75b798; | |||
| --bs-form-invalid-color: #ea868f; | |||
| --bs-form-invalid-border-color: #ea868f; | |||
| } | |||
| *, | |||
| *::before, | |||
| *::after { | |||
| box-sizing: border-box; | |||
| } | |||
| @media (prefers-reduced-motion: no-preference) { | |||
| :root { | |||
| scroll-behavior: smooth; | |||
| } | |||
| } | |||
| body { | |||
| margin: 0; | |||
| font-family: var(--bs-body-font-family); | |||
| font-size: var(--bs-body-font-size); | |||
| font-weight: var(--bs-body-font-weight); | |||
| line-height: var(--bs-body-line-height); | |||
| color: var(--bs-body-color); | |||
| text-align: var(--bs-body-text-align); | |||
| background-color: var(--bs-body-bg); | |||
| -webkit-text-size-adjust: 100%; | |||
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |||
| } | |||
| hr { | |||
| margin: 1rem 0; | |||
| color: inherit; | |||
| border: 0; | |||
| border-top: var(--bs-border-width) solid; | |||
| opacity: 0.25; | |||
| } | |||
| h6, h5, h4, h3, h2, h1 { | |||
| margin-top: 0; | |||
| margin-bottom: 0.5rem; | |||
| font-weight: 500; | |||
| line-height: 1.2; | |||
| color: var(--bs-heading-color); | |||
| } | |||
| h1 { | |||
| font-size: calc(1.375rem + 1.5vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h1 { | |||
| font-size: 2.5rem; | |||
| } | |||
| } | |||
| h2 { | |||
| font-size: calc(1.325rem + 0.9vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h2 { | |||
| font-size: 2rem; | |||
| } | |||
| } | |||
| h3 { | |||
| font-size: calc(1.3rem + 0.6vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h3 { | |||
| font-size: 1.75rem; | |||
| } | |||
| } | |||
| h4 { | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h4 { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| h5 { | |||
| font-size: 1.25rem; | |||
| } | |||
| h6 { | |||
| font-size: 1rem; | |||
| } | |||
| p { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| abbr[title] { | |||
| -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-right: 2rem; | |||
| } | |||
| ol, | |||
| ul, | |||
| dl { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| ol ol, | |||
| ul ul, | |||
| ol ul, | |||
| ul ol { | |||
| margin-bottom: 0; | |||
| } | |||
| dt { | |||
| font-weight: 700; | |||
| } | |||
| dd { | |||
| margin-bottom: 0.5rem; | |||
| margin-right: 0; | |||
| } | |||
| blockquote { | |||
| margin: 0 0 1rem; | |||
| } | |||
| b, | |||
| strong { | |||
| font-weight: bolder; | |||
| } | |||
| small { | |||
| font-size: 0.875em; | |||
| } | |||
| mark { | |||
| padding: 0.1875em; | |||
| background-color: var(--bs-highlight-bg); | |||
| } | |||
| sub, | |||
| sup { | |||
| position: relative; | |||
| font-size: 0.75em; | |||
| line-height: 0; | |||
| vertical-align: baseline; | |||
| } | |||
| sub { | |||
| bottom: -0.25em; | |||
| } | |||
| sup { | |||
| top: -0.5em; | |||
| } | |||
| a { | |||
| color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); | |||
| text-decoration: underline; | |||
| } | |||
| a:hover { | |||
| --bs-link-color-rgb: var(--bs-link-hover-color-rgb); | |||
| } | |||
| a:not([href]):not([class]), a:not([href]):not([class]):hover { | |||
| color: inherit; | |||
| text-decoration: none; | |||
| } | |||
| pre, | |||
| code, | |||
| kbd, | |||
| samp { | |||
| font-family: var(--bs-font-monospace); | |||
| font-size: 1em; | |||
| } | |||
| pre { | |||
| display: block; | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| overflow: auto; | |||
| font-size: 0.875em; | |||
| } | |||
| pre code { | |||
| font-size: inherit; | |||
| color: inherit; | |||
| word-break: normal; | |||
| } | |||
| code { | |||
| font-size: 0.875em; | |||
| color: var(--bs-code-color); | |||
| word-wrap: break-word; | |||
| } | |||
| a > code { | |||
| color: inherit; | |||
| } | |||
| kbd { | |||
| padding: 0.1875rem 0.375rem; | |||
| font-size: 0.875em; | |||
| color: var(--bs-body-bg); | |||
| background-color: var(--bs-body-color); | |||
| border-radius: 0.25rem; | |||
| } | |||
| kbd kbd { | |||
| padding: 0; | |||
| font-size: 1em; | |||
| } | |||
| figure { | |||
| margin: 0 0 1rem; | |||
| } | |||
| img, | |||
| svg { | |||
| vertical-align: middle; | |||
| } | |||
| table { | |||
| caption-side: bottom; | |||
| border-collapse: collapse; | |||
| } | |||
| caption { | |||
| padding-top: 0.5rem; | |||
| padding-bottom: 0.5rem; | |||
| color: var(--bs-secondary-color); | |||
| text-align: right; | |||
| } | |||
| th { | |||
| text-align: inherit; | |||
| text-align: -webkit-match-parent; | |||
| } | |||
| thead, | |||
| tbody, | |||
| tfoot, | |||
| tr, | |||
| td, | |||
| th { | |||
| border-color: inherit; | |||
| border-style: solid; | |||
| border-width: 0; | |||
| } | |||
| label { | |||
| display: inline-block; | |||
| } | |||
| button { | |||
| border-radius: 0; | |||
| } | |||
| button:focus:not(:focus-visible) { | |||
| outline: 0; | |||
| } | |||
| input, | |||
| button, | |||
| select, | |||
| optgroup, | |||
| 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; | |||
| } | |||
| select:disabled { | |||
| opacity: 1; | |||
| } | |||
| [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { | |||
| display: none !important; | |||
| } | |||
| button, | |||
| [type=button], | |||
| [type=reset], | |||
| [type=submit] { | |||
| -webkit-appearance: button; | |||
| } | |||
| button:not(:disabled), | |||
| [type=button]:not(:disabled), | |||
| [type=reset]:not(:disabled), | |||
| [type=submit]: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: right; | |||
| width: 100%; | |||
| padding: 0; | |||
| margin-bottom: 0.5rem; | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| line-height: inherit; | |||
| } | |||
| @media (min-width: 1200px) { | |||
| legend { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| legend + * { | |||
| clear: right; | |||
| } | |||
| ::-webkit-datetime-edit-fields-wrapper, | |||
| ::-webkit-datetime-edit-text, | |||
| ::-webkit-datetime-edit-minute, | |||
| ::-webkit-datetime-edit-hour-field, | |||
| ::-webkit-datetime-edit-day-field, | |||
| ::-webkit-datetime-edit-month-field, | |||
| ::-webkit-datetime-edit-year-field { | |||
| padding: 0; | |||
| } | |||
| ::-webkit-inner-spin-button { | |||
| height: auto; | |||
| } | |||
| [type=search] { | |||
| outline-offset: -2px; | |||
| -webkit-appearance: textfield; | |||
| } | |||
| [type="tel"], | |||
| [type="url"], | |||
| [type="email"], | |||
| [type="number"] { | |||
| direction: ltr; | |||
| } | |||
| ::-webkit-search-decoration { | |||
| -webkit-appearance: none; | |||
| } | |||
| ::-webkit-color-swatch-wrapper { | |||
| padding: 0; | |||
| } | |||
| ::-webkit-file-upload-button { | |||
| font: inherit; | |||
| -webkit-appearance: button; | |||
| } | |||
| ::file-selector-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; | |||
| } | |||
| /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ | |||