add officer order, some UI components

This commit is contained in:
Michael Fatemi 2021-01-05 22:58:56 -05:00
parent 0339c587a2
commit 4e93c4a948
24 changed files with 294 additions and 151 deletions

View File

@ -2,6 +2,7 @@ import React from 'react';
import ArticleRow from './ArticleRow'; import ArticleRow from './ArticleRow';
import sanity from '../sanity'; import sanity from '../sanity';
import '../css/article.css'; import '../css/article.css';
import BlueButton from './BlueButton';
export default function ArticleList() { export default function ArticleList() {
let [articles, setArticles] = React.useState<SGA.ArticleDocument[]>([]); let [articles, setArticles] = React.useState<SGA.ArticleDocument[]>([]);
@ -40,27 +41,30 @@ export default function ArticleList() {
return null; return null;
} }
let bottomComponent: any;
if (reachedEnd) {
bottomComponent = <div>No more articles to show</div>;
} else {
bottomComponent = (
<BlueButton
onClick={() => {
let { publish_date, title } = articles[articles.length - 1];
addArticles(publish_date, title);
}}
>
Load more articles
</BlueButton>
);
}
const articleList = articles.map((article) => (
<ArticleRow key={article._id} article={article} />
));
return ( return (
<div> <div>
{articles.map((article) => { {articleList}
return <ArticleRow key={article._id} article={article} />; <div className='text-center'>{bottomComponent}</div>
})}
<div className='text-center'>
{!reachedEnd ? (
<button
className='blue-button'
onClick={() => {
let lastArticle = articles[articles.length - 1];
addArticles(lastArticle.publish_date, lastArticle.title);
}}
>
Load more articles
</button>
) : (
<div>No more articles to show</div>
)}
</div>
</div> </div>
); );
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import imageUrl from '../imageUrl'; import imageUrl from '../lib/imageUrl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import '../css/article.css'; import '../css/article.css';

View File

@ -0,0 +1,23 @@
import BlockContent from '@sanity/block-content-to-react';
export default function BlockContentWithExternalLinks({
blocks,
}: {
blocks: any[];
}) {
return (
<div
ref={(ref) => {
// When this element loads, convert all the links to have target="_blank."
// This ensures that the links open in a new tab
if (ref) {
ref.querySelectorAll('a').forEach((link) => {
link.target = '_blank';
});
}
}}
>
<BlockContent blocks={blocks} />
</div>
);
}

View File

@ -0,0 +1,15 @@
import { MouseEventHandler } from 'react';
export default function BlueButton({
onClick,
children,
}: {
onClick?: MouseEventHandler;
children: React.ReactNode;
}) {
return (
<button onClick={onClick} className='blue-button'>
{children}
</button>
);
}

View File

@ -0,0 +1,23 @@
import { Link } from 'react-router-dom';
export default function BlueButtonLink({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
if (!href.startsWith('http')) {
return (
<Link to={href} className='blue-button'>
{children}
</Link>
);
} else {
return (
<a href={href} className='blue-button'>
{children}
</a>
);
}
}

View File

@ -0,0 +1,3 @@
export default function Centered({ children }: { children: React.ReactNode }) {
return <div style={{ textAlign: 'center' }}>{children}</div>;
}

View File

@ -1,5 +1,5 @@
import BlockContent from '@sanity/block-content-to-react'; import BlockContent from '@sanity/block-content-to-react';
import imageUrl from '../imageUrl'; import imageUrl from '../lib/imageUrl';
import '../css/initiative.css'; import '../css/initiative.css';
export default function InitiativeColumn({ name, thumbnail, content }) { export default function InitiativeColumn({ name, thumbnail, content }) {

View File

@ -1,5 +1,4 @@
import React from 'react'; import imageUrl from '../lib/imageUrl';
import imageUrl from '../imageUrl';
import BlockContent from '@sanity/block-content-to-react'; import BlockContent from '@sanity/block-content-to-react';
import '../css/article.css'; import '../css/article.css';

View File

@ -0,0 +1,15 @@
import { Link } from 'react-router-dom';
export default function LocalLinkClickable({
to,
children,
}: {
to: string;
children: React.ReactNode;
}) {
return (
<Link to={to} className='clickable-link'>
{children}
</Link>
);
}

View File

@ -1,4 +1,4 @@
import imageUrl from '../imageUrl'; import imageUrl from '../lib/imageUrl';
import '../css/article.css'; import '../css/article.css';
export default function MemberRow({ member }: { member: SGA.MemberDocument }) { export default function MemberRow({ member }: { member: SGA.MemberDocument }) {

View File

@ -0,0 +1,9 @@
export default function ParagraphHeader({
children,
}: {
children: React.ReactNode;
}) {
return (
<h2 style={{ marginTop: '4rem', marginBottom: '1.5rem' }}>{children}</h2>
);
}

View File

@ -0,0 +1,7 @@
export default function PrimaryHeader({
children,
}: {
children: React.ReactNode;
}) {
return <h1 className='my-4'>{children}</h1>;
}

View File

@ -0,0 +1,8 @@
import useQuery from './useQuery';
export default function useCommittee(committee: string) {
return useQuery<SGA.MemberDocument[]>(
`*[_type == 'member' && committee == $committee]`,
{ committee }
);
}

View File

@ -0,0 +1,12 @@
import { useEffect, useState } from 'react';
import sanity from '../sanity';
export default function useNewsArticle(articleId: string) {
let [article, setArticle] = useState<SGA.ArticleDocument>(null!);
useEffect(() => {
sanity.fetch('*[_id == $articleId] [0]', { articleId }).then(setArticle);
}, [articleId]);
return article;
}

View File

@ -1,5 +1,5 @@
import { default as ImageUrlBuilder } from '@sanity/image-url'; import { default as ImageUrlBuilder } from '@sanity/image-url';
import sanity from './sanity'; import sanity from '../sanity';
const builder = ImageUrlBuilder(sanity); const builder = ImageUrlBuilder(sanity);

24
src/lib/sortCommittee.ts Normal file
View File

@ -0,0 +1,24 @@
export default function sortCommittee(
members: SGA.MemberDocument[],
roleOrder: string[]
) {
const roleIndexes = {};
for (const { _id, role } of members) {
roleIndexes[_id] = roleOrder.findIndex((role_) => role_ === role);
}
return members.sort((a, b) => {
let roleDifference = roleIndexes[a._id] - roleIndexes[b._id];
if (roleDifference !== 0) {
return roleDifference;
} else {
if (a.name < b.name) {
return -1;
} else if (a.name > b.name) {
return 1;
} else {
return 0;
}
}
});
}

View File

@ -1,8 +1,11 @@
import Centered from '../components/Centered';
import PrimaryHeader from '../components/PrimaryHeader';
export default function NotFoundPage() { export default function NotFoundPage() {
return ( return (
<div style={{ textAlign: 'center' }}> <Centered>
<h1>404: Not Found</h1> <PrimaryHeader>404: Not Found</PrimaryHeader>
<p>This page wasn't found...</p> <p>This page wasn't found...</p>
</div> </Centered>
); );
} }

View File

@ -4,18 +4,18 @@ import MemberRow from '../components/MemberRow';
import useQuery from '../hooks/useQuery'; import useQuery from '../hooks/useQuery';
export default function ClassCouncil() { export default function ClassCouncil() {
let members = useQuery<SGA.MemberDocument[]>( let members =
`*[_type == 'member' && role == 'class'] | order (year desc)` useQuery<SGA.MemberDocument[]>(
); `*[_type == 'member' && role == 'class'] | order (year desc)`
) ?? [];
return ( return (
<> <>
<Hero heading='Class Council' /> <Hero heading='Class Council' />
<main> <main>
{members && {members.map((member) => (
members.map((member) => { <MemberRow key={member._id} member={member}></MemberRow>
return <MemberRow key={member._id} member={member}></MemberRow>; ))}
})}
</main> </main>
</> </>
); );

View File

@ -4,9 +4,10 @@ import MemberRow from '../components/MemberRow';
import useQuery from '../hooks/useQuery'; import useQuery from '../hooks/useQuery';
export default function Committee() { export default function Committee() {
let excomm = useQuery<SGA.MemberDocument[]>( let excomm =
`*[_type == 'member' && committee == 'excomm'] | order (role, year desc)` useQuery<SGA.MemberDocument[]>(
); `*[_type == 'member' && committee == 'excomm'] | order (role, year desc)`
) ?? [];
// year desc because seniority 8) // year desc because seniority 8)
return ( return (
@ -16,12 +17,9 @@ export default function Committee() {
imageURL='/images/who-we-are/excomm.png' imageURL='/images/who-we-are/excomm.png'
/> />
<main> <main>
<div> {excomm.map((member) => (
{excomm && <MemberRow key={member._id} member={member} />
excomm.map((member) => { ))}
return <MemberRow key={member._id} member={member} />;
})}
</div>
</main> </main>
</> </>
); );

View File

@ -4,19 +4,17 @@ import InitiativeRow from '../components/InitiativeRow';
import useQuery from '../hooks/useQuery'; import useQuery from '../hooks/useQuery';
export default function Initiatives() { export default function Initiatives() {
let initiatives = useQuery<SGA.InitiativeDocument[]>( let initiatives =
'*[_type == "initiative"]' useQuery<SGA.InitiativeDocument[]>('*[_type == "initiative"]') ?? [];
);
return ( return (
<> <>
<Hero heading='Initiatives' /> <Hero heading='Initiatives' />
<main> <main>
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
{initiatives && {initiatives.map((initiative) => (
initiatives.map((initiative) => { <InitiativeRow initiative={initiative} />
return <InitiativeRow initiative={initiative} />; ))}
})}
</div> </div>
</main> </main>
</> </>

View File

@ -1,25 +1,28 @@
import { SanityDocument } from '@sanity/client'; import { SanityDocument } from '@sanity/client';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import BlueButtonLink from '../components/BlueButtonLink';
import GetInvolvedRow from '../components/GetInvolvedRow'; import GetInvolvedRow from '../components/GetInvolvedRow';
import Hero from '../components/Hero'; import Hero from '../components/Hero';
import ParagraphHeader from '../components/ParagraphHeader';
import '../css/get-involved.css'; import '../css/get-involved.css';
import sanity from '../sanity'; import sanity from '../sanity';
export default function GetInvolved() { export default function GetInvolved() {
let [ways, setWays] = React.useState< let [getInvolved, setGetInvolved] = React.useState<
SanityDocument<SGA.GetInvolvedDocument> | undefined SanityDocument<SGA.GetInvolvedDocument> | undefined
>(); >();
React.useEffect(() => { React.useEffect(() => {
sanity.getDocument<SGA.GetInvolvedDocument>('get_involved').then(setWays); sanity
.getDocument<SGA.GetInvolvedDocument>('get_involved')
.then(setGetInvolved);
}, []); }, []);
return ( return (
<> <>
<Hero heading='Get Involved' /> <Hero heading='Get Involved' />
<main className='text-center'> <main className='text-center'>
<h2 className='my-2'>SGA Calendar</h2> <ParagraphHeader>SGA Calendar</ParagraphHeader>
<iframe <iframe
src='https://calendar.google.com/calendar/u/0/embed?src=mbftfg4hu7i8ueqrgcb5o7hc6k@group.calendar.google.com&ctz=America/New_York' src='https://calendar.google.com/calendar/u/0/embed?src=mbftfg4hu7i8ueqrgcb5o7hc6k@group.calendar.google.com&ctz=America/New_York'
title='SGA Calendar' title='SGA Calendar'
@ -32,19 +35,17 @@ export default function GetInvolved() {
idea or concern or get to know your representatives, reach out to us idea or concern or get to know your representatives, reach out to us
at <b>sga@tjhsst.edu</b>! at <b>sga@tjhsst.edu</b>!
</p> </p>
{ways ? ( <ParagraphHeader>
<> Here are some ways to connect with SGA:
<h2 style={{ marginTop: '4rem', marginBottom: '1.5rem' }}> </ParagraphHeader>
Here are some ways to connect with SGA:
</h2> {getInvolved
{ways.ways.map((way) => ( ? getInvolved.ways.map((way) => (
<GetInvolvedRow way={way} key={way._id} /> <GetInvolvedRow way={way} key={way._id} />
))} ))
</> : null}
) : null}
<Link className='blue-button' to='/feedback'> <BlueButtonLink href='/feedback'>Give Feedback</BlueButtonLink>
Give Feedback
</Link>
</main> </main>
</> </>
); );

View File

@ -2,6 +2,35 @@ import React from 'react';
import Hero from '../components/Hero'; import Hero from '../components/Hero';
import useMission from '../hooks/useMission'; import useMission from '../hooks/useMission';
import '../css/mission.css'; import '../css/mission.css';
import BlueButtonLink from '../components/BlueButtonLink';
import Centered from '../components/Centered';
function MissionQuote({ text, author }) {
return (
<div className='mission-quote'>
<span className='mission-quote-text'>{text}</span>
<br />
<br />
<span className='mission-quote-author'> {author}</span>
</div>
);
}
function MissionParagraph({ title, body }: { title: string; body: string }) {
return (
<div className='d-flex'>
<div className='flex-1 p-2'>
<span className='mission-header'>{title}</span>
</div>
<div className='flex-2 p-2'>
<p className='mission-para'>{body}</p>
</div>
</div>
);
}
const previousLeadershipLink =
'https://docs.google.com/spreadsheets/d/1a3RYdqrDi1IPG9BKWQ2xhoX3YCPQKUl_FsRLvIVEMPg/edit?usp=drive_open&ouid=0';
export default function Mission() { export default function Mission() {
let mission = useMission(); let mission = useMission();
@ -11,38 +40,19 @@ export default function Mission() {
<Hero heading='Mission and History' /> <Hero heading='Mission and History' />
{mission ? ( {mission ? (
<main> <main>
<div className='mission-quote'> <MissionQuote
<span className='mission-quote-text'>{mission.quote_text}</span> author={mission.quote_author}
<br /> text={mission.quote_text}
<br /> />
<span className='mission-quote-author'>
{mission.quote_author} <MissionParagraph title='Vision' body={mission.vision} />
</span> <MissionParagraph title='Mission' body={mission.mission} />
</div>
<div className='d-flex'> <Centered>
<div className='flex-1 p-2'> <BlueButtonLink href={previousLeadershipLink}>
<span className='mission-header'>Vision</span>
</div>
<div className='flex-2 p-2'>
<p className='mission-para'>{mission.vision}</p>
</div>
</div>
<div className='d-flex'>
<div className='flex-1 p-2'>
<span className='mission-header'>Mission</span>
</div>
<div className='flex-2 p-2'>
<p className='mission-para'>{mission.mission}</p>
</div>
</div>
<div className='text-center'>
<a
href='https://docs.google.com/spreadsheets/d/1a3RYdqrDi1IPG9BKWQ2xhoX3YCPQKUl_FsRLvIVEMPg/edit?usp=drive_open&ouid=0'
className='blue-button'
>
Previous Leadership Previous Leadership
</a> </BlueButtonLink>
</div> </Centered>
</main> </main>
) : null} ) : null}
</> </>

View File

@ -1,62 +1,43 @@
import React from 'react'; import React from 'react';
import { Link, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import Hero from '../components/Hero'; import Hero from '../components/Hero';
import imageUrl from '../imageUrl'; import imageUrl from '../lib/imageUrl';
import BlockContent from '@sanity/block-content-to-react';
import sanity from '../sanity';
import '../css/article.css'; import '../css/article.css';
import useNewsArticle from '../hooks/useNewsArticle';
import LocalLinkClickable from '../components/LocalLinkClickable';
import BlockContentWithExternalLinks from '../components/BlockContentWithExternalLinks';
import PrimaryHeader from '../components/PrimaryHeader';
export default function NewsArticle() { export default function NewsArticle() {
let { articleId } = useParams<{ articleId: string }>(); let { articleId } = useParams<{ articleId: string }>();
let [article, setArticle] = React.useState<SGA.ArticleDocument>(null!); let article = useNewsArticle(articleId);
React.useEffect(() => { let thumbnailUrl = '/images/hero.png';
sanity.fetch('*[_id == $articleId] [0]', { articleId }).then(setArticle);
}, [articleId]);
let thumbUrl: string;
if (article?.thumbnail) { if (article?.thumbnail) {
thumbUrl = imageUrl(article.thumbnail).url(); thumbnailUrl = imageUrl(article.thumbnail).url();
} else {
thumbUrl = '/images/hero.png';
} }
const goToAllNewsArticles = (
<LocalLinkClickable to='/news'>Go to all news articles</LocalLinkClickable>
);
return ( return (
<> <>
<Hero heading='News' imageURL={thumbUrl || undefined} /> <Hero heading='News' imageURL={thumbnailUrl || undefined} />
<main> <main>
{article ? ( {article ? (
<div style={{ maxWidth: '640px', margin: '2rem auto' }}> <div style={{ maxWidth: '640px', margin: '2rem auto' }}>
<Link to='/news' className='clickable-link'> {goToAllNewsArticles}
Go to all news articles <PrimaryHeader>{article.title}</PrimaryHeader>
</Link>
<h1>{article.title}</h1>
<i> <i>
Posted {article.publish_date} by {article.author || 'No author'} Posted {article.publish_date} by {article.author || 'No author'}
</i> </i>
<br /> <br />
{/* Wrap the BlockContent in a div because it expands to <></> */} <div id='article-content' className='article-paragraphs'>
<div <BlockContentWithExternalLinks blocks={article.content} />
id='article-content'
className='article-paragraphs'
ref={(ref) => {
/*
When this element loads, convert all the links to have target="_blank."
This ensures that the links open in a new tab
*/
if (ref) {
ref.querySelectorAll('a').forEach((link) => {
link.target = '_blank';
});
}
}}
>
<BlockContent blocks={article.content} />
</div> </div>
<br /> <br />
<Link to='/news' className='clickable-link'> {goToAllNewsArticles}
Go to all news articles
</Link>
</div> </div>
) : null} ) : null}
</main> </main>

View File

@ -1,27 +1,37 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import Hero from '../components/Hero'; import Hero from '../components/Hero';
import MemberRow from '../components/MemberRow'; import MemberRow from '../components/MemberRow';
import useQuery from '../hooks/useQuery'; import sortCommittee from '../lib/sortCommittee';
import sanity from '../sanity';
const officerOrder = [
'SGA President',
'SGA Vice-President',
'SGA Treasurer',
'SGA Secretary',
];
export default function Officers() { export default function Officers() {
let officers = useQuery<SGA.MemberDocument[]>( const [officers, setOfficers] = useState<SGA.MemberDocument[]>();
`*[_type == 'member' && committee == 'officer']`
);
if (!officers) { useEffect(() => {
return null; sanity
} .fetch("*[_type == 'member' && committee == $committee]", {
committee: 'officer',
})
.then(setOfficers);
}, []);
const officersSorted = sortCommittee(officers ?? [], officerOrder);
const officerList = officersSorted.map((officer) => (
<MemberRow member={officer} />
));
console.log(officersSorted);
return ( return (
<> <>
<Hero heading='Officers' imageURL='/images/who-we-are/officers.jpg' /> <Hero heading='Officers' imageURL='/images/who-we-are/officers.jpg' />
<main> <main>{officerList}</main>
{officers
? officers.map((officer) => {
return <MemberRow member={officer} />;
})
: null}
</main>
</> </>
); );
} }