refactor(site): use typescript (#225)

This commit is contained in:
Rickard Natt och Dag 2021-03-30 20:34:40 +02:00 committed by GitHub
parent a55ac31389
commit 9d8b6ddd18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 292 additions and 131 deletions

View File

@ -15,9 +15,8 @@ import { H1 } from './Typography'
import { useIntl } from 'react-intl'
const Banner = () => {
const intl = useIntl()
return (
<div className="header">
<div className="relative max-w-6xl mx-auto mt-5 mb-20 md:pt-32 md:mb-52">
@ -62,10 +61,8 @@ const Banner = () => {
/>
</div>
</div>
<H1>{intl.formatMessage({ id: 'general.title'})}</H1>
<p>
{intl.formatMessage({ id: 'general.description'})}
</p>
<H1>{intl.formatMessage({ id: 'general.title' })}</H1>
<p>{intl.formatMessage({ id: 'general.description' })}</p>
<p className="py-4 flex items-center sm:flex-row space-x-2 md:space-x-4">
<Link.External
className="inline-block"
@ -90,7 +87,7 @@ const Banner = () => {
/>
</Link.External>
</p>
<p className="flex flex-col sm:items-center mt-5 sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
<p className="flex flex-col mt-5 sm:items-center sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
<NextLink href="/integritet">
<a className="inline-block px-4 py-2 font-bold text-indigo-800 border-2 border-indigo-800 rounded-full md:px-8 md:py-4 hover:bg-indigo-800 hover:text-white">
Integritetspolicy

View File

@ -1,6 +1,13 @@
import Link from 'next/link'
import { ReactNode } from 'react'
const ButtonLink = ({ children, href, target }) => {
interface ButtonLinkProps {
children: ReactNode
href: string
target?: string
}
const ButtonLink = ({ children, href, target }: ButtonLinkProps) => {
return (
<a
href={href}
@ -13,7 +20,16 @@ const ButtonLink = ({ children, href, target }) => {
)
}
export const ButtonLinkInternal = ({ children, href }) => {
interface ButtonLinkInternalProps {
children: ReactNode
href: string
}
export const ButtonLinkInternal = ({
children,
href,
}: ButtonLinkInternalProps) => {
console.log(href)
return (
<Link href={href}>
<a className="inline-block px-4 py-2 font-bold text-indigo-800 border-2 border-indigo-800 rounded-full cursor-pointer md:px-8 md:py-4 hover:bg-indigo-800 hover:text-white">

View File

@ -1,4 +1,4 @@
import SwiperCore, { Autoplay, Pagination } from 'swiper'
import SwiperCore, { Autoplay, Pagination, SwiperOptions } from 'swiper'
import { Swiper, SwiperSlide } from 'swiper/react'
import img4 from '../assets/img/icons/goal.svg'
import img3 from '../assets/img/icons/planning.svg'
@ -42,7 +42,7 @@ const FEATURES_DATA = [
]
const Features = () => {
const swiperParams = {
const swiperParams: SwiperOptions = {
slidesPerView: 3,
slidesPerGroup: 3,
centeredSlides: true,

View File

@ -26,7 +26,7 @@ const FunFacts = () => {
startCounter: false,
})
const onVisibilityChange = (isVisible) => {
const onVisibilityChange = (isVisible: boolean) => {
if (isVisible) {
setCounter({ startCounter: true })
}

View File

@ -5,7 +5,9 @@ import NavLinks from './NavLinks'
import classnames from 'classnames'
import Icon from './Icon'
const useMobile = (cb) => {
type UseMobileCallback = (ev: MediaQueryListEvent) => any
const useMobile = (cb: UseMobileCallback) => {
React.useEffect(() => {
const mobileMql = window.matchMedia('(max-width: 480px)')
@ -20,7 +22,7 @@ const useMobile = (cb) => {
mobileMql.removeEventListener?.('change', cb)
if (mobileMql.removeListener) {
mobileMql.removeListener('change', cb)
mobileMql.removeListener(cb)
}
}
}, [])

View File

@ -1,10 +1,15 @@
import Head from 'next/head'
import React from 'react'
import { ReactNode } from 'react'
import favImg from '../assets/img/favicon.png'
import logo from '../assets/img/logo.png'
import { GA_TRACKING_ID } from './gtag'
const Layout = (props) => {
interface LayoutProps {
children: ReactNode
pageTitle: string
}
const Layout = ({ children, pageTitle }: LayoutProps) => {
return (
<div>
<Head>
@ -19,7 +24,7 @@ const Layout = (props) => {
content="Öppna Skolplattformen är en app för iOS och Android som gör det enklare för föräldrar att komma åt uppgifter i Skolplattformen."
/>
<meta property="og:image" content={logo} />
<title>{props.pageTitle}</title>
<title>{pageTitle}</title>
<link rel="shortcut icon" type="image/png" href={favImg} />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
@ -44,7 +49,7 @@ const Layout = (props) => {
/>
</Head>
<div className="page-wrapper" id="wrapper">
{props.children}
{children}
</div>
</div>
)

View File

@ -1,7 +1,13 @@
import Link from 'next/link'
import classnames from 'classnames'
import { ReactNode } from 'react'
const Internal = ({ href, children }) => {
interface LinkInternalProps {
children: ReactNode
href: string
}
const Internal = ({ href, children }: LinkInternalProps) => {
return (
<Link href={href}>
<a className="text-indigo-800">{children}</a>
@ -9,7 +15,19 @@ const Internal = ({ href, children }) => {
)
}
const External = ({ className, href, children, target = '_blank' }) => {
interface LinkExternalProps {
children: ReactNode
className?: string
href: string
target?: string
}
const External = ({
className,
href,
children,
target = '_blank',
}: LinkExternalProps) => {
return (
<a
className={classnames('text-indigo-800', className)}

View File

@ -4,11 +4,15 @@ import { Link as ScrollLink } from 'react-scroll'
import { pageview } from './gtag'
import { useIntl } from 'react-intl'
const NavLinks = ({ onClick }) => {
const { pathname } = useRouter()
const intl = useIntl();
interface NavLinksProps {
onClick?: () => void
}
const path = (href) => {
const NavLinks = ({ onClick }: NavLinksProps) => {
const { pathname } = useRouter()
const intl = useIntl()
const path = (href: string) => {
const hashIndex = href.indexOf('#')
if (hashIndex < 0) return href
return href.substring(0, hashIndex)
@ -40,7 +44,7 @@ const NavLinks = ({ onClick }) => {
<ul className="flex flex-col text-xl text-gray-800 md:flex-row md:items-center space-y-4 md:space-y-0 md:space-x-8 md:text-base">
<li>
<Link to="wrapper" href="/#">
{intl.formatMessage({ id: 'navigation.home'})}
{intl.formatMessage({ id: 'navigation.home' })}
</Link>
</li>
<li>
@ -48,17 +52,17 @@ const NavLinks = ({ onClick }) => {
</li>
<li>
<Link to="funktioner" href="/#funktioner">
{intl.formatMessage({ id: 'navigation.functions'})}
{intl.formatMessage({ id: 'navigation.functions' })}
</Link>
</li>
<li>
<Link to="screenshots" href="/#screenshots">
{intl.formatMessage({ id: 'navigation.screenshots'})}
{intl.formatMessage({ id: 'navigation.screenshots' })}
</Link>
</li>
<li>
<Link to="vad-kostar-det" href="/#vad-kostar-det">
{intl.formatMessage({ id: 'navigation.whatdoesitcost'})}
{intl.formatMessage({ id: 'navigation.whatdoesitcost' })}
</Link>
</li>
</ul>

View File

@ -1,8 +1,7 @@
import React from 'react'
import { formatPrice } from '../utils/intl'
import SectionTitle from './SectionTitle'
import Icon from './Icon'
import DownloadButtons from './DownloadButtons'
import Icon from './Icon'
import SectionTitle from './SectionTitle'
const price = 12

View File

@ -1,11 +1,19 @@
import classnames from 'classnames'
import { ReactNode } from 'react'
interface SectionProps {
bg?: string
children: ReactNode
id?: string
padding?: string
}
const Section = ({
bg = 'bg-gray-100',
padding = 'py-8 md:py-20',
children,
id,
}) => {
}: SectionProps) => {
return (
<section className={classnames('grid grid grid-main', bg, padding)} id={id}>
<div className="items-center grid gap-y-5 md:gap-y-20 col-start-3 col-end-4 grid-cols-1 md:grid-cols-2">

View File

@ -1,4 +1,9 @@
const SectionTitle = ({ text, title }) => {
interface SectionTitleProps {
text?: string
title: string
}
const SectionTitle = ({ text, title }: SectionTitleProps) => {
return (
<div className="mb-16 text-center space-y-5">
<h2 className="text-4xl md:text-5xl font-bold leading-tight text-gray-800">

View File

@ -1,6 +1,7 @@
import { TimelineEvent } from '../data/timelineEvents'
import Link from './Link'
const dateFormat = (date) => {
const dateFormat = (date: string) => {
const parsedDate = new Date(date)
return Intl
@ -11,7 +12,11 @@ const dateFormat = (date) => {
: `${parsedDate.getFullYear()}-${parsedDate.getMonth()}`
}
const Timeline = ({ events }) => {
interface TimelineProps {
events: TimelineEvent[]
}
const Timeline = ({ events }: TimelineProps) => {
return (
<ul className="max-w-2xl border-gray-200 md:mx-auto md:border-l-2 space-y-4 md:space-y-12">
{events.map(({ date, media, importantDates, overview }) => (

View File

@ -1,13 +0,0 @@
export const H1 = ({ children }) => {
return (
<h1 className="mb-5 text-4xl md:text-5xl font-semibold text-gray-800">
{children}
</h1>
)
}
export const H2 = ({ children }) => {
return (
<h2 className="mb-5 text-5xl font-semibold text-gray-800">{children}</h2>
)
}

View File

@ -0,0 +1,23 @@
import { ReactNode } from 'react'
interface H1Props {
children: ReactNode
}
export const H1 = ({ children }: H1Props) => {
return (
<h1 className="mb-5 text-4xl md:text-5xl font-semibold text-gray-800">
{children}
</h1>
)
}
interface H2Props {
children: ReactNode
}
export const H2 = ({ children }: H2Props) => {
return (
<h2 className="mb-5 text-5xl font-semibold text-gray-800">{children}</h2>
)
}

View File

@ -33,5 +33,5 @@ test('handles missing Intl', () => {
expect(screen.getAllByText(/2021-01/i)[0]).toBeInTheDocument()
global.intl = intl
global.Intl = intl
})

View File

@ -1,15 +0,0 @@
const en =
{
'navigation.home': 'Home',
'navigation.functions': 'Functions',
'navigation.screenshots': 'Screenshots',
'navigation.whatdoesitcost': 'What does it cost?',
'general.title': 'Öppna skolplattformen',
'general.description': `Whether you have three or seven children - there is a lot to keep in mind. Absence report number 17 in February. What is the name of the substitute
this week. The gym clothes. A poorly functioning school platform
who eats time and energy? There is no room for that. So we have
built a better one. With all the information you need as a parent.
Faster and above all - much less hassle.`
}
export default en;

View File

@ -0,0 +1,15 @@
const en = {
'navigation.home': 'Home',
'navigation.functions': 'Functions',
'navigation.screenshots': 'Screenshots',
'navigation.whatdoesitcost': 'What does it cost?',
'general.title': 'Öppna skolplattformen',
'general.description': `Whether you have three or seven children - there is a lot to keep in mind. Absence report number 17 in February. What is the name of the substitute
this week. The gym clothes. A poorly functioning school platform
who eats time and energy? There is no room for that. So we have
built a better one. With all the information you need as a parent.
Faster and above all - much less hassle.`,
}
export default en

View File

@ -1,4 +0,0 @@
import sv from "./sv";
import en from "./en";
export default { en, sv }

View File

@ -0,0 +1,5 @@
import sv from './sv'
import en from './en'
export default { en, sv }

View File

@ -1,16 +0,0 @@
const sv =
{
'navigation.home': 'Hem',
'navigation.functions': 'Funktioner',
'navigation.screenshots': 'Screenshots',
'navigation.whatdoesitcost': 'Vad kostar det?',
'general.title': 'Öppna skolplattformen',
'general.description': `Oavsett om du har tre eller sju barn det är mycket att hålla
reda . Frånvaroanmälan nummer 17 i februari. Vad vikarien heter
den här veckan. Gympakläderna. En dåligt fungerande Skolplattform
som äter tid och ork? Det finns inte plats för det. vi har
byggt en bättre. Med all information du behöver som förälder.
Snabbare och framförallt mycket mindre krångel.`
}
export default sv;

View File

@ -0,0 +1,16 @@
const sv = {
'navigation.home': 'Hem',
'navigation.functions': 'Funktioner',
'navigation.screenshots': 'Screenshots',
'navigation.whatdoesitcost': 'Vad kostar det?',
'general.title': 'Öppna skolplattformen',
'general.description': `Oavsett om du har tre eller sju barn det är mycket att hålla
reda . Frånvaroanmälan nummer 17 i februari. Vad vikarien heter
den här veckan. Gympakläderna. En dåligt fungerande Skolplattform
som äter tid och ork? Det finns inte plats för det. vi har
byggt en bättre. Med all information du behöver som förälder.
Snabbare och framförallt mycket mindre krångel.`,
}
export default sv

View File

@ -1,16 +1,30 @@
import { ReactNode } from 'react'
import Link from '../components/Link'
const fixEvent = (date) => ({
const fixEvent = (date: string) => ({
date,
description: 'Öppna skolplattformen släpper ny fix',
})
const sabotageEvent = (date) => ({
const sabotageEvent = (date: string) => ({
date,
description: 'Kommun saboterar Öppna Skolplattformen',
})
export const events = [
interface Event {
date: string
description: string
link?: string
}
export interface TimelineEvent {
overview: ReactNode
date: string
importantDates?: Event[]
media?: Event[]
}
export const events: TimelineEvent[] = [
{
overview: (
<p>

17
packages/site/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
declare module '*.png' {
const value: any
export = value
}
declare module '*.jpg' {
const value: any
export = value
}
declare module '*.svg' {
const value: any
export = value
}

View File

@ -1,21 +1,11 @@
const withImages = require('next-images')
// next.config.js
module.exports = {
...withImages(),
i18n: {
localeDetection: false,
locales: ['sv', 'en'],
defaultLocale: 'sv',
domains: [
{
domain: 'skolplattformen.org',
defaultLocale: 'sv',
},
{
domain: 'localhost',
defaultLocale: 'sv',
},
]
},
}
...withImages(),
i18n: {
localeDetection: false,
locales: ['sv', 'en'],
defaultLocale: 'sv',
},
}

View File

@ -4,6 +4,7 @@
"main": "index.js",
"license": "MIT",
"dependencies": {
"@types/classnames": "^2.2.11",
"classnames": "^2.2.6",
"next": "^10.0.7",
"next-images": "^1.4.0",
@ -24,12 +25,17 @@
"lint": "eslint './**/*.js'",
"test": "is-ci test:ci test:watch",
"test:ci": "jest",
"test:watch": "jest --watch"
"test:watch": "jest --watch",
"typecheck": "tsc --watch"
},
"devDependencies": {
"@tailwindcss/typography": "^0.4.0",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.5",
"@types/gtag.js": "^0.0.4",
"@types/jest": "^26.0.22",
"@types/react": "^17.0.3",
"@types/react-scroll": "^1.8.2",
"autoprefixer": "^10.2.4",
"babel-eslint": "10.1.0",
"eslint": "7.20.0",
@ -42,6 +48,7 @@
"jest-watch-typeahead": "0.6.1",
"postcss": "^8.2.6",
"prettier": "2.2.1",
"tailwindcss": "^2.0.3"
"tailwindcss": "^2.0.3",
"typescript": "^4.2.3"
}
}

View File

@ -1,12 +1,12 @@
import 'swiper/swiper-bundle.min.css'
import '../styles/global.css'
import { IntlProvider } from "react-intl"
import Layout from '../components/Layout'
import Footer from '../components/Footer'
import Header from '../components/Header'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { IntlProvider } from 'react-intl'
import { pageview } from '../components/gtag'
import messages from '../content/locale/'
@ -14,7 +14,7 @@ export default function App({ Component, pageProps }) {
const router = useRouter()
const { locale, defaultLocale } = router
const currentMessages = messages[locale];
const currentMessages = messages[locale]
// Google analytics
useEffect(() => {
@ -28,11 +28,11 @@ export default function App({ Component, pageProps }) {
}, [router.events])
return (
<IntlProvider
locale={locale}
defaultLocale={defaultLocale}
messages={currentMessages}
>
<IntlProvider
locale={locale}
defaultLocale={defaultLocale}
messages={currentMessages}
>
<Layout pageTitle="Skolplattformen">
<Header />
<Component {...pageProps} />

View File

@ -1,11 +1,6 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html lang="sv-SE">

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@ -1,9 +0,0 @@
export const formatNumber = (input, options = {}) =>
new Intl.NumberFormat('sv-SE', options).format(input)
export const formatPrice = (input) =>
formatNumber(input, {
currency: 'SEK',
minimumFractionDigits: 0,
style: 'currency',
})

View File

@ -0,0 +1,11 @@
export const formatNumber = (
input: number,
options: Intl.NumberFormatOptions = {}
) => new Intl.NumberFormat('sv-SE', options).format(input)
export const formatPrice = (input: number) =>
formatNumber(input, {
currency: 'SEK',
minimumFractionDigits: 0,
style: 'currency',
})

View File

@ -4,9 +4,16 @@ import { render as rtlRender } from '@testing-library/react'
import { IntlProvider } from 'react-intl'
import messages from '../content/locale'
function render(ui, { locale = 'sv', ...renderOptions } = {}) {
function render(
ui: React.ReactElement,
{ locale = 'sv', ...renderOptions } = {}
) {
function Wrapper({ children }) {
return <IntlProvider locale={locale} messages={messages[locale]}>{children}</IntlProvider>
return (
<IntlProvider locale={locale} messages={messages[locale]}>
{children}
</IntlProvider>
)
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}

View File

@ -741,6 +741,11 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/classnames@^2.2.11":
version "2.2.11"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf"
integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw==
"@types/graceful-fs@^4.1.2":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@ -748,6 +753,11 @@
dependencies:
"@types/node" "*"
"@types/gtag.js@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.4.tgz#81427d703b5a889b4fd1c37eaae6f2a6db228e5a"
integrity sha512-rn4yaEfryXklAtsiai4xHNUgLjGD8AEPQBNSCTa5QrvccFYYBWJQRGwaHiv2U/wdIzcjOuyyBC23YgdQfq5u/Q==
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@ -783,6 +793,14 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/jest@^26.0.22":
version "26.0.22"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.22.tgz#8308a1debdf1b807aa47be2838acdcd91e88fbe6"
integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==
dependencies:
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/json-schema@^7.0.6":
version "7.0.7"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
@ -808,7 +826,14 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/react@*":
"@types/react-scroll@^1.8.2":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@types/react-scroll/-/react-scroll-1.8.2.tgz#44bbbadabb9014517eb865d6fa47937535a2234a"
integrity sha512-oavV6BZLfaIghX4JSmrm6mJkeVayQlmsFx1Rz8ffGjMngHAI/juZkRZM/zV/H5D0pGqjzACvBmKYUU4YBecwLg==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.3":
version "17.0.3"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
@ -6144,6 +6169,11 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"