Merge branch 'election-css' into 'master'

Improved Elections CSS for the table

- Everything can be seen without scrolling sideways (unless you're on a small screen)
- Each column makes the same size
- Candidate description/program is now below its profile picture
- If the candidate does not have any profile picture, the default one is shown
- The Edit/Delete message has been replaced with their corresponding emojis (they takes fewer spaces and doesn't need to be translated)
- Modified links at the bottom to look like buttons

<details><summary>Before</summary>
![image](/uploads/fd42e2fa027786612582d41c97090277/image.png)
</details>

<details><summary>This MR (root)</summary>
![image](/uploads/8350518422392f971d98f3c7ee48a558/image.png)
</details>

<details><summary>This MR (lambda user)</summary>
![image](/uploads/e6b66730e47556ea21230e89d2d06f83/image.png)
</details>

<details><summary>When a candidate is selected</summary>
![image](/uploads/adde527405fb321ba2023c36e06f4dc3/image.png)
</details>

See merge request ae-utbm/Sith!313
This commit is contained in:
Alexandre | L'Sacienne 2022-06-15 19:13:47 +00:00
commit 3e8f1acb96
2 changed files with 369 additions and 296 deletions

View File

@ -0,0 +1,281 @@
$padding: 1.5rem;
$padding_smaller: .5rem;
$gap: .25rem;
$border: .01rem solid black;
$min_col_width: 100px;
.error {
color: red !important;
}
.radio-btn {
display: flex;
flex-direction: row;
gap: $gap;
> input,
> label {
margin: 0;
}
&:hover {
cursor: pointer;
}
}
.election_vote {
overflow-x: scroll !important;
}
.election_table {
width: 100%;
>.lists {
display: flex;
flex-direction: row;
>tr {
display: flex;
flex-direction: row;
width: 100%;
>.column {
display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: center;
padding: $padding;
border: $border;
border-collapse: collapse;
position: relative;
min-width: $min_col_width;
>a{
margin-left: $padding;
width: 20px;
height: 20px;
text-align: center;
padding: 5px;
border-radius: 25%;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: $gap;
top: $gap;
&:hover {
background-color: #ddd;
}
}
}
}
}
>.role {
display: flex;
flex-direction: column;
>tr {
display: flex;
flex-direction: row;
background-color: lightgrey;
&:hover {
background-color: lightgrey;
}
>.role_title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0;
padding: $padding;
width: 100%;
>.role_text {
>h4 {
margin: 0;
}
>p {
margin-top: .5em;
}
}
>.role_buttons {
display: flex;
flex-direction: row;
align-items: center;
gap: $gap;
> button,
> button > i,
> a {
width: 20px;
height: 20px;
background-color: #e9e9e9;
text-align: center;
padding: 5px;
border-radius: 25%;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
&:hover,
&:hover > i {
background-color: #fff;
}
}
> button {
width: 30px;
height: 30px;
}
> button[disabled] {
background-color: #eee;
cursor: not-allowed;
>i,
&:hover,
&:hover > i {
background-color: #eee;
}
}
}
}
>.list_per_role {
display: flex;
flex-direction: row;
justify-content: center;
border: $border;
border-collapse: collapse;
background-color: #fff;
padding: $padding_smaller;
margin: 0;
min-width: $min_col_width;
>.candidates {
margin: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: $gap;
>.candidate {
display: flex;
flex-direction: column;
align-items: center;
list-style-type: none;
gap: $gap;
>input[type="radio"]:checked + label,
>input[type="checkbox"]:checked + label {
background-color: lightgray;
border-radius: 10px;
>figure>.edit_btns>a:hover{
background-color: #fff;
}
}
>label>figure,
>figure {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: $gap;
padding: 10px;
>img {
max-width: 100%;
}
>figcaption {
h5 {
margin: 0;
text-align: center;
}
q {
margin: 5px 0;
}
}
>.edit_btns {
position: absolute;
display: flex;
flex-direction: column;
top: $gap;
right: $gap;
gap: $gap;
> a {
width: 20px;
height: 20px;
background-color: #e9e9e9;
text-align: center;
padding: 5px;
border-radius: 25%;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #d8d8d8;
}
}
}
}
}
}
}
}
}
}
.election_details {
margin: .5em 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: $gap;
}
.button {
border: none;
color: black;
text-decoration: none;
background-color: #f2f2f2;
padding: 0.4em;
margin: 0.1em;
font-size: 1.18em;
border-radius: 5px;
box-shadow: #dfdfdf 0px 0px 1px;
cursor: pointer;
&:hover {
color: black;
background: #d4d4d4;
}
&_send {
background-color: #59aee2;
&:hover {
background-color: rgb(130, 186, 235);
}
}
}

