diff --git a/packages/components/assets/icons/icon-views.svg b/packages/components/assets/icons/icon-views.svg
new file mode 100644
index 0000000000..da95992128
--- /dev/null
+++ b/packages/components/assets/icons/icon-views.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/src/Icon/Icon.tsx b/packages/components/src/Icon/Icon.tsx
index 089ea31a17..86325533f8 100644
--- a/packages/components/src/Icon/Icon.tsx
+++ b/packages/components/src/Icon/Icon.tsx
@@ -95,6 +95,7 @@ export const glyphs: IGlyphs = {
useful: iconMap.useful,
thunderbolt: ,
filter: ,
+ view: iconMap.view,
}
type WrapperProps = IProps & VerticalAlignProps & SpaceProps
diff --git a/packages/components/src/Icon/svgs.tsx b/packages/components/src/Icon/svgs.tsx
index 4d6fa9a629..5c6ddff39a 100644
--- a/packages/components/src/Icon/svgs.tsx
+++ b/packages/components/src/Icon/svgs.tsx
@@ -4,6 +4,7 @@ import starActiveSVG from '../../assets/icons/icon-star-active.svg'
import verifiedSVG from '../../assets/icons/icon-verified-badge.svg'
import usefulSVG from '../../assets/icons/icon-useful.svg'
import commentSVG from '../../assets/icons/icon-comment.svg'
+import viewSVG from '../../assets/icons/icon-views.svg'
const imgStyle = {
maxWidth: '100%',
@@ -16,4 +17,5 @@ export const iconMap = {
verified:
,
useful:
,
comment:
,
+ view:
,
}
diff --git a/packages/components/src/Icon/types.ts b/packages/components/src/Icon/types.ts
index b25a73f19f..24db17b822 100644
--- a/packages/components/src/Icon/types.ts
+++ b/packages/components/src/Icon/types.ts
@@ -40,5 +40,6 @@ export type availableGlyphs =
| 'useful'
| 'verified'
| 'filter'
+ | 'view'
export type IGlyphs = { [k in availableGlyphs]: JSX.Element }
diff --git a/packages/components/src/ViewsCounter/ViewsCounter.tsx b/packages/components/src/ViewsCounter/ViewsCounter.tsx
new file mode 100644
index 0000000000..9df37e4eed
--- /dev/null
+++ b/packages/components/src/ViewsCounter/ViewsCounter.tsx
@@ -0,0 +1,31 @@
+import { Text, Flex } from 'theme-ui'
+import { Icon } from '..'
+import { useTheme } from '@emotion/react'
+
+export interface IProps {
+ viewsCount: number
+}
+
+export const ViewsCounter = (props: IProps) => {
+ const theme: any = useTheme()
+
+ return (
+
+
+
+ {props.viewsCount}
+ {props.viewsCount !== 1 ? ' views' : ' view'}
+
+
+ )
+}
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index f32ad1384b..500b7b84a4 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -23,6 +23,7 @@ export { ImageGallery } from './ImageGallery/ImageGallery'
export { Loader } from './Loader/Loader'
export { LinkifyText } from './LinkifyText/LinkifyText'
export { UsefulStatsButton } from './UsefulStatsButton/UsefulStatsButton'
+export { ViewsCounter } from './ViewsCounter/ViewsCounter'
export { CategoryTag } from './CategoryTag/CategoryTag'
export { FileInformation } from './FileInformation/FileInformation'
export { OsmGeocoding } from './OsmGeocoding/OsmGeocoding'
diff --git a/src/models/howto.models.tsx b/src/models/howto.models.tsx
index 7c3a10daad..54c2479f8d 100644
--- a/src/models/howto.models.tsx
+++ b/src/models/howto.models.tsx
@@ -22,6 +22,7 @@ export interface IHowto extends IHowtoFormInput, IModerable {
// Comments were added in V2, old howto's may not have the property
comments?: IComment[]
total_downloads?: number
+ total_views?: number
mentions: UserMention[]
previousSlugs?: string[]
}
diff --git a/src/models/research.models.tsx b/src/models/research.models.tsx
index 4cd26bc372..9dd4c8d9e6 100644
--- a/src/models/research.models.tsx
+++ b/src/models/research.models.tsx
@@ -18,6 +18,7 @@ export namespace IResearch {
export interface Item extends FormInput {
updates: Update[]
_createdBy: string
+ total_views?: number
}
/** A research item update */
@@ -27,6 +28,7 @@ export namespace IResearch {
images: Array
videoUrl?: string
comments?: IComment[]
+ total_views?: number
}
export interface FormInput extends IModerable {
diff --git a/src/pages/Howto/Content/Howto/Howto.tsx b/src/pages/Howto/Content/Howto/Howto.tsx
index cf3174a741..c639b28b32 100644
--- a/src/pages/Howto/Content/Howto/Howto.tsx
+++ b/src/pages/Howto/Content/Howto/Howto.tsx
@@ -191,6 +191,7 @@ export class Howto extends React.Component<
<>
window.innerWidth ? 'column' : 'row'
interface IProps {
howto: IHowtoDB & { taglist: any }
@@ -44,59 +52,48 @@ interface IProps {
onUsefulClick: () => void
}
-interface IInjected extends IProps {
- howtoStore: HowtoStore
-}
-
-interface IState {
- fileDownloadCount: number | undefined
-}
+const HowtoDescription = ({ howto, loggedInUser, ...props }: IProps) => {
+ const [fileDownloadCount, setFileDownloadCount] = useState(
+ howto.total_downloads,
+ )
+ const [viewCount, setViewCount] = useState(howto.total_views)
+ const { stores } = useCommonStores()
-@inject('howtoStore')
-@observer
-export default class HowtoDescription extends PureComponent {
- private setFileDownloadCount = (val: number) => {
- this.setState({
- fileDownloadCount: val,
- })
+ const incrementDownloadCount = async () => {
+ const updatedDownloadCount = await stores.howtoStore.incrementDownloadCount(
+ howto._id,
+ )
+ setFileDownloadCount(updatedDownloadCount!)
}
- private incrementDownloadCount = async () => {
- const updatedDownloadCount =
- await this.injected.howtoStore.incrementDownloadCount(
- this.props.howto._id,
+
+ const incrementViewCount = async () => {
+ const sessionStorageArray = retrieveSessionStorageArray('howto')
+
+ if (!sessionStorageArray.includes(howto._id)) {
+ const updatedViewCount = await stores.howtoStore.incrementViewCount(
+ howto._id,
)
- this.setFileDownloadCount(updatedDownloadCount!)
+ addIDToSessionStorageArray('howto', howto._id)
+ setViewCount(updatedViewCount)
+ }
}
- private handleClick = async () => {
- const howtoDownloadCooldown = retrieveHowtoDownloadCooldown(
- this.props.howto._id,
- )
+
+ const handleClick = async () => {
+ const howtoDownloadCooldown = retrieveHowtoDownloadCooldown(howto._id)
if (
howtoDownloadCooldown &&
isHowtoDownloadCooldownExpired(howtoDownloadCooldown)
) {
- updateHowtoDownloadCooldown(this.props.howto._id)
- this.incrementDownloadCount()
+ updateHowtoDownloadCooldown(howto._id)
+ incrementDownloadCount()
} else if (!howtoDownloadCooldown) {
- addHowtoDownloadCooldown(this.props.howto._id)
- this.incrementDownloadCount()
- }
- }
- // eslint-disable-next-line
- constructor(props: IProps) {
- super(props)
- this.state = {
- fileDownloadCount: this.props.howto.total_downloads || 0,
+ addHowtoDownloadCooldown(howto._id)
+ incrementDownloadCount()
}
- this.handleClick = this.handleClick.bind(this)
- }
-
- get injected() {
- return this.props as IInjected
}
- private dateLastEditText(howto: IHowtoDB): string {
+ const dateLastEditText = (howto: IHowtoDB): string => {
const lastModifiedDate = format(new Date(howto._modified), 'DD-MM-YYYY')
const creationDate = format(new Date(howto._created), 'DD-MM-YYYY')
if (lastModifiedDate !== creationDate) {
@@ -106,239 +103,234 @@ export default class HowtoDescription extends PureComponent {
}
}
- public render() {
- const { howto, loggedInUser } = this.props
+ useEffect(() => {
+ incrementViewCount()
+ }, [howto._id])
- const iconFlexDirection =
- emStringToPx(theme.breakpoints[0]) > window.innerWidth ? 'column' : 'row'
- return (
+ return (
+
-
-
-
-
-
- {this.props.votedUsefulCount !== undefined && (
-
-
+
+
- )}
- {/* Check if pin should be moderated */}
- {this.props.needsModeration && (
-
-
-
+ Back
- )}
- {/* Check if logged in user is the creator of the how-to OR a super-admin */}
- {loggedInUser && isAllowToEditContent(howto, loggedInUser) && (
-
-
-
- )}
-
-
-
-
- {this.dateLastEditText(howto)}
-
-
- {/* HACK 2021-07-16 - new howtos auto capitalize title but not older */}
- {capitalizeFirstLetter(howto.title)}
-
-
- {howto.description}
-
-
-
-
-
-
+
+ {props.votedUsefulCount !== undefined && (
+
+
- {howto.steps.length} steps
-
-
-
+ )}
+
+
+
+
+
+ {/* Check if pin should be moderated */}
+ {props.needsModeration && (
+
+
-
- props.moderateHowto(false)}
/>
- {howto.difficulty_level}
-
-
-
- {howto.taglist &&
- howto.taglist.map((tag, idx) => (
-
- ))}
-
- {((howto.files && howto.files.length > 0) || howto.fileLink) && (
-
- {howto.fileLink && (
-
- )}
- {howto.files
- .filter(Boolean)
- .map(
- (file, index) =>
- file && (
-
- ),
- )}
- {typeof this.state.fileDownloadCount === 'number' && (
-
- {this.state.fileDownloadCount}
- {this.state.fileDownloadCount !== 1
- ? ' downloads'
- : ' download'}
-
- )}
)}
+ {/* Check if logged in user is the creator of the how-to OR a super-admin */}
+ {loggedInUser && isAllowToEditContent(howto, loggedInUser) && (
+
+
+
+ )}
-
-
+
- {howto.moderation !== 'accepted' && (
-
- )}
+
+ {dateLastEditText(howto)}
+
+
+ {/* HACK 2021-07-16 - new howtos auto capitalize title but not older */}
+ {capitalizeFirstLetter(howto.title)}
+
+
+ {howto.description}
+
+
+
+
+
+ {howto.steps.length} steps
+
+
+
+ {howto.time}
+
+
+
+ {howto.difficulty_level}
+
+
+
+ {howto.taglist &&
+ howto.taglist.map((tag, idx) => (
+
+ ))}
+
+ {((howto.files && howto.files.length > 0) || howto.fileLink) && (
+
+ {howto.fileLink && (
+
+ )}
+ {howto.files
+ .filter(Boolean)
+ .map(
+ (file, index) =>
+ file && (
+
+ ),
+ )}
+ {typeof fileDownloadCount === 'number' && (
+
+ {fileDownloadCount}
+ {fileDownloadCount !== 1 ? ' downloads' : ' download'}
+
+ )}
+
+ )}
- )
- }
+
+
+ {howto.moderation !== 'accepted' && (
+
+ )}
+
+
+ )
}
+
+export default HowtoDescription
diff --git a/src/pages/Research/Content/ResearchArticle.tsx b/src/pages/Research/Content/ResearchArticle.tsx
index 05a7f8cc51..2aa2290861 100644
--- a/src/pages/Research/Content/ResearchArticle.tsx
+++ b/src/pages/Research/Content/ResearchArticle.tsx
@@ -119,6 +119,7 @@ const ResearchArticle = observer((props: IProps) => {
void
}
-const ResearchDescription: React.FC = ({
- research,
- isEditable,
- ...props
-}) => {
+const ResearchDescription = ({ research, isEditable, ...props }: IProps) => {
const dateLastUpdateText = (research: IResearch.ItemDB): string => {
const lastModifiedDate = format(new Date(research._modified), 'DD-MM-YYYY')
const creationDate = format(new Date(research._created), 'DD-MM-YYYY')
@@ -40,6 +44,23 @@ const ResearchDescription: React.FC = ({
return ''
}
}
+ const store = useResearchStore()
+
+ const [viewCount, setViewCount] = useState(research.total_views)
+
+ const incrementViewCount = async () => {
+ const sessionStorageArray = retrieveSessionStorageArray('research')
+
+ if (!sessionStorageArray.includes(research._id)) {
+ const updatedViewCount = await store.incrementViewCount(research._id)
+ setViewCount(updatedViewCount)
+ addIDToSessionStorageArray('research', research._id)
+ }
+ }
+
+ useEffect(() => {
+ incrementViewCount()
+ }, [research._id])
return (
= ({
}}
>
-
+
{props.votedUsefulCount !== undefined && (
-
+
= ({
/>
)}
+
+
+
+
+
{/* Check if research should be moderated */}
{props.needsModeration && (
diff --git a/src/stores/Howto/howto.store.tsx b/src/stores/Howto/howto.store.tsx
index 3f4088cb7b..3dda98a461 100644
--- a/src/stores/Howto/howto.store.tsx
+++ b/src/stores/Howto/howto.store.tsx
@@ -185,11 +185,39 @@ export class HowtoStore extends ModuleStore {
total_downloads: totalDownloads! + 1,
}
- await this.updateHowtoItem(updatedHowto)
+ dbRef.set(
+ {
+ ...updatedHowto,
+ },
+ { keep_modified_timestamp: true },
+ )
+
return updatedHowto.total_downloads
}
}
+ public async incrementViewCount(howToID: string) {
+ const dbRef = this.db.collection(COLLECTION_NAME).doc(howToID)
+ const howToData = await toJS(dbRef.get())
+ const totalViews = howToData?.total_views || 0
+
+ if (howToData) {
+ const updatedHowto: IHowto = {
+ ...howToData,
+ total_views: totalViews! + 1,
+ }
+
+ dbRef.set(
+ {
+ ...updatedHowto,
+ },
+ { keep_modified_timestamp: true },
+ )
+
+ return updatedHowto.total_views
+ }
+ }
+
public updateSearchValue(query: string) {
this.searchValue = query
}
diff --git a/src/stores/Research/research.store.tsx b/src/stores/Research/research.store.tsx
index 6629cdd39c..2bcf46ec24 100644
--- a/src/stores/Research/research.store.tsx
+++ b/src/stores/Research/research.store.tsx
@@ -146,6 +146,28 @@ export class ResearchStore extends ModuleStore {
}
}
+ public async incrementViewCount(id: string) {
+ const dbRef = this.db.collection(COLLECTION_NAME).doc(id)
+ const researchData = await toJS(dbRef.get())
+ const totalViews = researchData?.total_views || 0
+
+ if (researchData) {
+ const updatedResearch: IResearchDB = {
+ ...researchData,
+ total_views: totalViews! + 1,
+ }
+
+ dbRef.set(
+ {
+ ...updatedResearch,
+ },
+ { keep_modified_timestamp: true },
+ )
+
+ return updatedResearch.total_views
+ }
+ }
+
public deleteResearchItem(id: string) {
this.db.collection('research').doc(id).delete()
}
diff --git a/src/stores/databaseV2/DocReference.tsx b/src/stores/databaseV2/DocReference.tsx
index 6198f444a6..4773372968 100644
--- a/src/stores/databaseV2/DocReference.tsx
+++ b/src/stores/databaseV2/DocReference.tsx
@@ -59,9 +59,9 @@ export class DocReference {
* If contains metadata fields (e.g. `_id`)
* then this will be used instead of generated id
*/
- async set(data: T) {
+ async set(data: T, options?: { keep_modified_timestamp: boolean }) {
const { serverDB, cacheDB } = this.clients
- const dbDoc: DBDoc = this._setDocMeta(data)
+ const dbDoc: DBDoc = this._setDocMeta(data, options)
await serverDB.setDoc(this.endpoint, dbDoc)
await cacheDB.setDoc(this.endpoint, dbDoc)
}
@@ -90,14 +90,19 @@ export class DocReference {
return this._setDocMeta(data)
}
- private _setDocMeta(data: any = {}): DBDoc {
+ private _setDocMeta(data: any = {}, options: any = {}): DBDoc {
const d = data
+ const o = options
+ const modifiedTimestamp = o.keep_modified_timestamp
+ ? d._modified
+ : new Date().toISOString()
+
return {
...d,
_created: d._created ? d._created : new Date().toISOString(),
_deleted: d._deleted ? d._deleted : false,
_id: this.id,
- _modified: new Date().toISOString(),
+ _modified: modifiedTimestamp,
}
}
diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts
new file mode 100644
index 0000000000..ee22b050a7
--- /dev/null
+++ b/src/utils/sessionStorage.ts
@@ -0,0 +1,14 @@
+export const retrieveSessionStorageArray = (key: string) => {
+ const viewsArray: string | null = sessionStorage.getItem(key)
+ if (typeof viewsArray === 'string') {
+ return JSON.parse(viewsArray)
+ } else {
+ return []
+ }
+}
+
+export const addIDToSessionStorageArray = (key: string, value: string) => {
+ const sessionStorageArray = retrieveSessionStorageArray(key)
+ sessionStorageArray.push(value)
+ sessionStorage.setItem(key, JSON.stringify(sessionStorageArray))
+}