diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index c9b81be4..db390542 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -30,6 +30,9 @@ "start": "Start", "end": "End", "comment": "Comment", + "trophies": { + "trophies": "Trophies" + }, "licenses": { "authors": "Author(s)", "authorProfile": "Link to author website or profile, if available", diff --git a/src/components/Dashboard/ConfigurableDashboard.tsx b/src/components/Dashboard/ConfigurableDashboard.tsx index 95ac26b1..9c074962 100644 --- a/src/components/Dashboard/ConfigurableDashboard.tsx +++ b/src/components/Dashboard/ConfigurableDashboard.tsx @@ -7,6 +7,7 @@ import { CalendarCard } from "components/Dashboard/CalendarCard"; import { MeasurementCard } from "components/Dashboard/MeasurementCard"; import { NutritionCard } from "components/Dashboard/NutritionCard"; import { RoutineCard } from "components/Dashboard/RoutineCard"; +import { TrophiesCard } from "components/Dashboard/TrophiesCard"; import { WeightCard } from "components/Dashboard/WeightCard"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Layout, Layouts, Responsive, WidthProvider } from "react-grid-layout"; @@ -30,7 +31,7 @@ type DashboardState = { const BREAKPOINTS = ['lg', 'md', 'sm', 'xs'] as const; // Define widget types for extensibility -export type WidgetType = "routine" | "nutrition" | "weight" | "calendar" | "measurement"; +export type WidgetType = "routine" | "nutrition" | "weight" | "calendar" | "measurement" | "trophies"; export interface WidgetConfig { id: string; @@ -84,6 +85,13 @@ export const AVAILABLE_WIDGETS: WidgetConfig[] = [ translationKey: 'measurements.measurements', defaultLayout: { w: 4, h: 4, x: 8, y: 1, minW: 3, minH: 2 }, }, + { + id: "trophies", + type: "trophies", + component: TrophiesCard, + translationKey: 'trophies.trophies', + defaultLayout: { w: 12, h: 2, x: 0, y: 2, minW: 3, minH: 2 }, + }, ]; /* diff --git a/src/components/Dashboard/DashboardCard.tsx b/src/components/Dashboard/DashboardCard.tsx index b7d428d5..9a5fca11 100644 --- a/src/components/Dashboard/DashboardCard.tsx +++ b/src/components/Dashboard/DashboardCard.tsx @@ -74,19 +74,18 @@ export interface DashboardCardProps { */ export const DashboardCard: React.FC = ( { - title, - subheader, - children, - actions, - headerAction, - contentHeight, - scrollable = true, - cardSx = {}, - contentSx = {}, - }) => { + title, + subheader, + children, + actions, + headerAction, + contentHeight, + scrollable = true, + cardSx = {}, + contentSx = {}, +}) => { return ( = ( ...cardSx, }} > - + {title !== '' && } { + const { t } = useTranslation(); + const trophiesQuery = useUserTrophiesQuery(); + + if (trophiesQuery.isLoading) { + return ; + } + + return trophiesQuery.data !== null ? ( + + ) : ( + + ); +}; + +function TrophiesCardContent(props: { trophies: UserTrophy[] }) { + const { t, i18n } = useTranslation(); + + const tooltipWidget = (tooltip: string) => + {tooltip} + ; + + return ( + + + } + > + + + {props.trophies.map((userTrophy) => ( + + + + + + + {userTrophy.trophy.name} + + + {/**/} + {/* {userTrophy.trophy.description}*/} + {/**/} + + + + ))} + + + ); +} diff --git a/src/components/Trophies/components/TrophiesDetail.test.tsx b/src/components/Trophies/components/TrophiesDetail.test.tsx new file mode 100644 index 00000000..a7cc3525 --- /dev/null +++ b/src/components/Trophies/components/TrophiesDetail.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; +import { testUserProgressionTrophies } from "tests/trophies/trophiesTestData"; +import { TrophiesDetail } from './TrophiesDetail'; + + +// Mock the trophies query hook +jest.mock('components/Trophies/queries/trophies', () => ({ + useUserTrophyProgressionQuery: () => ({ + isLoading: false, + data: testUserProgressionTrophies(), + }), +})); + +describe('TrophiesDetail', () => { + test('renders trophy names and progression values', () => { + + // Act + render(); + + // Assert + expect(screen.getByText('Beginner')).toBeInTheDocument(); + expect(screen.getByText('Unstoppable')).toBeInTheDocument(); + expect(screen.getByText('Complete your first workout')).toBeInTheDocument(); + expect(screen.getByText('Maintain a 30-day workout streak')).toBeInTheDocument(); + + // Progression value for the progressive trophy should be shown + expect(screen.getByText('4/30')).toBeInTheDocument(); + + // There should be at least one progressbar in the document + expect(screen.getAllByRole('progressbar').length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/src/components/Trophies/components/TrophiesDetail.tsx b/src/components/Trophies/components/TrophiesDetail.tsx new file mode 100644 index 00000000..9b8774c4 --- /dev/null +++ b/src/components/Trophies/components/TrophiesDetail.tsx @@ -0,0 +1,81 @@ +import { Card, CardContent, CardMedia, LinearProgress, LinearProgressProps, Typography } from "@mui/material"; +import Box from "@mui/system/Box"; +import Grid from "@mui/system/Grid"; +import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget"; +import { WgerContainerFullWidth } from "components/Core/Widgets/Container"; +import { UserTrophyProgression } from "components/Trophies/models/userTrophyProgression"; +import { useUserTrophyProgressionQuery } from "components/Trophies/queries/trophies"; +import React from "react"; +import { useTranslation } from "react-i18next"; + + +export const TrophiesDetail = () => { + const [t] = useTranslation(); + + const planQuery = useUserTrophyProgressionQuery(); + + if (planQuery.isLoading) { + return ; + } + + return + + {planQuery.data!.map((trophyProgression) => ( + + + + ))} + + ; +}; + + +function TrophyProgressCard(props: { trophyProgression: UserTrophyProgression }) { + return + + + + + {props.trophyProgression.trophy.name} + + + + {(props.trophyProgression.trophy.isProgressive && !props.trophyProgression.isEarned) && + + } + + + + {props.trophyProgression.trophy.description} + + + + ; +} + + +const LinearProgressWithLabel = (props: LinearProgressProps & { value: number, progressLabel?: string | null }) => { + // Extract custom prop so it doesn't get passed to the progress bar via the spread operator + const { progressLabel, ...linearProps } = props; + + return ( + + + + + + + {progressLabel ?? `${Math.round(props.value)}%`} + + + + ); +}; diff --git a/src/components/Trophies/models/trophy.test.ts b/src/components/Trophies/models/trophy.test.ts new file mode 100644 index 00000000..d426c344 --- /dev/null +++ b/src/components/Trophies/models/trophy.test.ts @@ -0,0 +1,33 @@ +import { Trophy } from "components/Trophies/models/trophy"; + + +describe('Test the trophy model', () => { + + test('correctly creates an object from the API response', () => { + // Arrange + const apiResponse = { + id: 1, + uuid: "5362e55b-eaf1-4e34-9ef8-661538a3bdd9", + name: "Beginner", + description: "Complete your first workout", + image: "http://localhost:8000/static/trophies/count/5362e55b-eaf1-4e34-9ef8-661538a3bdd9.png", + "trophy_type": "count", + "is_hidden": false, + "is_progressive": false, + order: 1 + }; + + // Act + const trophy = Trophy.fromJson(apiResponse); + + // Assert + expect(trophy.id).toBe(1); + expect(trophy.uuid).toBe("5362e55b-eaf1-4e34-9ef8-661538a3bdd9"); + expect(trophy.name).toBe("Beginner"); + expect(trophy.description).toBe("Complete your first workout"); + expect(trophy.image).toBe("http://localhost:8000/static/trophies/count/5362e55b-eaf1-4e34-9ef8-661538a3bdd9.png"); + expect(trophy.type).toBe("count"); + expect(trophy.isHidden).toBe(false); + expect(trophy.isProgressive).toBe(false); + }); +}); diff --git a/src/components/Trophies/models/trophy.ts b/src/components/Trophies/models/trophy.ts new file mode 100644 index 00000000..67dd6ccd --- /dev/null +++ b/src/components/Trophies/models/trophy.ts @@ -0,0 +1,75 @@ +import { Adapter } from "utils/Adapter"; + +type trophyType = 'time' | 'volume' | 'count' | 'sequence' | 'date' | 'pr' | 'other'; + +export interface ApiTrophyType { + id: number, + uuid: string, + name: string, + description: string, + image: string, + "trophy_type": trophyType, + "is_hidden": boolean, + "is_progressive": boolean, +} + +export type TrophyConstructorParams = { + id: number; + uuid: string; + name: string; + description: string; + image: string; + type: trophyType; + isHidden: boolean; + isProgressive: boolean; +}; + +export class Trophy { + + public id: number; + public uuid: string; + public name: string; + public description: string; + public image: string; + public type: string; + public isHidden: boolean; + public isProgressive: boolean; + + + constructor(params: TrophyConstructorParams) { + this.id = params.id; + this.uuid = params.uuid; + this.name = params.name; + this.description = params.description; + this.image = params.image; + this.type = params.type; + this.isHidden = params.isHidden; + this.isProgressive = params.isProgressive; + } + + static fromJson(json: ApiTrophyType): Trophy { + return adapter.fromJson(json); + } +} + +class TrophyAdapter implements Adapter { + fromJson(item: ApiTrophyType) { + return new Trophy({ + id: item.id, + uuid: item.uuid, + name: item.name, + description: item.description, + image: item.image, + type: item.trophy_type, + isHidden: item.is_hidden, + isProgressive: item.is_progressive, + }); + } + + toJson(_: Trophy) { + return {}; + } +} + + +const adapter = new TrophyAdapter(); \ No newline at end of file diff --git a/src/components/Trophies/models/userTrophy.test.ts b/src/components/Trophies/models/userTrophy.test.ts new file mode 100644 index 00000000..0939a81f --- /dev/null +++ b/src/components/Trophies/models/userTrophy.test.ts @@ -0,0 +1,38 @@ +import { UserTrophy } from "components/Trophies/models/userTrophy"; + + +describe('Test the user UserTrophy model', () => { + + test('correctly creates an object from the API response', () => { + // Arrange + const apiResponse = { + id: 4, + trophy: { + id: 9, + uuid: "32bb12da-b25f-4e18-81e4-b695eb65283e", + name: "Phoenix", + description: "Return to training after being inactive for 30 days", + image: "http://localhost:8000/static/trophies/other/32bb12da-b25f-4e18-81e4-b695eb65283e.png", + "trophy_type": "other", + "is_hidden": true, + "is_progressive": false, + order: 9 + }, + "earned_at": "2025-12-19T13:48:07.519497+01:00", + progress: 100.0, + "is_notified": false + }; + + // Act + const userTrophy = UserTrophy.fromJson(apiResponse); + + // Assert + expect(userTrophy.id).toBe(4); + expect(userTrophy.earnedAt).toStrictEqual(new Date("2025-12-19T13:48:07.519497+01:00")); + expect(userTrophy.progress).toBe(100.0); + expect(userTrophy.isNotified).toBe(false); + + expect(userTrophy.trophy.uuid).toBe("32bb12da-b25f-4e18-81e4-b695eb65283e"); + + }); +}); diff --git a/src/components/Trophies/models/userTrophy.ts b/src/components/Trophies/models/userTrophy.ts new file mode 100644 index 00000000..4e617ab0 --- /dev/null +++ b/src/components/Trophies/models/userTrophy.ts @@ -0,0 +1,67 @@ +import { ApiTrophyType, Trophy } from "components/Trophies/models/trophy"; +import { Adapter } from "utils/Adapter"; + +export interface ApiUserTrophyType { + id: number, + trophy: ApiTrophyType, + earned_at: string, + progress: number, + "is_notified": boolean, +} + +export type UserTrophyConstructorParams = { + id: number; + trophy: Trophy; + earnedAt: Date; + progress: number; + isNotified: boolean; +}; + +/* + * A list of trophies earned by a user, along with their progress. + */ +export class UserTrophy { + + public id: number; + public trophy: Trophy; + public earnedAt: Date; + public progress: number; + public isNotified: boolean; + + constructor(params: UserTrophyConstructorParams) { + this.id = params.id; + this.trophy = params.trophy; + this.earnedAt = params.earnedAt; + this.progress = params.progress; + this.isNotified = params.isNotified; + } + + static fromJson(json: ApiUserTrophyType): UserTrophy { + return adapter.fromJson(json); + } + + toJson() { + return adapter.toJson(this); + } +} + +class UserTrophyAdapter implements Adapter { + fromJson(item: ApiUserTrophyType) { + return new UserTrophy({ + id: item.id, + trophy: Trophy.fromJson(item.trophy), + earnedAt: new Date(item.earned_at), + progress: item.progress, + isNotified: item.is_notified, + }); + } + + toJson(trophy: UserTrophy) { + return { + id: trophy.id, + "is_notified": trophy.isNotified, + }; + } +} + +const adapter = new UserTrophyAdapter(); \ No newline at end of file diff --git a/src/components/Trophies/models/userTrophyProgression.test.ts b/src/components/Trophies/models/userTrophyProgression.test.ts new file mode 100644 index 00000000..2b3600a0 --- /dev/null +++ b/src/components/Trophies/models/userTrophyProgression.test.ts @@ -0,0 +1,42 @@ +import { UserTrophyProgression } from "components/Trophies/models/userTrophyProgression"; + + +describe('Test the UserTrophyProgression model', () => { + + test('correctly creates an object from the API response', () => { + // Arrange + const apiResponse = { + trophy: { + id: 1, + uuid: "5362e55b-eaf1-4e34-9ef8-661538a3bdd9", + name: "Beginner", + description: "Complete your first workout", + image: "http://localhost:8000/static/trophies/count/5362e55b-eaf1-4e34-9ef8-661538a3bdd9.png", + "trophy_type": "count", + "is_hidden": false, + "is_progressive": false, + order: 1 + }, + "is_earned": true, + "earned_at": "2025-12-19T13:48:07.513138+01:00", + progress: 100.0, + "current_value": null, + "target_value": null, + "progress_display": null + }; + + // Act + const userTrophyProgression = UserTrophyProgression.fromJson(apiResponse); + + // Assert + expect(userTrophyProgression.isEarned).toBe(true); + expect(userTrophyProgression.earnedAt).toStrictEqual(new Date("2025-12-19T13:48:07.513138+01:00")); + expect(userTrophyProgression.progress).toBe(100.0); + expect(userTrophyProgression.currentValue).toBe(null); + expect(userTrophyProgression.targetValue).toBe(null); + expect(userTrophyProgression.progressDisplay).toBe(null); + + expect(userTrophyProgression.trophy.uuid).toBe("5362e55b-eaf1-4e34-9ef8-661538a3bdd9"); + + }); +}); diff --git a/src/components/Trophies/models/userTrophyProgression.ts b/src/components/Trophies/models/userTrophyProgression.ts new file mode 100644 index 00000000..4461708a --- /dev/null +++ b/src/components/Trophies/models/userTrophyProgression.ts @@ -0,0 +1,68 @@ +import { ApiTrophyType, Trophy } from "components/Trophies/models/trophy"; +import { Adapter } from "utils/Adapter"; + +export interface ApiUserTrophyType { + trophy: ApiTrophyType, + is_earned: boolean + earned_at: string | null, + progress: number, + current_value: number | null, + target_value: number | null, + progress_display: string | null, +} + +export type UserTrophyConstructorParams = { + trophy: Trophy; + isEarned: boolean; + earnedAt: Date | null; + progress: number; + currentValue: number | null; + targetValue: number | null; + progressDisplay: string | null; +}; + +export class UserTrophyProgression { + + public trophy: Trophy; + public earnedAt: Date | null; + public isEarned: boolean; + public progress: number; + public currentValue: number | null; + public targetValue: number | null; + public progressDisplay: string | null; + + constructor(params: UserTrophyConstructorParams) { + this.trophy = params.trophy; + this.earnedAt = params.earnedAt; + this.isEarned = params.isEarned; + this.progress = params.progress; + this.currentValue = params.currentValue; + this.targetValue = params.targetValue; + this.progressDisplay = params.progressDisplay; + } + + static fromJson(json: ApiUserTrophyType): UserTrophyProgression { + return adapter.fromJson(json); + } +} + +class UserTrophyAdapter implements Adapter { + fromJson(item: ApiUserTrophyType) { + return new UserTrophyProgression({ + trophy: Trophy.fromJson(item.trophy), + earnedAt: item.earned_at !== null ? new Date(item.earned_at) : null, + isEarned: item.is_earned, + progress: item.progress, + currentValue: item.current_value, + targetValue: item.target_value, + progressDisplay: item.progress_display, + }); + } + + toJson(item: UserTrophyProgression) { + return {}; + } +} + + +const adapter = new UserTrophyAdapter(); \ No newline at end of file diff --git a/src/components/Trophies/queries/trophies.ts b/src/components/Trophies/queries/trophies.ts new file mode 100644 index 00000000..21fedcc2 --- /dev/null +++ b/src/components/Trophies/queries/trophies.ts @@ -0,0 +1,31 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { getTrophies } from "components/Trophies/services/trophies"; +import { getUserTrophies, setTrophyAsNotified } from "components/Trophies/services/userTrophies"; +import { getUserTrophyProgression } from "components/Trophies/services/userTrophyProgression"; +import { QueryKey } from "utils/consts"; + +export const useTrophiesQuery = () => useQuery({ + queryFn: () => getTrophies(), + queryKey: [QueryKey.TROPHIES], +}); + +export const useUserTrophiesQuery = () => useQuery({ + queryFn: () => getUserTrophies(), + queryKey: [QueryKey.USER_TROPHIES], +}); + +export const useSetTrophyAsNotifiedQuery = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (trophyId: number) => setTrophyAsNotified(trophyId), + onSuccess: () => queryClient.invalidateQueries({ + queryKey: [QueryKey.USER_TROPHIES] + }) + }); +}; + +export const useUserTrophyProgressionQuery = () => useQuery({ + queryFn: () => getUserTrophyProgression(), + queryKey: [QueryKey.USER_TROPHY_PROGRESSION], +}); diff --git a/src/components/Trophies/services/trophies.ts b/src/components/Trophies/services/trophies.ts new file mode 100644 index 00000000..dcd09f36 --- /dev/null +++ b/src/components/Trophies/services/trophies.ts @@ -0,0 +1,22 @@ +import { Trophy } from "components/Trophies/models/trophy"; +import { API_MAX_PAGE_SIZE, ApiPath } from "utils/consts"; +import { fetchPaginated } from "utils/requests"; +import { makeHeader, makeUrl } from "utils/url"; + + +export const getTrophies = async (): Promise => { + + const url = makeUrl( + ApiPath.API_TROPHIES_PATH, + { query: { limit: API_MAX_PAGE_SIZE } } + ); + const out: Trophy[] = []; + + for await (const page of fetchPaginated(url, makeHeader())) { + for (const logData of page) { + out.push(Trophy.fromJson(logData)); + } + } + return out; +}; + diff --git a/src/components/Trophies/services/userTrophies.ts b/src/components/Trophies/services/userTrophies.ts new file mode 100644 index 00000000..418d9fcc --- /dev/null +++ b/src/components/Trophies/services/userTrophies.ts @@ -0,0 +1,33 @@ +import axios from "axios"; +import { UserTrophy } from "components/Trophies/models/userTrophy"; +import { API_MAX_PAGE_SIZE, ApiPath } from "utils/consts"; +import { fetchPaginated } from "utils/requests"; +import { makeHeader, makeUrl } from "utils/url"; + + +export const getUserTrophies = async (): Promise => { + + const url = makeUrl( + ApiPath.API_USER_TROPHIES_PATH, + { query: { limit: API_MAX_PAGE_SIZE } } + ); + const out: UserTrophy[] = []; + + for await (const page of fetchPaginated(url, makeHeader())) { + for (const logData of page) { + out.push(UserTrophy.fromJson(logData)); + } + } + return out; +}; + + +export const setTrophyAsNotified = async (trophyId: number): Promise => { + + await axios.post( + makeUrl(ApiPath.API_USER_TROPHIES_PATH, { id: trophyId }), + { id: trophyId, "is_notified": true }, + { headers: makeHeader() } + ); + +}; diff --git a/src/components/Trophies/services/userTrophyProgression.ts b/src/components/Trophies/services/userTrophyProgression.ts new file mode 100644 index 00000000..5f9cef56 --- /dev/null +++ b/src/components/Trophies/services/userTrophyProgression.ts @@ -0,0 +1,16 @@ +import axios from "axios"; +import { ApiUserTrophyType, UserTrophyProgression } from "components/Trophies/models/userTrophyProgression"; +import { ApiPath } from "utils/consts"; +import { makeHeader, makeUrl } from "utils/url"; + + +export const getUserTrophyProgression = async (): Promise => { + + const { data: trophyData } = await axios.get( + makeUrl(ApiPath.API_USER_TROPHY_PROGRESSION_PATH), + { headers: makeHeader() } + ); + + return trophyData.map((item: ApiUserTrophyType) => UserTrophyProgression.fromJson(item)); +}; + diff --git a/src/routes.tsx b/src/routes.tsx index 45af23dd..60f649ea 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -6,6 +6,7 @@ import { BmiCalculator } from "components/Nutrition/components/BmiCalculator"; import { NutritionDiaryOverview } from "components/Nutrition/components/NutritionDiaryOverview"; import { PlanDetail } from "components/Nutrition/components/PlanDetail"; import { PlansOverview } from "components/Nutrition/components/PlansOverview"; +import { TrophiesDetail } from "components/Trophies/components/TrophiesDetail"; import { RoutineAdd } from "components/WorkoutRoutines/Detail/RoutineAdd"; import { RoutineDetail } from "components/WorkoutRoutines/Detail/RoutineDetail"; import { RoutineDetailsTable } from "components/WorkoutRoutines/Detail/RoutineDetailsTable"; @@ -123,10 +124,14 @@ export const WgerRoutes = () => { } /> } /> + + } /> + } /> + } /> - {/* This route matches when no other route match, so a 404 */} + {/* This route matches when no other route matches, so a 404 */} { + return [ + new Trophy({ + id: 123, + type: 'other', + isHidden: false, + uuid: 'trophy-123', + name: 'Beginner', + description: 'Complete your first workout', + image: 'https://example.com/images/beginner.png', + isProgressive: false, + }), + new Trophy({ + id: 456, + type: 'count', + isHidden: false, + uuid: 'trophy-456', + name: 'Unstoppable', + description: 'Maintain a 30-day workout streak', + image: 'https://example.com/images/unstoppaböe.png', + isProgressive: true, + }), + new Trophy({ + id: 789, + type: 'other', + isHidden: true, + uuid: 'trophy-789', + name: 'Secret Trophy', + description: 'This is a super secret trophy', + image: 'https://example.com/images/secret.png', + isProgressive: false, + }) + ]; +}; + +export const testUserProgressionTrophies = () => { + return [ + new UserTrophyProgression({ + trophy: testTrophies()[0], + isEarned: true, + earnedAt: new Date('2025-12-19T10:00:00Z'), + progress: 100, + currentValue: null, + targetValue: null, + progressDisplay: null, + }), + new UserTrophyProgression({ + trophy: testTrophies()[1], + isEarned: false, + earnedAt: new Date('2025-12-19T10:00:00Z'), + progress: 13.333333333333334, + currentValue: 4, + targetValue: 30, + progressDisplay: '4/30', + }), + ]; +}; \ No newline at end of file diff --git a/src/utils/consts.ts b/src/utils/consts.ts index 8519eae6..9f310d22 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -35,6 +35,8 @@ export const QUERY_MEASUREMENTS_CATEGORIES = 'measurements-categories'; * These don't have any meaning, they just need to be globally unique */ export enum QueryKey { + + // Routines ROUTINE_OVERVIEW = 'routine-overview', ROUTINE_DETAIL = 'routine-detail', SESSION_SEARCH = 'session-search', @@ -46,20 +48,23 @@ export enum QueryKey { ROUTINES_SHALLOW = 'routines-shallow', PRIVATE_TEMPLATES = 'private-templates', PUBLIC_TEMPLATES = 'public-templates', + ROUTINE_WEIGHT_UNITS = 'weight-units', + ROUTINE_REP_UNITS = 'rep-units', + // Nutrition NUTRITIONAL_PLANS = 'nutritional-plans', NUTRITIONAL_PLAN = 'nutritional-plan', NUTRITIONAL_PLAN_DIARY = 'nutritional-plan-diary', NUTRITIONAL_PLAN_LAST = 'nutritional-plan-last', INGREDIENT = 'ingredient', + // Body weight BODY_WEIGHT = 'body-weight', - ROUTINE_WEIGHT_UNITS = 'weight-units', - ROUTINE_REP_UNITS = 'rep-units', - + // Profile QUERY_PROFILE = 'profile', + // Exercises EXERCISES = 'exercises', EXERCISE_VARIATIONS = 'variations', EXERCISE_DETAIL = 'detail', @@ -68,6 +73,11 @@ export enum QueryKey { EQUIPMENT = 'equipment', MUSCLES = 'muscles', QUERY_NOTES = 'notes', + + // Trophies + TROPHIES = 'trophies', + USER_TROPHIES = 'user-trophies', + USER_TROPHY_PROGRESSION = 'user-trophy-progression', } /* @@ -106,6 +116,11 @@ export enum ApiPath { // Profile API_PROFILE_PATH = 'userprofile', + + // Trophies + API_TROPHIES_PATH = 'trophy', + API_USER_TROPHIES_PATH = 'user-trophy', + API_USER_TROPHY_PROGRESSION_PATH = 'trophy/progress', } diff --git a/src/utils/url.ts b/src/utils/url.ts index 8da88eed..1a927cbe 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -50,8 +50,10 @@ export function makeUrl(path: string, params?: makeUrlInterface) { export enum WgerLink { + // Dashboard DASHBOARD, + // Routines ROUTINE_OVERVIEW, ROUTINE_DETAIL, ROUTINE_EDIT, @@ -70,16 +72,20 @@ export enum WgerLink { PRIVATE_TEMPLATE_OVERVIEW, PUBLIC_TEMPLATE_OVERVIEW, + // Exercises EXERCISE_DETAIL, EXERCISE_OVERVIEW, EXERCISE_CONTRIBUTE, + // Body weight WEIGHT_OVERVIEW, WEIGHT_ADD, + // Measurements MEASUREMENT_OVERVIEW, MEASUREMENT_DETAIL, + // Nutrition NUTRITION_OVERVIEW, NUTRITION_DETAIL, NUTRITION_PLAN_PDF, @@ -88,7 +94,9 @@ export enum WgerLink { INGREDIENT_DETAIL, - CALENDAR + // Other + CALENDAR, + TROPHIES, } type UrlParams = { id: number, id2?: number, slug?: string, date?: string }; @@ -182,6 +190,9 @@ export function makeLink(link: WgerLink, language?: string, params?: UrlParams): case WgerLink.INGREDIENT_DETAIL: return `/${language}/nutrition/ingredient/${params!.id}/view`; + case WgerLink.TROPHIES: + return `/${language}/trophies`; + // Dashboard case WgerLink.DASHBOARD: default: