Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only lead climbs #60

Merged
merged 36 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f24cb45
carnet outdoor - liste seulement les croix en tête
pectum83 Sep 19, 2023
06370da
carnet outdoor - liste seulement les croix en tête
pectum83 Sep 19, 2023
8a84414
carnet outdoor - liste seulement les croix en tête - ajout d'une chec…
pectum83 Sep 30, 2023
aae75f3
extension de la modif aux autres graphes et a la vue analytik
pectum83 Sep 30, 2023
639d7b9
Remove package.json file
pectum83 Dec 8, 2024
a4d8b2a
Merge branch 'refs/heads/master' into onlyLeadClimbs
pectum83 Dec 8, 2024
8f602ab
fix(icon type and color) in CragRouteSmallLine when several ascent on…
pectum83 Dec 11, 2024
bc6a550
Refactor filters for outdoor ascents and analytics
pectum83 Dec 11, 2024
60bbf84
Revert bugfix icon - a remettre dans une autre PR
pectum83 Dec 12, 2024
23ed5ee
add AscentFiltersToggleBtn.vue input component
pectum83 Dec 12, 2024
1bf8531
fix to adapt other user outdoor vue to backend filters (but did not i…
pectum83 Dec 12, 2024
57ae19e
Add multi-filter functionality for ascents and climbing types
pectum83 Dec 24, 2024
3ace689
Refactor filter handling for ascended crag routes.
pectum83 Dec 24, 2024
5635633
Refactor filter handling for ascended crag routes.
pectum83 Dec 27, 2024
eb27516
remove package.json
pectum83 Dec 27, 2024
659c230
suppress otherFilters. Replaced by boolean no_double
pectum83 Dec 31, 2024
7f53ecd
lists in snakecase
pectum83 Dec 31, 2024
91e9ddf
suppress no_double filter
pectum83 Jan 1, 2025
9c62971
factorize stats api
pectum83 Jan 2, 2025
3edb896
POST pour les nested params et factorisation user et currentuser api …
pectum83 Jan 2, 2025
7ca72a4
call stats with GET and parse nested params in api
pectum83 Jan 5, 2025
d1f2ddc
reverse package.json
pectum83 Jan 7, 2025
850b170
several PR comments applied
pectum83 Jan 7, 2025
6dda412
camelcase objects filters et stats
pectum83 Jan 7, 2025
c7bb4d0
Revert "camelcase objects filters et stats"
pectum83 Jan 7, 2025
79aa86a
revert package.json
pectum83 Jan 7, 2025
fb751ff
revert package.json
pectum83 Jan 7, 2025
c820bcc
revert package.json
pectum83 Jan 7, 2025
35a6319
camelcase et filters à la racine
pectum83 Jan 8, 2025
aa553c7
revert lang files reformat
pectum83 Jan 8, 2025
0a349a0
suppress lead et onsight
pectum83 Jan 8, 2025
30214e2
no request until applyu filters
pectum83 Jan 8, 2025
f31f404
separation et duplication entre user et current user
pectum83 Jan 19, 2025
c385176
small changes
pectum83 Jan 24, 2025
25527f6
try to supress package.lock
pectum83 Jan 24, 2025
3c6814a
separation et duplication entre user et current user
pectum83 Jan 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions components/forms/AscentStatusInput.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<v-input
class="required-field"
:class="required ? 'required-field' : ''"
hide-details
>
<fieldset class="full-width custom-fieldset border rounded mt-n1 pb-0 px-2">
Expand All @@ -13,6 +13,7 @@
v-model="ascentStatus"
active-class="primary--text"
column
:multiple="multiple"
@change="onChange"
>
<v-chip
Expand All @@ -33,7 +34,11 @@
</v-chip-group>
</div>
</fieldset>
<v-chip v-if="multiple" class="ml-1" @click="selectAll()">
{{ $t('common.seeAll') }}
</v-chip>
</v-input>

<div class="mb-3">
<div class="text-right pr-1">
<span
Expand Down Expand Up @@ -90,15 +95,23 @@
</template>

<script>
import { mdiCropSquare, mdiCheckboxMarkedCircle, mdiRecordCircle, mdiFlash, mdiEye, mdiAutorenew, mdiChevronUp } from '@mdi/js'
import {
mdiCropSquare,
mdiCheckboxMarkedCircle,
mdiRecordCircle,
mdiFlash,
mdiEye,
mdiAutorenew,
mdiChevronUp
} from '@mdi/js'
import { InputHelpers } from '@/mixins/InputHelpers'

export default {
name: 'AscentStatusInput',
mixins: [InputHelpers],
props: {
value: {
type: String,
type: [String, Array], // array if multipleChoices true
default: null
},
withProject: {
Expand All @@ -112,12 +125,20 @@ export default {
withRepetition: {
type: Boolean,
default: true
},
multiple: {
type: Boolean,
default: false
},
required: { // required input of the form and display the red * on top of the component
type: Boolean,
default: true
}
},

data () {
return {
ascentStatus: this.value,
ascentStatus: this.value, // string or array according to multipleChoices false or true
showLegend: false,

mdiChevronUp
Expand Down Expand Up @@ -146,6 +167,11 @@ export default {
methods: {
onChange () {
this.$emit('input', this.ascentStatus)
},

selectAll () {
this.ascentStatus = this.ascentStatuses.map(status => status.value)
this.onChange()
}
}
}
Expand Down
51 changes: 50 additions & 1 deletion components/forms/ClimbingTypeInput.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<v-select
v-if="inputType === 'select'"
v-model="climbingTypes"
:items="climbByEnvironment()"
item-text="text"
Expand All @@ -13,13 +14,52 @@
outlined
@change="onChange"
/>
<v-input v-else-if="inputType === 'chips'">
<fieldset class="full-width custom-fieldset border rounded mt-n1 pb-0 px-2">
<legend class="v-label custom-fieldset-label">
{{ $t('components.input.climbing_type') }}
</legend>
<div>
<v-chip-group
v-model="climbingTypes"
active-class="primary--text"
column
:multiple="multiple"
@change="onChange"
>
<v-chip
v-for="(item, itemIndex) in climbByEnvironment()"
:key="`item-index-${itemIndex}`"
:value="item.value"
outlined
>
<v-icon
:class="`climbs-pastille ${item.value} mr-3`"
:color="climbingTypes === item.value ? 'green' : null"
small
left
>
{{ item.icon }}
</v-icon>
{{ item.text }}
</v-chip>
</v-chip-group>
</div>
</fieldset>
<v-chip v-if="multiple" class="ml-1" @click="selectAll()">
{{ $t('common.seeAll') }}
</v-chip>
</v-input>
</template>

<script>
export default {
name: 'ClimbingTypeInput',
props: {
value: [Array, String],
value: {
type: [Array, String],
default: null
},
environment: {
type: String,
default: 'crag'
Expand All @@ -43,6 +83,10 @@ export default {
clearable: {
type: Boolean,
default: false
},
inputType: { // possible values: 'select', 'chips'
type: String,
default: 'select'
}
},

Expand Down Expand Up @@ -91,6 +135,11 @@ export default {
} else if (this.environment === 'user') {
return this.climbingUserList
}
},

selectAll () {
this.climbingTypes = this.climbByEnvironment().map(climb => climb.value)
this.onChange()
}
}
}
Expand Down
52 changes: 45 additions & 7 deletions components/forms/RopingStatusInput.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<template>
<v-input class="required-field">
<v-input
:class="required ? 'required-field' : ''"
>
<fieldset class="full-width custom-fieldset border rounded mt-n1 pb-0 px-2">
<legend class="v-label custom-fieldset-label">
{{ $t('components.input.ropingStatusQuestion') }}
Expand All @@ -9,6 +11,7 @@
v-model="ropingStatus"
active-class="primary--text"
column
:multiple="multiple"
@change="onChange"
>
<v-chip
Expand All @@ -29,6 +32,9 @@
</v-chip-group>
</div>
</fieldset>
<v-chip v-if="multiple" class="ml-1" @click="selectAll()">
{{ $t('common.seeAll') }}
</v-chip>
</v-input>
</template>

Expand All @@ -45,7 +51,7 @@ export default {
mixins: [InputHelpers],
props: {
value: {
type: String,
type: [String, Array],
default: null
},
sportClimbingRopingStatuses: {
Expand All @@ -55,6 +61,14 @@ export default {
multiPithRopingStatuses: {
type: Boolean,
default: true
},
multiple: {
type: Boolean,
default: false
},
required: { // required input of the form and display the red * on top of the component
type: Boolean,
default: true
}
},

Expand All @@ -68,13 +82,33 @@ export default {
ropingStatuses () {
const statuses = []
if (this.sportClimbingRopingStatuses) {
statuses.push({ text: this.$t('models.ropingStatus.lead_climb'), value: 'lead_climb', icon: oblykRopingStatusLeadClimb })
statuses.push({ text: this.$t('models.ropingStatus.top_rope'), value: 'top_rope', icon: oblykRopingStatusTopRope })
statuses.push({
text: this.$t('models.ropingStatus.lead_climb'),
value: 'lead_climb',
icon: oblykRopingStatusLeadClimb
})
statuses.push({
text: this.$t('models.ropingStatus.top_rope'),
value: 'top_rope',
icon: oblykRopingStatusTopRope
})
}
if (this.multiPithRopingStatuses) {
statuses.push({ text: this.$t('models.ropingStatus.multi_pitch_leader'), value: 'multi_pitch_leader', icon: oblykRopingStatusMultiPitchLeader })
statuses.push({ text: this.$t('models.ropingStatus.multi_pitch_second'), value: 'multi_pitch_second', icon: oblykRopingStatusMultiPitchSecond })
statuses.push({ text: this.$t('models.ropingStatus.multi_pitch_alternate_lead'), value: 'multi_pitch_alternate_lead', icon: oblykRopingStatusLeadClimbMultiPitchAlternateLead })
statuses.push({
text: this.$t('models.ropingStatus.multi_pitch_leader'),
value: 'multi_pitch_leader',
icon: oblykRopingStatusMultiPitchLeader
})
statuses.push({
text: this.$t('models.ropingStatus.multi_pitch_second'),
value: 'multi_pitch_second',
icon: oblykRopingStatusMultiPitchSecond
})
statuses.push({
text: this.$t('models.ropingStatus.multi_pitch_alternate_lead'),
value: 'multi_pitch_alternate_lead',
icon: oblykRopingStatusLeadClimbMultiPitchAlternateLead
})
}
return statuses
}
Expand All @@ -83,6 +117,10 @@ export default {
methods: {
onChange () {
this.$emit('input', this.ropingStatus)
},
selectAll () {
this.ropingStatus = this.ropingStatuses.map(status => status.value)
this.onChange()
}
}
}
Expand Down
99 changes: 99 additions & 0 deletions components/logBooks/outdoors/AscentFiltersForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<div>
<v-btn v-if="!showForm" @click="showForm =! showForm">
{{ $t('components.logBook.filterTheLogBook') }}
</v-btn>
<v-form v-if="showForm" @submit.prevent="onSubmit()">
<ascent-status-input
v-model="filters.ascent_status_list"
multiple
with-repetition
with-sent
:with-project="false"
:required="false"
/>

<roping-status-input
v-model="filters.roping_status_list"
multiple
:required="false"
/>

<climbing-type-input
v-model="filters.climbing_types_list"
multiple
environment="crag"
input-type="chips"
/>

<submit-form
:overlay="false"
:submit-local-key="'actions.save'"
:go-back-btn="false"
/>
</v-form>
</div>
</template>

<script>
import AscentStatusInput from '~/components/forms/AscentStatusInput'
import RopingStatusInput from '~/components/forms/RopingStatusInput'
import SubmitForm from '~/components/forms/SubmitForm'
import ClimbingTypeInput from '~/components/forms/ClimbingTypeInput'

export default {
name: 'AscentFiltersForm',
components: { ClimbingTypeInput, SubmitForm, RopingStatusInput, AscentStatusInput },

data () {
return {
showForm: false,
filters: {
ascent_status_list: [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

les datas plutôt en camelCase

donc ascentStatusList, ropingStatusList, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai mis en snake case parce que tu l'as demandé pour le backend (oblyk/oblyk-api#9 (comment)). Et effectivement il y a forcément un des deux bouts qui n'est pas vraiment dans la norme. Pour etre plus cnofoirme la seule solution que je vois c'est de convertir le Camel en snake avant envoi de la requete comme ci-dessous. Est ce que ca te vas ?

// Fonction pour convertir camelCase en snake_case
function camelToSnake(obj) {
  const newObj = {}
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase()
      newObj[newKey] = obj[key]
    }
  }
  return newObj
}

class LogBookOutdoorApi extends BaseApi {
  // si user_id est null, il retournera le logbook de l'utilisateur actuel
  stats (stats_list = {}, filters = {}, user_id = null) {
    // Convertir les clés de filters en snake_case
    const snakeCaseFilters = camelToSnake(filters)

    return this.axios.request({
      method: 'GET',
      url: `${this.baseUrl}/current_users/log_books/outdoors/stats.json`,
      headers: {
        Authorization: this.authToken(),
        HttpApiAccessToken: this.apiAccessToken
      },
      params: {
        user_id,
        filters: snakeCaseFilters,
        stats_list
      },
      paramsSerializer: params => {
        return qs.stringify(params, { arrayFormat: 'brackets', encode: false })
      }
    })
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finalement c'est beaucoup trop lourd. Dans l'autre sens il faut aussi convertir tous les snakes en camelCase à la réception des données. Tu en penses quoi ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info: pour repérer ou j'avais des snakes case à remplacer j'ai voulu ajouter ' camelcase: ['error', { properties: 'always' }] ' au linter. Il a sorti 1884 errors. Donc je l'ai enlevé ... Mais ce n'est aps le seul endroit ou on a des snake cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alors oui ce n'est pas ultra régulier que tous soit en camelCase ^^
Grosso modo, sur ruby on rails la convention c'est le snake_case pour les variables
Sur nuxt / js c'est le cameCase, du moins c'est la convention que j'ai choisie

Donc ça fait forcément un mixte de convention.
Par exemple les données issu de l'API sont en snake_case, exemple ascent.roping_status
donc dans le formulaire d'ajout d'une croix, tu va avoir ce genre de code

data () {
  return {
    loadingAscent: true,
    data: {
      id: this.ascent.id,
      roping_status: this.ascent.roping_status
    }
  }
}

là par exemple tu as un mixe de variable issu de l'API dans this.data, et des variable qui servent le fonctionnement du front le this.loadingAscent

Donc maintenant ce pose la question de passage d'un paramètre du fonte vers le back, comme ici
Dans la plus part des cas je travail sur le front avec les convention du front, et sur le back avec les conventions du back, donc ça va être au niveau des serviceApi que la conversion va s’effectuer.

exemple:

 class LogBookOutdoorApi extends BaseApi {
  stats (stats_list = {}, filters = {}, user_id = null) {
    return this.axios.request({
      method: 'GET',
      url: `${this.baseUrl}/current_users/log_books/outdoors/stats.json`,
      headers: {
        Authorization: this.authToken(),
        HttpApiAccessToken: this.apiAccessToken
      },
      params: {
        user_id,
        filters: {
          roping_status: filters.ropingStatus,
          climbing_type: filters.climbingType
        },
        stats_list
      },
      paramsSerializer: params => {
        return qs.stringify(params, { arrayFormat: 'brackets', encode: false })
      }
    })
  }
}

personnellement ça ne me dérange pas de répéter les filtres passé à l'API, ça facilite la lecture : )

et on peut appeler cette fonction comme ça :

new LogBookOutdoorApi(this.$axios, this.$auth).stats(this.statsList, this.filters)

donc par exemple pour statList on travail en camelCase dans le reste du front, il est juste naturellement transformé en en snake_case dans LogBookOutdoorApi au passage par les paramètres

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

est ce qu'on est d'accord que par contre les data qui remontent de l'api (notamment l'objet stats) restent en snake case, donc par ex on aura

 data () {
    return {
      loadTheRest: false,
      loadingStats: true,

      filters: {},
      stats: {
        figures: {},
        climb_types_chart: {},
        grades_chart: {}
      },
      statsList: ['figure', 'climb_type_chart', 'grades_chart']
    }
  },

roping_status_list: [],
climbing_types_list: []
}
}
},

watch: {
// Watch for changes in filters and emit automatically
filters: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je ne suis pas sûr de comprend pourquoi ?

ça ne devrait pas être le onSubmit qui $emit ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On ouvre la form de filtre en cliquant sur le bouton "filtrer".
Tant qu'il est ouvert on peut changer les paramètres du filtre et on observe à chaque changement l'effet sur les stats (le watch) mais sans sauver dans le localStorage.
Quand on clique sur "Save" ca enregistre le noveau régalge de filtres dans le localStorage (le emit)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

En local ça doit bien marcher, mais en prod avec des temps de réponse plus long je ne suis pas sûr que l'expérience soit aussi gratifiante.
Surtout si on ne gère pas l'annulation de la requête précédente.
Si tu laisse l'utilisateur envoyer un second filtre avant d'avoir reçu le premier, tu n'as pas la garantie de l'ordre dans lequel tu va avoir les réponses, tu va peut-être recevoir la réponse du deuxième filter, puis du premier, et tu te retrouve avec les croix du premier filtre avec l'affichage du second filtre.
Il fois soit annuler la requête précédente, soit bloquer le formulaire temps que tu n'as pas reçu la réponse du premier, soit filtrer uniquement quand tu clic sur (filtrer)

Je pense que le plus compréhensible, performant et simple c'est la troisième solution.
Un seul bouton (filter) qui envoie la requête et enregistre le filtrer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok pour la solution 3

handler (newFilters) {
this.$emit('input', { ...newFilters }) // Emit fresh copy of changes
},
deep: true // Ensure nested changes in filters are detected
}
},

mounted () {
// recover from local storage and reset if one key is missing in the stored filters (to keep the app working if we change filters)
let storedFilters = localStorage.getItem('filters')
if (storedFilters) {
storedFilters = JSON.parse(storedFilters) || {}
this.filters = {
ascent_status_list: Array.isArray(storedFilters.ascent_status_list) ? storedFilters.ascent_status_list : this.getAllAscentStatus(),
roping_status_list: Array.isArray(storedFilters.roping_status_list) ? storedFilters.roping_status_list : this.getAllRopingStatus(),
climbing_types_list: Array.isArray(storedFilters.climbing_types_list) ? storedFilters.climbing_types_list : this.getAllClimbingTypes()
}
}
this.$emit('input', this.filters)
},

methods: {
onSubmit () {
this.showForm = false
localStorage.setItem('filters', JSON.stringify(this.filters))
},
getAllAscentStatus () {
return ['onsight', 'flash', 'red_point', 'project', 'sent', 'repetition']
},
getAllRopingStatus () {
return ['lead_climb', 'top_rope', 'multi_pitch_leader', 'multi_pitch_second', 'multi_pitch_alternate_lead']
},
getAllClimbingTypes () {
return ['sport_climbing', 'bouldering', 'multi_pitch', 'trad_climbing', 'aid_climbing', 'deep_water', 'via_ferrata']
}
}
}
</script>
5 changes: 4 additions & 1 deletion components/logBooks/outdoors/LogBookClimbingTypeChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export default {
name: 'LogBookClimbingTypeChart',
components: { DoughnutChart },
props: {
data: Object,
data: {
type: Object,
default: () => ({ labels: [], datasets: [] })
},
legend: Boolean,
heightClass: {
type: String,
Expand Down
Loading