Merge pull request #465 from DavidHenryThoreau/css

Add colors-dark.scss
This commit is contained in:
Sam 2023-12-20 11:01:55 +01:00 committed by GitHub
commit d207beb78f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1057 additions and 507 deletions

View File

@ -7,11 +7,11 @@
<link rel="stylesheet" href="/static/css/fork-awesome.min.css"/>
<link rel="stylesheet" href="/static/css/leaflet.css"/>
<title>FitTrackee</title>
<script type="module" crossorigin src="/static/index-0EDbp-Wc.js"></script>
<script type="module" crossorigin src="/static/index-0v1Cinoq.js"></script>
<link rel="modulepreload" crossorigin href="/static/charts-_RwsDDkL.js">
<link rel="modulepreload" crossorigin href="/static/maps-ZyuCPqes.js">
<link rel="stylesheet" crossorigin href="/static/css/maps-B7qTrBCW.css">
<link rel="stylesheet" crossorigin href="/static/css/index-kyYSvvW8.css">
<link rel="stylesheet" crossorigin href="/static/css/index-86bdJKFy.css">
</head>
<body>
<div id="app"></div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

457
fittrackee/dist/static/index-0v1Cinoq.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,34 @@
"""add dark theme preferences
Revision ID: 14f48e46f320
Revises: 24eb097614e4
Create Date: 2023-12-16 18:35:31.377007
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '14f48e46f320'
down_revision = '24eb097614e4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.add_column(
sa.Column('use_dark_mode', sa.Boolean(), nullable=True)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.drop_column('use_dark_mode')
# ### end Alembic commands ###

View File

@ -1459,6 +1459,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
imperial_units=True,
display_ascent=False,
start_elevation_at_zero=False,
use_dark_mode=True,
use_raw_gpx_speed=True,
date_format='yyyy-MM-dd',
)
@ -1478,6 +1479,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
assert data['data']['timezone'] == 'America/New_York'
assert data['data']['date_format'] == 'yyyy-MM-dd'
assert data['data']['weekm'] is True
assert data['data']['use_dark_mode'] is True
@pytest.mark.parametrize(
'client_scope, can_access',

View File

@ -77,6 +77,12 @@ class TestUserSerializeAsAuthUser(UserModelAssertMixin):
assert serialized_user['timezone'] == user_1.timezone
assert serialized_user['weekm'] == user_1.weekm
assert serialized_user['display_ascent'] == user_1.display_ascent
assert (
serialized_user['start_elevation_at_zero']
== user_1.start_elevation_at_zero
)
assert serialized_user['use_raw_gpx_speed'] == user_1.use_raw_gpx_speed
assert serialized_user['use_dark_mode'] == user_1.use_dark_mode
def test_it_returns_workouts_infos(self, app: Flask, user_1: User) -> None:
serialized_user = user_1.serialize(user_1)
@ -155,6 +161,9 @@ class TestUserSerializeAsAdmin(UserModelAssertMixin):
assert 'language' not in serialized_user
assert 'timezone' not in serialized_user
assert 'weekm' not in serialized_user
assert 'start_elevation_at_zero' not in serialized_user
assert 'use_raw_gpx_speed' not in serialized_user
assert 'use_dark_mode' not in serialized_user
def test_it_returns_workouts_infos(
self, app: Flask, user_1_admin: User, user_2: User

View File

@ -361,6 +361,7 @@ def get_authenticated_user_profile(
"total_ascent": 720.35,
"total_distance": 67.895,
"total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": false,
"username": "sam",
"weekm": false
@ -478,6 +479,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
"total_ascent": 720.35,
"total_distance": 67.895,
"total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": false,
"username": "sam"
"weekm": true,
@ -650,6 +652,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
"total_ascent": 720.35,
"total_distance": 67.895,
"total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": false,
"username": "sam"
"weekm": true,
@ -878,6 +881,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
"total_ascent": 720.35,
"total_distance": 67.895,
"total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": true,
"username": "sam"
"weekm": true,
@ -892,6 +896,8 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
:<json string language: language preferences
:<json boolean start_elevation_at_zero: do elevation plots start at zero?
:<json string timezone: user time zone
:<json boolean use_dark_mode: Display interface with dark mode if true.
If null, it uses browser preferences.
:<json boolean use_raw_gpx_speed: Use unfiltered gpx to calculate speeds
:<json boolean weekm: does week start on Monday?
@ -916,6 +922,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
'language',
'start_elevation_at_zero',
'timezone',
'use_dark_mode',
'use_raw_gpx_speed',
'weekm',
}
@ -928,6 +935,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
language = get_language(post_data.get('language'))
start_elevation_at_zero = post_data.get('start_elevation_at_zero')
use_raw_gpx_speed = post_data.get('use_raw_gpx_speed')
use_dark_mode = post_data.get('use_dark_mode')
timezone = post_data.get('timezone')
weekm = post_data.get('weekm')
@ -938,6 +946,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
auth_user.language = language
auth_user.start_elevation_at_zero = start_elevation_at_zero
auth_user.timezone = timezone
auth_user.use_dark_mode = use_dark_mode
auth_user.use_raw_gpx_speed = use_raw_gpx_speed
auth_user.weekm = weekm
db.session.commit()

View File

@ -63,6 +63,7 @@ class User(BaseModel):
db.Boolean, default=True, nullable=False
)
use_raw_gpx_speed = db.Column(db.Boolean, default=False, nullable=False)
use_dark_mode = db.Column(db.Boolean, default=False, nullable=True)
def __repr__(self) -> str:
return f'<User {self.username!r}>'
@ -217,6 +218,7 @@ class User(BaseModel):
'language': self.language,
'start_elevation_at_zero': self.start_elevation_at_zero,
'timezone': self.timezone,
'use_dark_mode': self.use_dark_mode,
'use_raw_gpx_speed': self.use_raw_gpx_speed,
'weekm': self.weekm,
},

View File

@ -123,7 +123,7 @@
.scroll-button {
background-color: var(--scroll-button-bg-color);
border-radius: $border-radius;
box-shadow: 1px 1px 3px lightgrey;
box-shadow: 1px 1px 3px var(--app-shadow-color);
display: none;
padding: 0 $default-padding;

View File

@ -70,14 +70,14 @@
.dropdown-list {
list-style-type: none;
background-color: #ffffff;
background-color: var(--dropdown-background-color);
padding: 0 !important;
margin-top: 5px;
margin-left: -20px !important;
position: absolute;
text-align: left;
border: solid 1px lightgrey;
box-shadow: 2px 2px 5px lightgrey;
border: solid 1px var(--dropdown-border-color);
box-shadow: 2px 2px 5px var(--app-shadow-color);
width: auto !important;
.dropdown-item {

View File

@ -51,6 +51,12 @@
width: 400px;
height: 225px;
z-index: 100;
filter: var(--map-display-hover-filter);
.map-attribution-text {
color: var(--map-display-hover-attribution-text);
background-color: var(--map-attribution-bg-color);
}
}
.bg-map-image {
@ -59,6 +65,7 @@
opacity: 0.6;
height: 200px;
width: 100%;
filter: var(--map-filter);
}
.map-attribution {
@ -69,7 +76,8 @@
}
.map-attribution-text {
background-color: rgba(255, 255, 255, 0.7);
color: var(--map-attribution-text);
background-color: var(--map-attribution-bg-color);
}
}
</style>

View File

@ -6,13 +6,16 @@
<script setup lang="ts">
import type { ChartOptions, LayoutItem } from 'chart.js'
import { computed, toRefs } from 'vue'
import { computed, type ComputedRef, toRefs } from 'vue'
import { Bar } from 'vue-chartjs'
import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'
import { ROOT_STORE } from '@/store/constants'
import type { IChartDataset } from '@/types/chart'
import type { TStatisticsDatasetKeys } from '@/types/statistics'
import { formatTooltipValue } from '@/utils/tooltip'
import { chartsColors } from '@/utils/workouts'
interface Props {
datasets: IChartDataset[]
@ -32,8 +35,23 @@
useImperialUnits,
} = toRefs(props)
const store = useStore()
const { t } = useI18n()
const darkMode: ComputedRef<boolean | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.DARK_MODE]
)
const lineColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.line
: chartsColors.ligthMode.line,
}))
const textColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.text
: chartsColors.ligthMode.text,
}))
const chartData = computed(() => ({
labels: labels.value,
// workaround to avoid dataset modification
@ -53,12 +71,23 @@
stacked: true,
grid: {
drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
},
ticks: {
...textColors.value,
},
},
y: {
stacked: displayedData.value !== 'average_speed',
grid: {
drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
},
ticks: {
maxTicksLimit: 6,
@ -71,6 +100,7 @@
getUnit(displayedData.value)
)
},
...textColors.value,
},
afterFit: function (scale: LayoutItem) {
scale.width = fullStats.value ? 90 : 60

View File

@ -34,7 +34,7 @@
padding: $default-padding * 0.5;
text-align: center;
text-transform: uppercase;
color: var(--app-color-light);
color: var(--calendar-day-color);
}
}
</style>

View File

@ -88,11 +88,11 @@
padding-left: 40px;
.more-workouts {
background: whitesmoke;
background: var(--calendar-workouts-color);
border-radius: 4px;
box-shadow:
0 4px 8px 0 rgba(0, 0, 0, 0.2),
0 6px 20px 0 rgba(0, 0, 0, 0.19);
0 4px 8px 0 var(--calendar-workouts-box-shadow-0),
0 6px 20px 0 var(--calendar-workouts-box-shadow-1);
position: absolute;
top: 52px;
left: 0;

View File

@ -60,12 +60,12 @@
{{ authUser.username }}
</router-link>
<button
class="logout-button transparent"
class="nav-button logout-button transparent"
@click="updateDisplayModal(true)"
:aria-label="$t('user.LOGOUT')"
:title="$t('user.LOGOUT')"
>
<i class="fa fa-sign-out logout-fa" aria-hidden="true" />
<span class="logout-text">{{ $t('user.LOGOUT') }}</span>
<i class="fa fa-sign-out nav-button-fa" aria-hidden="true" />
<span class="nav-button-text">{{ $t('user.LOGOUT') }}</span>
</button>
</div>
<div class="nav-items-group" v-else>
@ -76,6 +76,27 @@
{{ $t('user.REGISTER') }}
</router-link>
</div>
<div class="theme-button">
<button
class="nav-button transparent"
@click="toggleTheme"
:title="$t('user.TOGGLE_THEME')"
>
<i
v-if="darkTheme"
class="fa nav-button-fa fa-moon"
aria-hidden="true"
/>
<img
v-else
class="clear-theme"
src="/img/weather/clear-day.svg"
alt=""
aria-hidden="true"
/>
<span class="nav-button-text">{{ $t('user.TOGGLE_THEME') }}</span>
</button>
</div>
<Dropdown
v-if="availableLanguages && language"
class="nav-item"
@ -93,7 +114,7 @@
</template>
<script setup lang="ts">
import { computed, ref, capitalize } from 'vue'
import { computed, ref, capitalize, onBeforeMount, watch } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import UserPicture from '@/components/User/UserPicture.vue'
@ -119,6 +140,12 @@
)
const isMenuOpen: Ref<boolean> = ref(false)
const displayModal: Ref<boolean> = ref(false)
const darkMode: ComputedRef<boolean | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.DARK_MODE]
)
const darkTheme: ComputedRef<boolean> = computed(() => getDarkTheme())
onBeforeMount(() => setTheme())
function openMenu() {
isMenuOpen.value = true
@ -141,6 +168,32 @@
function updateDisplayModal(display: boolean) {
displayModal.value = display
}
function getDarkTheme() {
if (
darkMode.value === null &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
return true
}
return darkMode.value === true
}
function setTheme() {
if (darkTheme.value) {
document.body.setAttribute('data-theme', 'dark')
} else {
document.body.removeAttribute('data-theme')
}
}
function toggleTheme() {
store.commit(ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE, !darkTheme.value)
}
watch(
() => darkTheme.value,
() => {
setTheme()
}
)
</script>
<style scoped lang="scss">
@ -252,16 +305,22 @@
.nav-separator {
display: none;
}
.logout-button {
.nav-button {
padding: $default-padding * 0.5 $default-padding * 0.75;
margin-left: 2px;
.logout-fa {
.nav-button-fa {
display: block;
}
.logout-text {
.nav-button-text {
display: none;
}
}
.clear-theme {
filter: var(--workout-img-color);
height: 20px;
margin-bottom: -5px;
}
}
@media screen and (max-width: $medium-limit) {
@ -323,15 +382,16 @@
display: flex;
flex-direction: column;
.logout-button {
.nav-button {
padding: $default-padding $default-padding $default-padding
$default-padding * 2.4;
color: var(--app-a-color);
text-align: left;
.logout-fa {
.nav-button-fa {
display: none;
width: 36px;
}
.logout-text {
.nav-button-text {
display: block;
}
}
@ -361,6 +421,9 @@
padding: 0;
}
}
.theme-button {
margin-left: $default-padding * 2;
}
}
}
</style>

View File

@ -4,6 +4,8 @@
<dl>
<dt>{{ $t('user.PROFILE.LANGUAGE') }}:</dt>
<dd>{{ userLanguage }}</dd>
<dt>{{ $t('user.PROFILE.THEME_MODE.LABEL') }}:</dt>
<dd>{{ $t(`user.PROFILE.THEME_MODE.VALUES.${darkMode}`) }}</dd>
<dt>{{ $t('user.PROFILE.TIMEZONE') }}:</dt>
<dd>{{ timezone }}</dd>
<dt>{{ $t('user.PROFILE.DATE_FORMAT') }}:</dt>
@ -95,6 +97,13 @@
const display_ascent = computed(() =>
props.user.display_ascent ? 'DISPLAYED' : 'HIDDEN'
)
const darkMode = computed(() =>
props.user.use_dark_mode === true
? 'DARK'
: props.user.use_dark_mode === false
? 'LIGHT'
: 'DEFAULT'
)
</script>
<style lang="scss" scoped>

View File

@ -18,6 +18,22 @@
</option>
</select>
</label>
<label class="form-items">
{{ $t('user.PROFILE.THEME_MODE.LABEL') }}
<select
id="use_dark_mode"
v-model="userForm.use_dark_mode"
:disabled="loading"
>
<option
v-for="mode in useDarkMode"
:value="mode.value"
:key="mode.label"
>
{{ $t(`user.PROFILE.THEME_MODE.VALUES.${mode.label}`) }}
</option>
</select>
</label>
<label class="form-items">
{{ $t('user.PROFILE.TIMEZONE') }}
<TimezoneDropdown
@ -195,6 +211,7 @@
weekm: false,
start_elevation_at_zero: false,
use_raw_gpx_speed: false,
use_dark_mode: false,
})
const weekStart = [
{
@ -246,6 +263,20 @@
value: true,
},
]
const useDarkMode = [
{
label: 'DARK',
value: true,
},
{
label: 'DEFAULT',
value: null,
},
{
label: 'LIGHT',
value: false,
},
]
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
@ -279,6 +310,7 @@
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
userForm.date_format = user.date_format ? user.date_format : 'dd/MM/yyyy'
userForm.weekm = user.weekm ? user.weekm : false
userForm.use_dark_mode = user.use_dark_mode
}
function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PREFERENCES, userForm)
@ -339,7 +371,8 @@
}
#language,
#date_format {
#date_format,
#use_dark_mode {
padding: $default-padding * 0.5;
}
}

View File

@ -92,7 +92,7 @@
.policy-content {
height: 500px;
border: 1px solid #ccc;
border: 1px solid var(--policy-border-color);
overflow: auto;
margin: $default-margin;
border-radius: $border-radius;

View File

@ -238,6 +238,7 @@
height: 150px;
.no-map {
line-height: 150px;
filter: var(--no-map-filter);
}
::v-deep(.bg-map-image) {
height: 150px;

View File

@ -59,8 +59,10 @@
import type { ComputedRef } from 'vue'
import { Line } from 'vue-chartjs'
import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'
import { htmlLegendPlugin } from '@/components/Workout/WorkoutDetail/WorkoutChart/legend'
import { ROOT_STORE } from '@/store/constants'
import type { TUnit } from '@/types/units'
import type { IAuthUserProfile } from '@/types/user'
import type {
@ -69,7 +71,7 @@
TCoordinates,
} from '@/types/workouts'
import { units } from '@/utils/units'
import { getDatasets } from '@/utils/workouts'
import { chartsColors, getDatasets } from '@/utils/workouts'
interface Props {
authUser: IAuthUserProfile
@ -79,13 +81,22 @@
const emit = defineEmits(['getCoordinates'])
const store = useStore()
const { t } = useI18n()
const { authUser, workoutData } = toRefs(props)
const darkMode: ComputedRef<boolean | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.DARK_MODE]
)
const displayDistance = ref(true)
const beginElevationAtZero = ref(authUser.value.start_elevation_at_zero)
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
getDatasets(workoutData.value.chartData, t, authUser.value.imperial_units)
getDatasets(
workoutData.value.chartData,
t,
authUser.value.imperial_units,
darkMode.value !== false
)
)
const hasElevation = computed(
() => datasets.value && datasets.value.datasets.elevation.data.length > 0
@ -106,6 +117,17 @@
const coordinates: ComputedRef<TCoordinates[]> = computed(
() => datasets.value.coordinates
)
const lineColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.line
: chartsColors.ligthMode.line,
}))
const textColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.text
: chartsColors.ligthMode.text,
}))
const options = computed<ChartOptions<'line'>>(() => ({
responsive: true,
maintainAspectRatio: false,
@ -119,6 +141,10 @@
x: {
grid: {
drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
},
ticks: {
count: 10,
@ -127,6 +153,7 @@
? Number(value).toFixed(2)
: formatDuration(value)
},
...textColors.value,
},
type: 'linear',
bounds: 'data',
@ -135,16 +162,25 @@
text: displayDistance.value
? t('workouts.DISTANCE') + ` (${fromKmUnit})`
: t('workouts.DURATION'),
...textColors.value,
},
},
ySpeed: {
grid: {
drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
},
position: 'left',
title: {
display: true,
text: t('workouts.SPEED') + ` (${fromKmUnit}/h)`,
...textColors.value,
},
ticks: {
...textColors.value,
},
},
yElevation: {
@ -152,11 +188,19 @@
display: hasElevation.value,
grid: {
drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
},
position: 'right',
title: {
display: true,
text: t('workouts.ELEVATION') + ` (${fromMUnit})`,
...textColors.value,
},
ticks: {
...textColors.value,
},
},
},

View File

@ -143,6 +143,9 @@
.mountains {
padding-right: $default-padding * 0.5;
}
.mountains {
filter: var(--mountains-filter);
}
.workout-data {
padding: $default-padding * 0.5 0;

View File

@ -17,6 +17,7 @@
ref="workoutMap"
@ready="fitBounds(bounds)"
:use-global-leaflet="false"
class="map"
>
<LControlLayers />
<LControl
@ -212,13 +213,23 @@
}
.no-map {
line-height: 400px;
filter: var(--no-map-filter);
}
.map-control {
background: #ffffff;
padding: 5px 10px;
border: 2px solid #bfc0ab;
border-radius: 3px;
color: #000000;
.leaflet-container {
.map {
filter: var(--map-filter);
}
.map-control {
background: var(--map-control-bg-color);
padding: 5px 10px;
border: 2px solid var(--map-control-border-color);
border-radius: 3px;
color: var(--map-control-color);
&:hover {
background-color: var(--dropdown-hover-color);
}
}
}
::v-deep(.fullscreen) {
display: flex;

View File

@ -354,7 +354,7 @@
}
.static-map {
display: none;
box-shadow: 3px 3px 3px 1px lightgrey;
box-shadow: 3px 3px 3px 1px var(--workout-static-map-shadow-color);
}
}
.workout-title:hover .static-map {

View File

@ -115,6 +115,14 @@
"PROFILE": "profile",
"SPORTS": "sports"
},
"THEME_MODE": {
"LABEL": "Theme mode",
"VALUES": {
"DARK": "Dark",
"DEFAULT": "Browser preference",
"LIGHT": "Light"
}
},
"TIMEZONE": "Timezone",
"UNITS": {
"IMPERIAL": "Imperial system (ft, mi, mph, °F)",
@ -136,6 +144,7 @@
"REVIEW": "review",
"SHOW_PASSWORD": "show password",
"THIS_USER_ACCOUNT_IS_INACTIVE": "This user account is inactive.",
"TOGGLE_THEME": "Toggle theme (Light or Dark mode)",
"USERNAME": "Username",
"USERNAME_INFO": "3 to 30 characters required, only alphanumeric characters and the underscore character \"_\" allowed.",
"USER_PICTURE": "user picture",

View File

@ -115,6 +115,14 @@
"PROFILE": "profil",
"SPORTS": "sports"
},
"THEME_MODE": {
"LABEL": "Thème",
"VALUES": {
"DARK": "Sombre",
"DEFAULT": "Préférence du navigateur",
"LIGHT": "Clair"
}
},
"TIMEZONE": "Fuseau horaire",
"UNITS": {
"IMPERIAL": "Système impérial (ft, mi, mph, °F)",
@ -136,6 +144,7 @@
"REVIEW": "accepter",
"SHOW_PASSWORD": "afficher le mot de passe",
"THIS_USER_ACCOUNT_IS_INACTIVE": "Le compte de cet utilisateur est inactif.",
"TOGGLE_THEME": "Modifier le thème (Mode Clair ou Sombre)",
"USERNAME": "Nom d'utilisateur",
"USERNAME_INFO": "3 à 30 caractères requis, seuls les caractères alphanumériques et le caractère _ sont autorisés.",
"USER_PICTURE": "photo de l'utilisateur",

View File

@ -1,7 +1,12 @@
@import 'colors';
@import 'colors-dark';
@import 'fonts';
@import 'vars';
html [data-theme='dark'] {
color-scheme: dark;
}
body {
margin: 0;
overflow-y: scroll;
@ -55,6 +60,7 @@ select {
background-color: var(--input-bg-color);
border-radius: $border-radius;
border: solid 1px var(--input-border-color);
color: var(--input-color);
padding: $default-padding;
&:disabled {
@ -102,13 +108,13 @@ button {
&:disabled,
&.confirm:disabled {
border-color: transparent;
border-color: var(--disabled-border-color);
color: var(--disabled-color);
}
}
&:hover {
background: var(--app-color);
background: var(--button-transparent-hover-color);
color: var(--button-hover-color);
}
@ -128,7 +134,7 @@ button {
background: var(--button-cancel-bg-color);
color: var(--button-cancel-color);
&:hover {
background: var(--app-color);
background: var(--button-transparent-hover-color);
color: var(--button-hover-color);
}
}
@ -137,7 +143,7 @@ button {
background: var(--button-confirm-bg-color);
color: var(--button-confirm-color);
&:hover {
background: var(--app-color);
background: var(--button-transparent-hover-color);
color: var(--button-hover-color);
}
}
@ -194,7 +200,7 @@ button {
}
.form-info {
color: var(--alert-color);
color: var(--form-info);
font-size: 0.8em;
margin-top: -0.2 * $default-margin;
padding: 0 $default-padding * 1.5;

View File

@ -0,0 +1,113 @@
:root [data-theme='dark'] {
--dark-blue: #181a1b;
--light-grey: #cfd0d0;
--app-background-color: var(--dark-blue);
--app-color: var(--light-grey);
--app-color-light: #6f7070;
--app-a-color: var(--light-grey);
--app-shadow-color: #383d3f;
--app-loading-color: #f3f3f3;
--app-loading-top-color: var(--app-color);
--button-hover-color: var(--app-color);
--button-transparent-hover-color: #233240;
--button-cancel-bg-color: var(--dark-blue);
--button-cancel-color: var(--app-color);
--button-confirm-bg-color: var(--dark-blue);
--button-confirm-color: var(--app-color);
--button-danger-bg-color: var(--dark-blue);
--button-danger-color: #dc3545;
--button-danger-hover-bg-color: #dc3545;
--button-danger-hover-color: var(--dark-blue);
--card-border-color: #494f52;
--input-border-color: #494f52;
--input-bg-color: var(--dark-blue);
--input-color: var(--app-color);
--input-error-color: #dc3545;
--dropdown-hover-color: #233240;
--dropdown-background-color: var(--dark-blue);
--dropdown-border-color: var(--input-border-color);
--policy-border-color: #ccc;
--box-shadow-color: lightgrey;
--admin-disabled-input-color: var(--dark-blue);
--custom-checkbox-border-color: #665f54;
--custom-checkbox-checked-bg-color: #575e62;
--custom-checkbox-checked-color: #e8e6e3;
--calendar-border-color: var(--input-border-color);
--calendar-week-end-color: #1e2021;
--calendar-day-color: var(--app-color);
--calendar-today-color: #202324;
--calendar-workouts-color: #233240;
--calendar-workouts-box-shadow-0: rgba(0, 0, 0, 0.2);
--calendar-workouts-box-shadow-1: rgba(0, 0, 0, 0.19);
--modal-background-color: rgba(0, 0, 0, 0.3);
--nav-bar-background-color: var(--dark-blue);
--nav-bar-link-active: #ffffff;
--nav-border-color: var(--input-border-color);
--mobile-menu-selected-color: var(--dark-blue);
--mobile-menu-selected-bgcolor: #9da3af;
--footer-background-color: var(--dark-blue);
--footer-border-color: var(--input-border-color);
--footer-color: #9f968a;
--form-info: var(--app-color);
--alert-background-color: #d6dde3;
--alert-color: #3f3f3f;
--info-background-color: #33353a;
--info-color: var(--app-color);
--error-background-color: #4e0000;
--error-color: #ea464f;
--success-background-color: #24391c;
--success-color: #97cd97;
--disabled-background-color: var(--dark-blue);
--disabled-border-color: transparent;
--disabled-color: #727272;
--disabled-sport-color: #616161;
--scroll-button-bg-color: var(--dark-blue);
--workout-trophy-color: #daa520;
--workout-img-color: invert(22%) sepia(25%) saturate(646%) hue-rotate(169deg)
brightness(97%) contrast(96%);
--workout-no-map-bg-color: #eaeaea;
--workout-no-map-color: #585959;
--map-control-color: #000000;
--map-control-bg-color: #ffffff;
--map-control-border-color: #bfc0ab;
--map-attribution-text: #e8e8e8;
--map-display-hover-attribution-text: #444444;
--map-attribution-bg-color: none;
--map-filter: invert(1) hue-rotate(180deg) brightness(0.8) contrast(0.8);
--map-display-hover-filter: invert(1) hue-rotate(180deg) brightness(1.5)
contrast(0.6);
--map-layers-overlays: var(--app-color);
--map-control-bar: var(--app-color);
--no-map-filter: invert(1) brightness(1.5) contrast(0.9);
--workout-static-map-shadow-color: #d2d2d2;
--mountains-filter: invert(90%) sepia(19%) saturate(0%) hue-rotate(39deg)
brightness(86%) contrast(102%);
--cell-heading-bg-color: #383838;
--cell-heading-color: #eeeeee;
--svg-filter: drop-shadow(10px 10px 10px var(--app-shadow-color));
--password-bg-color: #d7dadf;
--password-color-weak: #831819;
--password-color-medium: #9e6906;
--password-color-good: #4b5826;
--password-color-strong: #4a8c32;
--scroll-thumb-color: #949697;
}

View File

@ -8,6 +8,7 @@
--app-loading-top-color: var(--app-color);
--button-hover-color: #ffffff;
--button-transparent-hover-color: var(--app-color);
--button-cancel-bg-color: #ffffff;
--button-cancel-color: var(--app-color);
--button-confirm-bg-color: #ffffff;
@ -20,8 +21,14 @@
--card-border-color: #c4c7cf;
--input-border-color: #9da3af;
--input-bg-color: #ffffff;
--input-color: var(--app-color);
--input-error-color: #dc3545;
--dropdown-hover-color: #eff0f5;
--dropdown-background-color: #ffffff;
--dropdown-border-color: lightgrey;
--policy-border-color: #ccc;
--box-shadow-color: lightgrey;
--admin-disabled-input-color: #ffffff;
--custom-checkbox-border-color: #6d797a;
--custom-checkbox-checked-bg-color: #6d797a;
@ -29,7 +36,11 @@
--calendar-border-color: #c4c7cf;
--calendar-week-end-color: #f5f5f5;
--calendar-day-color: var(--app-color-light);
--calendar-today-color: #eff1f3;
--calendar-workouts-color: whitesmoke;
--calendar-workouts-box-shadow-0: rgba(0, 0, 0, 0.2);
--calendar-workouts-box-shadow-1: rgba(0, 0, 0, 0.19);
--modal-background-color: rgba(0, 0, 0, 0.3);
@ -44,6 +55,8 @@
--footer-border-color: #ebeef3;
--footer-color: #6f7070;
--form-info: var(--alert-color);
--alert-background-color: #d6dde3;
--alert-color: #3f3f3f;
--info-background-color: #e5e7ea;
@ -54,6 +67,7 @@
--success-color: #306430;
--disabled-background-color: #e0e0e0;
--disabled-border-color: transparent;
--disabled-color: #727272;
--disabled-sport-color: #616161;
@ -64,6 +78,21 @@
brightness(97%) contrast(96%);
--workout-no-map-bg-color: #eaeaea;
--workout-no-map-color: #585959;
--map-control-color: #000000;
--map-control-bg-color: #ffffff;
--map-control-border-color: #bfc0ab;
--map-attribution-text: var(--app-color);
--map-display-hover-attribution-text: initial;
--map-attribution-bg-color: rgba(255, 255, 255, 0.7);
--map-filter: initial;
--map-display-hover-filter: initial;
--map-layers-overlays: initial;
--map-control-bar: #bfc0ab;
--no-map-filter: initial;
--workout-static-map-shadow-color: var(--app-shadow-color);
--mountains-filter: invert(19%) sepia(9%) saturate(2921%) hue-rotate(169deg)
brightness(85%) contrast(80%);
--cell-heading-bg-color: #eeeeee;
--cell-heading-color: #696969;

View File

@ -139,6 +139,10 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
res.data.data.language
)
}
context.commit(
ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE,
res.data.data.use_dark_mode
)
context.dispatch(SPORTS_STORE.ACTIONS.GET_SPORTS)
} else {
handleError(context, null)
@ -270,6 +274,10 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
AUTH_USER_STORE.MUTATIONS.UPDATE_AUTH_USER_PROFILE,
res.data.data
)
context.commit(
ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE,
res.data.data.use_dark_mode
)
context
.dispatch(
ROOT_STORE.ACTIONS.UPDATE_APPLICATION_LANGUAGE,

View File

@ -10,6 +10,7 @@ export enum RootGetters {
APP_CONFIG = 'APP_CONFIG',
APP_LOADING = 'APP_LOADING',
APP_STATS = 'APP_STATS',
DARK_MODE = 'DARK_MODE',
ERROR_MESSAGES = 'ERROR_MESSAGES',
LANGUAGE = 'LANGUAGE',
LOCALE = 'LOCALE', // date-fns
@ -22,5 +23,6 @@ export enum RootMutations {
UPDATE_APPLICATION_LOADING = 'UPDATE_APPLICATION_LOADING',
UPDATE_APPLICATION_PRIVACY_POLICY = 'UPDATE_APPLICATION_PRIVACY_POLICY',
UPDATE_APPLICATION_STATS = 'UPDATE_APPLICATION_STATS',
UPDATE_DARK_MODE = 'UPDATE_DARK_MODE',
UPDATE_LANG = 'UPDATE_LANG',
}

View File

@ -13,6 +13,9 @@ export const getters: GetterTree<IRootState, IRootState> & IRootGetters = {
[ROOT_STORE.GETTERS.APP_STATS]: (state: IRootState) => {
return state.application.statistics
},
[ROOT_STORE.GETTERS.DARK_MODE]: (state: IRootState) => {
return state.darkMode
},
[ROOT_STORE.GETTERS.ERROR_MESSAGES]: (state: IRootState) => {
return state.errorMessages
},

View File

@ -45,4 +45,10 @@ export const mutations: MutationTree<IRootState> & TRootMutations = {
state.language = language
state.locale = localeFromLanguage[language]
},
[ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE](
state: IRootState,
darkMode: boolean | null
) {
state.darkMode = darkMode
},
}

View File

@ -17,4 +17,5 @@ export const state: IRootState = {
},
},
appLoading: false,
darkMode: null,
}

View File

@ -22,6 +22,7 @@ export interface IRootState {
errorMessages: string | string[] | null
application: IApplication
appLoading: boolean
darkMode: boolean | null
}
export interface IRootActions {
@ -51,6 +52,8 @@ export interface IRootGetters {
[ROOT_STORE.GETTERS.APP_STATS](state: IRootState): IAppStatistics
[ROOT_STORE.GETTERS.DARK_MODE](state: IRootState): boolean | null
[ROOT_STORE.GETTERS.ERROR_MESSAGES](
state: IRootState
): string | string[] | null
@ -83,6 +86,10 @@ export type TRootMutations<S = IRootState> = {
statistics: IAppStatistics
): void
[ROOT_STORE.MUTATIONS.UPDATE_LANG](state: S, language: TLanguage): void
[ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE](
state: S,
darkMode: boolean | null
): void
}
export type TRootStoreModule<S = IRootState> = Omit<

View File

@ -35,6 +35,7 @@ export interface IAuthUserProfile extends IUserProfile {
timezone: string
date_format: string
weekm: boolean
use_dark_mode: boolean | null
}
export interface IUserPayload {
@ -73,6 +74,7 @@ export interface IUserPreferencesPayload {
timezone: string
date_format: string
weekm: boolean
use_dark_mode: boolean | null
}
export interface IUserSportPreferencesPayload {

View File

@ -7,24 +7,37 @@ import type {
} from '@/types/workouts'
import { convertStatsDistance } from '@/utils/units'
export const chartsColors = {
ligthMode: {
// default chartjs values
text: '#666',
line: 'rgba(0, 0, 0, 0.1)',
},
darkMode: {
text: '#a1a1a1',
line: '#3f3f3f',
},
}
export const getDatasets = (
chartData: IWorkoutApiChartData[],
t: CallableFunction,
useImperialUnits: boolean
useImperialUnits: boolean,
useDarkMode: boolean = false
): IWorkoutChartData => {
const datasets: TWorkoutDatasets = {
speed: {
label: t('workouts.SPEED'),
backgroundColor: ['#FFFFFF'],
borderColor: ['#8884d8'],
backgroundColor: ['transparent'],
borderColor: [useDarkMode ? '#5f5c97' : '#8884d8'],
borderWidth: 2,
data: [],
yAxisID: 'ySpeed',
},
elevation: {
label: t('workouts.ELEVATION'),
backgroundColor: ['#e5e5e5'],
borderColor: ['#cccccc'],
backgroundColor: [useDarkMode ? '#303030' : '#e5e5e5'],
borderColor: [useDarkMode ? '#222222' : '#cccccc'],
borderWidth: 1,
fill: true,
data: [],

View File

@ -72,8 +72,8 @@
&:disabled {
-webkit-appearance: none;
-moz-appearance: textfield;
background-color: white;
border-color: white;
background-color: var(--admin-disabled-input-color);
border-color: var(--admin-disabled-input-color);
color: var(--app-color);
}
}

View File

@ -21,7 +21,7 @@ describe('getDatasets', () => {
datasets: {
speed: {
label: 'vitesse',
backgroundColor: ['#FFFFFF'],
backgroundColor: ['transparent'],
borderColor: ['#8884d8'],
borderWidth: 2,
data: [],
@ -81,7 +81,7 @@ describe('getDatasets', () => {
datasets: {
speed: {
label: 'speed',
backgroundColor: ['#FFFFFF'],
backgroundColor: ['transparent'],
borderColor: ['#8884d8'],
borderWidth: 2,
data: [2.89, 20.64, 13.03],
@ -145,7 +145,7 @@ describe('getDatasets', () => {
datasets: {
speed: {
label: 'speed',
backgroundColor: ['#FFFFFF'],
backgroundColor: ['transparent'],
borderColor: ['#8884d8'],
borderWidth: 2,
data: [1.8, 12.83, 8.1],
@ -183,6 +183,90 @@ describe('getDatasets', () => {
})
})
describe('getDatasets with dark mode', () => {
const testparams = [
{
description: 'it returns dark mode color',
inputParams: {
charData: [],
locale: 'fr',
useImperialUnits: false,
useDarkMode: true,
},
expected: {
distance_labels: [],
duration_labels: [],
datasets: {
speed: {
label: 'vitesse',
backgroundColor: ['transparent'],
borderColor: ['#5f5c97'],
borderWidth: 2,
data: [],
yAxisID: 'ySpeed',
},
elevation: {
label: 'altitude',
backgroundColor: ['#303030'],
borderColor: ['#222222'],
borderWidth: 1,
fill: true,
data: [],
yAxisID: 'yElevation',
},
},
coordinates: [],
},
},
{
description: 'it returns light mode color',
inputParams: {
charData: [],
locale: 'fr',
useImperialUnits: false,
useDarkMode: false,
},
expected: {
distance_labels: [],
duration_labels: [],
datasets: {
speed: {
label: 'vitesse',
backgroundColor: ['transparent'],
borderColor: ['#8884d8'],
borderWidth: 2,
data: [],
yAxisID: 'ySpeed',
},
elevation: {
label: 'altitude',
backgroundColor: ['#e5e5e5'],
borderColor: ['#cccccc'],
borderWidth: 1,
fill: true,
data: [],
yAxisID: 'yElevation',
},
},
coordinates: [],
},
},
]
testparams.map((testParams) => {
it(testParams.description, () => {
locale.value = testParams.inputParams.locale
expect(
getDatasets(
testParams.inputParams.charData,
t,
testParams.inputParams.useImperialUnits,
testParams.inputParams.useDarkMode
)
).toStrictEqual(testParams.expected)
})
})
})
describe('getDonutDatasets', () => {
const testparams = [
{