-
Notifications
You must be signed in to change notification settings - Fork 193
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
Changes from 6 commits
8618440
782882e
79c8f54
d9e2f5b
131f3d1
b33278d
9100b3a
69b01a2
8b8df4c
02e682c
44cc2c1
21cc2c0
c60e68d
d769a98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. display courses and projects on page There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
) | ||
} | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
<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> | ||
) | ||
} |
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 = { | ||
|
@@ -36,3 +39,26 @@ export async function loadInstructor(slug: string) { | |
|
||
return instructor | ||
} | ||
|
||
const sanityInstructorHash = { | ||
'stephanie-eckles': stephanieEcklesQuery, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @theianjones helped me implement this.
This makes sure that we are only querying for instructor data on the pages that we want to load sanity data onto. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -181,6 +181,7 @@ export const getServerSideProps: GetServerSideProps = async function ({ | |
}) | ||
|
||
let initialInstructor = null | ||
let sanityInstructor = null | ||
let initialTopic = null | ||
|
||
const {rawResults, state} = resultsState | ||
|
@@ -211,6 +212,13 @@ export const getServerSideProps: GetServerSideProps = async function ({ | |
) | ||
try { | ||
initialInstructor = await loadInstructor(instructorSlug) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since we are using a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
|
There was a problem hiding this comment.
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