i18n
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Failing after 10s
CI/CD Pipeline / Build and Push App Docker Image (push) Skipped
CI/CD Pipeline / Deploy to Kubernetes (push) Skipped

This commit is contained in:
2026-08-08 15:29:40 -07:00
parent 22c282d8fa
commit 0a7b5fa00f
178 changed files with 13099 additions and 1285 deletions
+61 -59
View File
@@ -1,4 +1,5 @@
import { createContext, useCallback, useEffect, useMemo, useState, PropsWithChildren } from 'react';
import { useLingui } from '@lingui/react/macro';
import { SpotifyAuth } from '@/lib/spotify/auth';
import { useAuth } from './auth-context';
import { useConfig } from '@/hooks/use-config';
@@ -38,49 +39,51 @@ const deepEqual = (a: unknown, b: unknown): boolean => {
);
};
const makeSpotifyRequest = async (endpoint: string, options: RequestInit = {}) => {
const response = await fetch(`/api/spotify/${endpoint}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
let errorMessage = 'Request failed';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch {
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
}
throw new Error(errorMessage);
}
if (response.status === 204 || response.headers.get('content-length') === '0') {
return {};
}
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
console.warn(`Non-JSON response from ${endpoint}:`, contentType);
return {};
}
try {
return await response.json();
} catch (error) {
console.warn(`Failed to parse JSON response from ${endpoint}:`, error);
return {};
}
};
export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
const { roles } = useAuth();
const isAdmin = roles?.includes('Admin') || false;
const config = useConfig();
const { t } = useLingui();
// Defined inside the provider so error messages can be localized via `t`.
const makeSpotifyRequest = async (endpoint: string, options: RequestInit = {}) => {
const response = await fetch(`/api/spotify/${endpoint}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
let errorMessage = t`Request failed`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch {
errorMessage = t`HTTP ${response.status}: ${response.statusText}`;
}
throw new Error(errorMessage);
}
if (response.status === 204 || response.headers.get('content-length') === '0') {
return {};
}
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
console.warn(`Non-JSON response from ${endpoint}:`, contentType);
return {};
}
try {
return await response.json();
} catch (error) {
console.warn(`Failed to parse JSON response from ${endpoint}:`, error);
return {};
}
};
const [authState, setAuthState] = useState<SpotifyAuthState>(defaultSpotifyState);
@@ -150,10 +153,9 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
}
if (error) {
let errorMessage = `Authentication failed: ${error}`;
if (details) {
errorMessage += ` - ${decodeURIComponent(details)}`;
}
const errorMessage = details
? t`Authentication failed: ${error} - ${decodeURIComponent(details)}`
: t`Authentication failed: ${error}`;
setError(errorMessage);
console.error('Spotify OAuth Error:', { error, details });
@@ -203,7 +205,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const playTrack = useCallback(async (trackId: string, deviceId?: string, positionMs?: number) => {
if (!authState.isAuthenticated) return;
@@ -226,7 +228,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const pause = useCallback(async () => {
if (!authState.isAuthenticated) return;
@@ -249,7 +251,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const skipNext = useCallback(async () => {
if (!authState.isAuthenticated) return;
@@ -272,7 +274,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const skipPrevious = useCallback(async () => {
if (!authState.isAuthenticated) return;
@@ -295,7 +297,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const setVolume = useCallback(async (volumePercent: number) => {
if (!authState.isAuthenticated) return;
@@ -309,11 +311,11 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
body: JSON.stringify({ action: 'volume', volumePercent }),
});
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to set volume');
setError(error instanceof Error ? error.message : t`Failed to set volume`);
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const getDevices = useCallback(async () => {
if (!authState.isAuthenticated) return;
@@ -330,11 +332,11 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
setActiveDeviceState(active);
}
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to get devices');
setError(error instanceof Error ? error.message : t`Failed to get devices`);
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const setActiveDevice = useCallback(async (deviceId: string) => {
if (!authState.isAuthenticated) return;
@@ -355,11 +357,11 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
setTimeout(getDevices, 1000);
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to set active device');
setError(error instanceof Error ? error.message : t`Failed to set active device`);
} finally {
setIsLoading(false);
}
}, [authState.isAuthenticated, devices]);
}, [authState.isAuthenticated, devices, t]);
const refreshPlaybackState = useCallback(async () => {
if (!authState.isAuthenticated) return;
@@ -382,7 +384,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
} catch (error) {
console.warn('Failed to refresh playback state:', error);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
useEffect(() => {
if (!authState.isAuthenticated) return;
@@ -422,11 +424,11 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
setCapturedState(response.snapshot);
}
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to capture playback state');
setError(error instanceof Error ? error.message : t`Failed to capture playback state`);
} finally {
setIsCaptureLoading(false);
}
}, [authState.isAuthenticated]);
}, [authState.isAuthenticated, t]);
const resumePlaybackState = useCallback(async () => {
if (!authState.isAuthenticated || !capturedState) return;
@@ -442,11 +444,11 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
setTimeout(refreshPlaybackState, 1000);
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to resume playback state');
setError(error instanceof Error ? error.message : t`Failed to resume playback state`);
} finally {
setIsResumeLoading(false);
}
}, [authState.isAuthenticated, capturedState, refreshPlaybackState]);
}, [authState.isAuthenticated, capturedState, refreshPlaybackState, t]);
const clearCapturedState = useCallback(() => {
setCapturedState(null);