Skip to content

Commit

Permalink
refactor training pages to allow rendering on client-side instead of …
Browse files Browse the repository at this point in the history
…server-side
  • Loading branch information
sharon-odhiambo committed Oct 24, 2023
1 parent ecc9396 commit a82227e
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 88 deletions.
81 changes: 80 additions & 1 deletion app/assets/javascripts/actions/training_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,91 @@ import { extend } from 'lodash-es';
import {
RECEIVE_TRAINING_MODULE, MENU_TOGGLE, REVIEW_ANSWER,
SET_CURRENT_SLIDE, RECEIVE_ALL_TRAINING_MODULES,
EXERCISE_COMPLETION_UPDATE, SLIDE_COMPLETED, API_FAIL
EXERCISE_COMPLETION_UPDATE, SLIDE_COMPLETED, API_FAIL,
RECEIVE_TRAINING_LIBRARIES, RECEIVE_TRAINING_LIBRARY, SEARCH_LIBRARY
} from '../constants';
import request from '../utils/request';
import logErrorMessage from '../utils/log_error_message';
import { stringify } from 'query-string';

export const fetchTrainingLibraries = () => async (dispatch) => {
try {
const response = await request('/training.json');
if (!response.ok) {
const errorMessage = `Failed to fetch training libraries. Status: ${response.status}`;
throw new Error(errorMessage);
}

const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('Response is not valid JSON');
}

const data = await response.json();
return dispatch({ type: RECEIVE_TRAINING_LIBRARIES, data });
} catch (error) {
return dispatch({ type: API_FAIL, data: error });
}
};


export const searchTrainingLibraries = searchTerm => async (dispatch) => {
try {
const url = `/training.json?search_training=${encodeURIComponent(searchTerm)}`;

const response = await request(url);
if (!response.ok) {
const errorMessage = `Failed to fetch training libraries. Status: ${response.status}`;
throw new Error(errorMessage);
}

const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('Response is not valid JSON');
}

const data = await response.json();
const slides = data.slides;

return dispatch({ type: SEARCH_LIBRARY, data: slides });
} catch (error) {
return dispatch({ type: API_FAIL, data: error });
}
};




export const fetchTrainingLibrary = opts => async (dispatch) => {
try {
const response = await request(`/training.json?library_slug=${opts}`);
if (!response.ok) {
const errorMessage = `Failed to fetch training libraries. Status: ${response.status}`;
throw new Error(errorMessage);
}

const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('Response is not valid JSON');
}
const data = await response.json();

if (!data.libraries || !Array.isArray(data.libraries) || data.libraries.length === 0) {
throw new Error('No libraries found for the given slug');
}
const libraries = data.libraries;
const library = libraries.find(lib => lib.slug === opts);
if (!library) {
throw new Error('No library found for the given slug');
}
return dispatch({ type: RECEIVE_TRAINING_LIBRARY, data: library });
} catch (error) {
return dispatch({ type: API_FAIL, data: error });
}
};



