Skip to content

Commit

Permalink
feat(story-tags): add tags to story list items (#343)
Browse files Browse the repository at this point in the history
  • Loading branch information
pwambach authored May 19, 2020
1 parent f08b170 commit b602a14
Show file tree
Hide file tree
Showing 16 changed files with 186 additions and 9 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"cross-zip": "^3.0.0",
"framer-motion": "^1.8.4",
"lodash.debounce": "^4.0.8",
"lodash.intersection": "^4.4.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-intl": "^3.12.0",
Expand All @@ -57,6 +58,7 @@
"@types/cesium": "^1.66.0",
"@types/classnames": "^2.2.9",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.intersection": "^4.4.6",
"@types/node": "^12.12.6",
"@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5",
Expand Down
15 changes: 15 additions & 0 deletions src/scripts/actions/set-selected-story-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const SET_SELECTED_STORY_TAGS = 'SET_SELECTED_STORY_TAGS';

export interface SetSelectedStoryTagsAction {
type: typeof SET_SELECTED_STORY_TAGS;
tags: string[];
}

const setSelectedStoryTagsAction = (
tags: string[]
): SetSelectedStoryTagsAction => ({
type: SET_SELECTED_STORY_TAGS,
tags
});

export default setSelectedStoryTagsAction;
12 changes: 10 additions & 2 deletions src/scripts/components/story-list-item/story-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@ import React, {FunctionComponent} from 'react';
import {Link} from 'react-router-dom';
import cx from 'classnames';

import {StoryListItem as StoryListItemType} from '../../types/story-list';
import {StoryMode} from '../../types/story-mode';
import StoryTags from '../story-tags/story-tags';
import {replaceUrlPlaceholders} from '../../libs/replace-url-placeholders';
import {DownloadButton} from '../download-button/download-button';
import {getStoryMediaUrl} from '../../libs/get-story-media-url';
import config from '../../config/main';

import {StoryListItem as StoryListItemType} from '../../types/story-list';
import {StoryMode} from '../../types/story-mode';

import styles from './story-list-item.styl';

interface Props {
story: StoryListItemType;
mode: StoryMode;
selectedIndex: number;
selectedTags: string[];
onSelectStory: (id: string) => void;
}

const StoryListItemContent: FunctionComponent<Props> = ({
mode,
story,
selectedIndex,
selectedTags,
onSelectStory
}) => {
const classes = cx(
Expand All @@ -46,6 +50,7 @@ const StoryListItemContent: FunctionComponent<Props> = ({
<div className={styles.imageInfo}>
<p className={styles.title}>{story.title}</p>
<p className={styles.description}>{story.description}</p>
{story.tags && <StoryTags tags={story.tags} selected={selectedTags} />}
<div className={styles.downloadButton}>
<DownloadButton url={downloadUrl} id={downloadId} />
</div>
Expand All @@ -58,6 +63,7 @@ const StoryListItem: FunctionComponent<Props> = ({
story,
mode,
selectedIndex,
selectedTags,
onSelectStory
}) => {
const isShowcaseMode = mode === StoryMode.Showcase;
Expand All @@ -66,6 +72,7 @@ const StoryListItem: FunctionComponent<Props> = ({
<Link to={`/${mode}/${story.id}`}>
<StoryListItemContent
selectedIndex={selectedIndex}
selectedTags={selectedTags}
mode={mode}
story={story}
onSelectStory={id => onSelectStory(id)}
Expand All @@ -74,6 +81,7 @@ const StoryListItem: FunctionComponent<Props> = ({
) : (
<StoryListItemContent
selectedIndex={selectedIndex}
selectedTags={selectedTags}
mode={mode}
story={story}
onSelectStory={id => onSelectStory(id)}
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/components/story-list/story-list.styl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@require '../../../variables.styl'

.storyList
overflow-y: scroll
overflow-y: auto
height: 100%
background-color: $black

Expand Down
7 changes: 6 additions & 1 deletion src/scripts/components/story-list/story-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {useSelector} from 'react-redux';
import cx from 'classnames';

import {storyListSelector} from '../../selectors/story/list';
import {selectedTagsSelector} from '../../selectors/story/selected-tags';
import StoryListItem from '../story-list-item/story-list-item';
import {filterStories} from '../../libs/filter-stories';

import {StoryMode} from '../../types/story-mode';

Expand All @@ -21,6 +23,8 @@ const StoryList: FunctionComponent<Props> = ({
onSelectStory = () => {}
}) => {
const stories = useSelector(storyListSelector);
const selectedTags = useSelector(selectedTagsSelector);
const filteredStories = filterStories(stories, selectedTags);

const classes = cx(
styles.storyListGrid,
Expand All @@ -30,7 +34,7 @@ const StoryList: FunctionComponent<Props> = ({
return (
<div className={styles.storyList}>
<div className={classes}>
{stories.map(story => {
{filteredStories.map(story => {
let selectedIndex = selectedIds?.indexOf(story.id);

if (typeof selectedIndex !== 'number') {
Expand All @@ -42,6 +46,7 @@ const StoryList: FunctionComponent<Props> = ({
key={story.id}
story={story}
mode={mode}
selectedTags={selectedTags}
selectedIndex={selectedIndex}
onSelectStory={id => onSelectStory(id)}
/>
Expand Down
19 changes: 19 additions & 0 deletions src/scripts/components/story-tags/story-tags.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@require '../../../variables.styl'

.tags
display: flex

.tag
margin-right: 0.5em
padding: 0.2em 1em
border: 1px solid $darkGrey4
border-radius: 12px
text-transform: uppercase
font-size: 0.8em

&:hover
background: $darkGrey2

.selected, .selected:hover
background: $darkGrey4
color: $textColor
44 changes: 44 additions & 0 deletions src/scripts/components/story-tags/story-tags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, {FunctionComponent} from 'react';
import {useDispatch} from 'react-redux';
import cx from 'classnames';

import setSelectedStoryTags from '../../actions/set-selected-story-tags';

import styles from './story-tags.styl';

interface Props {
tags: string[];
selected: string[];
}

const StoryTags: FunctionComponent<Props> = ({tags, selected}) => {
const dispatch = useDispatch();
const toggleTag = (tag: string) => {
const newTags = selected.includes(tag)
? selected.filter(oldTag => oldTag !== tag)
: selected.concat([tag]);

dispatch(setSelectedStoryTags(newTags));
};
const getTagClasses = (tag: string) =>
cx(styles.tag, selected.includes(tag) && styles.selected);

return (
<div className={styles.tags}>
{tags.map(tag => (
<span
className={getTagClasses(tag)}
key={tag}
onClick={(event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
toggleTag(tag);
}}>
{tag}
</span>
))}
</div>
);
};

export default StoryTags;
12 changes: 12 additions & 0 deletions src/scripts/libs/filter-stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import intersection from 'lodash.intersection';

import {StoryList} from '../types/story-list';

// filter stories by tags - keep story if at lease one of the tag matches
export function filterStories(stories: StoryList, tags: string[]) {
if (tags.length === 0) {
return stories;
}

return stories.filter(story => intersection(story.tags, tags).length > 0);
}
27 changes: 27 additions & 0 deletions src/scripts/libs/tags-url-parameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const char = 'I';

// parses window.location and reads the story tags from query params
//
// note: we do not use the location.search prop here because the HashRouter
// stores the query parameters in the location.hash prop
export function parseUrl(): string[] {
const {hash} = location;
// only take the query portion of the hash string
const queryString = hash.substr(hash.indexOf('?'));
const urlParams = new URLSearchParams(queryString);
const tagsParam = urlParams.get('tags');

if (!tagsParam) {
return [];
}

return tagsParam.split(char);
}

export function getParamString(tags: string[]): string | null {
if (tags.length === 0) {
return null;
}

return tags.join(char);
}
4 changes: 3 additions & 1 deletion src/scripts/reducers/story/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {combineReducers} from 'redux';

import listReducer from './list';
import selectedReducer from './selected';
import selectedTagsReducer from './selected-tags';

const storiesReducer = combineReducers({
list: listReducer,
selected: selectedReducer
selected: selectedReducer,
selectedTags: selectedTagsReducer
});

export default storiesReducer;
Expand Down
19 changes: 19 additions & 0 deletions src/scripts/reducers/story/selected-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
SET_SELECTED_STORY_TAGS,
SetSelectedStoryTagsAction
} from '../../actions/set-selected-story-tags';
import {parseUrl} from '../../libs/tags-url-parameter';

function selectedStoryTagsReducer(
tagsState: string[] = parseUrl(),
action: SetSelectedStoryTagsAction
): string[] {
switch (action.type) {
case SET_SELECTED_STORY_TAGS:
return action.tags;
default:
return tagsState;
}
}

export default selectedStoryTagsReducer;
5 changes: 5 additions & 0 deletions src/scripts/selectors/story/selected-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {State} from '../../reducers/index';

export function selectedTagsSelector(state: State) {
return state.stories.selectedTags;
}
1 change: 1 addition & 0 deletions src/scripts/types/story-list.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface StoryListItem {
description: string;
link: string;
image: string;
tags: string[];
}

export type StoryList = StoryListItem[];
6 changes: 4 additions & 2 deletions storage/stories/stories-de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"id": "story1",
"title": "Planetarer Wärmespeicher",
"description": "",
"image": "assets/sst_large_18.jpg"
"image": "assets/sst_large_18.jpg",
"tags": ["a", "b"]
},
{
"id": "story2",
"title": "Geschichte 2",
"description": "Das ist Geschichte 2",
"image": "assets/story2.jpeg"
"image": "assets/story2.jpeg",
"tags": ["a", "c"]
},
{
"id": "story3",
Expand Down
6 changes: 4 additions & 2 deletions storage/stories/stories-en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"id": "story1",
"title": "Planetary Heat Store",
"description": "",
"image": "assets/sst_large_18.jpg"
"image": "assets/sst_large_18.jpg",
"tags": ["a", "b"]
},
{
"id": "story2",
"title": "Story 2",
"description": "This is story 2",
"image": "assets/story2.jpeg"
"image": "assets/story2.jpeg",
"tags": ["a", "c"]
},
{
"id": "story3",
Expand Down

0 comments on commit b602a14

Please sign in to comment.