Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
298920c
id:27 feat(visible-prompt): wip
mrbalov Feb 17, 2026
80ccbfd
id:27 feat(visible-prompt): wip
mrbalov Feb 17, 2026
363c3dc
id:27 feat(visible-prompt): wip
mrbalov Feb 17, 2026
3090c54
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 17, 2026
6c35aca
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 19, 2026
45cdf88
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 19, 2026
11dae11
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 20, 2026
fbab534
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 20, 2026
c4ae292
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 20, 2026
7d0448e
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 20, 2026
19138b1
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 23, 2026
5fdd4ce
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 23, 2026
468908a
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 23, 2026
3cdb3fc
Merge branch 'main' of github.com:mrbalov/strava-activity-image-gener…
mrbalov Feb 23, 2026
cee7481
id:65 feat(tanstack-query): improvements
mrbalov Feb 24, 2026
f2d99c8
id:65 feat(tanstack-query): improvements
mrbalov Feb 24, 2026
49fc5e4
id:65 feat(tanstack-query): improvements
mrbalov Feb 24, 2026
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
98 changes: 98 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "torq",
"version": "5.3.0",
"version": "5.4.0",
"description": "Generates AI images based on Strava activity data.",
"type": "module",
"private": true,
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_BASE_URL=http://localhost:3001
NEXT_PUBLIC_STRAVA_CLIENT_ID=12345
NEXT_PUBLIC_STRAVA_CLIENT_SECRET=abcdef
NEXT_PUBLIC_WITH_IMAGE_GENERATION=true
27 changes: 0 additions & 27 deletions packages/ui/src/api/authStatus/fetchAuthStatus.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/ui/src/api/authStatus/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as fetchAuthStatus } from './fetchAuthStatus';
export { default as useFetchAuthStatus } from './useFetchAuthStatus';
export { default as queryAuthStatus } from './queryAuthStatus';
export { default as useQueryAuthStatus } from './useQueryAuthStatus';
29 changes: 29 additions & 0 deletions packages/ui/src/api/authStatus/queryAuthStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import client from '../client';
import { Response } from './types';
import { API_ENDPOINTS } from '../constants';

/**
* Query authentication status.
* @returns {Promise<boolean>} Authentication status.
*/
const queryAuthStatus = async (): Promise<boolean> => {
try {
const response = await client<Response>(
API_ENDPOINTS.AUTH_STATUS,
{
/**
* Handles 401 Unauthorized response by returning
* a comparible response instead of throwing an error.
* @returns {Promise<Response>} Authentication status response.
*/
on401: () => Promise.resolve({ authenticated: false }),
},
);

return response?.authenticated ?? false;
} catch {
return false;
}
}