View File

@ -6,232 +6,14 @@
{% block head %} {% block head %}
{{ super() -}} {{ super() -}}
<style type="text/css"> <link rel="stylesheet" href="{{ scss('election/election.scss') }}">
time {
font-weight: bolder;
}
th {
padding: 5px;
margin: 5px;
border: solid 1px darkgrey;
border-collapse: collapse;
vertical-align: top;
overflow: hidden;
text-overflow: ellipsis;
}
.election__title {
margin: 0;
margin-bottom: 5px;
}
.election__description {
margin: 0;
}
.election__details {
margin-bottom: 5px;
}
.election__details p {
margin: 0;
}
.election__details p:not(:last-child) {
margin-bottom: 5px;
}
.election__elector-infos {
font-weight: bolder;
color: darkgreen;
}
.election__vote {
margin-bottom: 5px;
}
.election__vote-form {
width: auto;
}
.role {
}
.role .role__title {
background: lightgrey;
}
.role__multiple-choices-label {
color: darkgreen;
}
.role__error {
color: darkred;
}
.role .role_candidates {
background: white;
}
.list-per-role {
padding: 5px;
max-width: 310px;
}
.list-per-role__candidates {
list-style: none;
margin: 0;
}
.list-per-role__candidate:not(:last-child) {
margin-bottom: 15px;
}
.candidate__infos {
display: flex;
flex-flow: row nowrap;
}
.candidate__infos:not(:last-child) {
margin-bottom: 5px;
}
.candidate__picture-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 150px;
height: 150px;
min-width: 150px;
min-height: 150px;
background-color: lightgrey;
}
.candidate__picture {
max-width: 150px;
max-height: 150px;
}
.candidate__details {
margin-left: 5px;
}
.candidate__full-name {
display: block;
font-weight: bolder;
}
.candidate__nick-name {
font-style: italic;
}
.candidate__program {
display: block;
margin-top: 5px;
}
.candidate__vote-input {
position: absolute;
border: 0;
height: 1px;
width: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
.candidate__vote-choice {
margin-top: 5px;
padding: 15px;
border: solid 1px darkgrey;
color: dimgray;
text-align: center;
font-weight: bolder;
}
.candidate__vote-input:not(:checked):not(:disabled) + .candidate__vote-choice:hover,
.candidate__vote-input:not(:checked):not(:disabled) + .candidate__vote-choice:focus {
background: lightgrey;
}
.candidate__vote-input:checked + .candidate__vote-choice {
padding: 14px;
border-width: 2px;
border-color: darkgreen;
color: darkgreen;
}
.candidate__vote-input:not(:disabled) + .candidate__vote-choice {
cursor: pointer;
}
.candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:hover,
.candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:focus {
background: palegreen;
}
.role_error .candidate__vote-input:checked + .candidate__vote-choice {
border-color: darkred;
color: darkred;
}
.role_error .candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:hover,
.role_error .candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:focus {
background: indianred;
}
.election__results {
text-align: center;
}
.election__sumbit-section {
margin-bottom: 5px;
}
.election__sumbit-button {
display: block;
width: 100%;
padding: 20px;
background: white;
border: solid 15px #4081cb;
text-align: center;
font-size: 200%;
font-weight: bolder;
cursor: pointer;
}
.election__sumbit-button:hover,
.election__sumbit-button:focus {
background-color: lightskyblue;
}
.election__add-elements {
margin-bottom: 5px;
}
.election__add-elements a {
display: inline-block;
border: solid 1px darkgrey;
height: 20px;
line-height: 20px;
padding: 10px;
}
</style>
{%- endblock %} {%- endblock %}
{% block content %} {% block content %}
<h3 class="election__title">{{ election.title }}</h3> <h3 class="election__title">{{ election.title }}</h3>
<p class="election__description">{{ election.description }}</p> <p class="election__description">{{ election.description }}</p>
<hr> <hr>
<section class="election__details"> <section class="election_details">
<p> <p>
{%- if election.is_vote_active %} {%- if election.is_vote_active %}
{% trans %}Polls close {% endtrans %} {% trans %}Polls close {% endtrans %}
@ -256,35 +38,49 @@ th {
</p> </p>
{%- endif %} {%- endif %}
</section> </section>
<section class="election__vote"> <section class="election_vote">
<form action="{{ url('election:vote', election.id) }}" method="post" class="election__vote-form" name="vote-form" id="vote-form"> <form action="{{ url('election:vote', election.id) }}" method="post" class="election__vote-form" name="vote-form" id="vote-form">
{% csrf_token %} {% csrf_token %}
<table> <table class="election_table">
{%- set election_lists = election.election_lists.all() -%} {%- set election_lists = election.election_lists.all() -%}
<caption></caption> <caption></caption>
<thead> <thead class="lists">
<th>{% trans %}Blank vote{% endtrans %}</th> <tr>
<th class="column" style="width: {{ 100 / (election_lists.count() + 1) }}%">{% trans %}Blank vote{% endtrans %}</th>
{%- for election_list in election_lists %} {%- for election_list in election_lists %}
<th> <th class="column" style="width: {{ 100 / (election_lists.count() + 1) }}%">
{{ election_list.title }} <span>{{ election_list.title }}</span>
{% if user.can_edit(election_list) and election.is_vote_editable -%} {% if user.can_edit(election_list) and election.is_vote_editable -%}
- <a href="{{ url('election:delete_list', list_id=election_list.id) }}">{% trans %}Delete{% endtrans %}</a> <a href="{{ url('election:delete_list', list_id=election_list.id) }}"></a>
{% endif %} {% endif %}
</th> </th>
{%- endfor %} {%- endfor %}
</tr>
</thead> </thead>
{%- set role_list = election.roles.order_by('order').all() %} {%- set role_list = election.roles.order_by('order').all() %}
{%- for role in role_list %} {%- for role in role_list %}
{%- set count = [0] %} {%- set count = [0] %}
{%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %} {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %}
<tbody data-max-choice="{{role.max_choice}}" class="role{{ ' role_error' if role.title in election_form.errors else '' }}{{ ' role__multiple-choices' if role.max_choice > 1 else ''}}"> <tbody data-max-choice="{{role.max_choice}}" class="role{{ ' role_error' if role.title in election_form.errors else '' }}{{ ' role__multiple-choices' if role.max_choice > 1 else ''}}">
<tr class="role__title"> <tr>
<td colspan="{{ election_lists.count() + 1 }}"> <td class="role_title">
<span><b>{{ role.title }}</b></span> <div class="role_text">
<h4>{{ role.title }}</h4>
<p class="role_description">{{ role.description }}</p>
{%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %}
<strong>{% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %}</strong>
{%- endif %}
{%- if election_form.errors[role.title] is defined %}
{%- for error in election_form.errors.as_data()[role.title] %}
<strong class="error">{{ error.message }}</strong>
{%- endfor %}
{%- endif %}
</div>
{% if user.can_edit(role) and election.is_vote_editable -%} {% if user.can_edit(role) and election.is_vote_editable -%}
<a href="{{url('election:update_role', role_id=role.id)}}">{% trans %}Edit{% endtrans %}</a> <div class="role_buttons">
<a href="{{url('election:delete_role', role_id=role.id)}}">{% trans %}Delete{% endtrans %}</a> <a href="{{url('election:update_role', role_id=role.id)}}">✏️</a>
<span style="float:right"> <a href="{{url('election:delete_role', role_id=role.id)}}">❌</a>
{%- if role == role_list.last() %} {%- if role == role_list.last() %}
<button disabled><i class="fa fa-arrow-down"></i></button> <button disabled><i class="fa fa-arrow-down"></i></button>
<button disabled><i class="fa fa-caret-down"></i></button> <button disabled><i class="fa fa-caret-down"></i></button>
@ -299,27 +95,19 @@ th {
<button type="button" onclick="window.location.replace('?role={{ role.id }}&action=up');"><i class="fa fa-caret-up"></i></button> <button type="button" onclick="window.location.replace('?role={{ role.id }}&action=up');"><i class="fa fa-caret-up"></i></button>
<button type="button" onclick="window.location.replace('?role={{ role.id }}&action=top');"><i class="fa fa-arrow-up"></i></button> <button type="button" onclick="window.location.replace('?role={{ role.id }}&action=top');"><i class="fa fa-arrow-up"></i></button>
{% endif %} {% endif %}
</span> </div>
{%- endif -%} {%- endif -%}
<br><span class='role__description'><p>{{ role.description }}</p></span>
{%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %}
<br>
<strong class="role__multiple-choices-label">{% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %}</strong>
{%- endif %}
{%- if election_form.errors[role.title] is defined %}
{%- for error in election_form.errors.as_data()[role.title] %}
<strong class="role__error">{{ error.message }}</strong>
{%- endfor %}
{%- endif %}
</td> </td>
</tr> </tr>
<tr class="role_candidates"> <tr class="role_candidates">
<td class="list-per-role"> <td class="list_per_role" style="width: {{ 100 / (election_lists.count() + 1) }}%">
{%- if role.max_choice == 1 and election.can_vote(user) %} {%- if role.max_choice == 1 and election.can_vote(user) %}
<input id="id_{{ role.title }}_{{ count[0] }}" class="candidate__vote-input" type="radio" name="{{ role.title }}" value {{ '' if role_data in election_form else 'checked' }} {{ 'disabled' if election.has_voted(user) else '' }}> <div class="radio-btn">
<label for="id_{{ role.title }}_{{ count[0] }}" class="candidate__vote-choice"> <input id="id_{{ role.title }}_{{ count[0] }}" type="radio" name="{{ role.title }}" value {{ '' if role_data in election_form else 'checked' }} {{ 'disabled' if election.has_voted(user) else '' }}>
<label for="id_{{ role.title }}_{{ count[0] }}">
<span>{% trans %}Choose blank vote{% endtrans %}</span> <span>{% trans %}Choose blank vote{% endtrans %}</span>
</label> </label>
</div>
{%- set _ = count.append(count.pop() + 1) %} {%- set _ = count.append(count.pop() + 1) %}
{%- endif %} {%- endif %}
{%- if election.is_vote_finished %} {%- if election.is_vote_finished %}
@ -330,35 +118,38 @@ th {
{%- endif %} {%- endif %}
</td> </td>
{%- for election_list in election_lists %} {%- for election_list in election_lists %}
<td class="list-per-role"> <td class="list_per_role" style="width: {{ 100 / (election_lists.count() + 1) }}%">
<ul class="list-per-role__candidates"> <ul class="candidates">
{%- for candidature in election_list.candidatures.filter(role=role) %} {%- for candidature in election_list.candidatures.filter(role=role) %}
<li class="list-per-role__candidate candidate"> <li class="candidate">
<figure class="candidate__infos"> {%- if election.can_vote(user) %}
<div class="candidate__picture-wrapper"> <input id="id_{{ role.title }}_{{ count[0] }}" type="{{ 'checkbox' if role.max_choice > 1 else 'radio' }}" {{ 'checked' if candidature.id|string in role_data else '' }} {{ 'disabled' if election.has_voted(user) else '' }} name="{{ role.title }}" value="{{ candidature.id }}">
{%- if candidature.user.profile_pict and user.is_subscriber_viewable %} <label for="id_{{ role.title }}_{{ count[0] }}">
<img class="candidate__picture" src="{{ candidature.user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}">
{%- endif %} {%- endif %}
</div> <figure>
<figcaption class="candidate__details"> {%- if user.is_subscriber_viewable %}
<cite class="candidate__full-name">{{ candidature.user.first_name }} <em class="candidate__nick-name">{{candidature.user.nick_name or ''}} </em>{{ candidature.user.last_name }}</cite> {% if candidature.user.profile_pict %}
{%- if user.can_edit(candidature) -%} <img class="candidate__picture" src="{{ candidature.user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}">
{% if election.is_vote_editable %} {% else %}
<a href="{{url('election:update_candidate', candidature_id=candidature.id)}}">{% trans %}Edit{% endtrans %}</a> <img class="candidate__picture" src="{{ static('core/img/unknown.jpg') }}" alt="{% trans %}Profile{% endtrans %}">
{% endif %} {% endif %}
{% if election.is_vote_editable -%} {%- endif %}
<a href="{{url('election:delete_candidate', candidature_id=candidature.id)}}">{% trans %}Delete{% endtrans %}</a> <figcaption class="candidate__details">
{%- endif -%} <h5>{{ candidature.user.first_name }} <em>{{candidature.user.nick_name or ''}} </em>{{ candidature.user.last_name }}</h5>
{%- endif -%}
{%- if not election.is_vote_finished %} {%- if not election.is_vote_finished %}
<q class="candidate__program">{{ candidature.program | markdown or '' }}</q> <q class="candidate_program">{{ candidature.program | markdown or '' }}</q>
{%- endif %} {%- endif %}
</figcaption> </figcaption>
{%- if user.can_edit(candidature) -%}
{% if election.is_vote_editable %}
<div class="edit_btns">
<a href="{{url('election:update_candidate', candidature_id=candidature.id)}}">{% trans %}✏️{% endtrans %}</a>
<a href="{{url('election:delete_candidate', candidature_id=candidature.id)}}">{% trans %}{% endtrans %}</a>
</div>
{%- endif -%}
{%- endif -%}
</figure> </figure>
{%- if election.can_vote(user) %} {%- if election.can_vote(user) %}
<input id="id_{{ role.title }}_{{ count[0] }}" type="{{ 'checkbox' if role.max_choice > 1 else 'radio' }}" {{ 'checked' if candidature.id|string in role_data else '' }} {{ 'disabled' if election.has_voted(user) else '' }} name="{{ role.title }}" value="{{ candidature.id }}" class="candidate__vote-input">
<label for="id_{{ role.title }}_{{ count[0] }}" class="candidate__vote-choice">
<span>{% trans %}Choose{% endtrans %} {{ candidature.user.nick_name or candidature.user.first_name }}</span>
</label> </label>
{%- set _ = count.append(count.pop() + 1) %} {%- set _ = count.append(count.pop() + 1) %}
{%- endif %} {%- endif %}
@ -379,39 +170,40 @@ th {
</table> </table>
</form> </form>
</section> </section>
{%- if not election.has_voted(user) and election.can_vote(user) %} <section class="buttons">
<section class="election__sumbit-section">
<button class="election__sumbit-button" form="vote-form">{% trans %}Submit the vote !{% endtrans %}</button>
</section>
{%- endif %}
<section class="election__add-elements">
{%- if (election.can_candidate(user) and election.is_candidature_active) or (user.can_edit(election) and election.is_vote_editable) %} {%- if (election.can_candidate(user) and election.is_candidature_active) or (user.can_edit(election) and election.is_vote_editable) %}
<a href="{{ url('election:candidate', election_id=object.id) }}">{% trans %}Candidate{% endtrans %}</a> <a class="button" href="{{ url('election:candidate', election_id=object.id) }}">{% trans %}Candidate{% endtrans %}</a>
{%- endif %} {%- endif %}
{%- if election.is_vote_editable %} {%- if election.is_vote_editable %}
<a href="{{ url('election:create_list', election_id=object.id) }}">{% trans %}Add a new list{% endtrans %}</a> <a class="button" href="{{ url('election:create_list', election_id=object.id) }}">{% trans %}Add a new list{% endtrans %}</a>
{%- endif %} {%- endif %}
{%- if user.can_edit(election) %} {%- if user.can_edit(election) %}
{% if election.is_vote_editable %} {% if election.is_vote_editable %}
<a href="{{ url('election:create_role', election_id=object.id) }}">{% trans %}Add a new role{% endtrans %}</a> <a class="button" href="{{ url('election:create_role', election_id=object.id) }}">{% trans %}Add a new role{% endtrans %}</a>
{% endif %} {% endif %}
<a href="{{ url('election:update', election_id=object.id) }}">{% trans %}Edit{% endtrans %}</a> <a class="button" href="{{ url('election:update', election_id=object.id) }}">{% trans %}Edit{% endtrans %}</a>
{%- endif %} {%- endif %}
{%- if user.is_root %} {%- if user.is_root %}
<a href="{{ url('election:delete', election_id=object.id) }}">{% trans %}Delete{% endtrans %}</a> <a class="button" href="{{ url('election:delete', election_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
{%- endif %} {%- endif %}
</section> </section>
{%- if not election.has_voted(user) and election.can_vote(user) %}
<section class="buttons">
<button class="button button_send" form="vote-form">{% trans %}Submit the vote !{% endtrans %}</button>
</section>
{%- endif %}
{% endblock %} {% endblock %}
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script src="{{ static('core/js/shorten.min.js') }}"></script> <script src="{{ static('core/js/shorten.min.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
$('.role__description').shorten({ $('.role_description').shorten({
moreText: "{% trans %}Show more{% endtrans %}", moreText: "{% trans %}Show more{% endtrans %}",
lessText: "{% trans %}Show less{% endtrans %}" lessText: "{% trans %}Show less{% endtrans %}",
showChars: 50
}); });
$('.candidate__program').shorten({ $('.candidate_program').shorten({
moreText: "{% trans %}Show more{% endtrans %}", moreText: "{% trans %}Show more{% endtrans %}",
lessText: "{% trans %}Show less{% endtrans %}", lessText: "{% trans %}Show less{% endtrans %}",
showChars: 200 showChars: 200