diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 14ef0437..00000000 --- a/.babelrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "presets": ["next/babel"], - "plugins": [ - [ - "styled-components", - { - "ssr": true, - "displayName": true, - "preprocess": false - } - ] - ] -} diff --git a/README.md b/README.md index 8c16c268..673f2704 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,6 @@ That’s why we’re building a data-driven movement of climate-savvy developers #Klimatkollen #FreeClimateData -## Climate Data Pipeline Overview - -Please see full [description here](data/README.md). - -Feel free to explore the repository to understand more about how we collect, process, and display climate data. - ## Building and running locally If your're starting from scratch, and working with GitHub, NodeJS and so on is new to you, read [doc/getting-started.md](doc/getting-started.md). @@ -39,6 +33,37 @@ The project can also be run with docker (although with much slower refresh time) # starts the container docker run -t -i --rm -p 3000:3000 --name klimatkollen klimatkollen +# Data overview + +In very general terms, Klimatkollen presents: +- Detailed information about Swedish municipalities' emissions... +- ...and their remaining emission budget based on the Paris Agreement. +- Other key point indicators for sustainability transition, such electric car charger density. +- Contextual information to help understand the significance of the above. + +# File overview + +The toplevel directory contains a lot of files and folders. You can just ignore most of them. Take note of: +- `README.md` - this document. +- `data`: Our data processing pipeline, written in Python. This can more or less be used/edited independently of the rest of the repository. See `data/README.md`. + - `data/facts`: Copies of source datasets. +- `doc`: Documentation and guides, they might answer many questions. + - `doc/getting-started.md`: Detailed setup instructions for the web project. + - `doc/contributing.md`: Good to know before making your first contribution. +- `pages` and `components`: Source code for almost everything visible on the website's pages. +- `public`: Files that will be served directly on the website. + - `public/locales`: Language files defininig translations of the website. + +# Code architecture overview + +How does everything fit together, code-wise? +- Copies of source datasets are under `data/facts`. +- We run the Python scripts under `data` to produce `data/output/climate-data.json` from those datasets. +- The latest copy of `data/output/climate-data.json` is always checked into version control. +- The rest of the website source code loads `data/output/climate-data.json`. +- The framework `next.js` is used to compile actual HTML pages at runtime. +- `next.js` caches each page for serving, to serve it faster for each new visitor. + ## Contributing The idea behind Klimatkollen is to give citizens access to the climate data we need to meet the goals of the Paris Agreement – and save our own future. @@ -49,6 +74,8 @@ Looking for ideas on what needs to be done? We appreciate help on existing [issu Testing, bug fixes, typos or fact checking of our data is highly appreciated. +See [doc/contributing.md] before making your first contribution. + ## Contact Join the Discord server in the introduction or send an email to [hej@klimatkollen.se](mailto:hej@klimatkollen.se). diff --git a/__tests__/utils/generateMunipacitySitemap.test.ts b/__tests__/utils/generateMunipacitySitemap.test.ts index 8dd2f8f5..e6fee41b 100644 --- a/__tests__/utils/generateMunipacitySitemap.test.ts +++ b/__tests__/utils/generateMunipacitySitemap.test.ts @@ -1,6 +1,6 @@ import { TFunction } from 'next-i18next' import { - generateMunipacitySitemapData, + generateMunicipalitySitemapData, generateSitemap, } from '../../utils/generateMunipacitySitemap' import { Municipality } from '../../utils/types' @@ -11,7 +11,7 @@ const t = vi.fn((str) => str) as unknown as TFunction describe('generateSitemap', () => { it('should generate valid municipality sitemap data', () => { - const siteMap = generateMunipacitySitemapData({ municipalities }) + const siteMap = generateMunicipalitySitemapData({ municipalities }) expect(siteMap).toEqual([ { url: 'https://klimatkollen.se/kommun/stockholm', @@ -31,7 +31,7 @@ describe('generateSitemap', () => { }) it('should generate a valid sitemap XML string', () => { - const siteMap = generateMunipacitySitemapData({ municipalities }) + const siteMap = generateMunicipalitySitemapData({ municipalities }) const sitemapXml = generateSitemap(siteMap, t) expect(() => new DOMParser().parseFromString(sitemapXml, 'text/xml')).not.toThrow() }) diff --git a/components/Layout.tsx b/components/Layout.tsx index 71a6e5bd..5bd550a0 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -14,7 +14,7 @@ const Main = styled.main` } ` -export default function Layout({ children }: { children: JSX.Element }) { +export default function Layout({ children }: { children: JSX.Element | JSX.Element[] }) { return ( <>
diff --git a/components/Markdown.tsx b/components/Markdown.tsx index 30225d34..86094db1 100644 --- a/components/Markdown.tsx +++ b/components/Markdown.tsx @@ -2,6 +2,7 @@ import ReactMarkdown, { Components, Options } from 'react-markdown' import rehypeExternalLinks from 'rehype-external-links' import styled from 'styled-components' import Link from 'next/link' +import { Ref } from 'react' import { H1, H2, H3, Paragraph, ParagraphBold, ParagraphItalic, @@ -19,7 +20,20 @@ const defaultComponents: Partial = { // Workaround to inherit styles and just change the HTML element strong: styled(ParagraphBold).attrs({ as: 'strong' })``, em: styled(ParagraphItalic).attrs({ as: 'em' })``, - a: Link as Components['a'], + // Ensure public assets served from the same domain (currently only PDFs) are opened in a new tab + // This prevents unwanted behavior where Next.js tries to preload data for URLs that are outside of the Next.js app. + a: ({ + href, target, ref, rel, children, + }) => ( + } + > + {children} + + ), h1: H1, h2: H2, h3: H3, diff --git a/components/ToggleSection.tsx b/components/ToggleSection.tsx index f8113ab8..7bac71f0 100644 --- a/components/ToggleSection.tsx +++ b/components/ToggleSection.tsx @@ -25,27 +25,21 @@ const Arrow = styled(ArrowSvg)<{ open: boolean }>` const HeaderSection = styled.summary` display: flex; justify-content: space-between; + width: 100%; + list-style: none; /* remove default arrow in Firefox */ - & .arrow { - display: block; - } + &::-webkit-details-marker { + display: none; /* remove default arrow in Chrome */ &:hover { cursor: pointer; } + } ` const InfoSection = styled.div` display: flex; flex-direction: column; - - .mobile { - background: black; - } - - .desktop { - background: yellow; - } ` type Props = { @@ -60,7 +54,7 @@ function ToggleSection({ header, text }: Props) { setOpen((event.target as HTMLDetailsElement).open)}>
{header}
- +
{typeof text === 'string' ? {text} : text} diff --git a/doc/contributing.md b/doc/contributing.md new file mode 100644 index 00000000..e2473807 --- /dev/null +++ b/doc/contributing.md @@ -0,0 +1,78 @@ +# Klimatkollen contribution guidelines + +Thank you for wanting to contribute to Klimatkollen! Please read this before starting to code :) + +## What to expect from the team + +Klimatkollen is driven by a small team that does a lot more than coding. From media outreach and fundraising to data analysis and hackathons, there's a lot going on. We sometimes have to push changes through in sync with exernal deadlines, for example to publish some data at the same time as a debate article. In such times we might not be able to review all proposed changes as soon as we would like to. As a rule of thumb we aspire to respond to every issue/PR within a week, and often much faster than that. + +## How to get in touch + +The best way to get in touch with us is through the discord (link in README). It's a very active server and all the discussions happen there, as well as many other day-to-day updates. You can also email at hej@klimatkollen.se. + +## Guide: How to make your first contribution + +### Step 1. Find something to work on. + +Either pick an issue from the GitHub issues tab, or create a new one to describe your suggestion/idea/bug report. + +If you are creating a new issue, make sure to look for similar ones first. Use the search function! + +Issues labelled "good first issue" are the best to start with if you are new to the project. + +### Step 2. Let others know you're working on it. + +Once you decide to pick an issue up, leave a comment saying so, to avoid others starting on the same one at the same time. + +### Step 3. Make your changes + +For most issues you will start with making sure you can run Klimatkollen locally, see [doc/getting-started.md] for instructions on that. + +Run `git log -1` to see the latest commit in your local repo, and make sure this is the same as the `staging` branch on GitHub. This is to make sure you are up to date on everyone's changes. + +(optional but recommended) Run `git checkout -b `, replacing `` with the id of the issue or another good name, to create a branch for you changes. + +Make the code changes you would like to address the issue you are working on. It doesn't have to be perfect on first attempt, a prototype showing how an issue can be partially solved is already a step in the right direction. + +Commit your work. You can make multiple commits if you like, and we don't enforce any particular style commit messages. + +### Don't forget! + +Your code contribution must not introduce new warnings or errors, or reduce the quality of the code. We will check this as part of review, but you can make the process faster by making sure we won't find anything. + +Checklist before finishing your PR: + +[ ] Run `npm run build` to compile all pages. Make sure there are no new warnings or errors. +[ ] Run `npm run lint` - same story. +[ ] Run `npm run start` to test the site locally and click through a few pages. Particularly focus on the places where you made your changes. + +Also consider what it will be like for someone else to read your code. Will they be able to read it and understand what's going on? + +### Step 4. Publish your work. + +Push your commits to your fork of Klimatkollen. If you haven't created a fork already, use the "fork" button on GitHub and then add your fork as a remote by running `git remote add my-fork ` where `my-fork` is an arbitrary name for the fork locally. + +Create a pull request. Either follow the link git showed after pushing to your fork, or create it from the GitHub web interface. Double check the branches in the PR! Make sure your are merging from your own branch, on your own repo, into "staging" on "klimatkollen". A common mistake is to accidentally start merging into "staging" on your own repo. + +In the PR desscription, write "Fixes #XXX" where XXX is the id of the issue you are fixing. E.g. `Fixes #413` + +If the PR is not ready for review yet, prefix the title wiht "Draft: ". Remove this once you're ready. + +In the body, describe the changes you have made, what you have done to test your work, if there's anything you're unsure about, or if some parts of the code are unfinished. Focus on the changes and how they address the issues - but try to keep discussion of the issue itself, in the issue. + +### Step 5. Get your work reviewed. + +Once the PR is created and has left the "draft" stage, someone from the team will eventually review it. This can normally take at least a few days - please be patient. At the same time, it is completely fine to let us know you're waiting for a review on Discord, and ask if you are unsure about anything. + +The reviewer will most likely have some comments, questions, or feedback, which you will be expected to address. + +In some cases, the reviewer might make some changes to your PR themselves and commit it, if they need to get the fix in quickly. + +The reviewer might also conclude that the changes should not be made, or that there currently isn't time to review it. Such a scenario is unlikely but if it does happen, please be understanding and remember that the reviwer wants what's best for the project, just like you. + +Klimatkollen's team has the final say on what goes into the code. + +### Step 6. After the PR is merged + +It might take some days before code merged to `staging` is published to production. + diff --git a/next.config.js b/next.config.js index cea97c26..ea2b516e 100644 --- a/next.config.js +++ b/next.config.js @@ -5,6 +5,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) +/** @type {require('next').NextConfig} */ module.exports = withBundleAnalyzer({ webpack(config) { config.module.rules.push({ @@ -17,6 +18,17 @@ module.exports = withBundleAnalyzer({ }, reactStrictMode: true, + compiler: { + styledComponents: { + ssr: true, + displayName: true, + }, + }, + // i18n configuration + i18n: { + locales: ['sv'], + defaultLocale: 'sv', + }, i18n, // Redirects configuration diff --git a/pages/404.tsx b/pages/404.tsx index c42af2ed..b1137569 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -3,15 +3,10 @@ import styled from 'styled-components' import { useTranslation } from 'next-i18next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { GetStaticProps } from 'next' -import { H1 } from '../components/Typography' -const Wrapper = styled.div` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; -` +import { H1 } from '../components/Typography' +import Layout from '../components/Layout' +import PageWrapper from '../components/PageWrapper' const Button = styled.button` height: 56px; @@ -35,10 +30,12 @@ function FourOhFour() { const { t } = useTranslation() return ( - -

{t('common:errors.notFound')}

- -
+ + +

{t('common:errors.notFound')}

+ +
+
) } diff --git a/pages/_document.tsx b/pages/_document.tsx index 6e8d769b..3063293c 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,51 +1,79 @@ -import { - Html, Head, Main, NextScript, +/* eslint-disable react/jsx-props-no-spreading */ +import Document, { + DocumentContext, Head, Html, Main, NextScript, } from 'next/document' +import { ServerStyleSheet } from 'styled-components' -export default function Document() { - return ( - - - - - - - - - -
- - - - ) +export default class MyDocument extends Document { + render() { + return ( + + + + + + + + + +
+ + + + ) + } + + static async getInitialProps(ctx: DocumentContext) { + const sheet = new ServerStyleSheet() + const originalRenderPage = ctx.renderPage + + try { + ctx.renderPage = () => originalRenderPage({ + enhanceApp: (App) => (props) => sheet.collectStyles(), + }) + + const initialProps = await Document.getInitialProps(ctx) + return { + ...initialProps, + styles: ( + <> + {initialProps.styles} + {sheet.getStyleElement()} + + ), + } + } finally { + sheet.seal() + } + } } diff --git a/pages/kommun/[municipality]/[step].tsx b/pages/kommun/[municipality]/[step].tsx index 5a158b61..07ceaf22 100644 --- a/pages/kommun/[municipality]/[step].tsx +++ b/pages/kommun/[municipality]/[step].tsx @@ -7,7 +7,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { ClimateDataService } from '../../../utils/climateDataService' import { WikiDataService } from '../../../utils/wikiDataService' import { Municipality as TMunicipality } from '../../../utils/types' -import { PolitycalRuleService as PoliticalRuleService } from '../../../utils/politicalRuleService' +import { PoliticalRuleService } from '../../../utils/politicalRuleService' const Municipality = dynamic(() => import('../../../components/Municipality/Municipality')) @@ -36,7 +36,9 @@ export default function Step({ const onNext = () => { const next = CHARTS[stepIndex + 1] - if (!next) throw new Error(`Assertion failed: No step with index ${stepIndex + 1}`) + if (!next) { + throw new Error(`Assertion failed: No step with index ${stepIndex + 1}`) + } router.replace(`/kommun/${id}/${next}`, undefined, { shallow: true, scroll: false, @@ -45,7 +47,9 @@ export default function Step({ const onPrevious = () => { const prev = CHARTS[stepIndex - 1] - if (!prev) throw new Error(`Assertion failed: No step with index ${stepIndex - 1}`) + if (!prev) { + throw new Error(`Assertion failed: No step with index ${stepIndex - 1}`) + } router.replace(`/kommun/${id}/${prev}`, undefined, { shallow: true, scroll: false, @@ -74,7 +78,9 @@ export const getServerSideProps: GetServerSideProps = async ({ params, res, loca res.setHeader('Cache-Control', `public, stale-while-revalidate=60, max-age=${((60 * 60) * 24) * 7}`) const id = (params as Params).municipality as string - if (cache.get(id)) return cache.get(id) + if (cache.get(id)) { + return cache.get(id) + } const climateDataService = new ClimateDataService() const wikiDataService = new WikiDataService() diff --git a/pages/kommun/[municipality]/index.tsx b/pages/kommun/[municipality]/index.tsx index 992b3f0a..8085139d 100644 --- a/pages/kommun/[municipality]/index.tsx +++ b/pages/kommun/[municipality]/index.tsx @@ -4,15 +4,20 @@ import { useEffect } from 'react' import { ParsedUrlQuery } from 'querystring' import { CHARTS } from './[step]' +const municipalityRoute = (name: string) => `/kommun/${encodeURIComponent(name)}/${CHARTS[1]}` + export default function Index() { const router = useRouter() const municipality = router.query.municipality as string useEffect(() => { - if (municipality) router.replace(`/kommun/${municipality}/${CHARTS[0]}`) + if (municipality) { + const decodedName = decodeURIComponent(municipality) + router.replace(municipalityRoute(decodedName)) + } }, [municipality, router]) - return '' + return null } interface Params extends ParsedUrlQuery { @@ -22,9 +27,11 @@ interface Params extends ParsedUrlQuery { export const getServerSideProps: GetServerSideProps = async ({ params }) => { const id = (params as Params).municipality as string + const decodedId = decodeURIComponent(id) + return { redirect: { - destination: `/kommun/${id}/framtida-prognos`, + destination: municipalityRoute(decodedId), permanent: true, }, } diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts index de6f6193..b4644561 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -2,7 +2,7 @@ import { GetServerSideProps } from 'next' import { ClimateDataService } from '../utils/climateDataService' import { - generateMunipacitySitemapData, + generateMunicipalitySitemapData, generateSitemap, } from '../utils/generateMunipacitySitemap' import { getServerSideI18n } from '../utils/getServerSideI18n' @@ -11,7 +11,7 @@ export const getServerSideProps: GetServerSideProps = async ({ res, locale }) => const { t } = await getServerSideI18n(locale as string, ['common', 'sitemap']) const municipalities = new ClimateDataService().getMunicipalities() - const municipalitiesSitemap = generateMunipacitySitemapData({ municipalities }) + const municipalitiesSitemap = generateMunicipalitySitemapData({ municipalities }) // Generate the XML sitemap with the blog data const sitemap = generateSitemap(municipalitiesSitemap, t) diff --git a/public/locales/sv/about.json b/public/locales/sv/about.json index 941891a6..a0fcab5d 100644 --- a/public/locales/sv/about.json +++ b/public/locales/sv/about.json @@ -38,7 +38,7 @@ "title": "Vår styrelse" }, "content": { - "text": "I dag kan du på Klimatkollen se:\n\n- Koldioxidbudgetar för landets alla 290 kommuner. Klimatkollen utgår ifrån en [nationell koldioxidbudget](https://www.cemus.uu.se/wp-content/uploads/2023/12/Paris-compliant-carbon-budgets-for-Swedens-counties-.pdf) som beräknats av forskare vid Uppsala universitet enligt Tyndall-modellen och som sedan fördelats ut på kommunerna av Klimatkollen. (2022 koldioxidbudget [här](https://klimatkollen.se/Paris_compliant_Swedish_CO2_budgets-March_2022-Stoddard&Anderson.pdf))\n- En översikt i både kartvy och listvy över koldioxidutsläppen i landets kommuner. Kommunerna rankas baserat på genomsnittlig årlig utsläppsminskningstakt sedan Parisavtalet undertecknades 2015.\n- En visualisering av hur det gått med koldioxidutsläppen för varje kommun från 1990 tills idag, hur det borde gå för att klara Parisavtalets 1,5-gradersmål och en prognos för hur det går om utsläppen i kommunen fortsätter att utvecklas som de senaste fem åren.\n- För varje kommun finns även annan nyckelfakta, som koldioxidbudget, datum när budgeten tar slut med nuvarande utsläppstakt, politiskt styre och koldioxidutsläpp per invånare.\n\nKlimatkollen är utvecklad med öppen källkod. Det betyder att alla kan vara med och utveckla och förbättra sajten via vårt [Github-repo](https://github.com/Klimatbyran/klimatkollen).", + "text": "I dag kan du på Klimatkollen se:\n\n- Koldioxidbudgetar för landets alla 290 kommuner. Klimatkollen utgår ifrån en [nationell koldioxidbudget](https://www.cemus.uu.se/wp-content/uploads/2023/12/Paris-compliant-carbon-budgets-for-Swedens-counties-.pdf) som beräknats av forskare vid Uppsala universitet enligt Tyndall-modellen och som sedan fördelats ut på kommunerna av Klimatkollen. (2022 koldioxidbudget [här](/Paris_compliant_Swedish_CO2_budgets-March_2022-Stoddard&Anderson.pdf))\n- En översikt i både kartvy och listvy över koldioxidutsläppen i landets kommuner. Kommunerna rankas baserat på genomsnittlig årlig utsläppsminskningstakt sedan Parisavtalet undertecknades 2015.\n- En visualisering av hur det gått med koldioxidutsläppen för varje kommun från 1990 tills idag, hur det borde gå för att klara Parisavtalets 1,5-gradersmål och en prognos för hur det går om utsläppen i kommunen fortsätter att utvecklas som de senaste fem åren.\n- För varje kommun finns även annan nyckelfakta, som koldioxidbudget, datum när budgeten tar slut med nuvarande utsläppstakt, politiskt styre och koldioxidutsläpp per invånare.\n\nKlimatkollen är utvecklad med öppen källkod. Det betyder att alla kan vara med och utveckla och förbättra sajten via vårt [Github-repo](https://github.com/Klimatbyran/klimatkollen).", "title": "Vad finns på Klimatkollen?" }, "earlierProjects": { diff --git a/utils/generateMunipacitySitemap.ts b/utils/generateMunipacitySitemap.ts index bceb38ad..4a3fdaca 100644 --- a/utils/generateMunipacitySitemap.ts +++ b/utils/generateMunipacitySitemap.ts @@ -12,7 +12,7 @@ type SiteMap = { } const BASE_URL = 'https://klimatkollen.se' -export const generateMunipacitySitemapData = ({ +export const generateMunicipalitySitemapData = ({ municipalities, }: { municipalities: Municipality[] @@ -41,7 +41,7 @@ export const generateSitemap = ( ${new Date().toISOString()} monthly - + ${BASE_URL}/in-english In English ${new Date().toISOString()} diff --git a/utils/politicalRuleService.tsx b/utils/politicalRuleService.tsx index beb14055..4438d12f 100644 --- a/utils/politicalRuleService.tsx +++ b/utils/politicalRuleService.tsx @@ -2,7 +2,7 @@ // Fixme revisit when there's time import RawPoliticalRule from '../data/facts/political/RawPoliticalRule' -export class PolitycalRuleService { +export class PoliticalRuleService { public getPoliticalRule(municipalityName: string) : Array { const rawMunicipality = RawPoliticalRule .find((rawPM: any) => rawPM.kommun.toLowerCase() === `${municipalityName.toLowerCase()} kommun`