Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/LandingPage/LandingPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
-moz-osx-font-smoothing: grayscale;

width: 100%;
max-width: 100vw;
margin: 0;
padding: 0;
color: var(--color-primary);
background-color: var(--color-background);
max-width: 100vw;
font-family: 'Gabarito', serif;
font-optical-sizing: auto;
font-weight: normal;
Expand Down Expand Up @@ -128,7 +128,7 @@

@media (max-width: 480px) {
.landingPage {
--font-size-M: 15px;
--font-size-M: 14px;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/components/LandingPage/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import AcceptInviteErrorDialog from '@/components/Invites/AcceptInviteErrorDialo
import { logError } from '@/util/logger';

import styles from './LandingPage.module.css';
import './global.css';

export interface LandingPageProps {
className?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.sanityContentParagraph {
margin-bottom: 1em;
margin-bottom: 42px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
justify-content: flex-end;
align-items: center;
gap: 1em;
padding-right: 2em;
}

div.selectCurrency > div.select {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,34 @@

.swipeableCardsList > footer {
display: flex;
flex-wrap: nowrap;
flex-wrap: wrap;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0;
gap: 1em;
padding: 0;
padding-right: 16px;
}

.swipeableCardsList > footer.onlyOnSmallScreen {
display: none;
}

@media (max-width: 480px) {
.swipeableCardsList > footer.onlyOnSmallScreen {
display: flex;
}

.swipeableCardsList > footer {
flex-direction: column;
gap: 2em;
}

.swipeableCardsList > footer >div {
order: 2;
}
}

.swipeableCardsList nav {
flex: 0 0 auto;
display: flex;
Expand All @@ -42,6 +61,7 @@
align-items: center;
flex-wrap: nowrap;
gap: 8px;
order: 1;
}

.swipeableCardsList nav > button.page {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react/no-array-index-key */
/* eslint-disable no-param-reassign */
import React from 'react';
import React, { CSSProperties } from 'react';

import { IconChevronLeft } from '../../icons/IconChevronLeft';
import { IconChevronRight } from '../../icons/IconChevronRight';
Expand All @@ -15,7 +15,9 @@ export interface SwipeableCardsListProps {
buttonLabel?: string;
buttonOnClick?(): void;
hideFooter?: boolean;
footerOnSmallScreen?: boolean;
gap?: string;
style?: CSSProperties;
}

export default function SwipeableCardsList({
Expand All @@ -24,7 +26,9 @@ export default function SwipeableCardsList({
buttonLabel,
buttonOnClick,
hideFooter = false,
footerOnSmallScreen = false,
gap = '48px',
style = {},
}: SwipeableCardsListProps) {
const id = React.useId();
const makeId = (index: number) => `${id}_${index}`;
Expand All @@ -43,7 +47,7 @@ export default function SwipeableCardsList({
return (
<div
className={classNames(className, styles.swipeableCardsList)}
style={{ '--custom-gap': gap }}
style={{ ...style, '--custom-gap': gap }}
>
<div ref={ref} className={styles.scroll}>
{children.map((child, index) => {
Expand All @@ -56,7 +60,7 @@ export default function SwipeableCardsList({
})}
</div>
{!hideFooter && (
<footer>
<footer className={classNames(footerOnSmallScreen && styles.onlyOnSmallScreen)}>
<div>
{buttonLabel && (
<button type="button" onClick={buttonOnClick} className={styleButtonSquare}>
Expand Down
75 changes: 38 additions & 37 deletions src/components/LandingPage/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,35 @@ export function useSanity<T>(
query: string,
typeGuard: (data: unknown) => data is T
): T | undefined | null {
try {
const data = useSanityContent(query);
if (isUndefined(data)) return undefined;
const [data, setData] = React.useState<T | undefined | null>(undefined);
React.useEffect(() => {
fetchSanity(query, typeGuard)
.then(setData)
.catch((ex) => {
logError('There was an exception in this Sanity query:', query);
logError(ex);
setData(null);
});
}, [query, typeGuard]);
return data;
}

try {
if (typeGuard(data)) return data;
throw Error('Type guard rejeted this type, but without any explanation!');
} catch (ex) {
console.log('The following Sanity GROQ query returned a data of unexpected type:');
console.log(`%c${query}`, 'font-family: monospace; color: #0f0; bakground: #000');
console.log(data);
const msg = ex instanceof Error ? ex.message : `${ex}`;
console.log(`%c${msg}`, 'font-weight: bold; color: #fff; background: #b00');
return null;
}
export async function fetchSanity<T>(
query: string,
typeGuard: (data: unknown) => data is T
): Promise<T | undefined | null> {
const data = await fetchSanityContent(query);
if (isUndefined(data)) return undefined;

try {
if (typeGuard(data)) return data;
throw Error('Type guard rejeted this type, but without any explanation!');
} catch (ex) {
logError('There was an exception in this Sanity query:', query);
logError(ex);
console.log('The following Sanity GROQ query returned a data of unexpected type:');
console.log(`%c${query}`, 'font-family: monospace; color: #0f0; bakground: #000');
console.log(data);
const msg = ex instanceof Error ? ex.message : `${ex}`;
console.log(`%c${msg}`, 'font-weight: bold; color: #fff; background: #b00');
return null;
}
}
Expand Down Expand Up @@ -97,26 +108,16 @@ const cache = new Map<string, unknown>();
*
* @see https://open-brain-institute.sanity.studio
*/
function useSanityContent(query: string): unknown {
const [content, setContent] = React.useState<unknown>(() => cache.get(query));
React.useEffect(() => {
const action = async () => {
const fromCache = cache.get(query);
if (fromCache) {
setContent(fromCache);
return;
}
async function fetchSanityContent(query: string): Promise<unknown> {
const fromCache = cache.get(query);
if (fromCache) return fromCache;

try {
const data = await client.fetch(query);
cache.set(query, data);
setContent(data);
} catch (ex) {
logError('Unable to connect to Sanity!', ex);
setContent(null);
}
};
action();
}, [setContent, query]);
return content;
try {
const data = await client.fetch(query);
cache.set(query, data);
return data;
} catch (ex) {
logError('Unable to connect to Sanity!', ex);
return null;
}
}
10 changes: 7 additions & 3 deletions src/components/LandingPage/content/news.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface ContentForNewsItem {

export type ContentForNewsList = ContentForNewsItem[];

function isContentForNewsList(data: unknown): data is ContentForNewsList {
export function isContentForNewsList(data: unknown): data is ContentForNewsList {
return tryType('ContentForNews', data, [
'array',
{
Expand Down Expand Up @@ -74,10 +74,14 @@ export function useSanityContentForNewsItem(slug: string): ContentForNewsItem |
);
}

export function useSanityContentForNewsList(limit?: number): ContentForNewsList {
export function useSanityContentForNewsListCount(): number {
return useSanity(`count(*[_type=="news"])`, isNumber) ?? 0;
}

export function useSanityContentForNewsList(length = 0, start = 0): ContentForNewsList {
return sanitize(
useSanity(
`*[_type=="news"] | order(customDate desc) ${isNumber(limit) ? `[0..${limit - 1}]` : ''} {
`*[_type=="news"] | order(customDate desc) ${length > 0 ? `[${start}..${length - 1}]` : ''} {
"id": _id,
title,
"content": thumbnailIntroduction,
Expand Down
4 changes: 4 additions & 0 deletions src/components/LandingPage/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* Prevent the dorpdown to be hidden by sticky headers. */
div > div.ant-select-dropdown {
z-index: 9999999;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import React from 'react';

import Link from 'next/link';

import { DEFAULT_SECTION, MENU_ITEMS } from '../../../constants';
import { IconClose } from '../../../icons/IconClose';
import { classNames } from '@/util/utils';
Expand Down
98 changes: 78 additions & 20 deletions src/components/LandingPage/sections/SectionNews/SectionNews.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from 'react';

import { useSanityContentForNewsList } from '../../content';
import { styleBlockMedium } from '../../styles';
import {
ContentForNewsList,
isContentForNewsList,
useSanityContentForNewsListCount,
} from '../../content';
import { fetchSanity } from '../../content/content';
import { styleBlockMedium, styleButtonRounded } from '../../styles';
import CenteredColumn from '../../components/CenteredColumn';
import Card from './Card';
import CategoryButton from './CategoryButton';
import { classNames } from '@/util/utils';
Expand All @@ -13,9 +19,30 @@ export interface SectionNewsProps {
showHeader?: boolean;
}

const PAGE_SIZE = 10;

export default function SectionNews({ className, showHeader = false }: SectionNewsProps) {
const newsCount = useSanityContentForNewsListCount();
const [pageStart, setPageStart] = React.useState(0);
const [newsList, setNewsList] = React.useState<ContentForNewsList>([]);
const [categories, setCategories] = React.useState<string[]>(ALL_CATEGORY_IDS);
const newsList = useSanityContentForNewsList();
React.useEffect(() => {
setNewsList([]);
setPageStart(0);
}, [newsCount]);
React.useEffect(() => {
const action = async () => {
if (pageStart >= newsCount || newsList.length > pageStart) return;

const page = await fetchNewsPage(pageStart);
const list = [...newsList];
page.forEach((item, index) => {
list[pageStart + index] = item;
});
setNewsList(list);
};
action();
}, [pageStart, newsCount, newsList]);
const handleSwitchAll = () => {
setCategories(ALL_CATEGORY_IDS);
};
Expand All @@ -26,6 +53,8 @@ export default function SectionNews({ className, showHeader = false }: SectionNe
setCategories([...categories, catId]);
}
};
const newsListOBI = newsList.filter((item) => !item.isEPFL);
const newsListEPFL = newsList.filter((item) => item.isEPFL);

return (
<>
Expand Down Expand Up @@ -53,24 +82,34 @@ export default function SectionNews({ className, showHeader = false }: SectionNe
</header>
)}
<main>
{newsList
.filter((item) => !item.isEPFL)
.filter((item) => categories.includes(item.category))
.map((item) => (
<Card key={item.id} news={item} />
))}
{newsListOBI.map((item) => (
<Card key={item.id} news={item} />
))}
</main>
<h1 className={styles.separator}>BBP news highlight</h1>
<div className={styles.copyright}>Copyright © EPFL - BBP</div>
<hr className={styles.separator} />
<div className={styles.epfl}>
{newsList
.filter((item) => item.isEPFL)
.filter((item) => categories.includes(item.category))
.map((item) => (
<Card key={item.id} news={item} />
))}
</div>
{newsListEPFL.length > 0 && (
<>
<h1 className={styles.separator}>BBP news highlight</h1>
<div className={styles.copyright}>Copyright © EPFL - BBP</div>
<hr className={styles.separator} />
<div className={styles.epfl}>
{newsListEPFL.map((item) => (
<Card key={item.id} news={item} />
))}
</div>
</>
)}
{newsList.length < newsCount && (
<CenteredColumn>
<button
type="button"
className={styleButtonRounded}
onClick={() => setPageStart(pageStart + PAGE_SIZE)}
>
Load {Math.min(PAGE_SIZE, newsCount - newsList.length)} more article
{Math.min(PAGE_SIZE, newsCount - newsList.length) > 1 ? 's' : ''}
</button>
</CenteredColumn>
)}
</div>
</>
);
Expand All @@ -90,3 +129,22 @@ const CATEGORIES: Array<{ id: string; label: string }> = [
];

const ALL_CATEGORY_IDS = CATEGORIES.map((cat) => cat.id);

async function fetchNewsPage(start: number): Promise<ContentForNewsList> {
const query = `*[_type=="news"] | order(customDate desc) [${start}..${start + PAGE_SIZE - 1}] {
"id": _id,
title,
"content": thumbnailIntroduction,
"isEPFL": isBBPEPFLNews,
"slug": slug.current,
"link": externalLink,
category,
cardSize,
"imageURL": thumbnailImage.asset->url,
"imageWidth": thumbnailImage.asset->metadata.dimensions.width,
"imageHeight": thumbnailImage.asset->metadata.dimensions.height,
"date": customDate,
}`;
const data = await fetchSanity(query, isContentForNewsList);
return data ?? [];
}
Loading