export default queryAuthStatus;
3 changes: 3 additions & 0 deletions packages/ui/src/api/authStatus/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Response {
authenticated: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ import { API_ENDPOINTS } from '../constants';
* Checks authentication status.
* @returns {object} Authentication status data.
*/
const useFetchAuthStatus = () =>
const useQueryAuthStatus = () =>
useQuery({
queryKey: [API_ENDPOINTS.AUTH_STATUS],
/**
* Fetches authentication status from the internal API.
* Queries authentication status from the internal API.
* @returns {Promise<boolean>} Authentication status.
*/
queryFn: async (): Promise<boolean> => {
const { default: fetchAuthStatus } = await import('./fetchAuthStatus');
const { default: queryAuthStatus } = await import('./queryAuthStatus');

return fetchAuthStatus();
return queryAuthStatus();
},
});

export default useFetchAuthStatus;
export default useQueryAuthStatus;
78 changes: 48 additions & 30 deletions packages/ui/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,65 @@

import env from '@/env';

import { STATUS_CODES } from './constants';

interface Options<T> {
init?: RequestInit;
on401?: () => Promise<T | null>;
}

/**
* Custom error class for API request failures.
* Handles 401 Unauthorized responses by logging out the user
* and redirecting to the homepage.
* @returns {Promise<null>} Null after handling unauthorized response.
*/
export class APIError extends Error {
/**
* Creates an API error with HTTP status code.
* @param {number} status - HTTP status code
* @param {string} message - Error message
*/
constructor(
public status: number,
message: string,
) {
super(message);
this.name = 'APIError';
}
}
const handle401 = async (): Promise<null> => {
const { logout } = await import('./logout');

await logout();
window.location.replace('/');

return null;
};

/**
* Makes an authenticated API request to the backend.
* @param {string} endpoint - API endpoint path
* @param {RequestInit} [options] - Fetch options
* @returns {Promise<T>} Response data
*
* By default, handles unauthorized responses by
* logging out the user and redirecting to the homepage.
*
* @param {string} endpoint - API endpoint.
* @param {Options<T>} [options] - Fetch options.
* @returns {Promise<T | null>} Response data.
*/
export async function apiRequest<T>(endpoint: string, options?: RequestInit): Promise<T> {
const client = async <T>(
endpoint: string,
options?: Options<T>,
): Promise<T | null> => {
const response = await fetch(`${env.apiUrl}${endpoint}`, {
...options,
credentials: 'include', // Include cookies
...options?.init,
credentials: 'include', // Include cookies.
headers: {
'Content-Type': 'application/json',
...options?.headers,
...options?.init?.headers,
},
});
const isUnauthorized = (
!response.ok
&& response.status === STATUS_CODES.UNAUTHORIZED
);

if (!response.ok) {
const error = (await response.json().catch(() => ({ error: response.statusText }))) as {
error?: string;
message?: string;
};
throw new APIError(response.status, error.error ?? error.message ?? 'Unknown error');
if (isUnauthorized) {
console.error('Unauthorized!');

if (options?.on401) {
return await options.on401();
} else {
return await handle401();
}
} else {
return response.json() as Promise<T>;
}
};

return response.json() as Promise<T>;
}
export default client;
19 changes: 19 additions & 0 deletions packages/ui/src/api/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ export const API_ENDPOINTS = {
* @returns {string} Endpoint URL.
*/
STRAVA_ACTIVITY_SIGNALS: (id: string) => `/strava/activities/${id}/signals`,


/**
* Builds endpoint for generating Strava activity image.
* @param {string} activityId - Activity ID.
* @param {string} prompt - Image generation prompt.
* @returns {string} Endpoint URL.
*/
STRAVA_ACTIVITY_IMAGE_GENERATOR: (activityId: string, prompt: string) =>
`/strava/activities/${activityId}/image-generator?prompt=${encodeURIComponent(prompt)}`,

/**
* Builds endpoint for fetching Strava activity image generation prompt.
* @param {string} activityId - Activity ID.
* @param {string} signalsBase64 - Base64 encoded activity signals.
* @returns {string} Endpoint URL.
*/
STRAVA_ACTIVITY_IMAGE_GENERATION_PROMPT: (activityId: string, signalsBase64: string) =>
`/strava/activities/${activityId}/image-generator/prompt?signals=${signalsBase64}`,
};

export const STATUS_CODES = {
Expand Down
6 changes: 0 additions & 6 deletions packages/ui/src/api/generateStravaActivityImage/index.ts

This file was deleted.

26 changes: 15 additions & 11 deletions packages/ui/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ export {
logout,
} from './logout';
export {
fetchAuthStatus,
useFetchAuthStatus,
queryAuthStatus,
useQueryAuthStatus,
} from './authStatus';
export {
fetchStravaActivities,
useFetchStravaActivities,
queryStravaActivity,
useQueryStravaActivity,
} from './stravaActivity';
export {
queryStravaActivities,
useQueryStravaActivities,
} from './stravaActivities';
export {
useStravaActivitySignals,
fetchStravaActivitySignals,
queryStravaActivitySignals,
useQueryStravaActivitySignals,
} from './stravaActivitySignals';
export {
useStravaActivityImageGenerationPrompt,
fetchStravaActivityImageGenerationPrompt,
queryStravaActivityImageGenerationPrompt,
useQueryStravaActivityImageGenerationPrompt,
} from './stravaActivityImageGenerationPrompt';
export {
generateStravaActivityImage,
useGenerateStravaActivityImage,
} from './generateStravaActivityImage';
queryStravaActivityImage,
useQueryStravaActivityImage,
} from './stravaActivityImage';
6 changes: 3 additions & 3 deletions packages/ui/src/api/logout/logout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { apiRequest } from '../client';
import env from '@/env';

import { API_ENDPOINTS } from '../constants';

/**
Expand All @@ -7,8 +8,7 @@ import { API_ENDPOINTS } from '../constants';
* @returns {Promise<void>} Promise that resolves when logout is complete.
*/
const logout = async (): Promise<void> => {
await apiRequest<void>(API_ENDPOINTS.STRAVA_LOGOUT, {
method: 'POST',
await fetch(`${env.apiUrl}${API_ENDPOINTS.STRAVA_LOGOUT}`, {
credentials: 'include', // Include cookies.
headers: {
'Content-Type': 'application/json',
Expand Down
33 changes: 0 additions & 33 deletions packages/ui/src/api/strava.ts

This file was deleted.

13 changes: 0 additions & 13 deletions packages/ui/src/api/stravaActivities/fetchStravaActivities.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/ui/src/api/stravaActivities/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as fetchStravaActivities } from './fetchStravaActivities';
export { default as useFetchStravaActivities } from './useFetchStravaActivities';
export { default as queryStravaActivities } from './queryStravaActivities';
export { default as useQueryStravaActivities } from './useQueryStravaActivities';
13 changes: 13 additions & 0 deletions packages/ui/src/api/stravaActivities/queryStravaActivities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StravaActivity } from '@torq/strava-api';

import client from '../client';
import { API_ENDPOINTS } from '../constants';

/**
* Queries Strava activities.
* @returns {Promise<StravaActivity[]>} Strava activities.
*/
const queryStravaActivities = (): Promise<StravaActivity[] | null> =>
client<StravaActivity[]>(API_ENDPOINTS.STRAVA_ACTIVITIES);

export default queryStravaActivities;
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import { useQuery } from '@tanstack/react-query';
import { API_ENDPOINTS } from '../constants';

/**
* Fetches Strava activities.
* Queries Strava activities.
* @returns {object} Strava activities data.
*/
const useFetchStravaActivities = () =>
const useQueryStravaActivities = () =>
useQuery({
queryKey: [API_ENDPOINTS.STRAVA_ACTIVITIES],
/**
* Fetches Strava activities from the internal API.
* Queries Strava activities from the internal API.
* @returns {Promise<StravaActivity[]>} Strava activities.
*/
queryFn: async () => {
const { default: fetchStravaActivities } = await import('./fetchStravaActivities');
const { default: queryStravaActivities } = await import('./queryStravaActivities');

return fetchStravaActivities();
return queryStravaActivities();
},
});

export default useFetchStravaActivities;
export default useQueryStravaActivities;
2 changes: 2 additions & 0 deletions packages/ui/src/api/stravaActivity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as queryStravaActivity } from './queryStravaActivity';
export { default as useQueryStravaActivity } from './useQueryStravaActivity';
Loading