diff --git a/.env.example b/.env.example index e24495ca..e859b4bb 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,2 @@ -# Required -BRING_API_ID= -BRING_API_KEY= - - -# Optional -# API_URL= -# BL_WEB_URL= +NEXT_PUBLIC_API_URL= +NEXT_PUBLIC_BL_WEB_URL= diff --git a/.eslintrc.js b/.eslintrc.js index ece59f02..27f541c6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -61,13 +61,13 @@ module.exports = { prop: false, props: false, ref: false, + params: false, }, }, ], "unicorn/no-null": "off", // .textContent is very different from .innerText, not interchangeable "unicorn/prefer-dom-node-text-content": "off", - /** @see https://medium.com/weekly-webtips/how-to-sort-imports-like-a-pro-in-typescript-4ee8afd7258a */ "import/order": [ "error", diff --git a/.husky/pre-commit b/.husky/pre-commit index 8dd34d2d..046680a6 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - yarn prettier:check && yarn lint diff --git a/cypress/e2e/branch-select.spec.js b/cypress/e2e/branch-select.spec.js deleted file mode 100644 index 77718726..00000000 --- a/cypress/e2e/branch-select.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -describe("Info pages", () => { - beforeEach(() => { - cy.visit("/"); - }); - - describe("Branch selection", () => { - it("displays placeholder when branch is not present", () => { - cy.getBySel("branchSelect").should("be.visible"); - cy.getBySel("branchSelectLabel").should("contain", "Velg skole"); - }); - - it("can select a branch", () => { - cy.getBySel("branchSelect").click(); - cy.getBySel("branchOption").should("have.length", 4); - // eslint-disable-next-line cypress/unsafe-to-chain-command - cy.getBySel("branchOption") - .eq(0) - .click() - .should(() => { - expect(localStorage.getItem("bl-current-branch-id")).to.eq( - "60074e293309ff001a51b244", - ); - }); - - cy.visit("/"); - cy.getBySel("branchSelect").should(() => { - expect(localStorage.getItem("bl-current-branch-id")).to.eq( - "60074e293309ff001a51b244", - ); - }); - - cy.getBySel("branchSelect").click(); - // eslint-disable-next-line cypress/unsafe-to-chain-command - cy.getBySel("branchOption") - .eq(1) - .click() - .should(() => { - expect(localStorage.getItem("bl-current-branch-id")).to.eq( - "5dfa263e8eeee5001c83eacf", - ); - }); - }); - }); -}); diff --git a/next.config.js b/next.config.js index d8ebada4..642dbbee 100644 --- a/next.config.js +++ b/next.config.js @@ -4,8 +4,4 @@ module.exports = { eslint: { dirs: ["src", "cypress"], }, - publicRuntimeConfig: { - API_URL: process.env.API_URL ?? "http://localhost:1337/", - BL_WEB_URL: process.env.BL_WEB_URL ?? "http://localhost:4200/", - }, }; diff --git a/package.json b/package.json index 1a01b4ca..de2d0618 100644 --- a/package.json +++ b/package.json @@ -17,68 +17,67 @@ "prettier": "prettier --write '**/*.{js,ts,tsx,md,json,yml,css}' --ignore-path=.gitignore", "prettier:check": "prettier --check '**/*.{js,ts,tsx,md,json,yml,css}' --ignore-path=.gitignore", "lint": "next lint --ignore-path=.gitignore", - "prepare": "husky install" + "prepare": "husky" }, "dependencies": { "@date-io/moment": "^3.0.0", + "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.4", - "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.5", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "^5.15.15", + "@mui/icons-material": "^5.15.21", "@mui/lab": "^5.0.0-alpha.170", - "@mui/material": "^5.15.15", - "@mui/x-date-pickers": "^7.3.1", - "@reduxjs/toolkit": "^2.2.3", - "@yudiel/react-qr-scanner": "^2.0.1", - "axios": "^1.6.8", + "@mui/material": "^5.15.21", + "@mui/material-nextjs": "^5.15.11", + "@mui/x-date-pickers": "^7.8.0", + "@yudiel/react-qr-scanner": "^2.0.4", + "axios": "^1.7.2", "draft-js": "^0.11.7", "moment": "^2.30.1", - "next": "^14.2.3", + "next": "^14.2.4", "react": "18.3.1", "react-dom": "18.3.1", "react-draft-wysiwyg": "^1.15.0", - "react-hook-form": "^7.51.3", + "react-hook-form": "^7.52.1", "react-jwt": "^1.2.1", "react-quill": "^2.0.0", - "react-redux": "^9.1.1", "sanitize-html": "^2.13.0", - "sharp": "^0.33.3", + "sharp": "^0.33.4", "string-similarity": "^4.0.4", "swr": "^2.2.5", - "validator": "^13.11.0" + "validator": "^13.12.0", + "zustand": "^4.5.4" }, "devDependencies": { - "@boklisten/bl-model": "^0.25.41", + "@boklisten/bl-model": "^0.26.2", "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", - "@testing-library/react": "^15.0.5", + "@testing-library/react": "^16.0.0", "@types/draft-js": "^0.11.18", - "@types/node": "20.12.7", - "@types/react": "18.3.1", + "@types/node": "20.14.9", + "@types/react": "18.3.3", "@types/react-draft-wysiwyg": "^1.13.8", - "@types/react-redux": "^7.1.33", "@types/sanitize-html": "^2.11.0", "@types/string-similarity": "^4.0.2", - "@types/validator": "^13.11.9", - "@typescript-eslint/eslint-plugin": "^7.8.0", - "@typescript-eslint/parser": "^7.8.0", - "cypress": "^13.8.1", + "@types/validator": "^13.12.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "cypress": "^13.13.0", "eslint": "8.56.0", - "eslint-config-next": "14.2.3", + "eslint-config-next": "14.2.4", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-cypress": "^3.0.2", + "eslint-plugin-cypress": "^3.3.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-no-relative-import-paths": "^1.5.4", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-unicorn": "^52.0.0", + "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-no-relative-import-paths": "^1.5.5", + "eslint-plugin-promise": "^6.4.0", + "eslint-plugin-unicorn": "^54.0.0", "husky": ">=9.0.11", - "lint-staged": ">=15.2.2", - "prettier": "^3.2.5", + "lint-staged": ">=15.2.7", + "prettier": "^3.3.2", "pretty-quick": "^4.0.0", - "typescript": "^5.4.5" + "typescript": "^5.5.3" }, "browserslist": [ "defaults", diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 1f53798b..00000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / diff --git a/src/api/requests.ts b/src/api/requests.ts index 88b8ba8c..769f394a 100644 --- a/src/api/requests.ts +++ b/src/api/requests.ts @@ -16,8 +16,7 @@ export const fetchData = async (url: string, method: string, data: unknown) => { } }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const fetcher = async (url: string) => { +export const fetcher = async (url: string): Promise => { try { return await axios .get<{ data: T }>(url) @@ -26,6 +25,6 @@ export const fetcher = async (url: string) => { if (!((error as AxiosError).response?.status === 404)) { throw error; } - return []; + return null; } }; diff --git a/src/pages/info/about.tsx b/src/app/info/about/page.tsx similarity index 52% rename from src/pages/info/about.tsx rename to src/app/info/about/page.tsx index 1625ce77..2e1bb1cf 100644 --- a/src/pages/info/about.tsx +++ b/src/app/info/about/page.tsx @@ -1,22 +1,20 @@ import { Card } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import DynamicNav from "@/components/info/DynamicNav"; import Editor from "@/components/info/Editor"; import { infoPageTabs } from "@/utils/constants"; import { editorData } from "@/utils/mockData"; -const About: NextPage = () => { +export const metadata: Metadata = { + title: "Om oss | Boklisten.no", + description: + "Boklisten har mange års erfaring med kjøp og salg av pensumbøker. Les om vår historie, hvem vi er, og hva vi tilbyr.", +}; + +const AboutPage = () => { return ( <> - - Om oss | Boklisten.no - - @@ -25,4 +23,4 @@ const About: NextPage = () => { ); }; -export default About; +export default AboutPage; diff --git a/src/app/info/branch/[id]/page.tsx b/src/app/info/branch/[id]/page.tsx new file mode 100644 index 00000000..66fccf3a --- /dev/null +++ b/src/app/info/branch/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Branch, OpeningHour } from "@boklisten/bl-model"; +import moment from "moment"; +import { Metadata } from "next"; + +import { fetcher } from "@/api/requests"; +import LinkableBranchInfo from "@/components/LinkableBranchInfo"; +import BL_CONFIG from "@/utils/bl-config"; + +type Params = { params: { id: string } }; + +export const generateStaticParams = async () => { + const branchesUrl = `${BL_CONFIG.api.basePath}branches?&active=true`; + return (await fetcher(branchesUrl)) ?? []; +}; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ params }: Params): Promise { + const branchData = await fetcher( + `${BL_CONFIG.api.basePath}branches/${params.id}`, + ); + return { + title: `${branchData?.[0]?.name ?? "Skoler og åpningstider"} | Boklisten.no`, + description: + "Skal du hente eller levere bøker? Finn ut når vi står på stand på din skole.", + }; +} + +async function getBranchData(branchId: string) { + if (branchId === "select") { + return { branch: null, openingHours: [] }; + } + const branchUrl = `${BL_CONFIG.api.basePath}branches/${branchId}`; + const now = moment().startOf("day").format("DDMMYYYYHHmm"); + const openingHoursUrl = `${BL_CONFIG.api.basePath}openingHours?branch=${branchId}&from=>${now}`; + const [branchData, openingHoursData] = await Promise.all([ + fetcher(branchUrl), + fetcher(openingHoursUrl), + ]); + + return { + branch: branchData?.[0] ?? null, + openingHours: openingHoursData, + }; +} + +const BranchPage = async ({ params }: Params) => { + const { branch, openingHours } = await getBranchData(params.id); + + return ( + + ); +}; + +export default BranchPage; diff --git a/src/app/info/buyback/page.tsx b/src/app/info/buyback/page.tsx new file mode 100644 index 00000000..e29f3a9d --- /dev/null +++ b/src/app/info/buyback/page.tsx @@ -0,0 +1,32 @@ +import { Item } from "@boklisten/bl-model"; +import { Card } from "@mui/material"; +import { Metadata } from "next"; + +import { fetcher } from "@/api/requests"; +import BuybackList from "@/components/info/BuybackList"; +import DynamicNav from "@/components/info/DynamicNav"; +import BL_CONFIG from "@/utils/bl-config"; +import { infoPageTabs } from "@/utils/constants"; + +export const metadata: Metadata = { + title: "Innkjøpsliste | Boklisten.no", + description: + "Har du pensumbøker du ikke lenger har bruk for? Vi kjøper inn de aller fleste pensumbøker. Se oversikten over hvilke bøker vi tar imot her.", +}; + +const BuybackPage = async () => { + const buybackItems = await fetcher( + `${BL_CONFIG.api.basePath}items?buyback=true&sort=title`, + ); + + return ( + <> + + + + + + ); +}; + +export default BuybackPage; diff --git a/src/app/info/companies/page.tsx b/src/app/info/companies/page.tsx new file mode 100644 index 00000000..e674be95 --- /dev/null +++ b/src/app/info/companies/page.tsx @@ -0,0 +1,26 @@ +import { Card } from "@mui/material"; +import { Metadata } from "next"; + +import DynamicNav from "@/components/info/DynamicNav"; +import Editor from "@/components/info/Editor"; +import { infoPageTabs } from "@/utils/constants"; +import { editorData } from "@/utils/mockData"; + +export const metadata: Metadata = { + title: "For skolekunder | Boklisten.no", + description: + "Er du ansvarlig for en videregående eller privatist-skole? Vi tilbyr en rekke nyttige tjenester til dere! Les om våre tilbud til skoler, hvordan utlånsordningen fungrer og hvordan dere kan kjøpe bøker fra skyvearkivet.", +}; + +const Page = () => { + return ( + <> + + + + + + ); +}; + +export default Page; diff --git a/src/pages/info/contact.tsx b/src/app/info/contact/page.tsx similarity index 55% rename from src/pages/info/contact.tsx rename to src/app/info/contact/page.tsx index cd8c9b57..eba0a80d 100644 --- a/src/pages/info/contact.tsx +++ b/src/app/info/contact/page.tsx @@ -1,21 +1,19 @@ import { Card, Typography } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import ContactInfo from "@/components/info/ContactInfo"; import DynamicNav from "@/components/info/DynamicNav"; import { infoPageTabs } from "@/utils/constants"; -const Contact: NextPage = () => { +export const metadata: Metadata = { + title: "Kontakt oss | Boklisten.no", + description: + "Vi er tilgjengelig for spørsmål og henvendelser både på epost og telefon. Se vår kontaktinformasjon, med epost-adresse, telefonnummer og gateadresse.", +}; + +const ContactPage = () => { return ( <> - - Kontakt oss | Boklisten.no - - { ); }; -export default Contact; +export default ContactPage; diff --git a/src/pages/info/faq.tsx b/src/app/info/faq/page.tsx similarity index 53% rename from src/pages/info/faq.tsx rename to src/app/info/faq/page.tsx index 38fbc8ee..bc169115 100644 --- a/src/pages/info/faq.tsx +++ b/src/app/info/faq/page.tsx @@ -1,22 +1,20 @@ import { Card } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import DynamicNav from "@/components/info/DynamicNav"; import EditableQNA from "@/components/info/EditableQna"; import { infoPageTabs } from "@/utils/constants"; import { QNAs } from "@/utils/mockData"; -const FAQ: NextPage = () => { +export const metadata: Metadata = { + title: "Spørsmål og svar | Boklisten.no", + description: + "Hva betyr det at Boklisten alltid leverer riktig bok? Hvordan bestiller jeg bøker som privatist?", +}; + +const FaqPage = () => { return ( <> - - Spørsmål og svar | Boklisten.no - - @@ -25,4 +23,4 @@ const FAQ: NextPage = () => { ); }; -export default FAQ; +export default FaqPage; diff --git a/src/pages/info/general.tsx b/src/app/info/general/page.tsx similarity index 51% rename from src/pages/info/general.tsx rename to src/app/info/general/page.tsx index 57cbdb13..278fbe84 100644 --- a/src/pages/info/general.tsx +++ b/src/app/info/general/page.tsx @@ -1,22 +1,20 @@ import { Card } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import DynamicNav from "@/components/info/DynamicNav"; import Editor from "@/components/info/Editor"; import { infoPageTabs } from "@/utils/constants"; import { editorData } from "@/utils/mockData"; -const General: NextPage = () => { +export const metadata: Metadata = { + title: "Generell informasjon | Boklisten.no", + description: + "Velkommen til Boklisten.no! Her kan du enkelt kjøpe pensumbøker. Les om vårt konsept, og hvilke tjenester vi tilbyr her.", +}; + +const Page = () => { return ( <> - - Generell informasjon | Boklisten.no - - @@ -25,4 +23,4 @@ const General: NextPage = () => { ); }; -export default General; +export default Page; diff --git a/src/pages/info/policies/conditions.tsx b/src/app/info/policies/conditions/page.tsx similarity index 59% rename from src/pages/info/policies/conditions.tsx rename to src/app/info/policies/conditions/page.tsx index e2d382c6..da41d014 100644 --- a/src/pages/info/policies/conditions.tsx +++ b/src/app/info/policies/conditions/page.tsx @@ -1,6 +1,5 @@ import { Card } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import DynamicNav from "@/components/info/DynamicNav"; import DynamicSubNav from "@/components/info/DynamicSubNav"; @@ -8,16 +7,15 @@ import Editor from "@/components/info/Editor"; import { infoPageTabs, termsAndConditionsTabs } from "@/utils/constants"; import { editorData } from "@/utils/mockData"; -const Policies: NextPage = () => { +export const metadata: Metadata = { + title: "Betingelser | Boklisten.no", + description: + "Vi tar kundene våre på alvor. Derfor har vi laget detaljerte betingelser, slik at du vet hva som gjelder for din ordre.", +}; + +const ConditionsPage = () => { return ( <> - - Betingelser | Boklisten.no - - @@ -27,4 +25,4 @@ const Policies: NextPage = () => { ); }; -export default Policies; +export default ConditionsPage; diff --git a/src/pages/info/policies/privacy.tsx b/src/app/info/policies/privacy/page.tsx similarity index 58% rename from src/pages/info/policies/privacy.tsx rename to src/app/info/policies/privacy/page.tsx index 9a8896dd..c5c55530 100644 --- a/src/pages/info/policies/privacy.tsx +++ b/src/app/info/policies/privacy/page.tsx @@ -1,6 +1,5 @@ import { Card } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import DynamicNav from "@/components/info/DynamicNav"; import DynamicSubNav from "@/components/info/DynamicSubNav"; @@ -8,16 +7,15 @@ import Editor from "@/components/info/Editor"; import { infoPageTabs, termsAndConditionsTabs } from "@/utils/constants"; import { editorData } from "@/utils/mockData"; -const Terms: NextPage = () => { +export const metadata: Metadata = { + title: "Personvernavtale | Boklisten.no", + description: + "Vi tar personvern på alvor. Derfor har vi laget et dokument som viser en oversikt over hvordan din data bir behandlet hos oss.", +}; + +const PrivacyPage = () => { return ( <> - - Personvernavtale | Boklisten.no - - @@ -27,4 +25,4 @@ const Terms: NextPage = () => { ); }; -export default Terms; +export default PrivacyPage; diff --git a/src/pages/info/policies/terms.tsx b/src/app/info/policies/terms/page.tsx similarity index 59% rename from src/pages/info/policies/terms.tsx rename to src/app/info/policies/terms/page.tsx index e761e7de..947202aa 100644 --- a/src/pages/info/policies/terms.tsx +++ b/src/app/info/policies/terms/page.tsx @@ -1,6 +1,5 @@ import { Card } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import DynamicNav from "@/components/info/DynamicNav"; import DynamicSubNav from "@/components/info/DynamicSubNav"; @@ -8,16 +7,15 @@ import Editor from "@/components/info/Editor"; import { infoPageTabs, termsAndConditionsTabs } from "@/utils/constants"; import { editorData } from "@/utils/mockData"; -const Terms: NextPage = () => { +export const metadata: Metadata = { + title: "Vilkår | Boklisten.no", + description: + "Når du handler hos oss gjelder noen vilkår. Disse er her for å gi alle parter trygghet for hvilke regler som gjelder.", +}; + +const TermsPage = () => { return ( <> - - Vilkår | Boklisten.no - - @@ -27,4 +25,4 @@ const Terms: NextPage = () => { ); }; -export default Terms; +export default TermsPage; diff --git a/src/pages/info/pupils.tsx b/src/app/info/pupils/page.tsx similarity index 54% rename from src/pages/info/pupils.tsx rename to src/app/info/pupils/page.tsx index 1e90c524..499c050f 100644 --- a/src/pages/info/pupils.tsx +++ b/src/app/info/pupils/page.tsx @@ -1,22 +1,20 @@ import { Card } from "@mui/material"; -import type { NextPage } from "next"; -import Head from "next/head"; +import { Metadata } from "next"; import DynamicNav from "@/components/info/DynamicNav"; import Editor from "@/components/info/Editor"; import { infoPageTabs } from "@/utils/constants"; import { editorData } from "@/utils/mockData"; -const Pupils: NextPage = () => { +export const metadata: Metadata = { + title: "For VGS-elever | Boklisten.no", + description: + "Er du videregående-elev? Finn dine kontaktelever og når utdeling og innsamling skjer.", +}; + +const Page = () => { return ( <> - - For VGS-elever | Boklisten.no - - @@ -25,4 +23,4 @@ const Pupils: NextPage = () => { ); }; -export default Pupils; +export default Page; diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 00000000..a2d42a4c --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,65 @@ +import { Container } from "@mui/material"; +import CssBaseline from "@mui/material/CssBaseline"; +import { Box } from "@mui/system"; +import { ReactNode, Suspense } from "react"; +import * as React from "react"; + +import AuthLinker from "@/components/AuthLinker"; +import CustomThemeProvider from "@/components/CustomThemeProvider"; +import DynamicHeightProvider from "@/components/DynamicHeightProvider"; +import Footer from "@/components/Footer"; +import CustomLocalizationProvider from "@/components/LocalizationProvider"; +import NavBar from "@/components/NavBar"; + +import "@fontsource/roboto/300.css"; +import "@fontsource/roboto/400.css"; +import "@fontsource/roboto/500.css"; +import "@fontsource/roboto/700.css"; +import "@/globals.css"; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + + + + + + + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + + + + {children} + + + +