From fa409cdb3a193a539126d7a7088dbdd17aba674f Mon Sep 17 00:00:00 2001 From: TTalex Date: Sat, 5 Oct 2024 17:59:01 +0200 Subject: [PATCH 1/8] feat(Challenge Page): first draft --- src/components/PriceChart.vue | 12 +- src/router.js | 1 + src/views/CurrentChallenge.vue | 226 +++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 src/views/CurrentChallenge.vue diff --git a/src/components/PriceChart.vue b/src/components/PriceChart.vue index 0ec4216d614..b40da30f41c 100644 --- a/src/components/PriceChart.vue +++ b/src/components/PriceChart.vue @@ -13,6 +13,14 @@ export default { priceList: { type: Array, default: () => [] + }, + aggregate: { + type: String, + default: () => "mean" + }, + dateField: { + type: String, + default: () => "date" } }, data() { @@ -44,9 +52,9 @@ export default { tooltip: true }, encoding: { - x: {timeUnit: 'yearmonthdate', field: 'date', type: 'temporal', axis: { title: this.$t('Common.Date') }}, + x: {timeUnit: 'yearmonthdate', field: this.dateField, type: 'temporal', axis: { title: this.$t('Common.Date') }}, // y: {field: 'price', type: 'quantitative'} - y: {aggregate: 'mean', field: 'price', type: 'quantitative', axis: { title: this.$t('Common.Price') }}, + y: {aggregate: this.aggregate, field: 'price', type: 'quantitative', axis: { title: this.$t('Common.Price') }}, } } embed('#vega-lite-chart', vlSpec, {actions: false, theme: this.theme.global.name}) diff --git a/src/router.js b/src/router.js index 8c29083ff35..c279c247ebc 100644 --- a/src/router.js +++ b/src/router.js @@ -43,6 +43,7 @@ const routes = [ { path: '/stats', name: 'stats', component: () => import('./views/Stats.vue'), meta: { title: 'Stats', icon: 'mdi-chart-box-outline', drawerMenu: true, breadcrumbs: [{title: 'Stats', disabled: true }] }}, { path: '/settings', name: 'settings', component: () => import('./views/Settings.vue'), meta: { title: 'Settings', icon: 'mdi-cog-outline', drawerMenu: true, breadcrumbs: [{title: 'Settings', disabled: true }] }}, { path: '/about', name: 'about', component: () => import('./views/About.vue'), meta: { title: 'About', icon: 'mdi-information-outline', drawerMenu: true, breadcrumbs: [{title: 'About', disabled: true }] }}, + { path: '/challenge', name: 'challenge', component: () => import('./views/CurrentChallenge.vue'), meta: { title: 'Current Challenge', icon: 'mdi-medal-outline', drawerMenu: true }}, // Why this redirect? // The app used to be available at https://prices.openfoodfacts.org/app // It is now available at https://prices.openfoodfacts.org diff --git a/src/views/CurrentChallenge.vue b/src/views/CurrentChallenge.vue new file mode 100644 index 00000000000..970b2fe4093 --- /dev/null +++ b/src/views/CurrentChallenge.vue @@ -0,0 +1,226 @@ + + + From 0ef58473235b475b0210c3448ac3fe1d38588902 Mon Sep 17 00:00:00 2001 From: TTalex Date: Sat, 5 Oct 2024 20:16:31 +0200 Subject: [PATCH 2/8] feat(PriceChart): fit container size --- src/components/PriceChart.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PriceChart.vue b/src/components/PriceChart.vue index b40da30f41c..10d1e3fced2 100644 --- a/src/components/PriceChart.vue +++ b/src/components/PriceChart.vue @@ -1,5 +1,5 @@ + \ No newline at end of file diff --git a/src/components/ChallengeTimeline.vue b/src/components/ChallengeTimeline.vue new file mode 100644 index 00000000000..4260dc0e0a0 --- /dev/null +++ b/src/components/ChallengeTimeline.vue @@ -0,0 +1,58 @@ + + + + \ No newline at end of file diff --git a/src/components/ChallengeTopContributors.vue b/src/components/ChallengeTopContributors.vue new file mode 100644 index 00000000000..83d566821f1 --- /dev/null +++ b/src/components/ChallengeTopContributors.vue @@ -0,0 +1,38 @@ + + + + \ No newline at end of file diff --git a/src/views/CurrentChallenge.vue b/src/views/CurrentChallenge.vue index 970b2fe4093..a3ac93444cd 100644 --- a/src/views/CurrentChallenge.vue +++ b/src/views/CurrentChallenge.vue @@ -8,42 +8,34 @@ - - TODO: Explain the challenge, how to contribute etc.. - TODO: maybe add a place for proofs with missing prices (ideally proofs would be tagged for the challenge) - TODO: what about rewards ? badges for top 3 ? participation badges ? - TODO: Maybe a goal should be a number of product / prices, instead of the day + +
+

+ {{ challenge.icon }} Join other food price enthousiast (such as {{ challenge.topContributors[0]?.user_id || "you?" }}) in gathering information about {{ challenge.title }} {{ challenge.subtitle }}. +

+

+ {{ challenge.icon }} Your goal is to go nearby stores, find corresponding products and + + adding prices + +

+

+ {{ challenge.icon }} This challenge categories are + + {{ category }} + +

+
- - - -
- Start -
- -
- - - {{ challenge.end_date }} - -
- - {{ days_left_text }} - + +
+ + + + +

@@ -52,10 +44,10 @@ - + - + @@ -66,10 +58,10 @@

- + - +
@@ -77,55 +69,22 @@ - - - - - - - - {{ i+1 }}. {{ contributor.user_id }}, {{ contributor.price_count }} prices - - - - - + - - - - - - + +

+ Number of contributions per day +

+

Most recent contributions

- - + + @@ -142,7 +101,10 @@ export default { components: { StatCard: defineAsyncComponent(() => import('../components/StatCard.vue')), PriceCard: defineAsyncComponent(() => import('../components/PriceCard.vue')), - PriceChart: defineAsyncComponent(() => import('../components/PriceChart.vue')) + PriceChart: defineAsyncComponent(() => import('../components/PriceChart.vue')), + ChallengeTimeline: defineAsyncComponent(() => import('../components/ChallengeTimeline.vue')), + ChallengeExample: defineAsyncComponent(() => import('../components/ChallengeExample.vue')), + ChallengeTopContributors: defineAsyncComponent(() => import('../components/ChallengeTopContributors.vue')), }, data() { return { @@ -150,15 +112,16 @@ export default { title: "MILK", subtitle: "(and milk alternatives)", icon: "🥛", - start_date: "2024-09-01", - end_date: "2024-10-31", + startDate: "2024-09-01", + endDate: "2024-10-31", categories: ["en:milk-substitutes", "en:milks"], - top_contributors: [], - number_of_contributors: 0, - number_of_contributions: 0, - latest_contributions: [], - user_contributions: 0, - user_rank: 0 + topContributors: [], + numberOfContributors: 0, + numberOfContributions: 0, + latestContributions: [], + userContributions: 0, + userRank: 0, + exampleId: 32900 }, loading: false, } @@ -168,21 +131,6 @@ export default { username() { return this.appStore.user.username }, - nb_days() { - return Math.round((new Date(this.challenge.end_date) - new Date(this.challenge.start_date)) / (1000 * 60 * 60 * 24)) - }, - today_index() { - return Math.round((new Date() - new Date(this.challenge.start_date)) / (1000 * 60 * 60 * 24)) - }, - days_left_text() { - const days_left = this.nb_days - this.today_index - if (days_left <= 0) return "Challenge over" - if (days_left >= this.nb_days) return "Not started" - return `${days_left} days left` - }, - progress() { - return Math.min(100, this.today_index * 100 / this.nb_days) - } }, mounted() { this.getStats() @@ -191,36 +139,37 @@ export default { getStats() { this.loading = true // TODO: This should fetch only prices matching one of the product categories in this.challenge.categories - api.getPrices({ size: 100, created__gte: this.challenge.start_date, created__lte: this.challenge.end_date }) + // TODO: Also, this is both used to show a few recent contribution and to display the number of contributions per day + // The first requires only a size of 10, the second requires the entire data, so it probably should be another API point + api.getPrices({ size: 200, created__gte: this.challenge.startDate, created__lte: this.challenge.endDate }) .then((data) => { - this.challenge.latest_contributions = data.items - this.challenge.number_of_contributions = data.total + this.challenge.latestContributions = data.items + this.challenge.numberOfContributions = data.total this.loading = false }) // TODO: this should only fetch users that contributed to one of the product categories in this.challenge.categories, in the designated time range api.getUsers({ order_by: constants.USER_ORDER_LIST[0].key, size: 50}) .then((data) => { - this.challenge.top_contributors = data.items - this.challenge.number_of_contributors = data.total + this.challenge.topContributors = data.items + this.challenge.numberOfContributors = data.total for (let i = 0; i < data.items.length; i++) { const user = data.items[i] if (this.username && this.username == user.user_id) { - this.challenge.user_rank = i + this.challenge.userRank = i break } } }) if (this.username) { // TODO: This should fetch only prices matching one of the product categories in this.challenge.categories - api.getPrices({ owner: this.username, created__gte: this.challenge.start_date, created__lte: this.challenge.end_date }) + api.getPrices({ owner: this.username, created__gte: this.challenge.startDate, created__lte: this.challenge.endDate }) .then((data) => { - this.challenge.user_contributions = data.total + this.challenge.userContributions = data.total }) } } - } } From 3433cfcfa292814daa0b03a7ac3385fe5e70a46f Mon Sep 17 00:00:00 2001 From: TTalex Date: Sat, 11 Jan 2025 16:02:38 +0100 Subject: [PATCH 4/8] Update for nutella challenge --- src/components/ChallengeExample.vue | 46 ----- src/components/ChallengeTakePicturesCard.vue | 42 +++++ src/components/ChallengeTopContributors.vue | 5 +- src/components/ChallengeValidateCard.vue | 37 ++++ src/components/PriceChart.vue | 6 +- src/i18n/locales/en.json | 33 ++++ src/router.js | 2 +- src/services/api.js | 18 ++ src/views/CurrentChallenge.vue | 170 +++++++++++-------- src/views/Experiments.vue | 7 + 10 files changed, 242 insertions(+), 124 deletions(-) delete mode 100644 src/components/ChallengeExample.vue create mode 100644 src/components/ChallengeTakePicturesCard.vue create mode 100644 src/components/ChallengeValidateCard.vue diff --git a/src/components/ChallengeExample.vue b/src/components/ChallengeExample.vue deleted file mode 100644 index c2af967a198..00000000000 --- a/src/components/ChallengeExample.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/components/ChallengeTakePicturesCard.vue b/src/components/ChallengeTakePicturesCard.vue new file mode 100644 index 00000000000..035331f4ce4 --- /dev/null +++ b/src/components/ChallengeTakePicturesCard.vue @@ -0,0 +1,42 @@ + + + + \ No newline at end of file diff --git a/src/components/ChallengeTopContributors.vue b/src/components/ChallengeTopContributors.vue index 83d566821f1..61e5343c158 100644 --- a/src/components/ChallengeTopContributors.vue +++ b/src/components/ChallengeTopContributors.vue @@ -1,12 +1,13 @@