diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx new file mode 100644 index 000000000..31b1d429e --- /dev/null +++ b/apps/web/app/not-found.tsx @@ -0,0 +1,25 @@ +import { BaseCard } from "@components/card"; +import PageTitle from "@components/page-title"; +import { ExclamationCircleIcon } from "@heroicons/react/24/solid"; + +export const metadata = { + title: "404 | Sidan hittades inte | Partiguiden", +}; + +export default function Error404() { + return ( +
+ + 404 - Sidan hittades inte + +
+ +

+ Sidan du letade har kanske blivit borttagen, eller skrev du in en + felaktig URL. +

+
+
+
+ ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 9aae476a6..bedda51a1 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -10,7 +10,7 @@ export const metadata = { function PageTitleContainer() { return ( -

+

Hur vill Sveriges partier förbättra
+ {subject.name} +
+ {Object.entries(standpoints).map(([party, standpoints]) => ( + + ))} +
+ + ); +} diff --git a/apps/web/app/standpunkter/[id]/party-standpoints.tsx b/apps/web/app/standpunkter/[id]/party-standpoints.tsx new file mode 100644 index 000000000..93b6cd485 --- /dev/null +++ b/apps/web/app/standpunkter/[id]/party-standpoints.tsx @@ -0,0 +1,23 @@ +"use client"; + +import type { Party, Standpoint } from "@partiguiden/party-data/types"; +import { getPartyName } from "@partiguiden/party-data/utils"; +import { useState } from "react"; + +interface PartyStandpointsProps { + party: Party; + standpoints: Standpoint[]; +} + +export default function PartyStandpoints({ + party, + standpoints, +}: PartyStandpointsProps) { + const [visible, setVisible] = useState(false); + + function handleClick() { + setVisible((prevState) => !prevState); + } + + return
{getPartyName(party)}
; +} diff --git a/apps/web/app/standpunkter/page.tsx b/apps/web/app/standpunkter/page.tsx new file mode 100644 index 000000000..e89826a05 --- /dev/null +++ b/apps/web/app/standpunkter/page.tsx @@ -0,0 +1,53 @@ +import PageTitle from "@components/page-title"; +import { getSubjects } from "@partiguiden/party-data/reader"; +import { PencilSquareIcon } from "@heroicons/react/24/solid"; +import Link from "next/link"; +import { routes } from "@lib/navigation"; + +export const metadata = { + title: "Partiernas ståndpunkter | Partiguiden", + description: + "Vad tar Sveriges partier för ståndpunkter i olika ämnen och sakfrågor? Jämför Sveriges partier politik och hitta det parti du sympatiserar mest med nu!", +}; + +const shiftColors = [ + "[&:nth-child(3n)]:bg-gray-100", + "[&:nth-child(3n+1)]:bg-gray-200", + "[&:nth-child(3n+2)]:bg-gray-200/60", + "dark:[&:nth-child(3n)]:bg-background-elevated-dark", + "dark:[&:nth-child(3n+1)]:bg-background-elevated-dark", + "dark:[&:nth-child(3n+2)]:bg-background-elevated-dark", +].join(" "); + +export default function Subjects() { + const subjects = getSubjects().sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + + return ( +
+ Partiernas Ståndpunkter +
+
+ {subjects.map((subject) => ( + + + {subject.name} + + + ))} +
+
+
+ ); +} diff --git a/apps/web/components/header/header.tsx b/apps/web/components/header/header.tsx index 0e8a579ce..ebadab1da 100644 --- a/apps/web/components/header/header.tsx +++ b/apps/web/components/header/header.tsx @@ -13,8 +13,8 @@ function MainLogo() { export default function Header() { return ( <> -
-
+
+
diff --git a/apps/web/components/header/tab-navigation.tsx b/apps/web/components/header/tab-navigation.tsx index 319d6090e..034d3e488 100644 --- a/apps/web/components/header/tab-navigation.tsx +++ b/apps/web/components/header/tab-navigation.tsx @@ -81,7 +81,7 @@ export default function TabNavigation() { key={href} href={href} aria-current={href === pathname && "page"} - className="aria-current-page:border-b-2 min-w-tab-link border-primary-light dark:border-primary-elevated-light flex-shrink-0 whitespace-nowrap p-4 text-sm uppercase hover:opacity-80" + className="aria-current-page:border-b-2 border-primary-light dark:border-primary-elevated-light min-w-[90px] flex-shrink-0 whitespace-nowrap p-4 text-sm uppercase hover:opacity-80" > {title} diff --git a/apps/web/components/page-title.tsx b/apps/web/components/page-title.tsx new file mode 100644 index 000000000..df7657168 --- /dev/null +++ b/apps/web/components/page-title.tsx @@ -0,0 +1,12 @@ +type PageTitleProps = React.PropsWithChildren<{ + Icon?: React.ElementType; +}>; + +export default function PageTitle({ children, Icon }: PageTitleProps) { + return ( +
+ {Icon && } +

{children}

+
+ ); +} diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts new file mode 100644 index 000000000..2b116e56f --- /dev/null +++ b/apps/web/lib/constants.ts @@ -0,0 +1 @@ +export const ERROR_404_TITLE = "404 | Sidan hittades inte | Partiguiden"; diff --git a/apps/web/lib/navigation.ts b/apps/web/lib/navigation.ts index 04f413b50..947eee4c2 100644 --- a/apps/web/lib/navigation.ts +++ b/apps/web/lib/navigation.ts @@ -12,13 +12,15 @@ import { export const routes = { index: "/", cookiePolicy: "/cookie-policy", - aboutUs: "/about-us", + aboutUs: "/om-oss", polls: "/polls", - votes: "/vote", + votes: "/voteringar", vote: "/vote/[id]/[bet]", decisions: "/decisions", - standpoints: "/standpoints", - standpoint: "/standpoints/[id]", + standpoints: "/standpunkter", + standpoint(id: string) { + return `/standpunkter/${id}`; + }, party: "/party/[party]", members: "/member", member: "/member/[id]", @@ -29,19 +31,6 @@ export const routes = { debate: "/debate/[id]", }; -export const getVoteHref = (id: string, bet: number): string => - `/vote/${id}/${bet}`; - -export const getStandpointHref = (id: number): string => `/standpoints/${id}`; - -export const getPartyHref = (party: string): string => `/party/${party}`; - -export const getMemberHref = (id: string): string => `/member/${id}`; - -export const getDocumentHref = (id: string): string => `/document/${id}`; - -export const getDebateHref = (id: string): string => `/debate/${id}`; - export const mainNavigation = [ { href: routes.index, title: "Hem", Icon: HomeIcon }, { diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 56ad48d01..2aeb01aee 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -7,6 +7,7 @@ const withBundleAnalyzer = bundleAnalyzer({ let moduleExports = withBundleAnalyzer({ productionBrowserSourceMaps: true, + transpilePackages: ["@partiguiden/party-data"], basePath: "", images: { remotePatterns: [ diff --git a/apps/web/package.json b/apps/web/package.json index 4b763ba91..ec3b25c95 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,6 +26,7 @@ "@mui/material": "5.14.9", "@mui/system": "5.14.9", "@next/bundle-analyzer": "13.4.19", + "@partiguiden/party-data": "workspace:*", "@sentry/nextjs": "7.69.0", "isomorphic-unfetch": "4.0.2", "jsdom": "22.1.0", diff --git a/apps/web/src/containers/Subjects.tsx b/apps/web/src/containers/Subjects.tsx deleted file mode 100644 index c44f324db..000000000 --- a/apps/web/src/containers/Subjects.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import Link from "next/link"; -import React, { useState } from "react"; - -import { styled } from "@mui/material/styles"; -import Grid from "@mui/material/Grid"; - -import type { SubjectList } from "../types/subjects"; - -import * as ROUTES from "../lib/routes"; -import { ResponsiveAd } from "../components/Ad"; -import Search from "../components/Search"; - -const SearchContainer = styled("div")` - width: 100%; - margin-top: -1rem; -`; - -const Transition = styled("span")` - margin: 0; - background: linear-gradient( - to left, - transparent 50%, - ${({ theme }) => - theme.palette.mode === "dark" - ? theme.palette.primary.dark - : theme.palette.primary.main} - 50% - ); - background-size: 202% 100%; - background-position: right bottom; - background-repeat: no-repeat; - color: ${({ theme }) => - theme.palette.mode === "dark" - ? theme.palette.primary.contrastText - : theme.palette.grey[900]}; - line-height: 50px; - padding: 0 0.5rem; - transition: all 0.2s ease-in-out; -`; - -const Button = styled("a")` - text-decoration: none; - display: flex; - flex: 1; - font-size: 1rem; - justify-content: flex-start; - :hover span { - background-position: left bottom; - color: ${({ theme }) => theme.palette.grey[100]}; - } -`; - -const Item = styled(Grid)( - ({ theme }) => ` - ${theme.breakpoints.down("sm")} { - border-left: solid 2px ${ - theme.palette.mode === "dark" - ? theme.palette.primary.dark - : theme.palette.primary.main - }; - } - ${theme.breakpoints.up("md")} { - :nth-child(2n + 1) { - border-left: solid 2px ${ - theme.palette.mode === "dark" - ? theme.palette.primary.dark - : theme.palette.primary.main - }; - } - :nth-child(2n) { - border-right: solid 2px ${ - theme.palette.mode === "dark" - ? theme.palette.primary.dark - : theme.palette.primary.main - }; - } - } - :nth-child(3n) { - background-color: - ${ - theme.palette.mode === "dark" - ? theme.palette.background.paper - : theme.palette.grey[50] - }; - } - :nth-child(3n + 1) { - background-color: - ${ - theme.palette.mode === "dark" - ? theme.palette.background.paper - : theme.palette.grey[100] - }; - } - :nth-child(3n + 2) { - background-color: - ${ - theme.palette.mode === "dark" - ? theme.palette.background.paper - : theme.palette.grey[200] - }; - } - `, -); - -const Container = styled("div")( - ({ theme }) => ` - margin-left: auto; - margin-right: auto; - ${theme.breakpoints.down("md")} { - width: 100%; - } - ${theme.breakpoints.up("md")} { - max-width: 70%; - } - ${theme.breakpoints.up("lg")} { - max-width: 60%; - } -`, -); - -interface Props { - subjects: SubjectList; -} - -const Subjects: React.FC = ({ subjects }) => { - const [shownSubjects, setShownSubjects] = useState(subjects); - - return ( - - - - - - {shownSubjects.map((subject) => ( - - - - - - ))} - - - - ); -}; - -export default Subjects; diff --git a/apps/web/src/pages/standpoints/index.tsx b/apps/web/src/pages/standpoints/index.tsx deleted file mode 100644 index 57a9c8bfd..000000000 --- a/apps/web/src/pages/standpoints/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { GetStaticProps, InferGetStaticPropsType, NextPage } from "next"; -import Head from "next/head"; - -import NoteIcon from "@mui/icons-material/Note"; - -import PageTitle from "../../components/PageTitle"; - -import type { SubjectList } from "../../types/subjects"; -import { getSubjects } from "../../lib/api"; -import Subjects from "../../containers/Subjects"; - -const StandpointsListContainer: NextPage< - InferGetStaticPropsType -> = ({ subjects }) => ( - <> - - Partiernas ståndpunkter | Partiguiden - - - - - -); - -export const getStaticProps: GetStaticProps<{ - subjects: SubjectList; -}> = async () => { - const subjects = await getSubjects(); - - return { props: { subjects }, revalidate: 518400 }; -}; - -export default StandpointsListContainer; diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index e6b058421..53e32a88c 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -51,7 +51,6 @@ const config: Config = { }, minWidth: { screen: "100vw", - "tab-link": "90px", }, minHeight: { screen: [ diff --git a/packages/party-data/cli.ts b/packages/party-data/cli.ts index d1be79f71..5d650a490 100644 --- a/packages/party-data/cli.ts +++ b/packages/party-data/cli.ts @@ -1,10 +1,7 @@ import { Separator, select } from "@inquirer/prompts"; -import { - readNotCategorizedStandpoints, - readSubjects, - updateStandpoint, -} from "."; -import type { Standpoint } from "./client"; +import { readSubjects, readNotCategorizedStandpoints } from "./reader"; +import { updateStandpoint } from "./writer"; +import type { Standpoint } from "./types"; const promptTemplate = ( standpoint: Standpoint, diff --git a/packages/party-data/package.json b/packages/party-data/package.json index f38b75289..4052a1129 100644 --- a/packages/party-data/package.json +++ b/packages/party-data/package.json @@ -10,7 +10,10 @@ }, "exports": { ".": "./index.ts", - "./client": "./client.ts" + "./utils": "./utils.ts", + "./reader": "./reader.ts", + "./writer": "./writer.ts", + "./types": "./types.ts" }, "devDependencies": { "@partiguiden/eslint-config-base": "workspace:*", diff --git a/packages/party-data/client.ts b/packages/party-data/reader.ts similarity index 58% rename from packages/party-data/client.ts rename to packages/party-data/reader.ts index e9f0231f3..8c8fb7cbb 100644 --- a/packages/party-data/client.ts +++ b/packages/party-data/reader.ts @@ -7,37 +7,17 @@ import kd from "./parties/kd.json"; import c from "./parties/c.json"; import v from "./parties/v.json"; import subjects from "./subjects.json"; - -enum Party { - S = "s", - SD = "sd", - M = "m", - MP = "mp", - L = "l", - KD = "kd", - C = "c", - V = "v", -} - -export interface Subject { - id: string; - name: string; - relatedSubjects: string[]; -} - -export interface Standpoint { - title: string; - url: string; - opinions: string[]; - fetchDate: string; - party: string; - subject?: string; -} +import type { Standpoint } from "./types"; +import { Party, type Subject } from "./types"; export function getSubjects(): Subject[] { return Object.values(subjects); } +export function getSubject(id: string): Subject | undefined { + return getSubjects().find((subject) => subject.id === id); +} + function getPartyData(abbreviation: string) { switch (abbreviation.toLocaleLowerCase()) { case Party.S: @@ -61,7 +41,21 @@ function getPartyData(abbreviation: string) { } export function readPartyStandpoints(abbreviation: Party): Standpoint[] { - return Object.values(getPartyData(abbreviation) as unknown); + return Object.values(getPartyData(abbreviation)); +} + +export function getStandpointsForSubject(subject: string) { + return Object.values(Party) + .sort() + .reduce>( + (prev, party) => ({ + ...prev, + [party]: readPartyStandpoints(party).filter( + (standpoint) => standpoint.subject === subject, + ), + }), + {} as Record, + ); } export function readPartyDataForSubject(party: Party, subjectName: string) { @@ -72,3 +66,12 @@ export function readPartyDataForSubject(party: Party, subjectName: string) { export function readAllStandpoints(): Standpoint[] { return Object.values(Party).map(readPartyStandpoints).flat(); } + +export function readNotCategorizedStandpoints() { + const standpoints = readAllStandpoints(); + return standpoints.filter((standpoint) => standpoint.subject === undefined); +} + +export function readSubjects(): Subject[] { + return Object.values(subjects); +} diff --git a/packages/party-data/types.ts b/packages/party-data/types.ts new file mode 100644 index 000000000..db824cc87 --- /dev/null +++ b/packages/party-data/types.ts @@ -0,0 +1,35 @@ +export enum Party { + S = "s", + SD = "sd", + M = "m", + MP = "mp", + L = "l", + KD = "kd", + C = "c", + V = "v", +} + +export interface Subject { + id: string; + name: string; + relatedSubjects: string[]; +} + +export interface Standpoint { + title: string; + url: string; + opinions: string[]; + fetchDate: string; + party: string; + subject?: string; +} + +export interface SubjectData { + [id: string]: Subject; +} + +export interface PartyData { + [url: string]: Standpoint; +} + +export type PartyDataWithoutPartyName = Omit; diff --git a/packages/party-data/utils.ts b/packages/party-data/utils.ts new file mode 100644 index 000000000..7afae762f --- /dev/null +++ b/packages/party-data/utils.ts @@ -0,0 +1,22 @@ +import { Party } from "./types"; + +export function getPartyName(party: Party): string { + switch (party) { + case Party.C: + return "Centerpartiet"; + case Party.KD: + return "Kristdemokraterna"; + case Party.L: + return "Liberalerna"; + case Party.M: + return "Moderaterna"; + case Party.MP: + return "Miljöpartiet"; + case Party.S: + return "Socialdemokraterna"; + case Party.SD: + return "Sverigedemokraterna"; + case Party.V: + return "Vänsterpartiet"; + } +} diff --git a/packages/party-data/index.ts b/packages/party-data/writer.ts similarity index 67% rename from packages/party-data/index.ts rename to packages/party-data/writer.ts index 861ee088f..88bbafa3c 100644 --- a/packages/party-data/index.ts +++ b/packages/party-data/writer.ts @@ -1,18 +1,7 @@ import * as fs from "node:fs"; -import { readAllStandpoints, type Standpoint, type Subject } from "./client"; - -export interface PartyData { - [url: string]: Standpoint; -} - -export type PartyDataWithoutPartyName = Omit; - -interface SubjectData { - [id: string]: Subject; -} +import type { PartyData, PartyDataWithoutPartyName, Standpoint } from "./types"; const PARTIES_DIRECTORY = `${__dirname}/parties`; -const SUBJECTS_FILE = `${__dirname}/subjects.json`; const partyFileName = (abbreviation: string) => `${PARTIES_DIRECTORY}/${abbreviation.toLocaleLowerCase()}.json`; @@ -85,28 +74,3 @@ export function updateStandpoint(abbreviation: string, standpoint: Standpoint) { storedData[standpoint.url] = standpoint; fs.writeFileSync(fileName, JSON.stringify(storedData, null, 2) + "\n"); } - -function readSubjectData() { - return JSON.parse(fs.readFileSync(SUBJECTS_FILE).toString()) as SubjectData; -} - -export function readSubjects(): Subject[] { - return Object.values(readSubjectData()); -} - -export function readPartyData(abbreviation: string): Standpoint[] { - const partyData = JSON.parse( - fs.readFileSync(partyFileName(abbreviation)).toString(), - ) as PartyData; - return Object.values(partyData); -} - -export function readPartyDataForSubject(party: string, subjectName: string) { - const partyData = readPartyData(party); - return partyData.filter((subject) => subject.subject === subjectName); -} - -export function readNotCategorizedStandpoints() { - const standpoints = readAllStandpoints(); - return standpoints.filter((standpoint) => standpoint.subject === undefined); -} diff --git a/packages/scrapers/src/index.ts b/packages/scrapers/src/index.ts index 42d5bd914..137f38ba4 100644 --- a/packages/scrapers/src/index.ts +++ b/packages/scrapers/src/index.ts @@ -1,6 +1,6 @@ import { parseArgs } from "node:util"; import scrapers from "./scrapers"; -import { writePartyData } from "@partiguiden/party-data"; +import { writePartyData } from "@partiguiden/party-data/writer"; const { values: { party }, diff --git a/packages/scrapers/src/party/sd-scraper.ts b/packages/scrapers/src/party/sd-scraper.ts index abfd73c43..d0c4d0254 100644 --- a/packages/scrapers/src/party/sd-scraper.ts +++ b/packages/scrapers/src/party/sd-scraper.ts @@ -1,6 +1,6 @@ import * as pdfjs from "pdfjs-dist"; import Scraper from "../scraper"; -import type { PartyDataWithoutPartyName } from "@partiguiden/party-data"; +import type { PartyDataWithoutPartyName } from "@partiguiden/party-data/writer"; type SectionDestination = [ { num: number; gen: number }, diff --git a/packages/scrapers/src/scraper.ts b/packages/scrapers/src/scraper.ts index d40665eb3..ea704afe0 100644 --- a/packages/scrapers/src/scraper.ts +++ b/packages/scrapers/src/scraper.ts @@ -2,7 +2,7 @@ import type { Cheerio, CheerioAPI, Element } from "cheerio"; import type { PartyData, PartyDataWithoutPartyName, -} from "@partiguiden/party-data"; +} from "@partiguiden/party-data/writer"; import * as cheerio from "cheerio"; interface ScraperArgs { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08962d9f7..9938722c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: '@next/bundle-analyzer': specifier: 13.4.19 version: 13.4.19 + '@partiguiden/party-data': + specifier: workspace:* + version: link:../../packages/party-data '@sentry/nextjs': specifier: 7.69.0 version: 7.69.0(next@13.4.19)(react@18.2.0)