Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull sanity data for curated instructor pages #602

Merged
merged 14 commits into from
May 4, 2021
Merged
2 changes: 2 additions & 0 deletions src/components/search/instructors/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SearchLaurieBarth from './laurie-barth'
import SearchFlavioCorpa from './flavio-corpa'
import SearchHirokoNishimura from './hiroko-nishimura'
import SearchChrisBiscardi from './chris-biscardi'
import SearchStephanieEckles from './stephanie-eckles'

const InstructorsIndex: any = {
'dan-abramov': SearchDanAbramov,
Expand All @@ -14,6 +15,7 @@ const InstructorsIndex: any = {
'flavio-corpa': SearchFlavioCorpa,
'hiro-nishimura': SearchHirokoNishimura,
'chris-biscardi': SearchChrisBiscardi,
'stephanie-eckles': SearchStephanieEckles,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add stephanie to the instructors index for curated pages

}

export default InstructorsIndex
1 change: 0 additions & 1 deletion src/components/search/instructors/instructor-essential.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ const SearchInstructorEssential: FunctionComponent<InstructorProps> = ({
</div>
{CTAComponent ? CTAComponent : <DefaultCTA location={location} />}
</div>

{children}
</div>
)
Expand Down
284 changes: 284 additions & 0 deletions src/components/search/instructors/stephanie-eckles/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import React, {FunctionComponent} from 'react'
import SearchInstructorEssential from '../instructor-essential'
import Image from 'next/image'
import {get} from 'lodash'
import Link from 'next/link'
import groq from 'groq'
import Markdown from 'react-markdown'

import Card, {CardResource} from 'components/pages/home/card'

import {bpMinMD} from 'utils/breakpoints'
import {track} from 'utils/analytics'
import ExternalTrackedLink from 'components/external-tracked-link'

export default function SearchStephanieEckles({instructor}: {instructor: any}) {
const combinedInstructor = {...instructor}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could actually be combined at a higher level


const {projects, courses} = instructor
const [
primaryCourse,
secondCourse,
thirdCourse,
fourthCourse,
] = courses.resources

return (
<div>
<SearchInstructorEssential
instructor={combinedInstructor}
CTAComponent={
<CssFormStyling
resource={primaryCourse}
location="Stephanie Eckles instructor page"
/>
}
/>
<section className="grid grid-cols-4 gap-3 -mt-10 mb-10 pb-10 xl:px-0 px-5 max-w-screen-xl mx-auto dark:bg-gray-900 w-full">
<ProjectStack className="col-span-1" data={projects.resources} />
<div className="col-span-3 grid grid-cols-2 gap-3">
<CardHorizontal className="col-span-2" resource={secondCourse} />
<CardHorizontal className="col-span-1" resource={thirdCourse} />
<CardHorizontal className="col-span-1" resource={fourthCourse} />
</div>
</section>
</div>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

display courses and projects on page

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This layout needs work at all breakpoints. Using the CardHorizontal component doesn't seem to be the right approach for this layout.

CleanShot 2021-04-29 at 12 54 13@2x

)
}

export const stephanieEcklesQuery = groq`*[_type == 'resource' && slug.current == "stephanie-eckles-landing-page"][0]{
'projects': resources[slug.current == 'instructor-landing-page-projects'][0]{
resources[]{
title,
'path': url,
description,
image
}
},
'courses': resources[slug.current == 'instructor-landing-page-featured-courses'][0]{
resources[]->{
title,
'description': summary,
path,
byline,
image,
'background': images[label == 'feature-card-background'][0].url,
'instructor': collaborators[]->[role == 'instructor'][0]{
'name': person->.name
},
}
},
}`
Comment on lines +53 to +75
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanity query


type CardProps = {
data: CardResource
className?: string
memberTitle?: string
}

const ProjectStack: FunctionComponent<any> = ({data, className}) => {
return (
<Card className={className}>
<>
<h2 className="uppercase font-semibold text-xs mb-1 text-gray-700 dark:text-gray-300">
Stephanie's Projects
</h2>
<hr />
<div>
<ul>
{data.map((item: any) => {
const {description, title, image, path} = item
return (
<li key={path}>
<div className="space-y-3">
{path && (
<Link href={path}>
<a
onClick={() => {
track('clicked instructor project resource', {
resource: path,
linkType: 'image',
location,
})
}}
className="flex sm:flex-row flex-col sm:space-x-5 space-x-0 space-y-5 sm:space-y-5 items-center sm:text-left text-center overflow-x-scroll"
tabIndex={-1}
>
<div className="block flex-shrink-0 sm:w-auto">
{image && (
<Image
src={get(image, 'src', image)}
width="40"
height="40"
alt={`illustration for ${title}`}
/>
)}
</div>
<div className="flex flex-col justify-center sm:items-start items-center space-y-1">
<h2 className="text-lg font-bold leading-tighter">
{title}
</h2>
<p className="prose dark:prose-dark dark:prose-dark-sm prose-sm max-w-none">
{description}
</p>
</div>
</a>
</Link>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description is awkward and lacks legibility. I would consider exploring expanding the project section or a smaller font.

image

<hr />
</div>
</li>
)
})}
</ul>
</div>
</>
</Card>
)
}

export const CardHorizontal: FunctionComponent<{
resource: CardResource
className?: string
location?: string
}> = ({resource, className = 'border-none my-4', location = 'home'}) => {
return (
<Card className={className}>
<>
<div className="flex sm:flex-row flex-col sm:space-x-5 space-x-0 sm:space-y-0 space-y-5 items-center sm:text-left text-center">
{resource.image && (
<Link href={resource.path}>
<a
onClick={() => {
track('clicked resource', {
resource: resource.path,
linkType: 'image',
location,
})
}}
className="block flex-shrink-0 sm:w-auto m:w-24 w-36"
tabIndex={-1}
>
<Image
src={get(resource.image, 'src', resource.image)}
width={160}
height={160}
layout="fixed"
className="object-cover rounded-md"
alt={`illustration for ${resource.title}`}
/>
</a>
</Link>
)}
<div className="flex flex-col justify-center sm:items-start items-center">
<h2 className=" uppercase font-semibold text-xs tracking-tight text-gray-700 dark:text-gray-300 mb-1">
{resource.name}
</h2>
<Link href={resource.path}>
<a
onClick={() => {
track('clicked resource', {
resource: resource.path,
linkType: 'text',
location,
})
}}
className="hover:text-blue-600 dark:hover:text-blue-300"
>
<h3 className="text-xl font-bold leading-tighter">
{resource.title}
</h3>
</a>
</Link>
<div className="text-xs text-gray-600 dark:text-gray-300 mb-2 mt-1">
{resource.byline}
</div>
<Markdown
source={resource.description || ''}
className="prose dark:prose-dark dark:prose-dark-sm prose-sm max-w-none"
/>
</div>
</div>
</>
</Card>
)
}

const CssFormStyling: React.FC<{location: string; resource: any}> = ({
location,
resource,
}) => {
const {path, title, byline, description, image, background} = resource
return (
<ExternalTrackedLink
eventName="clicked CSS page CTA"
params={{location}}
className="block md:col-span-4 rounded-md w-full h-full overflow-hidden border-0 border-gray-100 relative text-center"
href={path}
>
<div
className="md:-mt-5 flex items-center justify-center bg-white dark:bg-gray-900 text-white overflow-hidden rounded-b-lg md:rounded-t-none rounded-t-lg shadow-sm"
css={{
[bpMinMD]: {
minHeight: 477,
},
}}
>
<div className="absolute top-0 left-0 bg-gradient-to-r from-yellow-500 to-lightBlue-500 w-full h-2 z-20" />
<div className="relative z-10 px-5 sm:py-16 py-10 sm:text-left text-center">
<div className="space-y-5 mx-auto flex items-center justify-center max-w-screen-xl">
<div className="flex flex-col items-center justify-center sm:space-x-5 sm:space-y-0 space-y-5">
<div className="flex-shrink-0">
<Link href={path}>
<a
tabIndex={-1}
onClick={() =>
track('clicked jumbotron resource', {
resource: path,
linkType: 'image',
})
}
>
<Image
quality={100}
src={get(image, 'src', image)}
width={250}
height={250}
alt={get(image, 'alt', `illustration for ${title}`)}
/>
</a>
</Link>
</div>
<div className="flex flex-col sm:items-start items-center">
<h2 className="text-xs text-gray-900 dark:text-white uppercase font-semibold mb-2">
{byline}
</h2>
<Link href={path}>
<a
className="text-xl font-extrabold leading-tighter text-gray-900 dark:text-white hover:text-blue-300"
onClick={() =>
track('clicked jumbotron resource', {
resource: path,
linkType: 'text',
})
}
>
<h1>{title}</h1>
</a>
</Link>
<p className="mt-4 text-gray-900 dark:text-white">
{description}
</p>
</div>
</div>
</div>
</div>
<img
className="absolute top-0 left-0 z-0 w-full"
src={background}
alt=""
/>
</div>
</ExternalTrackedLink>
)
}
26 changes: 26 additions & 0 deletions src/lib/instructors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {request} from 'graphql-request'
import {sanityClient} from 'utils/sanity-client'
import groq from 'groq'
import {stephanieEcklesQuery} from 'components/search/instructors/stephanie-eckles'
import config from './config'

export type Instructor = {
Expand Down Expand Up @@ -36,3 +39,26 @@ export async function loadInstructor(slug: string) {

return instructor
}

const sanityInstructorHash = {
'stephanie-eckles': stephanieEcklesQuery,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this pattern!

}

type SelectedInstructor = keyof typeof sanityInstructorHash

const canLoadSanityInstructor = (
selectedInstructor: string,
): selectedInstructor is SelectedInstructor => {
const keyNames = Object.keys(sanityInstructorHash)

return keyNames.includes(selectedInstructor)
}

export const loadSanityInstructor = async (selectedInstructor: string) => {
if (!canLoadSanityInstructor(selectedInstructor)) return

const query = sanityInstructorHash[selectedInstructor]
if (!query) return

return await sanityClient.fetch(query)
}
Comment on lines +42 to +64
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theianjones helped me implement this.

loadSanityInstructor uses a type guard (canLoadSanityInstructor) to check that the instructor slug (the one that will be passed in via getServerSideProps) is present in the defined sanityInstructorHash as a key. If it is, that key is associated with the query that it will call.

This makes sure that we are only querying for instructor data on the pages that we want to load sanity data onto.

10 changes: 9 additions & 1 deletion src/pages/q/[[...all]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {createUrl, parseUrl, titleFromPath} from 'lib/search-url-builder'
import {isEmpty, get, first, isArray} from 'lodash'
import queryParamsPresent from 'utils/query-params-present'

import {loadInstructor} from 'lib/instructors'
import {loadInstructor, loadSanityInstructor} from 'lib/instructors'
import nameToSlug from 'lib/name-to-slug'

import getTracer from 'utils/honeycomb-tracer'
Expand Down Expand Up @@ -181,6 +181,7 @@ export const getServerSideProps: GetServerSideProps = async function ({
})

let initialInstructor = null
let sanityInstructor = null
let initialTopic = null

const {rawResults, state} = resultsState
Expand Down Expand Up @@ -211,6 +212,13 @@ export const getServerSideProps: GetServerSideProps = async function ({
)
try {
initialInstructor = await loadInstructor(instructorSlug)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here’s where I’d consider a single “load instructor” that loads from all sources

sanityInstructor = await loadSanityInstructor(instructorSlug)

initialInstructor = {
...initialInstructor,
...sanityInstructor,
}
Comment on lines +220 to +225
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the data is being overridden but it makes sense to me that sanity data would take precedence over the initialInstructor data

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this pattern makes a lot of sense so we don't have two+ data sources at the component, we just bring it together at the server level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are using a stale-while-revalidate cache header, it should be performant!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it was also super easy to refactor

} catch (error) {
console.error(error)
}
Expand Down