-
Notifications
You must be signed in to change notification settings - Fork 10
Archive
TODO: Review this content and move relevant information to the wiki.
At the moment, the frontend implements these features:
-
Entities. Serlo consists of different entities like articles, videos or taxonomy terms. The frontend uses the data from the API to render them. You can access an entity by alias (e.g. https://frontend.serlo.org/mathe) or by id (e.g. https://frontend.serlo.org/54210). Look further down for a complete list of supported entity types.
-
Navigation. The frontend adds a header, breadcrumbs, secondary navigation and a footer to every page (where applicable).
-
Language versions. The UI changes language if you are viewing an entity of another language instance. You can access them by using the id or by prefixing the alias with a language subfolder (e.g. https://frontend.serlo.org/en/serlo).
-
Custom pages. Some pages are built separately in the frontend, like the landing page or the german donation page (https://frontend.serlo.org/spenden).
-
Horizon. The german version contains a horizon that features selected items.
-
Google Programmable Search. Search with the built-in search input or by visiting the search page: https://frontend.serlo.org/search?q=hypotenuse
-
Login. You can login to your account with your username (not e-mail) and the password
123456
(currently only available on staging and localhost). -
Notifications. After login, you can view your notifications by clicking on the notification icon in the top menu.
-
Menus for editing. After login, you can view several menus that allows you to edit the content. The links are pointing to the legacy server and are not handled by the frontend.
The frontend provides several means of navigation from one page to another.
Header and footer are present on every page (only exception: donation page). The entries are hard-coded in /src/data
, changing them needs a new deployment.
Some pages have a secondary navigation associated with them. This show up as a horizontal scrolling menu or on the left side. The data is fetched from the backend.
If no secondary navigation is present, most entities have a path within the taxonomy that is shown as breadcrumbs.
One or three entries are shown at the bottom of an entity in the horizon. The data is also hard-coded.
All links within entities and the navigation should use the default alias. The frontend looks up links that are using ids and use this information to render all links as pretty links.
Clicking a link in the frontend will trigger a backend request instead of a browser navigation, the page switches without a full reload. The request is cached for the duration of the session.
Previous documentation below here, pretty much still valid.
Routes are mapped to individual files in the pages
-folder. Create a page by adding following file:
// src/pages/hello-world.tsx
export default function HelloWorld() {
return <p>Welcome to the frontend!</p>
}
Visit localhost:3000/helloworld
to view this page.
You can attach styles to html elements and use them in your component:
// src/pages/hello-world.tsx
import styled from 'styled-components'
export default function HelloWorld() {
return <BigParagraph>Welcome to the frontend!</BigParagraph>
}
const BigParagraph = styled.p`
text-align: center;
font-size: 3rem;
color: lightgreen;
`
Use functional components and hooks to split your code into reusable pieces. Some basic features are shown in this example:
// src/pages/hello-world.tsx
import { useState } from 'react'
import styled from 'styled-components'
export default function HelloWorld() {
return <ClickMeTitle title="Welcome to the frontend!" />
}
function ClickMeTitle({ title }) {
const [clicked, setClicked] = useState(false)
const smiley = clicked ? ' :)' : ''
return (
<BigParagraph onClick={() => setClicked(!clicked)}>
{title + smiley}
</BigParagraph>
)
}
const BigParagraph = styled.p`
text-align: center;
font-size: 3rem;
color: lightgreen;
`
Visit localhost:3000/hello-world
. Click on the text. Every click should toggle a smiley face:
We love types. They help us to maintain code and keep the codebase consistent. We also love rapid development and prototyping. You decide: Add your type declarations immediately as you code or later when the codebase stabilizes. The choice is up to you:
export default function HelloWorld() {
return <Greeter title="Hello" subline="Welcome to the frontend!" />
}
interface GreeterProps {
title: string
subline?: string
}
function Greeter({ title, subline }: GreeterProps) {
return (
<>
<h1>{title}</h1>
{subline && <small>{subline}</small>}
</>
)
}
The frontend is a growing collection of components. Package every part of the UI as a component, save them in src/components
and let the file name match the components name in kebab-case. Export the component and type the props. A complete component file would look like this:
// src/components/greeter.tsx
interface GreeterProps {
title: string
subline?: string
}
export function Greeter({ title, subline }: GreeterProps) {
return (
<>
<h1>{title}</h1>
{subline && <small>{subline}</small>}
</>
)
}
Users will come to the frontend using very different devices, from narrow smartphones to very wide screens. Adapt your components and change there appearing with media queries:
import styled from 'styled-components'
export function HelloWorld() {
return (
<ResponsiveBox>
<GrowingParagraph>Hallo</GrowingParagraph>
<GrowingParagraph>Welt</GrowingParagraph>
</ResponsiveBox>
)
}
const ResponsiveBox = styled.div`
display: flex;
@media (max-width: 500px) {
flex-direction: column;
}
`
const GrowingParagraph = styled.p`
flex-grow: 1;
text-align: center;
font-size: 2rem;
padding: 16px;
background-color: lightgreen;
`
This example makes use of flexbox. On wide screens, both paragraphs are shown next to each other:
On smaller screens, they are below each other:
We can improve the previous example by extracting commenly used constants like breakpoints or colors into a theme. The file src/theme.tsx
defines our global theme which you can access in every component:
import styled from 'styled-components'
export function HelloWorld() {
return (
<ResponsiveBox>
<GrowingParagraph>Hallo</GrowingParagraph>
<GrowingParagraph>Welt</GrowingParagraph>
</ResponsiveBox>
)
}
const ResponsiveBox = styled.div`
display: flex;
@media (max-width: ${(props) => props.theme.breakpoints.sm}) {
flex-direction: column;
}
`
const GrowingParagraph = styled.p`
flex-grow: 1;
text-align: center;
font-size: 2rem;
padding: 16px;
background-color: ${(props) => props.theme.colors.brand};
`
There exists a bunch of different length units. Most of the time, px is fine. Sometimes there are better alternativs, especially in regard of a11y:
- Use
rem
forfont-size
, so users can zoom the text (e.g. farsighted people or users on 4k monitors) - Use dimensionless values for
line-height
to scale well. - Test your component how it behaves when text zooms and eventually make adjustments.
Add some eye candy by using icons. We integrated Font Awesome and adding icons is straight forward:
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCoffee } from '@fortawesome/free-solid-svg-icons'
export function HelloWorld() {
return (
<BigIcon>
<FontAwesomeIcon icon={faCoffee} size="1x" />
</BigIcon>
)
}
const BigIcon = styled.div`
text-align: center;
font-size: 3rem;
color: brown;
margin: 30px;
`
Often you need two components with only slightly different styles. Adapt your styles based on props:
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCandyCane } from '@fortawesome/free-solid-svg-icons'
export function HelloWorld() {
return (
<BigIcon iconColor="pink">
<FontAwesomeIcon icon={faCandyCane} size="1x" />
</BigIcon>
)
}
const BigIcon = styled.div<{ iconColor: string }>`
text-align: center;
font-size: 3rem;
color: ${(props) => props.iconColor};
margin: 30px;
`
This is one of the rare places where types are mandatory.
To boost your creativity, we included a bunch of useful css helper from polished:
import { useState } from 'react'
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCandyCane } from '@fortawesome/free-solid-svg-icons'
import { lighten } from 'polished'
export function HelloWorld() {
const [lighter, setLighter] = useState(0)
return (
<>
<p>Click it:</p>
<BigIcon lighter={lighter} onClick={() => setLighter(lighter + 0.01)}>
<FontAwesomeIcon icon={faCandyCane} size="1x" />
</BigIcon>
</>
)
}
const BigIcon = styled.div<{ lighter: number }>`
text-align: center;
font-size: 3rem;
color: ${(props) => lighten(props.lighter, 'pink')};
margin: 30px;
`
Import your helper from polished and use it in interpolations.
Put static content like images or documents into the public/_assets
folder.
Example: The file public/_assets/img/placeholder.png
is accessible via localhost:3000/_assets/img/placeholder.png
You can use assets in your components:
export function HelloWorld() {
return <img src="/_assets/img/placeholder.png" alt="placeholder" />
}
You can import a svg directly. They are inlined and usable as component:
import ParticipateSVG from '@/assets-webkit/img/footer-participate.svg'
export function HelloWorld() {
return <ParticipateSVG />
}
You can add elements that pop out of the page with Tippy. A basic drop button looks like this:
import styled from 'styled-components'
import Tippy from '@tippyjs/react'
export function HelloWorld() {
return (
<Wall>
<Tippy
content={<Drop>Surprise )(</Drop>}
trigger="click"
placement="bottom-start"
>
<button>Click Me!</button>
</Tippy>
</Wall>
)
}
const Wall = styled.div`
margin-top: 100px;
display: flex;
justify-content: center;
`
const Drop = styled.div`
background-color: lightgreen;
padding: 5px;
box-shadow: 8px 8px 2px 1px rgba(0, 255, 0, 0.2);
`
Surround the target element with the Tippy
component and pass the content to it. There are many more props to explore.
Show information to the user with modals. react-modal provides the necessary functionality. This example shows how you can get started:
import { useState } from 'react'
import { Modal } from '@/components/Modal' // our wrapper
const centeredModal = {
overlay: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
content: {
position: 'static',
},
}
export function HelloWorld() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open modal</button>
<Modal
isOpen={open}
onRequestClose={() => setOpen(false)}
style={centeredModal}
>
This is the content of the modal
</Modal>
</>
)
}
You handle the state by yourself. The Modal
component has many options available. Import the modal from src/reactmodal.tsx
. This takes care of the app element.
You can use KaTeX to render formulas:
import styled from 'styled-components'
import { Math } from '@/components/content/Math'
export function HelloWorld() {
return (
<>
<Paragraph>
This changed the world:{' '}
<Math formula={'c = \\pm\\sqrt{a^2 + b^2}'} inline />.
</Paragraph>
<Paragraph>This too:</Paragraph>
<CenteredParagraph>
<Math formula={'E = mc^2'} />
</CenteredParagraph>
</>
)
}
const Paragraph = styled.p`
margin: 20px;
font-size: 18px;
`
const CenteredParagraph = styled.p`
text-align: center;
font-size: 18px;
`
Our math component takes two props: formula
is the LaTeX string, inline
is optional and will make the formula a bit smaller. The rendered formula is a span
that can be placed anywhere.
If some part of a page is heavy and only relevant for a smaller fraction of users, import it dynamically. Write your component as usual:
// src/components/fancy-component.tsx
export function FancyComponent() {
return <p>This is some heavy component</p>
}
Use a dynamic import to load the component:
// src/pages/hello-world.tsx
import { useState } from 'react'
import dynamic from 'next/dynamic'
const FancyComponent = dynamic(() =>
import('@/components/fancy-component').then((mod) => mod.FancyComponent)
)
export default function HelloWorld() {
const [visible, setVisible] = useState(false)
return (
<>
<p>
<button onClick={() => setVisible(true)}>Load ...</button>
</p>
{visible && <FancyComponent />}
</>
)
}
The source code of FancyComponent
is splitting into a separate chunk and is only loaded when users click the button.
You can extend components by adding style snippets. These snippets are functions that add new props to a styled component:
import styled from 'styled-components'
export function HelloWorld() {
return (
<>
<ChatParagraph side="left">Hey, how are you?</ChatParagraph>
<ChatParagraph side="right">I'm fine!</ChatParagraph>
</>
)
}
interface SideProps {
side: 'left' | 'right'
}
function withSide(props: SideProps) {
if (props.side === 'left') {
return `
color: blue;
text-align: left;
`
} else if (props.side === 'right') {
return `
color: green;
text-align: right;
`
} else {
return ''
}
}
const ChatParagraph = styled.p<SideProps>`
${withSide}
margin: 20px;
`
This example adds the side
prop to the ChatParagraph
and allows users to change the appearance of the component.
You can reuse this function in another component:
const AnotherChatParagraph = styled.p<SideProps>`
${withSide}
margin: 15px;
border: 3px solid gray;
`
Your pages get wrapped in two components, _document.tsx and _app.tsx. You can override both files. The document contains everything that is outside of your react app, especially the html and body tag. This is a good place to set styles on these or to define the language. The document is rendered on the server only.
The app is the entrypoint of your page and is rendered client-side as well. You can add global providers or import css files here.
Here is a list of included peer dependencies:
-
styled-components
depends onreact-is
-
graphiql
depends onprop-types
-
babel-jest
depends on@babel/core
No, we are not using any css resets. Each component should reset their own styles.
No, styled components takes care of this already.
Only if it is absolutely necessary. You are able to import external .css
files in src/pages/_app.tsx
. These stylesheets are always global and included in every page. If possible, use a package that supports styled components.
Some client specific objects (window, document) are causing trouble with server side rendering. What can I do?
Delay these parts of the code after your component mounted, using the useEffect
hook:
import { useState, useEffect } from 'react'
import styled from 'styled-components'
function HelloWorld() {
const [href, setHref] = useState(undefined)
useEffect(() => {
setHref(window.location.href)
}, [])
return href ? <BigDiv>Your site's url is {href}</BigDiv> : null
}
const BigDiv = styled.div`
text-align: center;
margin-top: 100px;
`
export default HelloWorld
Using the state is important: This ensures that server side rendering and client side hydration matches up.
The most idomatic way to do this is checking the type of window:
if (typeof window === 'undefined') {
// serverside
}
Attention: Make sure that the result of SSR and client side rendering is the same! Making a difference between environments can cause inconsistencies and will lead to react warnings.
To focus a html element, you need access to the underlying DOM node. Use the ref hook for this.
JavaScript compilers allow a greater range of syntax. Here is a small cheatsheet.
const { title, url } = props
// -->
const title = props.title
const url = props.url
const [open, setOpen] = useState(false)
// -->
const __temp = useState(false)
const open = __temp[0]
const setOpen = __temp[1]
return { title, content }
// -->
return { title: title, content: content }
return `The key ${key} can not be found in ${db}.`
// -->
return 'The key ' + key + ' can not be found in ' + db + '.'
return <h1>This is a heading</h1>
// -->
import {jsx as _jsx} from 'react/jsx-runtime';
[…]
return _jsx('h1', { children: 'This is a heading' });
Generally, you can't and shouldn't. Extract the state to the parent instead and pass change handlers:
import { useState } from 'react'
function HelloWorld() {
return <Parent />
}
function Parent() {
const [msg, setMsg] = useState('hello')
return (
<>
<Brother setMsg={setMsg} />
<hr />
<Sister msg={msg} />
</>
)
}
function Brother(props) {
const { setMsg } = props
return <button onClick={() => setMsg('Yeah!')}>Click here</button>
}
function Sister(props) {
const { msg } = props
return <p>{msg}</p>
}
The brother can pass a message to its sister by declaring the state in the parent. React takes care of updating and rendering.
You can change the port by running yarn dev --port 8080
.
Beware that some important features rely on the default port (authentication, backend fetch). Change port on your own risk.