const fetchAllTrainingModulesPromise = async () => {
const response = await request('/training_modules.json');
if (!response.ok) {
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/components/util/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const CampaignsHandler = lazy(() => import('../campaign/campaigns_handler.jsx'))
const DetailedCampaignList = lazy(() => import('../campaign/detailed_campaign_list'));
const Explore = lazy(() => import('../explore/explore.jsx'));
const TrainingApp = lazy(() => import('../../training/components/training_app.jsx'));
const TrainingLibrariesList = lazy(() => import('../../training/components/training_libraries_list.jsx'));
const ActiveCoursesHandler = lazy(() => import('../active_courses/active_courses_handler.jsx'));
const CoursesByWikiHandler = lazy(() => import('../courses_by_wiki/courses_by_wiki_handler.jsx'));

Expand Down Expand Up @@ -60,6 +61,7 @@ const routes = () => {
<Route path="/alerts_list" element={<AdminAlerts />} />
<Route path="/settings" element={<SettingsHandler />} />
<Route path="/article_finder" element={<ArticleFinder />} />
<Route path="/training" element={<TrainingLibrariesList />} />
<Route path="/training/*" element={<TrainingApp />} />
<Route path="/tickets/dashboard" element={<TicketsHandler />} />
<Route path="/tickets/dashboard/:id" element={<TicketShowHandler />} />
Expand Down
3 changes: 3 additions & 0 deletions app/assets/javascripts/constants/training.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ export const SET_CURRENT_SLIDE = 'SET_CURRENT_SLIDE';
export const RECEIVE_ALL_TRAINING_MODULES = 'RECEIVE_ALL_TRAINING_MODULES';
export const SLIDE_COMPLETED = 'SLIDE_COMPLETED';
export const EXERCISE_COMPLETION_UPDATE = 'EXERCISE_COMPLETION_UPDATE';
export const RECEIVE_TRAINING_LIBRARIES = 'RECEIVE_TRAINING_LIBRARIES';
export const RECEIVE_TRAINING_LIBRARY = 'RECEIVE_TRAINING_LIBRARY';
export const SEARCH_LIBRARY = 'SEARCH_LIBRARY';
10 changes: 9 additions & 1 deletion app/assets/javascripts/reducers/training.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { findIndex } from 'lodash-es';
import {
RECEIVE_TRAINING_MODULE, MENU_TOGGLE, REVIEW_ANSWER,
SET_CURRENT_SLIDE, RECEIVE_ALL_TRAINING_MODULES,
SLIDE_COMPLETED
SLIDE_COMPLETED, RECEIVE_TRAINING_LIBRARIES, RECEIVE_TRAINING_LIBRARY
} from '../constants';
import { SEARCH_LIBRARY } from '../constants/training';

const reviewAnswer = function (state, answer) {
const answerId = parseInt(answer);
Expand Down Expand Up @@ -55,6 +56,7 @@ const update = (state) => {

const initialState = {
libraries: [],
library: {},
modules: [],
module: {},
slides: [],
Expand Down Expand Up @@ -86,6 +88,12 @@ export default function training(state = initialState, action) {
}
return { ...newState, loading: false };
}
case RECEIVE_TRAINING_LIBRARIES:
return { ...state, libraries: data.libraries };
case RECEIVE_TRAINING_LIBRARY:
return { ...state, library: data };
case SEARCH_LIBRARY:
return { ...state, slides: data };
case MENU_TOGGLE:
return { ...state, menuIsOpen: !data.currently };
case REVIEW_ANSWER:
Expand Down
22 changes: 22 additions & 0 deletions app/assets/javascripts/training/components/search_results.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

const SearchResults = ({ slides }) => {
if (slides && slides.length > 0) {
return (
<ul className="training-libraries no-bullets no-margin action-card-text">
{slides.map((slide, index) => (
<li key={index}>
<a href={`https://dashboard.wikiedu.org/training/${slide.path}`} target="_blank">
<span dangerouslySetInnerHTML={{ __html: slide.title }} /> ({slide.module_name})
</a>
<br />
<div dangerouslySetInnerHTML={{ __html: slide.excerpt }} />
</li>
))}
</ul>
);
}
return null;
};

export default SearchResults;
2 changes: 2 additions & 0 deletions app/assets/javascripts/training/components/training_app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { Route, Routes } from 'react-router-dom';

import TrainingModuleHandler from './training_module_handler.jsx';
import TrainingSlideHandler from './training_slide_handler.jsx';
import TrainingLibrary from './training_library.jsx';

const TrainingApp = () => (
<div>
<Routes>
<Route path=":library_id" element={<TrainingLibrary />} />
<Route path=":library_id/:module_id" element={<TrainingModuleHandler />} />
<Route path=":library_id/:module_id/:slide_id" element={<TrainingSlideHandler />} />
</Routes>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import SearchResults from './search_results.jsx';
import { fetchTrainingLibraries, searchTrainingLibraries } from '../../actions/training_actions';

const TrainingLibraries = () => {
const libraries = useSelector(state => state.training.libraries);
const focusedLibrarySlug = useSelector(state => state.training.focusedLibrarySlug);
const slides = useSelector(state => state.training.slides);
const [search, setSearch] = useState('');
const [showSearchResults, setShowSearchResults] = useState(false);
const dispatch = useDispatch();

useEffect(() => {
dispatch(fetchTrainingLibraries());
}, [dispatch]);

useEffect(() => {
setShowSearchResults(showSearchResults);
}, [slides]);

const handleSearch = (e) => {
setSearch(e.target.value);
};

const handleSubmit = (e) => {
e.preventDefault();
dispatch(searchTrainingLibraries(search));
setShowSearchResults(true);
};

return (
<div>
<h1>Training Libraries</h1>
<div className="search-bar" style={{ position: 'relative' }}>
<form onSubmit={handleSubmit}>
<input
type="text"
value={search}
onChange={e => handleSearch(e)}
placeholder="Search for training resources"
style={{ width: '100%', height: '3rem', fontSize: '15px' }}
/>
<button type="submit" id="training_search_button" style={{ position: 'absolute', right: '20px', top: '10px' }}>
<i className="icon icon-search" />
</button>
</form>
</div>
{showSearchResults ? (
<SearchResults slides={slides} message="No training resources match your search." />
) : (
<ul className="training-libraries no-bullets no-margin">
{libraries
.filter(library => !library.exclude_from_index)
.map((library, index) => {
const isFocused = focusedLibrarySlug === library.slug;
let libraryClass = 'training-libraries__individual-library no-left-margin';
if (isFocused) {
libraryClass += ' training-library-focus';
} else if (focusedLibrarySlug) {
libraryClass += ' training-library-defocus';
}

return (
<li key={index} className={libraryClass}>
<a href={`/training/${library.slug}`} className="action-card action-card-index">
<header className="action-card-header">
<h3 className="action-card-title">{library.name}</h3>
<span className="icon-container">
<i className="action-card-icon icon icon-rt_arrow" />
</span>
</header>
</a>
<div className="action-card-text">
<h3>Included Modules:</h3>
<ul>
{library.categories.map(category =>
category.modules.map((trainingModule, moduleIndex) => (
<li key={moduleIndex}>
<a href={`/training/${library.slug}/${trainingModule.slug}`} target="_blank">
{trainingModule.name}
</a>
</li>
))
)}
</ul>
</div>
</li>
);
})}
</ul>
)}
</div>
);
};

export default TrainingLibraries;
70 changes: 70 additions & 0 deletions app/assets/javascripts/training/components/training_library.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { fetchTrainingLibrary } from '../../actions/training_actions';

const TrainingLibrary = () => {
const { library_id } = useParams();
const library = useSelector(state => state.training.library);
const dispatch = useDispatch();

useEffect(() => {
dispatch(fetchTrainingLibrary(library_id));
},
[dispatch, library_id]);
if (!library) {
return <div>Loading...</div>;
}
return (
<div className="training__section-overview container">
<section className="training__header">
<h1>{library.name}</h1>
<p>{library.introduction}</p>
</section>
{library.categories ? (
<ul className="training__categories">
{library.categories.map((libCategory, index) => (
<li key={index}>
<div className="training__category__header">
<h1 className="h3">{libCategory.title}</h1>
<p>{libCategory.description}</p>
{library.wiki_page && (
<div className="training__category__source">
<a href={`https://meta.wikimedia.org/wiki/${library.wiki_page}`}>
View Library Source
</a>
</div>
)}
</div>
<ul className="training__categories__modules">
{libCategory.modules.map((libModule, moduleIndex) => (
<li key={moduleIndex}>
<a href={`/training/${library.slug}/${libModule.slug}`} className="action-card">

<header className="action-card-header">
<h3 className="action-card-title">{libModule.name}</h3>
<span className="pull-right action-card-title__completion">
{libModule.percent_complete}
</span>
<span className="icon-container">
<i className="action-card-icon icon icon-rt_arrow" />
</span>
</header>
<p className="action-card-text">
<span>{libModule.description}</span>
</p>
</a>
</li>
))}
</ul>
</li>
))}
</ul>
) : (
<p>No categories available.</p>
)}
</div>
);
};

export default TrainingLibrary;
Loading

0 comments on commit a82227e

Please sign in to comment.