Skip to content

Commit

Permalink
Improved documentation and website (#1082)
Browse files Browse the repository at this point in the history
* build(website): add required deps

* fix(website): do not expect intent for action

* docs: remove information about config builder

* docs: remove information about file loading

* docs: change name of html file

* ci(docs): change default route of dev server

* feat(docs): add server admin access token example

* docs: remove information about keys

* docs: remove info about configuration keys

* docs: make push integrations clearer

* docs: change order of integrations

* docs: improve komga section

* docs: add more info about local

* docs: change heading

* feat(website): remove un-needed column

* chore(frontend): remove showing notifications

* chore(website): add basic features page

* feat(backend): collapse two queries into one

* chore(frontend): adapt to new gql schema

* fix(website): send requests to correct action

* feat(website): generate numeric otp

* fix(website): use correct validator for otp input

* feat(website): add warning about unkey key id

* chore(website): no useless handling

* feat(website): prefill paddle checkout with email

* chore(website): order of props

* chore(website): close checkout

* refactor(website): extract common route value

* refactor(website): change exported location of variable

* feat(website): close checkout overlay when it is completed

* feat(website): add new endpoint for trigger

* feat(website): start endpoint for trigger to invalidate users with expired subscriptions

* feat(website): more trigger stuff

* feat(website): add more processing stuff

* feat(website): start adding new paddle webhook

* build(website): add remix pwa deps

* feat(website): handle webhook correctly

* Revert "build(website): add remix pwa deps"

This reverts commit 7417297.

* feat(website): use inbuilt nodejs logger

* feat(website): return early if customer not found

* feat(website): complete entire paddle flow through webhooks

* fix(website): add guard to prevent multiple initializations

* refactor(website): always fetch customer with request

* fix(website): handle all edge cases for cookie

* chore(website): display event received

* feat(website): perform new transactions using `customData`

* feat(website): display loading after purchase

* feat(website): handle subscription paused and cancelled webhooks

* feat(website): allow cancelled customers to create new subscriptions

* fix(website): update transactions correctly

* refactor(website): destructure stuff

* feat(website): handle subscription resumed event

* feat(website): update unkey key meta and expiry

* feat(website): handle past due subscription events too

* chore(website): associate customer id with unkey keys

* feat(website): allow regenerating unkey ids

* refactor(ts-utils): accept multiple types

* refactor(ts-utils): change names

* refactor(backend): remove useless extra function

* chore(utils): remove useless unwrap

* refactor(utils): create references for vars

* feat(frontend): allow adding or removing rest timer

* fix(website): change order of attributes

* refactor(website): remove useless var

* chore(website): add new plan type

* chore(website): regenerate sql

* refactor(frontend): change name of var

* fix(backend): change order of log statements

* fix(frontend): add tooltip on why it is disabled

* chore(website): remove showcase section and associated images

* chore(website): also remove the `renewOn` column from customer

* Revert "chore(website): also remove the `renewOn` column from customer"

This reverts commit 5ba5fd7.

* chore(website): use finally

* feat(website): display logo in features page

* feat(frontend): always show season overview

* feat(website): change colors of features section

* chore(website): change tags used

* docs: small changes

* docs: add restore command

* feat(website): add link to features

* chore(website): remove useless import

* feat(website): add titles to all pages

* feat(website): add features for media tracking

* feat(website): add features for fitness tracking

* feat(website): add goodies features section

* fix(website): add exhaustive matching

* refactor(website): accept all details for creation

* feat(website): add another pro feature

* feat(website/features): make page more presentable

* feat(website): add basic images support to features page

* refactor(ts-utils,website): move utility around

* refactor(ts-utils,frontend): use utility from common package

* fix(frontend): invalidate correct query key

* feat(website): adjust for mobile views

* chore(website): remove useless page

* chore(website): better contrast for purchase btns

* feat(website): add themes feature

* feat(frontend): add field to store if workout is complete

* feat(frontend): get rid of active workout cookie

* feat(website): add images for fitness tracking section

* feat(website): handle cases when no images configured

* fix(frontend): allow adding exercises to templates
  • Loading branch information
IgnisDa authored Nov 1, 2024
1 parent c399b0d commit 5520313
Show file tree
Hide file tree
Showing 73 changed files with 1,280 additions and 785 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ You can also open an issue on GitHub if you find any bugs or have feature reques
and fitness
- ✅ Import data from Goodreads, Trakt, Strong App [etc](https://docs.ryot.io/importing.html)
- ✅ Integration with Jellyfin, Kodi, Plex, Emby, Audiobookshelf [etc](https://docs.ryot.io/integrations.html)
-[Supports](https://docs.ryot.io/guides/openid.html) OpenID Connect
-[Supports](https://docs.ryot.io/guides/authentication.html) OpenID Connect
- ✅ Sends notifications to Discord, Ntfy, Apprise etc
- ✅ Self-hosted
- ✅ PWA enabled
Expand Down
5 changes: 2 additions & 3 deletions apps/frontend/app/lib/generals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,11 @@ export const emptyDecimalString = z
.transform((v) => (!v ? undefined : Number.parseFloat(v).toString()))
.nullable();

export const CurrentWorkoutKey = "CurrentWorkout";
export const LOGO_IMAGE_URL =
"https://raw.githubusercontent.com/IgnisDa/ryot/main/libs/assets/icon-512x512.png";
export const redirectToQueryParam = "redirectTo";
export const pageQueryParam = "page";
export const AUTH_COOKIE_NAME = "Auth";
export const FRONTEND_AUTH_COOKIE_NAME = "Auth";
export const toastKey = "Toast";
export const PRO_REQUIRED_MESSAGE = "Ryot pro is required to use this feature";

Expand Down Expand Up @@ -316,7 +315,7 @@ export const clientGqlService = new GraphQLClient(
`${applicationBaseUrl}/backend/graphql`,
{
headers: () => {
const data = Cookies.get(AUTH_COOKIE_NAME);
const data = Cookies.get(FRONTEND_AUTH_COOKIE_NAME);
return { authorization: data ? `Bearer ${data}` : "" };
},
},
Expand Down
17 changes: 11 additions & 6 deletions apps/frontend/app/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import { $path } from "remix-routes";
import invariant from "tiny-invariant";
import { useInterval } from "usehooks-ts";
import {
CurrentWorkoutKey,
type FitnessAction,
FitnessAction,
dayjsLib,
getMetadataDetailsQuery,
getStringAsciiValue,
Expand Down Expand Up @@ -104,10 +103,6 @@ export const useGetWorkoutStarter = () => {

const fn = (wkt: InProgressWorkout, action: FitnessAction) => {
setCurrentWorkout(wkt);
Cookies.set(CurrentWorkoutKey, action, {
expires: 2,
sameSite: "Strict",
});
navigate($path("/fitness/:action", { action }));
revalidator.revalidate();
};
Expand Down Expand Up @@ -208,3 +203,13 @@ export const useComplexJsonUpdate = () => {

return { reset, appendPref, toUpdatePreferences };
};

export const useIsFitnessActionActive = () => {
const [currentWorkout] = useCurrentWorkout();
const action = currentWorkout?.currentActionOrCompleted;
return (
action !== undefined &&
action !== true &&
[FitnessAction.LogWorkout, FitnessAction.CreateTemplate].includes(action)
);
};
21 changes: 13 additions & 8 deletions apps/frontend/app/lib/state/fitness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ import { queryOptions } from "@tanstack/react-query";
import { createDraft, finishDraft } from "immer";
import { atom, useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import Cookies from "js-cookie";
import { $path } from "remix-routes";
import { match } from "ts-pattern";
import { withFragment } from "ufo";
import { v4 as randomUUID } from "uuid";
import {
CurrentWorkoutKey,
type FitnessAction,
clientGqlService,
dayjsLib,
getTimeOfDay,
Expand Down Expand Up @@ -80,12 +79,13 @@ export type InProgressWorkout = {
exercises: Array<Exercise>;
replacingExerciseIdx?: number;
updateWorkoutTemplateId?: string;
currentActionOrCompleted: FitnessAction | true;
};

type CurrentWorkout = InProgressWorkout | null;

const currentWorkoutAtom = atomWithStorage<CurrentWorkout>(
CurrentWorkoutKey,
"CurrentWorkout",
null,
);

Expand All @@ -101,13 +101,16 @@ export const useGetSetAtIndex = (exerciseIdx: number, setIdx: number) => {
return exercise?.sets[setIdx];
};

export const getDefaultWorkout = (): InProgressWorkout => {
export const getDefaultWorkout = (
fitnessEntity: FitnessAction,
): InProgressWorkout => {
const date = new Date();
return {
images: [],
videos: [],
supersets: [],
exercises: [],
currentActionOrCompleted: fitnessEntity,
startTime: date.toISOString(),
name: `${getTimeOfDay(date.getHours())} Workout`,
};
Expand Down Expand Up @@ -259,6 +262,7 @@ export const useMeasurementsDrawerOpen = () =>

export const duplicateOldWorkout = async (
name: string,
fitnessEntity: FitnessAction,
workoutInformation: WorkoutInformation,
coreDetails: ReturnType<typeof useCoreDetails>,
userFitnessPreferences: UserFitnessPreferences,
Expand All @@ -269,7 +273,7 @@ export const duplicateOldWorkout = async (
updateWorkoutTemplateId?: string;
},
) => {
const inProgress = getDefaultWorkout();
const inProgress = getDefaultWorkout(fitnessEntity);
inProgress.name = name;
inProgress.repeatedFrom = params.repeatedFromId;
inProgress.templateId = params.templateId;
Expand Down Expand Up @@ -345,6 +349,7 @@ export const addExerciseToWorkout = async (
setCurrentWorkout: (v: InProgressWorkout) => void,
selectedExercises: Array<{ name: string; lot: ExerciseLot }>,
) => {
if (currentWorkout.currentActionOrCompleted === true) return;
const draft = createDraft(currentWorkout);
const idxOfNextExercise = draft.exercises.length;
for (const [_exerciseIdx, ex] of selectedExercises.entries()) {
Expand Down Expand Up @@ -390,11 +395,11 @@ export const addExerciseToWorkout = async (
}
const finishedDraft = finishDraft(draft);
setCurrentWorkout(finishedDraft);
const currentEntity = Cookies.get(CurrentWorkoutKey);
if (!currentEntity) return;
navigate(
withFragment(
$path("/fitness/:action", { action: currentEntity }),
$path("/fitness/:action", {
action: currentWorkout.currentActionOrCompleted,
}),
idxOfNextExercise.toString(),
),
);
Expand Down
39 changes: 17 additions & 22 deletions apps/frontend/app/lib/utilities.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ import {
type RequestDocument,
type Variables,
} from "graphql-request";
import { jwtDecode } from "jwt-decode";
import type { VariablesAndRequestHeadersArgs } from "node_modules/graphql-request/build/legacy/helpers/types";
import { $path } from "remix-routes";
import { match } from "ts-pattern";
import { withoutHost } from "ufo";
import { v4 as randomUUID } from "uuid";
import { z } from "zod";
import {
AUTH_COOKIE_NAME,
CurrentWorkoutKey,
FitnessAction,
FRONTEND_AUTH_COOKIE_NAME,
dayjsLib,
emptyDecimalString,
emptyNumberString,
Expand Down Expand Up @@ -103,7 +102,7 @@ export const getCookieValue = (request: Request, cookieName: string) =>
parse(request.headers.get("cookie") || "")[cookieName];

export const getAuthorizationCookie = (request: Request) =>
getCookieValue(request, AUTH_COOKIE_NAME);
getCookieValue(request, FRONTEND_AUTH_COOKIE_NAME);

export const redirectIfNotAuthenticatedOrUpdated = async (request: Request) => {
const userDetails = await getCachedUserDetails(request);
Expand Down Expand Up @@ -156,6 +155,14 @@ export const MetadataSpecificsSchema = z.object({
mangaVolumeNumber: emptyNumberString,
});

export const getDecodedJwt = (request: Request) => {
const token = getAuthorizationCookie(request) ?? "";
return jwtDecode<{
sub: string;
access_link?: { id: string; is_demo?: boolean };
}>(token);
};

export const getCachedCoreDetails = async () => {
return await queryClient.ensureQueryData({
queryKey: queryFactory.miscellaneous.coreDetails().queryKey,
Expand All @@ -165,9 +172,9 @@ export const getCachedCoreDetails = async () => {
};

const getCachedUserDetails = async (request: Request) => {
const token = getAuthorizationCookie(request);
const decodedJwt = getDecodedJwt(request);
return await queryClient.ensureQueryData({
queryKey: queryFactory.users.details(token ?? "").queryKey,
queryKey: queryFactory.users.details(decodedJwt.sub).queryKey,
queryFn: () =>
serverGqlService
.authenticatedRequest(request, UserDetailsDocument, undefined)
Expand Down Expand Up @@ -349,13 +356,15 @@ export const getCookiesForApplication = async (
(tokenValidForDays || coreDetails.tokenValidForDays) * 24 * 60 * 60;
const options = { maxAge, path: "/" } satisfies SerializeOptions;
return combineHeaders({
"set-cookie": serialize(AUTH_COOKIE_NAME, token, options),
"set-cookie": serialize(FRONTEND_AUTH_COOKIE_NAME, token, options),
});
};

export const getLogoutCookies = () => {
return combineHeaders({
"set-cookie": serialize(AUTH_COOKIE_NAME, "", { expires: new Date(0) }),
"set-cookie": serialize(FRONTEND_AUTH_COOKIE_NAME, "", {
expires: new Date(0),
}),
});
};

Expand All @@ -367,20 +376,6 @@ export const extendResponseHeaders = (
responseHeaders.append(key, value);
};

export const getWorkoutCookieValue = (request: Request) => {
return parse(request.headers.get("cookie") || "")[CurrentWorkoutKey];
};

export const isWorkoutActive = (request: Request) => {
const cookieValue = getWorkoutCookieValue(request);
const inProgress =
cookieValue &&
[FitnessAction.LogWorkout, FitnessAction.UpdateWorkout]
.map(String)
.includes(cookieValue);
return inProgress;
};

export const getEnhancedCookieName = async (path: string, request: Request) => {
const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
return `SearchParams__${userDetails.id}__${path}`;
Expand Down
51 changes: 28 additions & 23 deletions apps/frontend/app/routes/_dashboard.fitness.$action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ import {
IconReorder,
IconReplace,
IconTrash,
IconZzz,
} from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
import { Howl } from "howler";
import { produce } from "immer";
import { RESET } from "jotai/utils";
import Cookies from "js-cookie";
import { useEffect, useMemo, useRef, useState } from "react";
import Webcam from "react-webcam";
import { $path } from "remix-routes";
Expand All @@ -109,7 +109,6 @@ import {
displayWeightWithUnit,
} from "~/components/fitness";
import {
CurrentWorkoutKey,
FitnessAction,
FitnessEntity,
PRO_REQUIRED_MESSAGE,
Expand Down Expand Up @@ -146,25 +145,11 @@ import {
useMeasurementsDrawerOpen,
useTimerAtom,
} from "~/lib/state/fitness";
import { isWorkoutActive, redirectWithToast } from "~/lib/utilities.server";

const workoutCookieName = CurrentWorkoutKey;

export const loader = async ({ params, request }: LoaderFunctionArgs) => {
export const loader = async ({ params }: LoaderFunctionArgs) => {
const { action } = zx.parseParams(params, {
action: z.nativeEnum(FitnessAction),
});
await match(action)
.with(FitnessAction.LogWorkout, FitnessAction.UpdateWorkout, async () => {
const inProgress = isWorkoutActive(request);
if (!inProgress)
throw await redirectWithToast($path("/"), {
type: "error",
message: "No workout in progress",
});
})
.with(FitnessAction.CreateTemplate, async () => {})
.exhaustive();
return {
action,
isUpdatingWorkout: action === FitnessAction.UpdateWorkout,
Expand Down Expand Up @@ -427,6 +412,11 @@ export default function Page() {
});
return;
}
setCurrentWorkout(
produce(currentWorkout, (draft) => {
draft.currentActionOrCompleted = true;
}),
);
const yes = await confirmWrapper({
confirmation: loaderData.isCreatingTemplate
? "Only sets that have data will added. Are you sure you want to save this template?"
Expand Down Expand Up @@ -472,11 +462,6 @@ export default function Page() {
]),
)
.exhaustive();
notifications.show({
color: "green",
message: "Saved successfully",
});
Cookies.remove(workoutCookieName);
revalidator.revalidate();
if (loaderData.action === FitnessAction.LogWorkout)
events.createWorkout();
Expand Down Expand Up @@ -515,7 +500,6 @@ export default function Page() {
}
navigate($path("/"), { replace: true });
revalidator.revalidate();
Cookies.remove(workoutCookieName);
setCurrentWorkout(RESET);
}
}}
Expand Down Expand Up @@ -1634,6 +1618,27 @@ const SetDisplay = (props: {
>
{!set.note ? "Add" : "Remove"} note
</Menu.Item>
<Menu.Item
fz="xs"
leftSection={<IconZzz size={14} />}
onClick={() => {
setCurrentWorkout(
produce(currentWorkout, (draft) => {
const hasRestTimer = !!set.restTimer;
if (hasRestTimer)
draft.exercises[props.exerciseIdx].sets[
props.setIdx
].restTimer = undefined;
else
draft.exercises[props.exerciseIdx].sets[
props.setIdx
].restTimer = { duration: 60 };
}),
);
}}
>
{!set.restTimer ? "Add" : "Remove"} timer
</Menu.Item>
<Menu.Item
color="red"
fz="xs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export default function Page() {
setIsWorkoutLoading(true);
const workout = await duplicateOldWorkout(
loaderData.entityName,
params.action,
loaderData.information,
coreDetails,
userPreferences.fitness,
Expand Down
18 changes: 8 additions & 10 deletions apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,14 @@ export default function Page() {
});
return;
}
startWorkout(
getDefaultWorkout(),
match(loaderData.entity)
.with(FitnessEntity.Workouts, () => FitnessAction.LogWorkout)
.with(
FitnessEntity.Templates,
() => FitnessAction.CreateTemplate,
)
.exhaustive(),
);
const action = match(loaderData.entity)
.with(FitnessEntity.Workouts, () => FitnessAction.LogWorkout)
.with(
FitnessEntity.Templates,
() => FitnessAction.CreateTemplate,
)
.exhaustive();
startWorkout(getDefaultWorkout(action), action);
}}
>
<IconPlus size={16} />
Expand Down
Loading

0 comments on commit 5520313

Please sign in to comment.