Update dependencies, refactor authentication, and enhance UI components

- Upgraded @reduxjs/toolkit to version 2.9.0 and added new dependencies including @techniker-me/pcast-api and moment.
- Refactored authentication logic and added middleware for improved request handling.
- Introduced new UI components such as buttons, loaders, and forms, along with a theme system following SOLID principles.
- Updated routing to include protected routes and improved the login form with better error handling.
- Removed unused CSS and organized the project structure for better maintainability.
This commit is contained in:
2025-09-04 01:10:03 -04:00
parent 04488c43c5
commit 1469c7f52f
85 changed files with 3610 additions and 125 deletions

View File

@@ -46,6 +46,18 @@ export const selectSessionInfo = createSelector([selectAuthentication], authenti
roles: authentication.roles
}));
export const selectApplicationId = createSelector([selectAuthentication], authentication => authentication.applicationId);
export const selectSecret = createSelector([selectAuthentication], authentication => authentication.secret);
export const selectSessionId = createSelector([selectAuthentication], authentication => authentication.sessionId);
export const selectRoles = createSelector([selectAuthentication], authentication => authentication.roles);
export const selectHasRole = createSelector([selectAuthentication, (_, role: string) => role], (authentication, role) => authentication.roles.includes(role));
export const selectIsOnline = createSelector([selectAuthentication], authentication => authentication.status === 'Online');
const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse, {applicationId: string; secret: string}>(
'authentication/authenticate',
async (credentials, {rejectWithValue}) => {
@@ -56,6 +68,7 @@ const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse,
} catch (error) {
// Convert error to serializable format
const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
return rejectWithValue(errorMessage);
}
}
@@ -67,6 +80,7 @@ const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejec
} catch (error) {
// Convert error to serializable format
const errorMessage = error instanceof Error ? error.message : 'Signout failed';
return rejectWithValue(errorMessage);
}
});
@@ -95,14 +109,16 @@ const authenticationSlice = createSlice({
setSessionId: (state, action: PayloadAction<string>) => {
state.sessionId = action.payload;
},
setIsAuthenticated: (state, action: PayloadAction<boolean>) => {
state.isAuthenticated = action.payload;
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
setRoles: (state, action: PayloadAction<string[]>) => {
state.roles = action.payload;
},
setApplicationId: (state, action: PayloadAction<string>) => {
state.applicationId = action.payload;
setUnauthorized: state => {
state.isAuthenticated = false;
state.isLoading = false;
state.error = 'Unauthorized';
state.secret = null;
state.status = 'Offline';
state.roles = [];
}
},
extraReducers: builder => {
@@ -119,17 +135,18 @@ const authenticationSlice = createSlice({
state.sessionId = authenticationResponse.sessionId ?? null;
state.isAuthenticated = true;
state.roles = authenticationResponse.roles ?? [];
state.status = 'Online';
state.isLoading = false;
} else {
state.applicationId = null;
state.sessionId = null;
state.isAuthenticated = false;
state.secret = null;
state.roles = [];
state.status = 'Offline';
state.error = 'Invalid credentials. Please check your Application ID and Secret.';
state.isLoading = false;
}
state.status = 'Online';
state.isLoading = false;
state.error = null;
})
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
state.applicationId = null;
@@ -164,6 +181,6 @@ const authenticationSlice = createSlice({
}
});
export const {setIsLoading, setCredentials, clearState, setSessionId, setIsAuthenticated, setRoles, setApplicationId} = authenticationSlice.actions;
export const {setUnauthorized, setIsLoading, setCredentials, clearState, setSessionId, setError} = authenticationSlice.actions;
export {authenticateCredentialsThunk};
export default authenticationSlice.reducer;
export default authenticationSlice.reducer;

View File

@@ -0,0 +1,103 @@
import {createAsyncThunk, createSelector, createSlice, PayloadAction, WritableDraft} from '@reduxjs/toolkit';
import {ApplicationCredentials, Channel} from '@techniker-me/pcast-api';
import PCastApiService from 'services/PCastApi.service';
export interface IChannelsState {
isLoading: boolean;
channels: Channel[];
selectedChannel: Channel | null;
error: string | null;
}
export const initialChannelsState: IChannelsState = {
isLoading: false,
channels: [],
selectedChannel: null,
error: null
};
export const selectChannels = (state: {channels: IChannelsState}) => state.channels;
export const selectChannelList = createSelector([selectChannels], channels => channels.channels);
export const fetchChannelList = createAsyncThunk('channels/fetchChannelList', async (_, {rejectWithValue}) => {
try {
return PCastApiService.channels.list();
} catch (error) {
return rejectWithValue(error);
}
});
export const fetchChannelsListPublisherStatus = createAsyncThunk(
'channels/fetchChannelsListPublisherStatus',
async (channels: Channel[], {rejectWithValue}) => {
try {
const channelResponses = await Promise.all(
channels.map(async channel => {
const publisherCount = await PCastApiService.channels.getPublisherCount(channel.channelId);
return {
...channel,
isActivePublisher: publisherCount > 0
};
})
);
return channelResponses as Channel[];
} catch (error) {
return rejectWithValue(error);
}
}
);
const channelsSlice = createSlice({
name: 'channels',
initialState: {...initialChannelsState},
reducers: {
initializeChannels: (state, action: PayloadAction<{pcastUri: string; applicationCredentials: ApplicationCredentials}>) => {
PCastApiService.initialize(action.payload.pcastUri, action.payload.applicationCredentials);
state.isLoading = false;
state.error = null;
},
setChannels: (state, action: PayloadAction<Channel[]>) => {
state.channels = action.payload as WritableDraft<Channel>[];
},
setIsLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setSelectedChannel: (state, action: PayloadAction<Channel | null>) => {
state.selectedChannel = action.payload as WritableDraft<Channel> | null;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
}
},
extraReducers: builder => {
builder.addCase(fetchChannelList.pending, state => {
state.isLoading = true;
});
builder.addCase(fetchChannelList.fulfilled, (state, action) => {
state.channels = action.payload as WritableDraft<Channel[]>;
state.isLoading = false;
state.error = null;
});
builder.addCase(fetchChannelList.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
builder.addCase(fetchChannelsListPublisherStatus.pending, state => {
state.isLoading = true;
});
builder.addCase(fetchChannelsListPublisherStatus.fulfilled, (state, action) => {
state.channels = action.payload as WritableDraft<Channel[]>;
state.isLoading = false;
state.error = null;
});
builder.addCase(fetchChannelsListPublisherStatus.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
}
});
export const {initializeChannels, setChannels, setIsLoading, setSelectedChannel, setError} = channelsSlice.actions;
export default channelsSlice.reducer;