Skip to content

Commit

Permalink
"Expiration Date" feature for Community Hero and Events (#1281)
Browse files Browse the repository at this point in the history
* Skip rendering events if none are in the future.

* Add moment.js to dependencies and run yarn upgrade

Gatsby already depends on moment, this just lets us use it in our code. This
branch uses it to work with expiry dates.

The yarn upgrade just affects yarn.lock, any pinned packages (like the image
ones we need for caching to work) are left alone.

* Add support for community page expiration dates

The hero and events in `community` now accept an optional `expires` field, which
stops the item in question from rendering when the site is built past that date.

This is done through a new shared expiration helper and its child function
`isExpired`, which uses the `moment` library that Gatsby already depends on to
determine if an arbitrary content object is expired.

Any object with a `date` and/or `expires` key that can be parsed by `moment` is
usable in `isExpired`. Objects without an `expires` will be treated as if they
have an `expires` 7 days after their `date`; this is configurable in a constant.

Also, the expiration helper is in ES5 so it can be used on both during the build
and at runtime.

* Add an expiration date to the current Hero data

* new header

* Create Community/Events and JsonFile models

These enable data previously imported from JSON files to run through
the Gatsby GraphQL interface, allowing us to do arbitrary transformations
and queries of this data.

Building on top of these models allows for more complex features to be
built either on top or in place of the previously imported JSON files.

This is used in expiration dates to do all date parsing and comparisons
at build time, allowing us to query for non-expired events in whatever order
we want (including the original source order) in either direction.

The JsonFiles model differs from `gatsby-source-json` by taking the approach of
simply exposing parsed JSON content to other models to consume instead of
inferring a node schema. It's particularly useful for our large JSON files, but
also simpler to adapt to the possibility of us breaking those files up or even
getting the data from another source. It actually came from one of my personal
branches where I make the sidebar run on this logic, but that ended up being
unnecessary without a feature to justify the change. It will likely be reused in
the sidebar overhaul.

The Community model will handle parsing the community JSON data and reading it
into GraphQL. This becomes much easier by moving the `data.json` from the
`Community` component into the `content` folder. I know we value co-location,
but co-locating this file would mean pointing `gatsby-source-filesystem` at our
entire source folder. It makes a lot of sense for this to be in `content` as
well.

This also changes the expiration helpers, splitting the old
`isExpired({date,expires})` into `getExpirationDate({date,expires})` and
`isExpired(expires)`. This allows consumers to get the exact expiration date as
well as be able to use a boolean.

The new expiration helpers also consider `expires=false` to mean the given
object never expires. These objects will return `false` when given to
`getExpirationDate` and `isExpired` will immediately return `false` when given
`false`. Since the result of both helpers are added to the GraphQL node, these
implementation details may never come up.

* white background header

* skinny header

* Move Community data into GraphQL

This allows for better build-time processing, enabling things like
build-time querying for non-expired Events and Heroes.

Instead of importing the JSON file directly, Community content data is now
accessed through a custom React hook that wraps `useStaticQuery` and massages
the data into a more usable form.

Events and Heroes are given custom node types, and everything else in the
community json file is carried over in a unique "rest" node that exposes the
rest of the properties in JSON.

The Hero and Event components on the community page are adapted to the new
custom nodes, and the other Community components are successfully working
through the Rest node- they can be truly ported later or left as-is if they
don't need to change.

* Change Community Hero link to fragment

- url changed to `#subscribe` fragment in `data.json`
- hard-coded `target="_blank"` removed from component
- `scroll-behavior: smooth` added to Page base CSS so Fragment links
  smooth-scroll.

* mobile banner skinny

* Add Hero from the header branch as a non-expiring fallback.

This also includes the fixes to `target`. This should be handled in the `Link`
wrapper, so we don't have to hard-code it in the Hero component.

* remove border on mobile

* no border for real

* update user generated content links

* update ambassador link

* Create, export, and import interfaces to fix TS issues

This outfits the `useCommunityData` hook with TS definitions which allow it to
pass `yarn ts-lint`. I'm sure they'll have to be changed in the future, but now
that we have them refactoring will be less of an issue.

* Revert global CSS smooth scrolling

* Change DVC Ambassador link to root-relative _self

* Delete banner-mobile.png

* Delete banner.png

* Delete fowler_icon.ico

* Delete ml-axis-of-change.png

* Delete donuts.png

* Add smooth scrolling to local fragment Hero

* Fix TS issues

* Remove duration option which broke animation

I'm probably just using it wrong.

* Change false string to literal false in community.json

* Improve and fix schema

- Add a GraphQL type definition for Hero entries
- Make expiration date helpers safer for Gatsby schema,
  as that's the only place they're used.

* move marcel pic

* Integrate smooth scroll into regular link component

Instead of a separate component, the Link component now accepts a
`scrollOptions` object that passes options to the internal local fragment scroll
handler.

This means you can provide a `scrollOptions` object to a link to affect its
scrolling behavior, and this is compatible with links that could be of any type
as the `scrollOptions` object is only read when the `Link` wrapper detects a
local fragment.

This commit also removes special `Link` behavior/imports from the `Hero`
component.

* Rename default banner images to a more generic name

* remove unused import from community model

* Fix unrelated unused imports

These were reported by `yarn ts-lint`, and this commit cleans all warnings from
the output.

* Fix handling of 0 usable events

- Remove test `expires: false`
- Make hook return null if there are no events, but a padded 3-length array in
  any other case.
- Fix oversight where `expires` Moment wasn't being converted to `Date` for
  Gatsby.

* fix TS issues

* Fix link in community JSON data

There was a duplicate link before that was not only wrong, but caused a
duplicate key issue because we currently use the URL as the key for user blog
post entries.

* Remove TODO

We don't do this here, whoops!

* Change "Women in San Diego" event date

* Display a message in a situation with no events.

* Simplify getExpirationDate

* Explicitly define sourceIndex on Hero items

* Change events placeholder to use a common "gray text" class

I couldn't quickly find any existing class to compose over, so I made one in the
main Community module.

* Write tests for Expiration helpers

* Fix a typo in Event css

* Change shared gray text class to be named "gray"

This avoids a css lint issue, and arguably makes more sense.

Co-authored-by: Elle <[email protected]>
  • Loading branch information
rogermparent and elleobrien authored May 15, 2020
1 parent dc0cb53 commit 5e5a993
Show file tree
Hide file tree
Showing 22 changed files with 2,629 additions and 2,275 deletions.
17 changes: 10 additions & 7 deletions src/components/Community/data.json → content/community.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"hero": {
"pictureDesktop": "/img/community/header_mailing_list_skinny.png",
"pictureMobile": "/img/community/mobile_header_mailing_list_skinny.png",
"url": "#subscribe"
},
"hero": [
{
"pictureDesktop": "/img/community/default-banner.png",
"pictureMobile": "/img/community/default-mobile-banner.png",
"url": "#subscribe",
"expires": false
}
],
"section": {
"contribute": {
"title": "Contribute",
Expand Down Expand Up @@ -52,7 +55,7 @@
"pictureUrl": "/img/community/ugc/medium_logo.png"
},
{
"url": "https://blog.codecentric.de/en/2020/01/remote-training-gitlab-ci-dvc/",
"url": "https://mribeirodantas.xyz/blog/index.php/2020/03/05/r-dvc-and-rmarkdown/",
"title": "Manage your Data Science Project in R",
"author": "Marcel Ribeiro-Dantas",
"date": "2020-03-05",
Expand All @@ -72,7 +75,7 @@
"title": "Women in Data Science San Diego",
"description": "Elle O'Brien is talking about data catalogs and feature stores.",
"city": "San Diego",
"date": "2020-05-10"
"date": "2020-03-27"
},
{
"url": "https://www.mlprague.com/#schedule-saturday",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"iso-url": "^0.4.7",
"isomorphic-fetch": "^2.2.1",
"lodash": "^4.17.15",
"moment": "^2.25.3",
"nanoid": "^3.0.2",
"node-cache": "^5.1.0",
"perfect-scrollbar": "^1.5.0",
Expand Down
3 changes: 0 additions & 3 deletions scripts/deploy-with-s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
'use strict'
require('dotenv').config()
const path = require('path')
const crypto = require('crypto')
const PRODUCTION_PREFIX = 'dvc-org-prod'

const { DEPLOY_OPTIONS } = process.env
Expand Down Expand Up @@ -61,8 +60,6 @@ const cacheDirs = [
['.cache', '-cache/']
]

const fs = require('fs')

const { s3Prefix, withEntries, prefixIsEmpty } = require('./s3-utils')
const { move } = require('fs-extra')
const { downloadAllFromS3, uploadAllToS3, cleanAllLocal } = withEntries(
Expand Down
2 changes: 1 addition & 1 deletion scripts/s3-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const path = require('path')
const s3 = require('s3-client')
const { s3Prefix, s3Bucket } = require('../src/server/config')
const { remove, move, ensureDir } = require('fs-extra')
const { remove, ensureDir } = require('fs-extra')

const {
AWS_REGION,
Expand Down
12 changes: 9 additions & 3 deletions src/components/Community/Contribute/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@ import CommunityBlock from '../Block'
import CommunitySection from '../Section'
import { logEvent } from '../../../utils/front/ga'

import data from '../data.json'
import { useCommunityData } from '../../../utils/front/community'
import sharedStyles from '../styles.module.css'

const { description, mobileDescription, title } = data.section.contribute

const logPR = (): void => logEvent('community', 'contribute-pr')
const logBlogpost = (): void => logEvent('community', 'contribute-blogpost')
const logTalk = (): void => logEvent('community', 'contribute-talk')
const logAmbassador = (): void => logEvent('community', 'contribute-ambassador')

const Contribute: React.FC<{ theme: ICommunitySectionTheme }> = ({ theme }) => {
const {
rest: {
section: {
contribute: { description, mobileDescription, title }
}
}
} = useCommunityData()

return (
<LayoutWidthContainer className={sharedStyles.wrapper}>
<CommunitySection
Expand Down
45 changes: 23 additions & 22 deletions src/components/Community/Events/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import Link from '../../Link'
import Block from '../Block'
import Section from '../Section'
import { logEvent } from '../../../utils/front/ga'
import { useCommunityData } from '../../../utils/front/community'

import data from '../data.json'
import sharedStyles from '../styles.module.css'
import styles from './styles.module.css'

interface IEvent {
export interface IEvent {
theme: ICommunitySectionTheme
city: string
date: string
Expand All @@ -23,19 +23,6 @@ interface IEvent {
url: string
}

const { description, mobileDescription, title } = data.section.events
const { events } = data
const eventsItems = ((): Array<IEvent | null> => {
const items: Array<IEvent | null> = events.slice(0, 3) as Array<IEvent>
const itemLength = items.length

for (let i = itemLength; i < 3; i++) {
items.push(null)
}

return items
})()

const Event: React.FC<IEvent> = ({
theme,
city,
Expand Down Expand Up @@ -97,9 +84,14 @@ const Event: React.FC<IEvent> = ({
}

const Events: React.FC<{ theme: ICommunitySectionTheme }> = ({ theme }) => {
if (!events.length) {
return null
}
const {
events,
rest: {
section: {
events: { description, mobileDescription, title }
}
}
} = useCommunityData()

return (
<LayoutWidthContainer className={sharedStyles.wrapper}>
Expand All @@ -112,11 +104,20 @@ const Events: React.FC<{ theme: ICommunitySectionTheme }> = ({ theme }) => {
title={title}
>
<div className={sharedStyles.items}>
{eventsItems.map((event, key) => (
<div className={sharedStyles.item} key={key}>
{event && <Event {...event} theme={theme} key={event.url} />}
{events ? (
events.map((event, key) => (
<div className={sharedStyles.item} key={key}>
{event && <Event {...event} theme={theme} key={event.url} />}
</div>
))
) : (
<div className={styles.eventsPlaceholder}>
No upcoming events. Subscribe to be up to date!{' '}
<span role="img" aria-label="Subscribe below">
👇
</span>
</div>
))}
)}
</div>
</Section>
</LayoutWidthContainer>
Expand Down
11 changes: 11 additions & 0 deletions src/components/Community/Events/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,14 @@
display: block;
margin: -10px -20px 0;
}

.eventsPlaceholder {
composes: gray from '../styles.module.css';
text-align: center;
margin: auto;
font-size: 1rem;

@media screen and (min-width: 480px) {
font-size: 1.25rem;
}
}
59 changes: 20 additions & 39 deletions src/components/Community/Hero/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,44 @@ import React from 'react'

import LayoutWidthContainer from '../../LayoutWidthContainer'
import ShowOnly from '../../ShowOnly'
import Link, { ILinkProps } from '../../Link'
import Link from '../../Link'
import { useCommunityData } from '../../../utils/front/community'
import { logEvent } from '../../../utils/front/ga'
import { scrollIntoLayout } from '../../../utils/front/scroll'

import data from '../data.json'
import styles from './styles.module.css'

const logHero = (): void => logEvent('community', 'hero')

// This special link component will smooth-scroll on local fragment links
const MaybeSmoothLink: React.FC<ILinkProps> = props => {
const { href, children } = props
if (href.startsWith('#')) {
// Intercept local fragment links and turn them into a special
// smooth-scrolling `a` element
return (
<Link
{...props}
onClick={(): void => {
logHero()
scrollIntoLayout(document.getElementById(href.slice(1)), {
smooth: true
})
}}
>
{children}
</Link>
)
} else {
// Pass through all props to a normal link otherwise
return <Link {...props} />
}
export interface IHero {
url: string
pictureDesktop: string
pictureMobile: string
}

const Hero: React.FC = () => {
if (!data.hero) {
const { hero } = useCommunityData()

if (!hero) {
return null
}

return (
<LayoutWidthContainer className={styles.container}>
<MaybeSmoothLink className={styles.link} href={data.hero.url}>
<Link
className={styles.link}
href={hero.url}
onClick={logHero}
scrollOptions={{
smooth: true
}}
>
<ShowOnly on="desktop">
<img
className={styles.picture}
src={data.hero.pictureDesktop}
alt=""
/>
<img className={styles.picture} src={hero.pictureDesktop} alt="" />
</ShowOnly>
<ShowOnly on="mobile">
<img
className={styles.picture}
src={data.hero.pictureMobile}
alt=""
/>
<img className={styles.picture} src={hero.pictureMobile} alt="" />
</ShowOnly>
</MaybeSmoothLink>
</Link>
</LayoutWidthContainer>
)
}
Expand Down
17 changes: 12 additions & 5 deletions src/components/Community/Learn/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ import Section from '../Section'
import { logEvent } from '../../../utils/front/ga'
import { getFirstPage } from '../../../utils/shared/sidebar'
import { useCommentsCount } from '../../../utils/front/api'
import { useCommunityData } from '../../../utils/front/community'
import getPosts from '../../../queries/posts'
import { pluralizeComments } from '../../../utils/front/i18n'

import data from '../data.json'
import sharedStyles from '../styles.module.css'
import styles from './styles.module.css'

const docsPage = getFirstPage()
const { description, mobileDescription, title } = data.section.learn
const { documentation, userContent } = data

const logPostAll = (): void => logEvent('community', 'blog', 'all')
const logDocumentationAll = (): void =>
Expand Down Expand Up @@ -92,7 +90,7 @@ const BlogPost: React.FC<ICommunityBlogPost> = ({
)
}

interface ICommunityUserContentProps {
export interface ICommunityUserContentProps {
author: string
color: string
date: string
Expand Down Expand Up @@ -147,7 +145,7 @@ const UserContent: React.FC<ICommunityUserContentProps> = ({
)
}

interface ICommunityDocumentationProps {
export interface ICommunityDocumentationProps {
color: string
description: string
title: string
Expand Down Expand Up @@ -182,6 +180,15 @@ const Documentation: React.FC<ICommunityDocumentationProps> = ({

const Learn: React.FC<{ theme: ICommunitySectionTheme }> = ({ theme }) => {
const posts = getPosts()
const {
rest: {
documentation,
userContent,
section: {
learn: { description, mobileDescription, title }
}
}
} = useCommunityData()

return (
<LayoutWidthContainer className={sharedStyles.wrapper}>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Community/Meet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ import {
IDiscussTopic
} from '../../../utils/front/api'

import data from '../data.json'
import { useCommunityData } from '../../../utils/front/community'
import sharedStyles from '../styles.module.css'
import styles from './styles.module.css'

const { description, mobileDescription, title } = data.section.meet
const logIssueAll = (): void => logEvent('community', 'issue', 'all')
const logTopicAll = (): void => logEvent('community', 'topic', 'all')
const logDiscord = (): void => logEvent('community', 'discord')
Expand Down Expand Up @@ -99,6 +98,8 @@ const Issue: React.FC<{ color: string } & IGithubIssue> = ({
}

const Meet: React.FC<{ theme: ICommunitySectionTheme }> = ({ theme }) => {
const data = useCommunityData().rest
const { description, mobileDescription, title } = data.section.meet
const { error: issuesError, ready: issuesReady, result: issues } = useIssues()
const { error: topicsError, ready: topicsReady, result: topics } = useTopics()

Expand Down
2 changes: 1 addition & 1 deletion src/components/Community/Section/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { screens } from '../../../../config/postcss/media'

import styles from './styles.module.css'

interface ICommunitySection {
export interface ICommunitySection {
anchor: string
background?: string
color: string
Expand Down
5 changes: 5 additions & 0 deletions src/components/Community/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.gray {
color: #838d93;
color: var(--color-gray);
}

.content {
@media (--xs-scr) {
padding-bottom: 0;
Expand Down
Loading

0 comments on commit 5e5a993

Please sign in to comment.