├── .babelrc
├── .editorconfig
├── .gitignore
├── .isort.cfg
├── .nvmrc
├── Dockerfile
├── Pipfile
├── Pipfile.lock
├── README.md
├── conf
├── gunicorn.service
├── gunicorn.socket
└── nginx.conf
├── contact
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20191208_0303.py
│ └── __init__.py
└── models.py
├── dev.txt
├── flex
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_flexpage_body.py
│ ├── 0003_auto_20191207_0436.py
│ ├── 0004_auto_20191208_0129.py
│ └── __init__.py
└── models.py
├── frontend
├── js
│ └── src
│ │ ├── alert.js
│ │ ├── button.js
│ │ ├── carousel.js
│ │ ├── collapse.js
│ │ ├── dropdown.js
│ │ ├── index.js
│ │ ├── modal.js
│ │ ├── popover.js
│ │ ├── scrollspy.js
│ │ ├── tab.js
│ │ ├── toast.js
│ │ ├── tools
│ │ └── sanitizer.js
│ │ ├── tooltip.js
│ │ └── util.js
└── scss
│ ├── _alert.scss
│ ├── _badge.scss
│ ├── _breadcrumb.scss
│ ├── _button-group.scss
│ ├── _buttons.scss
│ ├── _card.scss
│ ├── _carousel.scss
│ ├── _close.scss
│ ├── _code.scss
│ ├── _custom-forms.scss
│ ├── _custom.scss
│ ├── _dropdown.scss
│ ├── _forms.scss
│ ├── _functions.scss
│ ├── _grid.scss
│ ├── _images.scss
│ ├── _input-group.scss
│ ├── _jumbotron.scss
│ ├── _list-group.scss
│ ├── _media.scss
│ ├── _mixins.scss
│ ├── _modal.scss
│ ├── _nav.scss
│ ├── _navbar.scss
│ ├── _pagination.scss
│ ├── _popover.scss
│ ├── _print.scss
│ ├── _progress.scss
│ ├── _reboot.scss
│ ├── _root.scss
│ ├── _spinners.scss
│ ├── _tables.scss
│ ├── _toasts.scss
│ ├── _tooltip.scss
│ ├── _transitions.scss
│ ├── _type.scss
│ ├── _utilities.scss
│ ├── _variables.scss
│ ├── bootstrap-grid.scss
│ ├── bootstrap-reboot.scss
│ ├── bootstrap.scss
│ ├── mixins
│ ├── _alert.scss
│ ├── _background-variant.scss
│ ├── _badge.scss
│ ├── _border-radius.scss
│ ├── _box-shadow.scss
│ ├── _breakpoints.scss
│ ├── _buttons.scss
│ ├── _caret.scss
│ ├── _clearfix.scss
│ ├── _deprecate.scss
│ ├── _float.scss
│ ├── _forms.scss
│ ├── _gradients.scss
│ ├── _grid-framework.scss
│ ├── _grid.scss
│ ├── _hover.scss
│ ├── _image.scss
│ ├── _list-group.scss
│ ├── _lists.scss
│ ├── _nav-divider.scss
│ ├── _pagination.scss
│ ├── _reset-text.scss
│ ├── _resize.scss
│ ├── _screen-reader.scss
│ ├── _size.scss
│ ├── _table-row.scss
│ ├── _text-emphasis.scss
│ ├── _text-hide.scss
│ ├── _text-truncate.scss
│ ├── _transition.scss
│ └── _visibility.scss
│ ├── static
│ └── images
│ │ └── link-arrow.svg
│ ├── utilities
│ ├── _align.scss
│ ├── _background.scss
│ ├── _borders.scss
│ ├── _clearfix.scss
│ ├── _display.scss
│ ├── _embed.scss
│ ├── _flex.scss
│ ├── _float.scss
│ ├── _overflow.scss
│ ├── _position.scss
│ ├── _screenreaders.scss
│ ├── _shadows.scss
│ ├── _sizing.scss
│ ├── _spacing.scss
│ ├── _stretched-link.scss
│ ├── _text.scss
│ └── _visibility.scss
│ └── vendor
│ └── _rfs.scss
├── home
├── __init__.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_create_homepage.py
│ ├── 0003_auto_20191202_1816.py
│ ├── 0004_homepage_body.py
│ ├── 0005_auto_20191207_0211.py
│ ├── 0006_auto_20191207_0436.py
│ └── __init__.py
├── models.py
├── static
│ └── css
│ │ └── welcome_page.css
└── templates
│ └── home
│ └── home_page.html
├── manage.py
├── menus
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_menuitem.py
│ ├── 0003_menuitem_page.py
│ └── __init__.py
├── models.py
├── templatetags
│ └── menu_tags.py
└── wagtail_hooks.py
├── package-lock.json
├── package.json
├── requirements.txt
├── rocketman
├── __init__.py
├── settings
│ ├── __init__.py
│ ├── base.py
│ ├── dev.py
│ └── production.py
├── static
│ ├── css
│ │ ├── bootstrap.css
│ │ └── bootstrap.css.map
│ ├── images
│ │ ├── contact-overlay.png
│ │ ├── facebook-og.png
│ │ ├── facebook.svg
│ │ ├── grey-pattern.png
│ │ ├── instagram.svg
│ │ ├── link-arrow.svg
│ │ ├── logo.png
│ │ ├── snapchat.svg
│ │ ├── twitter-og.png
│ │ ├── twitter.svg
│ │ └── youtube.svg
│ └── js
│ │ ├── index.js
│ │ └── index.js.map
├── templates
│ ├── 404.html
│ ├── 500.html
│ ├── base.html
│ ├── contact
│ │ ├── contact_page.html
│ │ └── contact_page_landing.html
│ ├── flex
│ │ └── flex_page.html
│ ├── includes
│ │ ├── footer.html
│ │ └── header.html
│ ├── services
│ │ ├── service_listing_page.html
│ │ └── service_page.html
│ ├── streams
│ │ ├── call_to_action_block.html
│ │ ├── cards_block.html
│ │ ├── image_and_text_block.html
│ │ ├── large_image_block.html
│ │ ├── pricing_table_block.html
│ │ ├── simple_richtext_block.html
│ │ ├── testimonial_block.html
│ │ └── title_block.html
│ └── wagtailadmin
│ │ └── base.html
├── urls.py
└── wsgi.py
├── services
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_servicepage_body.py
│ └── __init__.py
└── models.py
├── site_settings
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_contactsettings.py
│ ├── 0003_hourssettings.py
│ ├── 0004_footerctasettings.py
│ ├── 0005_auto_20191208_0408.py
│ └── __init__.py
└── models.py
├── streams
├── __init__.py
├── apps.py
├── blocks.py
└── migrations
│ └── __init__.py
└── testimonials
├── __init__.py
├── admin.py
├── apps.py
├── migrations
├── 0001_initial.py
└── __init__.py
└── models.py
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Defines the coding style for different editors and IDEs.
2 | # http://editorconfig.org
3 |
4 | # top-most EditorConfig file
5 | root = true
6 |
7 | # Rules for source code.
8 | [*]
9 | charset = utf-8
10 | end_of_line = lf
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 2
15 |
16 | # Rules for Python code.
17 | [*.py]
18 | indent_size = 4
19 |
20 | # Rules for tool configuration.
21 | [{package.json,*.yml, *.yaml}]
22 | indent_size = 2
23 |
24 | # Rules for markdown documents.
25 | [*.md]
26 | trim_trailing_whitespace = false
27 |
28 | # Rules for makefile
29 | [Makefile]
30 | indent_style = tabs
31 | indent_size = 4
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | *.py[cod]
3 | *.egg*
4 | local_dev.py
5 | .mypy_cache
6 | __pycache__/
7 |
8 | # Editors
9 | .idea/
10 | .vscode/
11 |
12 | *.swp
13 | *.pyc
14 | .DS_Store
15 | /.coverage
16 | /dist/
17 | /build/
18 | /MANIFEST
19 | /wagtail.egg-info/
20 | /docs/_build/
21 | /.tox/
22 | /venv
23 | /node_modules/
24 | npm-debug.log*
25 | *.idea/
26 | /*.egg/
27 | /.cache/
28 | /.pytest_cache/
29 | /media/
30 | /rocketmanvenv/
31 | .venv/
32 |
33 | ### JetBrains
34 | .idea/
35 | *.iml
36 | *.ipr
37 | *.iws
38 | coverage/
39 | client/node_modules
40 |
41 | ### Databases
42 | *.sqlite3
43 |
44 | ### Local static files
45 | /static/
46 |
47 | # Temporary files
48 | *.swp
49 | *.swo
50 | *.tmp
51 | *~
52 | *.todo
53 |
54 |
55 | # Local files
56 | local.py
57 | .env
58 |
59 | nginx-error.log
60 |
61 |
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | indent=' '
3 | multi_line_output=5
4 | known_django=django
5 | sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
6 | skip=migrations
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 10.15.3
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use an official Python runtime as a parent image
2 | FROM python:3.7
3 | LABEL maintainer="hello@wagtail.io"
4 |
5 | # Set environment varibles
6 | ENV PYTHONUNBUFFERED 1
7 | ENV DJANGO_ENV dev
8 |
9 | COPY ./requirements.txt /code/requirements.txt
10 | RUN pip install --upgrade pip
11 | # Install any needed packages specified in requirements.txt
12 | RUN pip install -r /code/requirements.txt
13 | RUN pip install gunicorn
14 |
15 | # Copy the current directory contents into the container at /code/
16 | COPY . /code/
17 | # Set the working directory to /code/
18 | WORKDIR /code/
19 |
20 | RUN python manage.py migrate
21 |
22 | RUN useradd wagtail
23 | RUN chown -R wagtail /code
24 | USER wagtail
25 |
26 | EXPOSE 8000
27 | CMD exec gunicorn rocketman.wsgi:application --bind 0.0.0.0:8000 --workers 3
28 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 |
8 | [packages]
9 | wagtail = "==2.8"
10 | Django = "==2.2.10"
11 | django-extensions = "==2.2.1"
12 | django-widget-tweaks = "==1.4.5"
13 | sentry-sdk = "==0.13.5"
14 | django-debug-toolbar = "==2.1"
15 | pudb = "==2019.2"
16 |
17 | [requires]
18 | python_version = "3.7"
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rocketman: A Wagtail for Beginners Course
2 |
3 | View the production website at [rocketman.learnwagtail.com](http://rocketman.learnwagtail.com)
4 |
5 | View the course at [https://learnwagtail.com/wagtail-for-beginners/](https://learnwagtail.com/wagtail-for-beginners/)
6 |
7 | > The purpose of this repository is to provide code support form the [Wagtail for Beginners course](https://learnwagtail.com/wagtail-for-beginners/).
8 |
9 | ## Installation
10 | There are several installation methods. For local development, you have three main options that are used in this course:
11 |
12 | #### venv
13 | ```bash
14 | python3 -m venv rocketmanvenv
15 | source rocketmanvenv/bin/activate
16 | python manage.py migrate
17 | python manage.py runserver 0.0.0.0:8000
18 | ```
19 |
20 | #### pipenv
21 | ```bash
22 | pipenv install
23 | pipenv shell
24 | python manage.py migrate
25 | python manage.py runserver 0.0.0.0:8000
26 | ```
27 |
28 | #### virtualenv
29 | ```bash
30 | virtualenv rocketmanvenv
31 | source rocketmanvenv/bin/activate
32 | python manage.py migrate
33 | python manage.py runserver 0.0.0.0:8000
34 | ```
35 |
36 | ## Launching your site
37 | Make sure you watch the video on launching your Wagtail CMS site on [Digital Ocean](https://m.do.co/c/7598914bd459). If you decide to clone this project as a test, make sure you update the `@todo`s in `production.py`.
38 |
39 | There's also a sample nginx config, and a gunicorn files in the `conf/` directory. That will show you how rocketman.learnwagtail.com is setup.
40 |
41 | ## Digital Ocean Hosting Perks
42 | If you [use this link](https://m.do.co/c/7598914bd459), you'll get $100 in credit over the first 60 days to setup your Wagtail website using [Digital Ocean](https://m.do.co/c/7598914bd459)
43 |
44 | ## General dev support
45 | If you have questions about adapting this source code to your project, there are two primary places to turn to:
46 |
47 | 1. [Slack](https://wagtail.io/slack) — join us in the #support channel.
48 | 2. [Learn Wagtail Tutorials](https://wagtail.io/course) — Over 50 free helpful videos on various subjects
49 |
50 | ## Frontend development
51 | Wagtail for Beginners does _not_ cover any of the frontend build. This was a conscious decision so the course can focus entirely on Wagtail and Django.
52 |
53 | But if you want to extend the frontend you can get it setup with `npm i` and run any of the commands in the `package.json` file.
54 |
--------------------------------------------------------------------------------
/conf/gunicorn.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=gunicorn daemon
3 | Requires=gunicorn.socket
4 | After=network.target
5 |
6 | [Service]
7 | User=kalob
8 | Group=www-data
9 | WorkingDirectory=/home/kalob/rocketman
10 | ExecStart=/home/kalob/rocketman/rocketmanvenv/bin/gunicorn \
11 | --access-logfile - \
12 | --workers 3 \
13 | --bind unix:/run/gunicorn.sock \
14 | --timeout 60 \
15 | rocketman.wsgi:application
16 |
17 | [Install]
18 | WantedBy=multi-user.target
19 |
--------------------------------------------------------------------------------
/conf/gunicorn.socket:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=gunicorn socket
3 |
4 | [Socket]
5 | ListenStream=/run/gunicorn.sock
6 |
7 | [Install]
8 | WantedBy=sockets.target
9 |
--------------------------------------------------------------------------------
/conf/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80;
4 | server_name rocketman.learnwagtail.com;
5 | charset UTF-8;
6 |
7 | error_log /home/kalob/rocketman/nginx-error.log;
8 |
9 | client_max_body_size 10m;
10 | client_body_buffer_size 16k;
11 | client_header_timeout 30s;
12 | client_body_timeout 60s;
13 |
14 | send_timeout 60s;
15 |
16 | location = /favicon.ico { access_log off; log_not_found off; }
17 | location /static/ {
18 | alias /home/kalob/rocketman/static/;
19 | }
20 |
21 | location /media/ {
22 | alias /home/kalob/rocketman/media/;
23 | }
24 |
25 | location / {
26 | include proxy_params;
27 | proxy_read_timeout 30s;
28 | proxy_connect_timeout 30s;
29 | proxy_http_version 1.1;
30 | proxy_pass http://unix:/run/gunicorn.sock;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/contact/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingForEverybody/rocketman/210be13dc05f07ce82748bf31155818c2c363f5d/contact/__init__.py
--------------------------------------------------------------------------------
/contact/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ContactConfig(AppConfig):
5 | name = 'contact'
6 |
--------------------------------------------------------------------------------
/contact/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.8 on 2019-12-08 02:27
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import modelcluster.fields
6 | import wagtail.core.fields
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'),
15 | ('wagtailimages', '0001_squashed_0021'),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name='ContactPage',
21 | fields=[
22 | ('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')),
23 | ('to_address', models.CharField(blank=True, help_text='Optional - form submissions will be emailed to these addresses. Separate multiple addresses by comma.', max_length=255, verbose_name='to address')),
24 | ('from_address', models.CharField(blank=True, max_length=255, verbose_name='from address')),
25 | ('subject', models.CharField(blank=True, max_length=255, verbose_name='subject')),
26 | ('intro', wagtail.core.fields.RichTextField(blank=True)),
27 | ('thank_you_text', wagtail.core.fields.RichTextField(blank=True)),
28 | ('map_url', models.URLField(blank=True, help_text='Optional. If you provide a link here the map image will become a link')),
29 | ('map_image', models.ForeignKey(help_text='Image will be cropped to 580px by 355px', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.Image')),
30 | ],
31 | options={
32 | 'abstract': False,
33 | },
34 | bases=('wagtailcore.page',),
35 | ),
36 | migrations.CreateModel(
37 | name='FormField',
38 | fields=[
39 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
40 | ('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
41 | ('label', models.CharField(help_text='The label of the form field', max_length=255, verbose_name='label')),
42 | ('field_type', models.CharField(choices=[('singleline', 'Single line text'), ('multiline', 'Multi-line text'), ('email', 'Email'), ('number', 'Number'), ('url', 'URL'), ('checkbox', 'Checkbox'), ('checkboxes', 'Checkboxes'), ('dropdown', 'Drop down'), ('multiselect', 'Multiple select'), ('radio', 'Radio buttons'), ('date', 'Date'), ('datetime', 'Date/time'), ('hidden', 'Hidden field')], max_length=16, verbose_name='field type')),
43 | ('required', models.BooleanField(default=True, verbose_name='required')),
44 | ('choices', models.TextField(blank=True, help_text='Comma separated list of choices. Only applicable in checkboxes, radio and dropdown.', verbose_name='choices')),
45 | ('default_value', models.CharField(blank=True, help_text='Default value. Comma separated values supported for checkboxes.', max_length=255, verbose_name='default value')),
46 | ('help_text', models.CharField(blank=True, max_length=255, verbose_name='help text')),
47 | ('page', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_fields', to='contact.ContactPage')),
48 | ],
49 | options={
50 | 'ordering': ['sort_order'],
51 | 'abstract': False,
52 | },
53 | ),
54 | ]
55 |
--------------------------------------------------------------------------------
/contact/migrations/0002_auto_20191208_0303.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.8 on 2019-12-08 03:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('contact', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='formfield',
15 | name='field_type',
16 | field=models.CharField(choices=[('singleline', 'Single line text'), ('multiline', 'Multi-line text'), ('email', 'Email'), ('url', 'URL')], max_length=16, verbose_name='Field Type'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/contact/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingForEverybody/rocketman/210be13dc05f07ce82748bf31155818c2c363f5d/contact/migrations/__init__.py
--------------------------------------------------------------------------------
/contact/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.translation import ugettext_lazy as _
3 |
4 | from modelcluster.models import ParentalKey
5 | from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
6 | from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
7 | from wagtail.core.fields import RichTextField
8 | from wagtail.images.edit_handlers import ImageChooserPanel
9 |
10 | FORM_FIELD_CHOICES = (
11 | ('singleline', _('Single line text')),
12 | ('multiline', _('Multi-line text')),
13 | ('email', _('Email')),
14 | ('url', _('URL')),
15 | )
16 |
17 | class CustomAbstractFormField(AbstractFormField):
18 | field_type = models.CharField(
19 | verbose_name="Field Type",
20 | max_length=16,
21 | choices=FORM_FIELD_CHOICES,
22 | )
23 |
24 | class Meta:
25 | abstract = True
26 | ordering = ["sort_order"]
27 |
28 |
29 | class FormField(CustomAbstractFormField):
30 | page = ParentalKey(
31 | "ContactPage",
32 | on_delete=models.CASCADE,
33 | related_name='form_fields'
34 | )
35 |
36 |
37 | class ContactPage(AbstractEmailForm):
38 |
39 | template = "contact/contact_page.html"
40 | landing_page_template = "contact/contact_page_landing.html"
41 | subpage_types = []
42 | max_count = 1
43 |
44 | intro = RichTextField(blank=True, features=["bold", "link", "ol", "ul"])
45 | thank_you_text = RichTextField(
46 | blank=True,
47 | features=["bold", "link", "ol", "ul"]
48 | )
49 | map_image = models.ForeignKey(
50 | 'wagtailimages.Image',
51 | null=True,
52 | blank=False,
53 | on_delete=models.SET_NULL,
54 | help_text='Image will be cropped to 580px by 355px',
55 | related_name='+',
56 | )
57 | map_url = models.URLField(
58 | blank=True,
59 | help_text='Optional. If you provide a link here the map image will become a link'
60 | )
61 |
62 | content_panels = AbstractEmailForm.content_panels + [
63 | FieldPanel("intro"),
64 | ImageChooserPanel("map_image"),
65 | FieldPanel("map_url"),
66 | InlinePanel("form_fields", label='Form Fields'),
67 | FieldPanel("thank_you_text"),
68 | FieldPanel("from_address"),
69 | FieldPanel("to_address"),
70 | FieldPanel("subject"),
71 | ]
72 |
--------------------------------------------------------------------------------
/dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | django-debug-toolbar==2.1
3 | pudb==2019.2
4 |
--------------------------------------------------------------------------------
/flex/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingForEverybody/rocketman/210be13dc05f07ce82748bf31155818c2c363f5d/flex/__init__.py
--------------------------------------------------------------------------------
/flex/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class FlexConfig(AppConfig):
5 | name = 'flex'
6 |
--------------------------------------------------------------------------------
/flex/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.8 on 2019-12-07 00:35
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='FlexPage',
18 | fields=[
19 | ('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')),
20 | ],
21 | options={
22 | 'verbose_name': 'Flex (misc) page',
23 | 'verbose_name_plural': 'Flex (misc) pages',
24 | },
25 | bases=('wagtailcore.page',),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/flex/migrations/0002_flexpage_body.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.8 on 2019-12-07 04:19
2 |
3 | from django.db import migrations
4 | import streams.blocks
5 | import wagtail.core.blocks
6 | import wagtail.core.fields
7 | import wagtail.images.blocks
8 | import wagtail.snippets.blocks
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | ('flex', '0001_initial'),
15 | ]
16 |
17 | operations = [
18 | migrations.AddField(
19 | model_name='flexpage',
20 | name='body',
21 | field=wagtail.core.fields.StreamField([('title', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.CharBlock(help_text='Text to display', required=True))])), ('cards', wagtail.core.blocks.StructBlock([('cards', wagtail.core.blocks.ListBlock(wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock(help_text='Bold title text for this card. Max length of 100 characters.', max_length=100)), ('text', wagtail.core.blocks.TextBlock(help_text='Optional text for this card. Max length is 255 characters.', max_length=255, required=False)), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Image will be automagically cropped 570px by 370px')), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))], help_text='Enter a link or select a page'))])))])), ('image_and_text', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(help_text='Image the automagically cropped to 786px by 552px')), ('image_alignment', wagtail.core.blocks.ChoiceBlock(choices=[('left', 'Image to the left'), ('right', 'Image to the right')], help_text='Image on the left with text on the right. Or image on the right with text on the left.')), ('title', wagtail.core.blocks.CharBlock(help_text='Max length of 60 characters.', max_length=60)), ('text', wagtail.core.blocks.CharBlock(max_length=140, required=False)), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))]))])), ('cta', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock(help_text='Max length of 200 characters.', max_length=200)), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))]))])), ('testimonial', wagtail.snippets.blocks.SnippetChooserBlock(target_model='testimonials.Testimonial', template='streams/testimonial_block.html')), ('pricing_table', streams.blocks.PricingTableBlock(table_options={'autoColumnSize': False, 'colHeaders': False, 'contextMenu': ['row_above', 'row_below', '---------', 'col_left', 'col_right', '---------', 'remove_row', 'remove_col', '---------', 'undo', 'redo'], 'editor': 'text', 'minSpareRows': 0, 'renderer': 'text', 'rowHeaders': True, 'startCols': 4, 'startRows': 4, 'stretchH': 'all'}))], blank=True, null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/flex/migrations/0003_auto_20191207_0436.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.8 on 2019-12-07 04:36
2 |
3 | from django.db import migrations
4 | import streams.blocks
5 | import wagtail.core.blocks
6 | import wagtail.core.fields
7 | import wagtail.images.blocks
8 | import wagtail.snippets.blocks
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | ('flex', '0002_flexpage_body'),
15 | ]
16 |
17 | operations = [
18 | migrations.AlterField(
19 | model_name='flexpage',
20 | name='body',
21 | field=wagtail.core.fields.StreamField([('title', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.CharBlock(help_text='Text to display', required=True))])), ('cards', wagtail.core.blocks.StructBlock([('cards', wagtail.core.blocks.ListBlock(wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock(help_text='Bold title text for this card. Max length of 100 characters.', max_length=100)), ('text', wagtail.core.blocks.TextBlock(help_text='Optional text for this card. Max length is 255 characters.', max_length=255, required=False)), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Image will be automagically cropped 570px by 370px')), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))], help_text='Enter a link or select a page'))])))])), ('image_and_text', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(help_text='Image the automagically cropped to 786px by 552px')), ('image_alignment', wagtail.core.blocks.ChoiceBlock(choices=[('left', 'Image to the left'), ('right', 'Image to the right')], help_text='Image on the left with text on the right. Or image on the right with text on the left.')), ('title', wagtail.core.blocks.CharBlock(help_text='Max length of 60 characters.', max_length=60)), ('text', wagtail.core.blocks.CharBlock(max_length=140, required=False)), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))]))])), ('cta', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock(help_text='Max length of 200 characters.', max_length=200)), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))]))])), ('testimonial', wagtail.snippets.blocks.SnippetChooserBlock(target_model='testimonials.Testimonial', template='streams/testimonial_block.html')), ('pricing_table', streams.blocks.PricingTableBlock(table_options={'autoColumnSize': False, 'colHeaders': False, 'contextMenu': ['row_above', 'row_below', '---------', 'col_left', 'col_right', '---------', 'remove_row', 'remove_col', '---------', 'undo', 'redo'], 'editor': 'text', 'minSpareRows': 0, 'renderer': 'text', 'rowHeaders': True, 'startCols': 4, 'startRows': 4, 'stretchH': 'all'})), ('richtext', wagtail.core.blocks.RichTextBlock(features=['bold', 'italic', 'ol', 'ul', 'link'], template='streams/simple_richtext_block.html'))], blank=True, null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/flex/migrations/0004_auto_20191208_0129.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.8 on 2019-12-08 01:29
2 |
3 | from django.db import migrations
4 | import streams.blocks
5 | import wagtail.core.blocks
6 | import wagtail.core.fields
7 | import wagtail.images.blocks
8 | import wagtail.snippets.blocks
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | ('flex', '0003_auto_20191207_0436'),
15 | ]
16 |
17 | operations = [
18 | migrations.AlterField(
19 | model_name='flexpage',
20 | name='body',
21 | field=wagtail.core.fields.StreamField([('title', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.CharBlock(help_text='Text to display', required=True))])), ('cards', wagtail.core.blocks.StructBlock([('cards', wagtail.core.blocks.ListBlock(wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock(help_text='Bold title text for this card. Max length of 100 characters.', max_length=100)), ('text', wagtail.core.blocks.TextBlock(help_text='Optional text for this card. Max length is 255 characters.', max_length=255, required=False)), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Image will be automagically cropped 570px by 370px')), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))], help_text='Enter a link or select a page'))])))])), ('image_and_text', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(help_text='Image the automagically cropped to 786px by 552px')), ('image_alignment', wagtail.core.blocks.ChoiceBlock(choices=[('left', 'Image to the left'), ('right', 'Image to the right')], help_text='Image on the left with text on the right. Or image on the right with text on the left.')), ('title', wagtail.core.blocks.CharBlock(help_text='Max length of 60 characters.', max_length=60)), ('text', wagtail.core.blocks.CharBlock(max_length=140, required=False)), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))]))])), ('cta', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock(help_text='Max length of 200 characters.', max_length=200)), ('link', wagtail.core.blocks.StructBlock([('link_text', wagtail.core.blocks.CharBlock(default='More Details', max_length=50)), ('internal_page', wagtail.core.blocks.PageChooserBlock(required=False)), ('external_link', wagtail.core.blocks.URLBlock(required=False))]))])), ('testimonial', wagtail.snippets.blocks.SnippetChooserBlock(target_model='testimonials.Testimonial', template='streams/testimonial_block.html')), ('pricing_table', streams.blocks.PricingTableBlock(table_options={'autoColumnSize': False, 'colHeaders': False, 'contextMenu': ['row_above', 'row_below', '---------', 'col_left', 'col_right', '---------', 'remove_row', 'remove_col', '---------', 'undo', 'redo'], 'editor': 'text', 'minSpareRows': 0, 'renderer': 'text', 'rowHeaders': True, 'startCols': 4, 'startRows': 4, 'stretchH': 'all'})), ('richtext', wagtail.core.blocks.RichTextBlock(features=['bold', 'italic', 'ol', 'ul', 'link'], template='streams/simple_richtext_block.html')), ('large_image', wagtail.images.blocks.ImageChooserBlock(help_text='This image will be cropped to 1200px by 775px', template='streams/large_image_block.html'))], blank=True, null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/flex/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingForEverybody/rocketman/210be13dc05f07ce82748bf31155818c2c363f5d/flex/migrations/__init__.py
--------------------------------------------------------------------------------
/flex/models.py:
--------------------------------------------------------------------------------
1 | from wagtail.admin.edit_handlers import StreamFieldPanel
2 | from wagtail.core import blocks as wagtail_blocks
3 | from wagtail.core.fields import StreamField
4 | from wagtail.core.models import Page
5 | from wagtail.images.blocks import ImageChooserBlock
6 | from wagtail.snippets.blocks import SnippetChooserBlock
7 |
8 | from home.models import NEW_TABLE_OPTIONS
9 | from streams import blocks
10 |
11 |
12 | class FlexPage(Page):
13 | parent_page_types = ["home.HomePage", "flex.FlexPage"]
14 | body = StreamField([
15 | ("title", blocks.TitleBlock()),
16 | ("cards", blocks.CardsBlock()),
17 | ("image_and_text", blocks.ImageAndTextBlock()),
18 | ("cta", blocks.CallToActionBlock()),
19 | ("testimonial", SnippetChooserBlock(
20 | target_model='testimonials.Testimonial',
21 | template="streams/testimonial_block.html",
22 | )),
23 | ("pricing_table", blocks.PricingTableBlock(
24 | table_options=NEW_TABLE_OPTIONS,
25 | )),
26 | ("richtext", wagtail_blocks.RichTextBlock(
27 | template="streams/simple_richtext_block.html",
28 | features=["bold", "italic", "ol", "ul", "link"]
29 | )),
30 | ("large_image", ImageChooserBlock(
31 | help_text='This image will be cropped to 1200px by 775px',
32 | template="streams/large_image_block.html"
33 | ))
34 | ], null=True, blank=True)
35 |
36 | content_panels = Page.content_panels + [
37 | StreamFieldPanel("body"),
38 | ]
39 |
40 | class Meta:
41 | verbose_name = "Flex (misc) page"
42 | verbose_name_plural = "Flex (misc) pages"
43 |
--------------------------------------------------------------------------------
/frontend/js/src/index.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | // import Alert from './alert'
3 | import Button from './button'
4 | // import Carousel from './carousel'
5 | import Collapse from './collapse'
6 | import Dropdown from './dropdown'
7 | // import Modal from './modal'
8 | // import Popover from './popover'
9 | // import Scrollspy from './scrollspy'
10 | // import Tab from './tab'
11 | // import Toast from './toast'
12 | import Tooltip from './tooltip'
13 | import Util from './util'
14 |
15 | /**
16 | * --------------------------------------------------------------------------
17 | * Bootstrap (v4.3.1): index.js
18 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
19 | * --------------------------------------------------------------------------
20 | */
21 |
22 | (() => {
23 | if (typeof $ === 'undefined') {
24 | throw new TypeError('Bootstrap\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\'s JavaScript.')
25 | }
26 |
27 | const version = $.fn.jquery.split(' ')[0].split('.')
28 | const minMajor = 1
29 | const ltMajor = 2
30 | const minMinor = 9
31 | const minPatch = 1
32 | const maxMajor = 4
33 |
34 | if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {
35 | throw new Error('Bootstrap\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0')
36 | }
37 | })()
38 |
39 | export {
40 | Util,
41 | // Alert,
42 | Button,
43 | // Carousel,
44 | Collapse,
45 | Dropdown,
46 | // Modal,
47 | // Popover,
48 | // Scrollspy,
49 | // Tab,
50 | // Toast,
51 | Tooltip
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/js/src/tools/sanitizer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * --------------------------------------------------------------------------
3 | * Bootstrap (v4.3.1): tools/sanitizer.js
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | * --------------------------------------------------------------------------
6 | */
7 |
8 | const uriAttrs = [
9 | 'background',
10 | 'cite',
11 | 'href',
12 | 'itemtype',
13 | 'longdesc',
14 | 'poster',
15 | 'src',
16 | 'xlink:href'
17 | ]
18 |
19 | const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
20 |
21 | export const DefaultWhitelist = {
22 | // Global attributes allowed on any supplied element below.
23 | '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
24 | a: ['target', 'href', 'title', 'rel'],
25 | area: [],
26 | b: [],
27 | br: [],
28 | col: [],
29 | code: [],
30 | div: [],
31 | em: [],
32 | hr: [],
33 | h1: [],
34 | h2: [],
35 | h3: [],
36 | h4: [],
37 | h5: [],
38 | h6: [],
39 | i: [],
40 | img: ['src', 'alt', 'title', 'width', 'height'],
41 | li: [],
42 | ol: [],
43 | p: [],
44 | pre: [],
45 | s: [],
46 | small: [],
47 | span: [],
48 | sub: [],
49 | sup: [],
50 | strong: [],
51 | u: [],
52 | ul: []
53 | }
54 |
55 | /**
56 | * A pattern that recognizes a commonly useful subset of URLs that are safe.
57 | *
58 | * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
59 | */
60 | const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
61 |
62 | /**
63 | * A pattern that matches safe data URLs. Only matches image, video and audio types.
64 | *
65 | * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
66 | */
67 | const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
68 |
69 | function allowedAttribute(attr, allowedAttributeList) {
70 | const attrName = attr.nodeName.toLowerCase()
71 |
72 | if (allowedAttributeList.indexOf(attrName) !== -1) {
73 | if (uriAttrs.indexOf(attrName) !== -1) {
74 | return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
75 | }
76 |
77 | return true
78 | }
79 |
80 | const regExp = allowedAttributeList.filter((attrRegex) => attrRegex instanceof RegExp)
81 |
82 | // Check if a regular expression validates the attribute.
83 | for (let i = 0, l = regExp.length; i < l; i++) {
84 | if (attrName.match(regExp[i])) {
85 | return true
86 | }
87 | }
88 |
89 | return false
90 | }
91 |
92 | export function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
93 | if (unsafeHtml.length === 0) {
94 | return unsafeHtml
95 | }
96 |
97 | if (sanitizeFn && typeof sanitizeFn === 'function') {
98 | return sanitizeFn(unsafeHtml)
99 | }
100 |
101 | const domParser = new window.DOMParser()
102 | const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
103 | const whitelistKeys = Object.keys(whiteList)
104 | const elements = [].slice.call(createdDocument.body.querySelectorAll('*'))
105 |
106 | for (let i = 0, len = elements.length; i < len; i++) {
107 | const el = elements[i]
108 | const elName = el.nodeName.toLowerCase()
109 |
110 | if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {
111 | el.parentNode.removeChild(el)
112 |
113 | continue
114 | }
115 |
116 | const attributeList = [].slice.call(el.attributes)
117 | const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
118 |
119 | attributeList.forEach((attr) => {
120 | if (!allowedAttribute(attr, whitelistedAttributes)) {
121 | el.removeAttribute(attr.nodeName)
122 | }
123 | })
124 | }
125 |
126 | return createdDocument.body.innerHTML
127 | }
128 |
--------------------------------------------------------------------------------
/frontend/scss/_alert.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Base styles
3 | //
4 |
5 | .alert {
6 | position: relative;
7 | padding: $alert-padding-y $alert-padding-x;
8 | margin-bottom: $alert-margin-bottom;
9 | border: $alert-border-width solid transparent;
10 | @include border-radius($alert-border-radius);
11 | }
12 |
13 | // Headings for larger alerts
14 | .alert-heading {
15 | // Specified to prevent conflicts of changing $headings-color
16 | color: inherit;
17 | }
18 |
19 | // Provide class for links that match alerts
20 | .alert-link {
21 | font-weight: $alert-link-font-weight;
22 | }
23 |
24 |
25 | // Dismissible alerts
26 | //
27 | // Expand the right padding and account for the close button's positioning.
28 |
29 | .alert-dismissible {
30 | padding-right: $close-font-size + $alert-padding-x * 2;
31 |
32 | // Adjust close link position
33 | .close {
34 | position: absolute;
35 | top: 0;
36 | right: 0;
37 | padding: $alert-padding-y $alert-padding-x;
38 | color: inherit;
39 | }
40 | }
41 |
42 |
43 | // Alternate styles
44 | //
45 | // Generate contextual modifier classes for colorizing the alert.
46 |
47 | @each $color, $value in $theme-colors {
48 | .alert-#{$color} {
49 | @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/scss/_badge.scss:
--------------------------------------------------------------------------------
1 | // Base class
2 | //
3 | // Requires one of the contextual, color modifier classes for `color` and
4 | // `background-color`.
5 |
6 | .badge {
7 | display: inline-block;
8 | padding: $badge-padding-y $badge-padding-x;
9 | @include font-size($badge-font-size);
10 | font-weight: $badge-font-weight;
11 | line-height: 1;
12 | text-align: center;
13 | white-space: nowrap;
14 | vertical-align: baseline;
15 | @include border-radius($badge-border-radius);
16 | @include transition($badge-transition);
17 |
18 | @at-root a#{&} {
19 | @include hover-focus {
20 | text-decoration: none;
21 | }
22 | }
23 |
24 | // Empty badges collapse automatically
25 | &:empty {
26 | display: none;
27 | }
28 | }
29 |
30 | // Quick fix for badges in buttons
31 | .btn .badge {
32 | position: relative;
33 | top: -1px;
34 | }
35 |
36 | // Pill badges
37 | //
38 | // Make them extra rounded with a modifier to replace v3's badges.
39 |
40 | .badge-pill {
41 | padding-right: $badge-pill-padding-x;
42 | padding-left: $badge-pill-padding-x;
43 | @include border-radius($badge-pill-border-radius);
44 | }
45 |
46 | // Colors
47 | //
48 | // Contextual variations (linked badges get darker on :hover).
49 |
50 | @each $color, $value in $theme-colors {
51 | .badge-#{$color} {
52 | @include badge-variant($value);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/scss/_breadcrumb.scss:
--------------------------------------------------------------------------------
1 | .breadcrumb {
2 | display: flex;
3 | flex-wrap: wrap;
4 | padding: $breadcrumb-padding-y $breadcrumb-padding-x;
5 | margin-bottom: $breadcrumb-margin-bottom;
6 | list-style: none;
7 | background-color: $breadcrumb-bg;
8 | @include border-radius($breadcrumb-border-radius);
9 | }
10 |
11 | .breadcrumb-item {
12 | // The separator between breadcrumbs (by default, a forward-slash: "/")
13 | + .breadcrumb-item {
14 | padding-left: $breadcrumb-item-padding;
15 |
16 | &::before {
17 | display: inline-block; // Suppress underlining of the separator in modern browsers
18 | padding-right: $breadcrumb-item-padding;
19 | color: $breadcrumb-divider-color;
20 | content: $breadcrumb-divider;
21 | }
22 | }
23 |
24 | // IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built
25 | // without `
`s. The `::before` pseudo-element generates an element
26 | // *within* the .breadcrumb-item and thereby inherits the `text-decoration`.
27 | //
28 | // To trick IE into suppressing the underline, we give the pseudo-element an
29 | // underline and then immediately remove it.
30 | + .breadcrumb-item:hover::before {
31 | text-decoration: underline;
32 | }
33 | // stylelint-disable-next-line no-duplicate-selectors
34 | + .breadcrumb-item:hover::before {
35 | text-decoration: none;
36 | }
37 |
38 | &.active {
39 | color: $breadcrumb-active-color;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/frontend/scss/_button-group.scss:
--------------------------------------------------------------------------------
1 | // stylelint-disable selector-no-qualifying-type
2 |
3 | // Make the div behave like a button
4 | .btn-group,
5 | .btn-group-vertical {
6 | position: relative;
7 | display: inline-flex;
8 | vertical-align: middle; // match .btn alignment given font-size hack above
9 |
10 | > .btn {
11 | position: relative;
12 | flex: 1 1 auto;
13 |
14 | // Bring the hover, focused, and "active" buttons to the front to overlay
15 | // the borders properly
16 | @include hover {
17 | z-index: 1;
18 | }
19 | &:focus,
20 | &:active,
21 | &.active {
22 | z-index: 1;
23 | }
24 | }
25 | }
26 |
27 | // Optional: Group multiple button groups together for a toolbar
28 | .btn-toolbar {
29 | display: flex;
30 | flex-wrap: wrap;
31 | justify-content: flex-start;
32 |
33 | .input-group {
34 | width: auto;
35 | }
36 | }
37 |
38 | .btn-group {
39 | // Prevent double borders when buttons are next to each other
40 | > .btn:not(:first-child),
41 | > .btn-group:not(:first-child) {
42 | margin-left: -$btn-border-width;
43 | }
44 |
45 | // Reset rounded corners
46 | > .btn:not(:last-child):not(.dropdown-toggle),
47 | > .btn-group:not(:last-child) > .btn {
48 | @include border-right-radius(0);
49 | }
50 |
51 | > .btn:not(:first-child),
52 | > .btn-group:not(:first-child) > .btn {
53 | @include border-left-radius(0);
54 | }
55 | }
56 |
57 | // Sizing
58 | //
59 | // Remix the default button sizing classes into new ones for easier manipulation.
60 |
61 | .btn-group-sm > .btn { @extend .btn-sm; }
62 | .btn-group-lg > .btn { @extend .btn-lg; }
63 |
64 |
65 | //
66 | // Split button dropdowns
67 | //
68 |
69 | .dropdown-toggle-split {
70 | padding-right: $btn-padding-x * .75;
71 | padding-left: $btn-padding-x * .75;
72 |
73 | &::after,
74 | .dropup &::after,
75 | .dropright &::after {
76 | margin-left: 0;
77 | }
78 |
79 | .dropleft &::before {
80 | margin-right: 0;
81 | }
82 | }
83 |
84 | .btn-sm + .dropdown-toggle-split {
85 | padding-right: $btn-padding-x-sm * .75;
86 | padding-left: $btn-padding-x-sm * .75;
87 | }
88 |
89 | .btn-lg + .dropdown-toggle-split {
90 | padding-right: $btn-padding-x-lg * .75;
91 | padding-left: $btn-padding-x-lg * .75;
92 | }
93 |
94 |
95 | // The clickable button for toggling the menu
96 | // Set the same inset shadow as the :active state
97 | .btn-group.show .dropdown-toggle {
98 | @include box-shadow($btn-active-box-shadow);
99 |
100 | // Show no shadow for `.btn-link` since it has no other button styles.
101 | &.btn-link {
102 | @include box-shadow(none);
103 | }
104 | }
105 |
106 |
107 | //
108 | // Vertical button groups
109 | //
110 |
111 | .btn-group-vertical {
112 | flex-direction: column;
113 | align-items: flex-start;
114 | justify-content: center;
115 |
116 | > .btn,
117 | > .btn-group {
118 | width: 100%;
119 | }
120 |
121 | > .btn:not(:first-child),
122 | > .btn-group:not(:first-child) {
123 | margin-top: -$btn-border-width;
124 | }
125 |
126 | // Reset rounded corners
127 | > .btn:not(:last-child):not(.dropdown-toggle),
128 | > .btn-group:not(:last-child) > .btn {
129 | @include border-bottom-radius(0);
130 | }
131 |
132 | > .btn:not(:first-child),
133 | > .btn-group:not(:first-child) > .btn {
134 | @include border-top-radius(0);
135 | }
136 | }
137 |
138 |
139 | // Checkbox and radio options
140 | //
141 | // In order to support the browser's form validation feedback, powered by the
142 | // `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
143 | // `display: none;` or `visibility: hidden;` as that also hides the popover.
144 | // Simply visually hiding the inputs via `opacity` would leave them clickable in
145 | // certain cases which is prevented by using `clip` and `pointer-events`.
146 | // This way, we ensure a DOM element is visible to position the popover from.
147 | //
148 | // See https://github.com/twbs/bootstrap/pull/12794 and
149 | // https://github.com/twbs/bootstrap/pull/14559 for more information.
150 |
151 | .btn-group-toggle {
152 | > .btn,
153 | > .btn-group > .btn {
154 | margin-bottom: 0; // Override default `