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 sanity from '../sanity';
import '../css/article.css';
import BlueButton from './BlueButton';
export default function ArticleList() {
let [articles, setArticles] = React.useState<SGA.ArticleDocument[]>([]);
@ -40,27 +41,30 @@ export default function ArticleList() {
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 (
<div>
{articles.map((article) => {
return <ArticleRow key={article._id} article={article} />;
})}
<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>
{articleList}
<div className='text-center'>{bottomComponent}</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
import React from 'react';
import imageUrl from '../imageUrl';
import imageUrl from '../lib/imageUrl';
import { Link } from 'react-router-dom';
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 imageUrl from '../imageUrl';
import imageUrl from '../lib/imageUrl';
import '../css/initiative.css';
export default function InitiativeColumn({ name, thumbnail, content }) {

View File

@ -1,5 +1,4 @@
import React from 'react';
import imageUrl from '../imageUrl';
import imageUrl from '../lib/imageUrl';
import BlockContent from '@sanity/block-content-to-react';
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';
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 sanity from './sanity';
import sanity from '../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() {
return (
<div style={{ textAlign: 'center' }}>
<h1>404: Not Found</h1>
<Centered>
<PrimaryHeader>404: Not Found</PrimaryHeader>
<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';
export default function ClassCouncil() {
let members = useQuery<SGA.MemberDocument[]>(
`*[_type == 'member' && role == 'class'] | order (year desc)`
);
let members =
useQuery<SGA.MemberDocument[]>(
`*[_type == 'member' && role == 'class'] | order (year desc)`
) ?? [];
return (
<>
<Hero heading='Class Council' />
<main>
{members &&
members.map((member) => {
return <MemberRow key={member._id} member={member}></MemberRow>;
})}
{members.map((member) => (
<MemberRow key={member._id} member={member}></MemberRow>
))}
</main>
</>
);

View File

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

View File

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

View File

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

View File

@ -2,6 +2,35 @@ import React from 'react';
import Hero from '../components/Hero';
import useMission from '../hooks/useMission';
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() {
let mission = useMission();
@ -11,38 +40,19 @@ export default function Mission() {
<Hero heading='Mission and History' />
{mission ? (
<main>
<div className='mission-quote'>
<span className='mission-quote-text'>{mission.quote_text}</span>
<br />
<br />
<span className='mission-quote-author'>
{mission.quote_author}
</span>
</div>
<div className='d-flex'>
<div className='flex-1 p-2'>
<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'
>
<MissionQuote
author={mission.quote_author}
text={mission.quote_text}
/>
<MissionParagraph title='Vision' body={mission.vision} />
<MissionParagraph title='Mission' body={mission.mission} />
<Centered>
<BlueButtonLink href={previousLeadershipLink}>
Previous Leadership
</a>
</div>
</BlueButtonLink>
</Centered>
</main>
) : null}
</>

View File

@ -1,62 +1,43 @@
import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import Hero from '../components/Hero';
import imageUrl from '../imageUrl';
import BlockContent from '@sanity/block-content-to-react';
import sanity from '../sanity';
import imageUrl from '../lib/imageUrl';
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() {
let { articleId } = useParams<{ articleId: string }>();
let [article, setArticle] = React.useState<SGA.ArticleDocument>(null!);
let article = useNewsArticle(articleId);
React.useEffect(() => {
sanity.fetch('*[_id == $articleId] [0]', { articleId }).then(setArticle);
}, [articleId]);
let thumbUrl: string;
let thumbnailUrl = '/images/hero.png';
if (article?.thumbnail) {
thumbUrl = imageUrl(article.thumbnail).url();
} else {
thumbUrl = '/images/hero.png';
thumbnailUrl = imageUrl(article.thumbnail).url();
}
const goToAllNewsArticles = (
<LocalLinkClickable to='/news'>Go to all news articles</LocalLinkClickable>
);
return (
<>
<Hero heading='News' imageURL={thumbUrl || undefined} />
<Hero heading='News' imageURL={thumbnailUrl || undefined} />
<main>
{article ? (
<div style={{ maxWidth: '640px', margin: '2rem auto' }}>
<Link to='/news' className='clickable-link'>
Go to all news articles
</Link>
<h1>{article.title}</h1>
{goToAllNewsArticles}
<PrimaryHeader>{article.title}</PrimaryHeader>
<i>
Posted {article.publish_date} by {article.author || 'No author'}
</i>
<br />
{/* Wrap the BlockContent in a div because it expands to <></> */}
<div
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 id='article-content' className='article-paragraphs'>
<BlockContentWithExternalLinks blocks={article.content} />
</div>
<br />
<Link to='/news' className='clickable-link'>
Go to all news articles
</Link>
{goToAllNewsArticles}
</div>
) : null}
</main>

View File

@ -1,27 +1,37 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import Hero from '../components/Hero';
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() {
let officers = useQuery<SGA.MemberDocument[]>(
`*[_type == 'member' && committee == 'officer']`
);
const [officers, setOfficers] = useState<SGA.MemberDocument[]>();
if (!officers) {
return null;
}
useEffect(() => {
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 (
<>
<Hero heading='Officers' imageURL='/images/who-we-are/officers.jpg' />
<main>
{officers
? officers.map((officer) => {
return <MemberRow member={officer} />;
})
: null}
</main>
<main>{officerList}</main>
</>
);
}