mirror of
https://github.com/navikt/aksel.git
synced 2026-10-19 12:12:03 +00:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f317878fc | ||
|
|
c3064a01f7 | ||
|
|
02f0a911b0 | ||
|
|
ee8524229f | ||
|
|
d9932e203a | ||
|
|
b14fc23ecb | ||
|
|
c16f75d6f8 | ||
|
|
a01be91608 | ||
|
|
8826a79021 | ||
|
|
c2f3da84cc | ||
|
|
2c1476022d | ||
|
|
dacf648c47 | ||
|
|
5ec986771b | ||
|
|
ca6af8a5f7 | ||
|
|
9410840f74 | ||
|
|
d88884f785 | ||
|
|
0c8239ee00 | ||
|
|
987118fd47 | ||
|
|
8d13f11689 | ||
|
|
158e18454e | ||
|
|
be26bf22c9 | ||
|
|
c2968a0e6d | ||
|
|
3f92773273 | ||
|
|
0c3ab1d0e7 | ||
|
|
3a73a029f6 | ||
|
|
6fd0f501f5 | ||
|
|
a11b8ddf4e | ||
|
|
171db6a813 |
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 6.4.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.1",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 6.4.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.1",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
|
||||
"repository": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^6.5.0",
|
||||
"@navikt/ds-tokens": "^6.5.0",
|
||||
"@navikt/ds-css": "^6.6.1",
|
||||
"@navikt/ds-tokens": "^6.6.1",
|
||||
"concurrently": "7.2.1",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 6.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Aksel CLI: Fjernet deprecated komponenter fra css oversikt. ([#2860](https://github.com/navikt/aksel/pull/2860))
|
||||
|
||||
- Updated dependencies [[`3a73a029f`](https://github.com/navikt/aksel/commit/3a73a029f6272f76e10cb6baefbaa2040feaef6b)]:
|
||||
- @navikt/ds-css@6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.1",
|
||||
"description": "Aksel command line interface. Handles css-imports, codemods and more",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
|
||||
"dependencies": {
|
||||
"@navikt/ds-css": "6.5.0",
|
||||
"@navikt/ds-css": "6.6.1",
|
||||
"axios": "1.6.0",
|
||||
"chalk": "4.1.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Ny komponent FormSummary ([#2802](https://github.com/navikt/aksel/pull/2802))
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 6.4.1
|
||||
|
||||
@@ -97,10 +97,6 @@ const StyleMappings = {
|
||||
main: formCss,
|
||||
dependencies: [typoCss],
|
||||
},
|
||||
{
|
||||
component: "ContentContainer",
|
||||
main: "content-container.css",
|
||||
},
|
||||
{
|
||||
component: "CopyButton",
|
||||
main: "copybutton.css",
|
||||
@@ -125,7 +121,6 @@ const StyleMappings = {
|
||||
main: formCss,
|
||||
dependencies: [typoCss, "button.css", "loader.css", "link.css"],
|
||||
},
|
||||
{ component: "Grid", main: "grid.css", dependencies: [typoCss] },
|
||||
{
|
||||
component: "GuidePanel",
|
||||
main: "guide-panel.css",
|
||||
@@ -178,6 +173,11 @@ const StyleMappings = {
|
||||
{ component: "Tag", main: "tag.css", dependencies: [typoCss] },
|
||||
{ component: "TextField", main: formCss, dependencies: [typoCss] },
|
||||
{ component: "Textarea", main: formCss, dependencies: [typoCss] },
|
||||
{
|
||||
component: "FormSummary",
|
||||
main: formCss,
|
||||
dependencies: [typoCss, "link.css"],
|
||||
},
|
||||
{
|
||||
component: "ToggleGroup",
|
||||
main: "toggle-group.css",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.navds-form-summary {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--a-border-subtle);
|
||||
border-radius: var(--a-border-radius-large);
|
||||
}
|
||||
|
||||
.navds-form-summary__header {
|
||||
background-color: var(--a-surface-subtle);
|
||||
padding: var(--a-spacing-4) var(--a-spacing-6);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0 var(--a-spacing-2);
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.navds-form-summary__header {
|
||||
padding: var(--a-spacing-3) var(--a-spacing-4);
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.navds-form-summary__edit {
|
||||
flex-shrink: 0;
|
||||
margin-top: var(--a-spacing-1);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.navds-form-summary > .navds-form-summary__answers {
|
||||
border-top: 1px solid var(--a-border-subtle);
|
||||
}
|
||||
|
||||
.navds-form-summary__answers {
|
||||
padding: var(--a-spacing-5) var(--a-spacing-6) var(--a-spacing-6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.navds-form-summary__answers {
|
||||
padding: var(--a-spacing-4);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:not(:last-child) {
|
||||
margin-bottom: var(--a-spacing-4);
|
||||
padding-bottom: var(--a-spacing-4);
|
||||
border-bottom: 1px solid var(--a-border-divider);
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:has(.navds-form-summary__answer) {
|
||||
padding-bottom: var(--a-spacing-6);
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:has(.navds-form-summary__answer):last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.navds-form-summary__answer:not(:last-child) {
|
||||
margin-bottom: var(--a-spacing-3);
|
||||
padding-bottom: var(--a-spacing-3);
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:has(.navds-form-summary__answer) {
|
||||
padding-bottom: var(--a-spacing-5);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-form-summary__value:first-of-type {
|
||||
margin-top: var(--a-spacing-1);
|
||||
}
|
||||
|
||||
.navds-form-summary__value :where(.navds-form-summary__answers) {
|
||||
margin-top: var(--a-spacing-2);
|
||||
padding: var(--a-spacing-4);
|
||||
background: var(--a-surface-selected);
|
||||
border-radius: var(--a-border-radius-large);
|
||||
}
|
||||
@@ -13,3 +13,4 @@
|
||||
@import "textarea.css";
|
||||
@import "search.css";
|
||||
@import "combobox.css";
|
||||
@import "form-summary.css";
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.1",
|
||||
"description": "CSS for NAV Designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -27,7 +27,7 @@
|
||||
"css:get-version": "node config/get-version.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^6.5.0",
|
||||
"@navikt/ds-tokens": "^6.6.1",
|
||||
"cssnano": "6.0.0",
|
||||
"fast-glob": "3.2.11",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 6.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Spacer: Er nå `span` (tidligere `div`). ([#2874](https://github.com/navikt/aksel/pull/2874))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.6.1
|
||||
- @navikt/aksel-icons@6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Ny komponent FormSummary ([#2802](https://github.com/navikt/aksel/pull/2802))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Combobox: Prevents "Enter" while Combobox is focused from submitting form. ([#2861](https://github.com/navikt/aksel/pull/2861))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.6.0
|
||||
- @navikt/aksel-icons@6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.1",
|
||||
"description": "React components from the Norwegian Labour and Welfare Administration.",
|
||||
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
|
||||
"license": "MIT",
|
||||
@@ -24,7 +24,8 @@
|
||||
"/cjs",
|
||||
"/esm",
|
||||
"/src",
|
||||
"!**/*.tsbuildinfo"
|
||||
"!**/*.tsbuildinfo",
|
||||
"!**/*.stories.*"
|
||||
],
|
||||
"main": "./cjs/index.js",
|
||||
"module": "./esm/index.js",
|
||||
@@ -549,6 +550,16 @@
|
||||
"default": "./cjs/form/file-upload/index.js"
|
||||
}
|
||||
},
|
||||
"./FormSummary": {
|
||||
"import": {
|
||||
"types": "./esm/form/form-summary/index.d.ts",
|
||||
"default": "./esm/form/form-summary/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./cjs/form/form-summary/index.d.ts",
|
||||
"default": "./cjs/form/form-summary/index.js"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -562,8 +573,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.25.4",
|
||||
"@navikt/aksel-icons": "^6.5.0",
|
||||
"@navikt/ds-tokens": "^6.5.0",
|
||||
"@navikt/aksel-icons": "^6.6.1",
|
||||
"@navikt/ds-tokens": "^6.6.1",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"react-day-picker": "8.10.0"
|
||||
|
||||
@@ -183,3 +183,10 @@ export const Chromatic: Story = {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
|
||||
export const ChromaticForcedColors: Story = {
|
||||
render: Chromatic.render,
|
||||
parameters: {
|
||||
chromatic: { disable: false, forcedColors: "active" },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createContext } from "../util/create-context";
|
||||
import { CollapsibleBaseProps } from "./Collapsible.types";
|
||||
|
||||
export type CollapsibleContextProps = Pick<CollapsibleBaseProps, "open"> & {
|
||||
/**
|
||||
* Allows connecting trigger to content
|
||||
*/
|
||||
contentId: string;
|
||||
/**
|
||||
* Allows connecting content to trigger
|
||||
*/
|
||||
triggerId: string;
|
||||
/**
|
||||
* Callback for toggling open state
|
||||
*/
|
||||
onOpenToggle: () => void;
|
||||
/**
|
||||
* Allows always rendering children in content when closed
|
||||
*/
|
||||
lazy: boolean;
|
||||
/**
|
||||
* Utility for getting state as string. Usefull for data-attributes.
|
||||
*/
|
||||
state: "open" | "closed";
|
||||
};
|
||||
|
||||
export const [CollapsibleContextProvider, useCollapsibleContext] =
|
||||
createContext<CollapsibleContextProps>({
|
||||
name: "CollapsibleContext",
|
||||
errorMessage:
|
||||
"<Collapsible.Trigger> and <Collapsible.Content> must be used within a <Collapsible>",
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { Meta, StoryFn, StoryObj } from "@storybook/react";
|
||||
import React, { useState } from "react";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "../button";
|
||||
import { Box } from "../layout/box";
|
||||
import { HStack, VStack } from "../layout/stack";
|
||||
import { Collapsible } from "./Collapsible";
|
||||
|
||||
const meta: Meta<typeof Collapsible> = {
|
||||
title: "Utilities/Collapsible",
|
||||
component: Collapsible,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Collapsible>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (...props) => (
|
||||
<Collapsible {...props}>
|
||||
<Collapsible.Trigger>Trigger</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
|
||||
corporis maxime aliquam, voluptates nobis numquam, non odit optio
|
||||
architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
|
||||
tempore optio aliquid!
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
),
|
||||
args: {
|
||||
open: false,
|
||||
defaultOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const InContext: StoryFn<typeof Collapsible> = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<HStack justify="space-between" gap="4" align="center">
|
||||
Steg 1 av 8{" "}
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
iconPosition="right"
|
||||
icon={
|
||||
open ? (
|
||||
<ChevronUpIcon aria-hidden />
|
||||
) : (
|
||||
<ChevronDownIcon aria-hidden />
|
||||
)
|
||||
}
|
||||
>
|
||||
Vis steg
|
||||
</Button>
|
||||
</Collapsible.Trigger>
|
||||
</HStack>
|
||||
<Collapsible.Content>
|
||||
<p>Lorem ipsum dolor sit</p>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export const Animated: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible lazy>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
Animer open/lukk
|
||||
</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className="collapsible">
|
||||
<div className="collapsibleContent">
|
||||
<Box padding="8" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
Animated.decorators = [
|
||||
(Story) => (
|
||||
<div style={{ height: 300 }}>
|
||||
<style>{`
|
||||
.collapsible {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
overflow: hidden;
|
||||
transition: grid-template-rows 1s ease-in-out;
|
||||
}
|
||||
|
||||
.collapsible[data-state="closed"] {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.collapsible[data-state="open"] {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.collapsibleContent {
|
||||
min-height: 0;
|
||||
transition: visibility 1s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.collapsible[data-state="open"] .collapsibleContent {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
`}</style>
|
||||
{Story()}
|
||||
</div>
|
||||
),
|
||||
];
|
||||
|
||||
export const Lazy: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible lazy>
|
||||
<Collapsible.Trigger>Trigger</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
|
||||
corporis maxime aliquam, voluptates nobis numquam, non odit optio
|
||||
architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
|
||||
tempore optio aliquid!
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const AsChild: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const DefaultOpen: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible defaultOpen>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const ControlledOpen: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible open>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const Disabled: StoryFn<typeof Collapsible> = ({ open = false }) => (
|
||||
<Collapsible open={open}>
|
||||
<Collapsible.Trigger asChild disabled>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="8">
|
||||
<div>
|
||||
<h2>In context</h2>
|
||||
<InContext />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Animated</h2>
|
||||
<Animated />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Lazy</h2>
|
||||
<Lazy />
|
||||
</div>
|
||||
<div>
|
||||
<h2>AsChild</h2>
|
||||
<AsChild />
|
||||
</div>
|
||||
<div>
|
||||
<h2>DefaultOpen</h2>
|
||||
<DefaultOpen />
|
||||
</div>
|
||||
<div>
|
||||
<h2>ControlledOpen</h2>
|
||||
<ControlledOpen />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disabled</h2>
|
||||
<Disabled />
|
||||
<Disabled open />
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { forwardRef, useCallback } from "react";
|
||||
import { useControllableState, useId } from "../util/hooks";
|
||||
import { CollapsibleContextProvider } from "./Collapsible.context";
|
||||
import { CollapsibleBaseProps } from "./Collapsible.types";
|
||||
import CollapsibleContent from "./parts/Collapsible.Content";
|
||||
import CollapsibleTrigger from "./parts/Collapsible.Trigger";
|
||||
|
||||
export interface CollapsibleProps
|
||||
extends CollapsibleBaseProps,
|
||||
React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
interface CollapsibleComponent
|
||||
extends React.ForwardRefExoticComponent<
|
||||
CollapsibleProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* @see 🏷️ {@link CollapsibleTriggerProps}
|
||||
*/
|
||||
Trigger: typeof CollapsibleTrigger;
|
||||
/**
|
||||
* @see 🏷️ {@link CollapsibleContentProps}
|
||||
*/
|
||||
Content: typeof CollapsibleContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsible is a component that allows you to toggle visibility of content.
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Collapsible>
|
||||
* <Collapsible.Trigger>Trigger</Collapsible.Trigger>
|
||||
* <Collapsible.Content>
|
||||
* Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
|
||||
* corporis maxime aliquam, voluptates nobis numquam, non odit optio
|
||||
* architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
|
||||
* tempore optio aliquid!
|
||||
* </Collapsible.Content>
|
||||
* </Collapsible>
|
||||
* ```
|
||||
*
|
||||
* @example With asChild
|
||||
* ```jsx
|
||||
* <Collapsible>
|
||||
* <Collapsible.Trigger asChild>
|
||||
* <Button>Button</Button>
|
||||
* </Collapsible.Trigger>
|
||||
* <Collapsible.Content asChild>
|
||||
* <Box padding="4" background="surface-alt-3-subtle">
|
||||
* <div>lorem ipsum</div>
|
||||
* </Box>
|
||||
* </Collapsible.Content>
|
||||
* </Collapsible>
|
||||
* ```
|
||||
*/
|
||||
export const Collapsible = forwardRef<HTMLDivElement, CollapsibleProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
open,
|
||||
defaultOpen = false,
|
||||
onOpenChange,
|
||||
lazy = false,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [_open, setOpen] = useControllableState({
|
||||
value: open,
|
||||
defaultValue: defaultOpen,
|
||||
onChange: onOpenChange,
|
||||
});
|
||||
|
||||
const internalId = useId();
|
||||
const state = _open ? "open" : "closed";
|
||||
|
||||
return (
|
||||
<CollapsibleContextProvider
|
||||
open={_open}
|
||||
onOpenToggle={useCallback(
|
||||
() => setOpen((prevOpen) => !prevOpen),
|
||||
[setOpen],
|
||||
)}
|
||||
contentId={`collapsible-content-${internalId}`}
|
||||
triggerId={`collapsible-trigger-${internalId}`}
|
||||
lazy={lazy}
|
||||
state={state}
|
||||
>
|
||||
<div ref={ref} data-state={state} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
</CollapsibleContextProvider>
|
||||
);
|
||||
},
|
||||
) as CollapsibleComponent;
|
||||
|
||||
Collapsible.Trigger = CollapsibleTrigger;
|
||||
Collapsible.Content = CollapsibleContent;
|
||||
|
||||
export default Collapsible;
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface CollapsibleBaseProps {
|
||||
/**
|
||||
* The open state of the collapsible when it is initially rendered. Use when you do not need to control its open state.
|
||||
*/
|
||||
defaultOpen?: boolean;
|
||||
/**
|
||||
* The controlled open state of the collapsible. Must be used in conjunction with onOpenChange.
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Event handler called when the open state of the collapsible changes.
|
||||
*/
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/**
|
||||
* Render the collapsible content lazily. This means that the content will not be rendered until the collapsible is open.
|
||||
* @default false
|
||||
*/
|
||||
lazy?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export { default as Collapsible, type CollapsibleProps } from "./Collapsible";
|
||||
export {
|
||||
default as CollapsibleTrigger,
|
||||
type CollapsibleTriggerProps,
|
||||
} from "./parts/Collapsible.Trigger";
|
||||
export {
|
||||
default as CollapsibleContent,
|
||||
type CollapsibleContentProps,
|
||||
} from "./parts/Collapsible.Content";
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Slot } from "../../util/Slot";
|
||||
import { useCollapsibleContext } from "../Collapsible.context";
|
||||
|
||||
export interface CollapsibleContentProps
|
||||
extends Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
"hidden" | "aria-controls" | "id"
|
||||
> {
|
||||
/**
|
||||
* When true, will render element as its child. This merges classes, styles and event handlers.
|
||||
* @default false
|
||||
*/
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const CollapsibleContent = forwardRef<
|
||||
HTMLDivElement,
|
||||
CollapsibleContentProps
|
||||
>(({ children, asChild, ...rest }, ref) => {
|
||||
const ctx = useCollapsibleContext();
|
||||
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
{...rest}
|
||||
data-state={ctx.state}
|
||||
hidden={!ctx.open}
|
||||
aria-controls={ctx.open ? ctx.triggerId : undefined}
|
||||
id={ctx.contentId}
|
||||
>
|
||||
{ctx.lazy || ctx.open ? children : null}
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
|
||||
export default CollapsibleContent;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Slot } from "../../util/Slot";
|
||||
import { composeEventHandlers } from "../../util/composeEventHandlers";
|
||||
import { useCollapsibleContext } from "../Collapsible.context";
|
||||
|
||||
export interface CollapsibleTriggerProps
|
||||
extends Omit<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
"id" | "aria-controls" | "aria-expanded"
|
||||
> {
|
||||
/**
|
||||
* When true, will render element as its child. This merges classes, styles and event handlers.
|
||||
* @default false
|
||||
*/
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const CollapsibleTrigger = forwardRef<
|
||||
HTMLButtonElement,
|
||||
CollapsibleTriggerProps
|
||||
>(({ children, asChild, onClick, ...rest }, ref) => {
|
||||
const ctx = useCollapsibleContext();
|
||||
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
type="button"
|
||||
data-state={ctx.state}
|
||||
onClick={composeEventHandlers(onClick, ctx.onOpenToggle)}
|
||||
{...rest}
|
||||
id={ctx.triggerId}
|
||||
aria-controls={ctx.open ? ctx.contentId : undefined}
|
||||
aria-expanded={ctx.open}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
|
||||
export default CollapsibleTrigger;
|
||||
@@ -97,9 +97,17 @@ export const FilteredOptionsProvider = ({
|
||||
[filteredOptionsUtils.getAddNewOptionId(id)]: allowNewValues
|
||||
? toComboboxOption(value)
|
||||
: undefined,
|
||||
...customOptions.reduce(
|
||||
(acc, customOption) => ({
|
||||
...acc,
|
||||
[filteredOptionsUtils.getOptionId(id, customOption.label)]:
|
||||
customOption,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
),
|
||||
[allowNewValues, id, options, value],
|
||||
[allowNewValues, customOptions, id, options, value],
|
||||
);
|
||||
|
||||
useClientLayoutEffect(() => {
|
||||
|
||||
@@ -128,6 +128,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
removeSelectedOption(lastSelectedOption);
|
||||
}
|
||||
}
|
||||
} else if (e.key === "Enter" || e.key === "Accept") {
|
||||
if (activeDecendantId || value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.key === "ArrowDown") {
|
||||
// Check that cursor position is at the end of the input field,
|
||||
// so we don't interfere with text editing
|
||||
@@ -182,6 +186,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
{...omit(inputProps, ["aria-invalid"])}
|
||||
ref={ref}
|
||||
value={value}
|
||||
onBlur={() => virtualFocus.moveFocusToTop()}
|
||||
onChange={onChangeHandler}
|
||||
type="text"
|
||||
role="combobox"
|
||||
|
||||
@@ -624,3 +624,71 @@ export const TestHoverAndFocusSwitching: StoryObject = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TestEnterNotSubmittingForm: StoryObj<{
|
||||
onSubmit: ReturnType<typeof fn>;
|
||||
}> = {
|
||||
args: {
|
||||
onSubmit: fn(),
|
||||
},
|
||||
render: ({ onSubmit }) => {
|
||||
return (
|
||||
<form action="https://www.aksel.nav.no" method="get" onSubmit={onSubmit}>
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er dine favorittfrukter?"
|
||||
isMultiSelect
|
||||
allowNewValues
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
args.onSubmit.mockClear();
|
||||
const canvas = within(canvasElement);
|
||||
const waitTime = 0; // Change for debugging
|
||||
|
||||
await sleep(waitTime);
|
||||
|
||||
const getInput = () =>
|
||||
canvas.getByRole("combobox", {
|
||||
name: "Hva er dine favorittfrukter?",
|
||||
});
|
||||
|
||||
const getOption = (name: string, selected: boolean) =>
|
||||
canvas.getByRole("option", { name, selected });
|
||||
await userEvent.click(getInput(), { delay: waitTime });
|
||||
|
||||
await userEvent.keyboard("{ArrowDown}", { delay: waitTime });
|
||||
expect(getInput().getAttribute("aria-activedescendant")).toBe(
|
||||
getOption("banana", false).getAttribute("id"),
|
||||
);
|
||||
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime });
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
expect(getOption("banana", true)).toBeVisible();
|
||||
|
||||
await userEvent.keyboard("{Shift>}{Tab}", { delay: waitTime });
|
||||
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime });
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
await userEvent.keyboard("test"); // Type option that does not exist
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime });
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
await userEvent.keyboard("{ArrowDown}", { delay: waitTime }); // Select "test" custom option
|
||||
expect(
|
||||
canvas.getByRole("option", { name: "test", selected: true }),
|
||||
).toBeVisible();
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime }); // De-select "test"
|
||||
expect(
|
||||
canvas.queryByRole("option", { name: "test", selected: false }),
|
||||
).toBeNull();
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
await userEvent.keyboard("{Escape}", { delay: waitTime }); // Clear input field
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime }); // Enter on empty Input with closed list
|
||||
expect(args.onSubmit).toHaveBeenCalledOnce();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,388 @@
|
||||
import { Meta, StoryFn, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { LinkIcon } from "@navikt/aksel-icons";
|
||||
import { VStack } from "../../layout/stack";
|
||||
import { Link } from "../../link";
|
||||
import FormSummary from "./FormSummary";
|
||||
|
||||
const meta: Meta<typeof FormSummary> = {
|
||||
title: "ds-react/FormSummary",
|
||||
component: FormSummary,
|
||||
decorators: [
|
||||
(story) => (
|
||||
<div style={{ width: 600, maxWidth: "100%", margin: "0 auto" }}>
|
||||
{story()}
|
||||
</div>
|
||||
),
|
||||
],
|
||||
parameters: { layout: "padded", chromatic: { disable: true } },
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Adresse</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
Gate 123
|
||||
<br />
|
||||
1234 Sted
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Fødselsnummer</FormSummary.Label>
|
||||
<FormSummary.Value>12345678910</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Barn nr. 1</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Kari Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kjønn</FormSummary.Label>
|
||||
<FormSummary.Value>Jente</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Alder</FormSummary.Label>
|
||||
<FormSummary.Value>6</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Barn nr. 2</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Per Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kjønn</FormSummary.Label>
|
||||
<FormSummary.Value>Gutt</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Alder</FormSummary.Label>
|
||||
<FormSummary.Value>7</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kontaktinformasjon</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<Link>Fyll ut kontaktinformasjon</Link>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Hvordan vil du bli varslet?</FormSummary.Label>
|
||||
<FormSummary.Value>E-post</FormSummary.Value>
|
||||
<FormSummary.Value>SMS</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
export const LongTexts: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">
|
||||
Arbeidsforhold som du har i eller utenfor EØS-området
|
||||
</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#">
|
||||
<LinkIcon />
|
||||
Jeg vil endre dette svaret
|
||||
</FormSummary.EditLink>
|
||||
</FormSummary.Header>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>
|
||||
Har du jobbet i et annet EØS-land, Sveits eller Storbritannia i løpet
|
||||
av de siste 36 månedene? Om ja, hvilket land og hvor lenge jobbet du
|
||||
der? Kan du fortelle litt om hva du jobbet med?
|
||||
</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
Nei, jeg har ikke jobbet i et annet EØS-land, Sveits eller
|
||||
Storbritannia i løpet av de siste 36 månedene. Og jeg har heller ikke
|
||||
jobbet i Norge i løpet av de siste 36 månedene. Men jeg har jobbet i
|
||||
et annet land utenfor EØS-området. Det var i Sør-Korea, og jeg jobbet
|
||||
som lærer på en internasjonal skole. Jeg jobbet der i 12 måneder, og
|
||||
det var en veldig spennende opplevelse. Jeg lærte mye om koreansk
|
||||
kultur og språk, og jeg fikk mange nye venner. Jeg savner Sør-Korea
|
||||
veldig mye, og jeg håper å kunne dra tilbake.
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>
|
||||
Har du jobbet i et annet EØS-land, Sveits eller Storbritannia i
|
||||
løpet av de siste 36 månedene? Om ja, hvilket land og hvor lenge
|
||||
jobbet du der? Kan du fortelle litt om hva du jobbet med?
|
||||
</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
Nei, jeg har ikke jobbet i et annet EØS-land, Sveits eller
|
||||
Storbritannia i løpet av de siste 36 månedene. Og jeg har heller
|
||||
ikke jobbet i Norge i løpet av de siste 36 månedene. Men jeg har
|
||||
jobbet i et annet land utenfor EØS-området. Det var i Sør-Korea,
|
||||
og jeg jobbet som lærer på en internasjonal skole. Jeg jobbet
|
||||
der i 12 måneder, og det var en veldig spennende opplevelse. Jeg
|
||||
lærte mye om koreansk kultur og språk, og jeg fikk mange nye
|
||||
venner. Jeg savner Sør-Korea veldig mye, og jeg håper å kunne
|
||||
dra tilbake.
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
export const NoLink: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">
|
||||
Arbeidsforhold som du har i eller utenfor EØS-området
|
||||
</FormSummary.Heading>
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Fødselsnummer</FormSummary.Label>
|
||||
<FormSummary.Value>12345678910</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
const answers = [
|
||||
{
|
||||
label: "Fortell din livshistorie",
|
||||
value: `Det hele startet da jeg ble født en kald vinterdag i desember.
|
||||
Jeg var en litt spesiell baby, for jeg hadde masse hår på hodet. Mamma
|
||||
og pappa ble veldig overrasket da de så meg for første gang. De hadde egentlig
|
||||
forventet at jeg skulle være helt normal og kjedelig. Du vil kanskje ikke høre resten
|
||||
av historien min, for den er ganske lang og kjedelig...`,
|
||||
},
|
||||
{
|
||||
label: "Fødselsnummer",
|
||||
value: "12345678910",
|
||||
},
|
||||
{
|
||||
label: "Ting du har i sekken.",
|
||||
value: [
|
||||
{
|
||||
label: "Saks x2",
|
||||
value: `Jeg liker å ha med meg en saks i sekken min. Det er alltid kjekt å ha en saks. Jeg pakker gjerne med meg to saks. Da har jeg en reserve hvis den første saksen min skulle bli borte. Det er veldig kjekt.`,
|
||||
},
|
||||
{
|
||||
label: "Blyant",
|
||||
value:
|
||||
"Det er ikke mulig å skrive uten en blyant. Da blir det bare rot.",
|
||||
},
|
||||
{
|
||||
label: "Kake",
|
||||
value: `Jeg har alltid en kake i sekken min. Den blir litt most etter en
|
||||
stund i sekken min men det gjør ingenting. Jeg liker moste kaker. Det er veldig godt.`,
|
||||
},
|
||||
{
|
||||
label: "Kaffekopp",
|
||||
value: "Dette har jeg ikke i sekken min",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Flere ting du har i sekken.",
|
||||
value: [
|
||||
{
|
||||
label: "Saks",
|
||||
value: `En saks til`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const RealisticUsage: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">
|
||||
Diverse informasjon om forskjellige ting og tang. Kjekt å vite.
|
||||
</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
{answers.map((answer, index) => (
|
||||
<FormSummary.Answer key={index}>
|
||||
<FormSummary.Label>{answer.label}</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
{Array.isArray(answer.value) ? (
|
||||
<FormSummary.Answers>
|
||||
{answer.value.map((subAnswer, subIndex) => (
|
||||
<FormSummary.Answer key={subIndex}>
|
||||
<FormSummary.Label>{subAnswer.label}</FormSummary.Label>
|
||||
<FormSummary.Value>{subAnswer.value}</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
))}
|
||||
</FormSummary.Answers>
|
||||
) : (
|
||||
answer.value
|
||||
)}
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
))}
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
export const Empty: StoryFn<typeof FormSummary> = () => (
|
||||
<VStack gap="8">
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Just Header</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Answers</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>{null}</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Answer</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>{null}</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Label & Value</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>{null}</FormSummary.Label>
|
||||
<FormSummary.Value>{null}</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Value</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Adresse</FormSummary.Label>
|
||||
<FormSummary.Value>{null}</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
export const Chromatic: StoryObj<typeof FormSummary> = {
|
||||
render: () => (
|
||||
<div>
|
||||
<div>
|
||||
<h2>Default</h2>
|
||||
<Default>{null}</Default>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Long Texts</h2>
|
||||
<LongTexts>{null}</LongTexts>
|
||||
</div>
|
||||
<div>
|
||||
<h2>No Link</h2>
|
||||
<NoLink>{null}</NoLink>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Realistic Usage</h2>
|
||||
<RealisticUsage>{null}</RealisticUsage>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Empty</h2>
|
||||
<Empty>{null}</Empty>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: {
|
||||
disable: false,
|
||||
modes: {
|
||||
default: {
|
||||
viewport: {
|
||||
width: 620,
|
||||
},
|
||||
},
|
||||
sm: {
|
||||
viewport: {
|
||||
width: 479,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CommaSeparated: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>
|
||||
Hvilket transportmiddel er du mest kjent for?
|
||||
</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
sykkel, tog, bil, buss, sparkesykkel
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Favoritt slagord?</FormSummary.Label>
|
||||
<FormSummary.Value>{`I'll be back.`}</FormSummary.Value>
|
||||
<FormSummary.Value>Hasta la vista, baby.</FormSummary.Value>
|
||||
<FormSummary.Value>Say hello to my little friend.</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
@@ -0,0 +1,106 @@
|
||||
import cl from "clsx";
|
||||
import React, { HTMLAttributes, forwardRef } from "react";
|
||||
import FormSummaryAnswer from "./FormSummaryAnswer";
|
||||
import FormSummaryAnswers from "./FormSummaryAnswers";
|
||||
import FormSummaryEditLink from "./FormSummaryEditLink";
|
||||
import FormSummaryHeader from "./FormSummaryHeader";
|
||||
import FormSummaryHeading from "./FormSummaryHeading";
|
||||
import FormSummaryLabel from "./FormSummaryLabel";
|
||||
import FormSummaryValue from "./FormSummaryValue";
|
||||
|
||||
interface FormSummaryComponent
|
||||
extends React.ForwardRefExoticComponent<
|
||||
FormSummaryProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
|
||||
*/
|
||||
Header: typeof FormSummaryHeader;
|
||||
/**
|
||||
* Typographic Heading to use in the `FormSummary.Header` component.
|
||||
*/
|
||||
Heading: typeof FormSummaryHeading;
|
||||
/**
|
||||
* Link to edit the answers to use in the `FormSummary.Header` component. Should link to the relevant part of the form.
|
||||
*/
|
||||
EditLink: typeof FormSummaryEditLink;
|
||||
/**
|
||||
* Wrapper component for the answers.
|
||||
*/
|
||||
Answers: typeof FormSummaryAnswers;
|
||||
/**
|
||||
* Wrapper component for each answer. To be used in the `FormSummary.Answers` component.
|
||||
*/
|
||||
Answer: typeof FormSummaryAnswer;
|
||||
/**
|
||||
* Corresponds to the question in the form. To be used in the `FormSummary.Answer` component.
|
||||
*/
|
||||
Label: typeof FormSummaryLabel;
|
||||
/**
|
||||
* Corresponds to the answer in the form. To be used in the `FormSummary.Answer` component.
|
||||
*/
|
||||
Value: typeof FormSummaryValue;
|
||||
}
|
||||
|
||||
export interface FormSummaryProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Must include:
|
||||
*
|
||||
* - `<FormSummary.Header>`
|
||||
* - `<FormSummary.Answers>`
|
||||
*
|
||||
* @example
|
||||
* <FormSummary>
|
||||
* <FormSummary.Header>
|
||||
* <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
|
||||
* <FormSummary.EditLink href="#" />
|
||||
* </FormSummary.Header>
|
||||
* <FormSummary.Answers>
|
||||
* <FormSummary.Answer>
|
||||
* <FormSummary.Label>Navn</FormSummary.Label>
|
||||
* <FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
* </FormSummary.Answer>
|
||||
* </FormSummary.Answers>
|
||||
* </FormSummary>
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A summary of a previously answered form.
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/formsummary)
|
||||
*
|
||||
* @example
|
||||
* <FormSummary>
|
||||
* <FormSummary.Header>
|
||||
* <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
|
||||
* <FormSummary.EditLink href="#" />
|
||||
* </FormSummary.Header>
|
||||
* <FormSummary.Answers>
|
||||
* <FormSummary.Answer>
|
||||
* <FormSummary.Label>Navn</FormSummary.Label>
|
||||
* <FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
* </FormSummary.Answer>
|
||||
* </FormSummary.Answers>
|
||||
* </FormSummary>
|
||||
*/
|
||||
export const FormSummary = forwardRef<HTMLDivElement, FormSummaryProps>(
|
||||
({ children, className, ...rest }, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...rest} className={cl("navds-form-summary", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
) as FormSummaryComponent;
|
||||
|
||||
FormSummary.Header = FormSummaryHeader;
|
||||
FormSummary.Heading = FormSummaryHeading;
|
||||
FormSummary.EditLink = FormSummaryEditLink;
|
||||
FormSummary.Answers = FormSummaryAnswers;
|
||||
FormSummary.Answer = FormSummaryAnswer;
|
||||
FormSummary.Label = FormSummaryLabel;
|
||||
FormSummary.Value = FormSummaryValue;
|
||||
|
||||
export default FormSummary;
|
||||
@@ -0,0 +1,27 @@
|
||||
import cl from "clsx";
|
||||
import React from "react";
|
||||
|
||||
export interface FormSummaryAnswerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Must include:
|
||||
* - `<FormSummary.Label>`
|
||||
* - `<FormSummary.Value>`
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryAnswer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
FormSummaryAnswerProps
|
||||
>(({ children, className, ...rest }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__answer", className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default FormSummaryAnswer;
|
||||
@@ -0,0 +1,25 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
export interface FormSummaryAnswersProps
|
||||
extends React.HTMLAttributes<HTMLDListElement> {
|
||||
/**
|
||||
* Must include one or more of `<FormSummary.Answer>`.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryAnswers = forwardRef<
|
||||
HTMLDListElement,
|
||||
FormSummaryAnswersProps
|
||||
>(({ children, className, ...rest }: FormSummaryAnswersProps, ref) => (
|
||||
<dl
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__answers", className)}
|
||||
>
|
||||
{children}
|
||||
</dl>
|
||||
));
|
||||
|
||||
export default FormSummaryAnswers;
|
||||
@@ -0,0 +1,35 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { Link } from "../../link";
|
||||
import { OverridableComponent } from "../../util/types";
|
||||
|
||||
// export type FormSummaryEditProps = Partial<LinkProps>;
|
||||
export interface FormSummaryEditProps
|
||||
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
/**
|
||||
* URL to the relevant part of the form, where the answers can be edited.
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* Link text.
|
||||
*
|
||||
* [How to write good links](https://aksel.nav.no/god-praksis/artikler/lenker).
|
||||
* @default "Endre svar"
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryEditLink: OverridableComponent<
|
||||
FormSummaryEditProps,
|
||||
HTMLAnchorElement
|
||||
> = forwardRef(({ children = "Endre svar", className, ...rest }, ref) => (
|
||||
<Link
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__edit", className)}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
));
|
||||
|
||||
export default FormSummaryEditLink;
|
||||
@@ -0,0 +1,25 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
export interface FormSummaryHeaderProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryHeader = forwardRef<
|
||||
HTMLDivElement,
|
||||
FormSummaryHeaderProps
|
||||
>(({ children, className, ...rest }, ref) => (
|
||||
<header
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__header", className)}
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
));
|
||||
|
||||
export default FormSummaryHeader;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Heading, HeadingProps } from "../../typography";
|
||||
|
||||
export interface FormSummaryHeadingProps
|
||||
extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
/**
|
||||
* Heading text.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* The heading level.
|
||||
*/
|
||||
level: Exclude<HeadingProps["level"], "1">;
|
||||
}
|
||||
|
||||
export const FormSummaryHeading = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
FormSummaryHeadingProps
|
||||
>((props: FormSummaryHeadingProps, ref) => (
|
||||
<Heading ref={ref} {...props} size="medium" />
|
||||
));
|
||||
|
||||
export default FormSummaryHeading;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Label } from "../../typography";
|
||||
|
||||
export interface FormSummaryLabelProps
|
||||
extends React.HTMLAttributes<HTMLElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryLabel = forwardRef<HTMLElement, FormSummaryLabelProps>(
|
||||
({ children, ...rest }, ref) => (
|
||||
<Label ref={ref} {...rest} as="dt">
|
||||
{children}
|
||||
</Label>
|
||||
),
|
||||
);
|
||||
|
||||
export default FormSummaryLabel;
|
||||
@@ -0,0 +1,24 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { BodyLong } from "../../typography";
|
||||
|
||||
export interface FormSummaryValueProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryValue = forwardRef<
|
||||
HTMLDivElement,
|
||||
FormSummaryValueProps
|
||||
>(({ children, className, ...rest }, ref) => (
|
||||
<BodyLong
|
||||
ref={ref}
|
||||
{...rest}
|
||||
as="dd"
|
||||
className={cl("navds-form-summary__value", className)}
|
||||
>
|
||||
{children}
|
||||
</BodyLong>
|
||||
));
|
||||
|
||||
export default FormSummaryValue;
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
export { default as FormSummary, type FormSummaryProps } from "./FormSummary";
|
||||
export {
|
||||
default as FormSummaryAnswer,
|
||||
type FormSummaryAnswerProps,
|
||||
} from "./FormSummaryAnswer";
|
||||
export {
|
||||
default as FormSummaryAnswers,
|
||||
type FormSummaryAnswersProps,
|
||||
} from "./FormSummaryAnswers";
|
||||
export {
|
||||
default as FormSummaryEditLink,
|
||||
type FormSummaryEditProps,
|
||||
} from "./FormSummaryEditLink";
|
||||
export {
|
||||
default as FormSummaryHeader,
|
||||
type FormSummaryHeaderProps,
|
||||
} from "./FormSummaryHeader";
|
||||
export {
|
||||
default as FormSummaryHeading,
|
||||
type FormSummaryHeadingProps,
|
||||
} from "./FormSummaryHeading";
|
||||
export {
|
||||
default as FormSummaryLabel,
|
||||
type FormSummaryLabelProps,
|
||||
} from "./FormSummaryLabel";
|
||||
export {
|
||||
default as FormSummaryValue,
|
||||
type FormSummaryValueProps,
|
||||
} from "./FormSummaryValue";
|
||||
@@ -26,8 +26,8 @@ export { Dropdown, type DropdownProps } from "./dropdown";
|
||||
export { ExpansionCard, type ExpansionCardProps } from "./expansion-card";
|
||||
export {
|
||||
GuidePanel,
|
||||
type GuidePanelProps,
|
||||
GuidePanelDefaultIllustration,
|
||||
type GuidePanelProps,
|
||||
} from "./guide-panel";
|
||||
export { HelpText, type HelpTextProps } from "./help-text";
|
||||
export {
|
||||
@@ -125,6 +125,21 @@ export {
|
||||
} from "./form/confirmation-panel";
|
||||
export { ErrorSummary, type ErrorSummaryProps } from "./form/error-summary";
|
||||
export { Fieldset, type FieldsetProps } from "./form/fieldset";
|
||||
export {
|
||||
UNSAFE_FileUpload,
|
||||
type FileAccepted,
|
||||
type FileItem,
|
||||
type FileMetadata,
|
||||
type FileObject,
|
||||
type FileRejected,
|
||||
type FileRejectedPartitioned,
|
||||
type FileRejectionReason,
|
||||
type FileUploadDropzoneProps,
|
||||
type FileUploadItemProps,
|
||||
type FileUploadTriggerProps,
|
||||
type FilesPartitioned,
|
||||
} from "./form/file-upload";
|
||||
export { FormSummary, type FormSummaryProps } from "./form/form-summary";
|
||||
export {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
@@ -136,20 +151,6 @@ export { Select, type SelectProps } from "./form/select";
|
||||
export { Switch, type SwitchProps } from "./form/switch";
|
||||
export { Textarea, type TextareaProps } from "./form/textarea";
|
||||
export { TextField, type TextFieldProps } from "./form/textfield";
|
||||
export {
|
||||
UNSAFE_FileUpload,
|
||||
type FileUploadDropzoneProps,
|
||||
type FileUploadTriggerProps,
|
||||
type FileObject,
|
||||
type FileRejected,
|
||||
type FileAccepted,
|
||||
type FileRejectedPartitioned,
|
||||
type FilesPartitioned,
|
||||
type FileRejectionReason,
|
||||
type FileUploadItemProps,
|
||||
type FileItem,
|
||||
type FileMetadata,
|
||||
} from "./form/file-upload";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
|
||||
@@ -12,6 +12,6 @@ import React from "react";
|
||||
* <MyComponent />
|
||||
* </HStack>
|
||||
*/
|
||||
export const Spacer = () => <div className="navds-stack__spacer" />;
|
||||
export const Spacer = () => <span className="navds-stack__spacer" />;
|
||||
|
||||
export default Spacer;
|
||||
@@ -143,7 +143,21 @@ export class DescendantsManager<
|
||||
};
|
||||
|
||||
private registerNode = (node: T | null, options?: DescendantOptions<K>) => {
|
||||
if (!node || this.descendants.has(node)) return;
|
||||
if (!node) return;
|
||||
|
||||
/**
|
||||
* While the node is registered, we have to make sure to sync options
|
||||
* This is useful when ex. a node is disabled after it has been registered
|
||||
*/
|
||||
const mapNode = this.descendants.get(node);
|
||||
if (mapNode) {
|
||||
this.descendants.set(node, {
|
||||
index: mapNode.index,
|
||||
node,
|
||||
...options,
|
||||
} as Descendant<T, K>);
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Array.from(this.descendants.keys()).concat(node);
|
||||
const sorted = sortNodes(keys);
|
||||
|
||||
@@ -17,11 +17,6 @@ function useDescendants<
|
||||
K extends Record<string, any> = object,
|
||||
>() {
|
||||
const descendants = useRef(new DescendantsManager<T, K>()).current;
|
||||
useClientLayoutEffect(() => {
|
||||
return () => {
|
||||
descendants.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
return descendants;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @navikt/ds-tailwind
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 6.4.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.1",
|
||||
"description": "Tailwind config based on @navikt/ds-tokens",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -25,7 +25,7 @@
|
||||
"directory": "@navikt/core/tailwind"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^6.5.0",
|
||||
"@navikt/ds-tokens": "^6.6.1",
|
||||
"color": "4.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"tailwindcss": "^3.3.3",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @navikt/ds-tokens
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 6.4.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.1",
|
||||
"description": "Design-tokens for NAV designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
|
||||
+416
-398
File diff suppressed because it is too large.
Load diff
@@ -15,10 +15,10 @@
|
||||
"playroom"
|
||||
],
|
||||
"dependencies": {
|
||||
"@navikt/aksel-icons": "^6.5.0",
|
||||
"@navikt/ds-css": "^6.5.0",
|
||||
"@navikt/ds-react": "^6.5.0",
|
||||
"@navikt/ds-tailwind": "^6.5.0",
|
||||
"@navikt/ds-tokens": "^6.5.0"
|
||||
"@navikt/aksel-icons": "^6.6.1",
|
||||
"@navikt/ds-css": "^6.6.1",
|
||||
"@navikt/ds-react": "^6.6.1",
|
||||
"@navikt/ds-tailwind": "^6.6.1",
|
||||
"@navikt/ds-tokens": "^6.6.1"
|
||||
}
|
||||
}
|
||||
@@ -18,20 +18,25 @@ const config: StorybookConfig = {
|
||||
return loadCsf(code, { ...opts, fileName }).parse().indexInputs;
|
||||
};
|
||||
|
||||
const exampleIndexer = async (fileName: string, opts) => {
|
||||
const customIndexer = async (fileName: string, opts) => {
|
||||
let code = readFileSync(fileName, "utf-8").toString();
|
||||
|
||||
code = code.replace(
|
||||
/^\s*export default withDsExample\(Example[\s\S]*?;\s*/gm,
|
||||
"",
|
||||
);
|
||||
const isTemplate = fileName.toLowerCase().includes("templates");
|
||||
const templatesOrExamples = isTemplate ? "Templates" : "Eksempler";
|
||||
|
||||
code = code.replace("export const args =", "const args =");
|
||||
code = code.split(
|
||||
/\/\/ EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE/,
|
||||
)[0];
|
||||
|
||||
code += `\nexport default { title: "Eksempler/${fileName
|
||||
.split("pages/eksempler/")[1]
|
||||
code += `\nexport default { title: "${templatesOrExamples}/${fileName
|
||||
.split(`pages/${templatesOrExamples.toLowerCase()}/`)[1]
|
||||
.replace(".tsx", "")}" };\n`;
|
||||
|
||||
code += `\nexport const Demo = {\n
|
||||
render: Example,\n
|
||||
};
|
||||
`;
|
||||
|
||||
return loadCsf(code, { ...opts, fileName }).parse().indexInputs;
|
||||
};
|
||||
|
||||
@@ -42,8 +47,12 @@ const config: StorybookConfig = {
|
||||
createIndex: csfIndexer,
|
||||
},
|
||||
{
|
||||
test: /pages\/eksempler\/|(".+")|(.+.tsx)$/,
|
||||
createIndex: exampleIndexer,
|
||||
test: /pages\/eksempler\/.+?.tsx$/,
|
||||
createIndex: customIndexer,
|
||||
},
|
||||
{
|
||||
test: /pages\/templates\/.+?.tsx$/,
|
||||
createIndex: customIndexer,
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -52,6 +61,7 @@ const config: StorybookConfig = {
|
||||
"../**/*.mdx",
|
||||
"../**/*.stories.@(ts|tsx)",
|
||||
"../pages/eksempler/**/*.tsx",
|
||||
"../pages/templates/**/*.tsx",
|
||||
],
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-essentials"),
|
||||
|
||||
@@ -108,7 +108,7 @@ export function ArticleSections({
|
||||
{utArticles.map((article) => (
|
||||
<li key={article.slug}>
|
||||
<GpArticleCard
|
||||
href={`${article.slug}`.replace("god-praksis", "gp")}
|
||||
href={article.slug}
|
||||
innholdstype={article.innholdstype}
|
||||
undertema={article.undertema}
|
||||
publishedAt={article.publishedAt}
|
||||
|
||||
@@ -2,6 +2,7 @@ import cl from "clsx";
|
||||
import Link from "next/link";
|
||||
import { FileFillIcon, TagFillIcon } from "@navikt/aksel-icons";
|
||||
import { BodyLong, Detail, Heading } from "@navikt/ds-react";
|
||||
import ErrorBoundary from "@/error-boundary";
|
||||
import { AnimatedChevron } from "@/web/AnimatedChevron";
|
||||
|
||||
type GpArticleCardProps = {
|
||||
@@ -45,11 +46,14 @@ function GpArticleCard({
|
||||
<BodyLong className="mt-1 line-clamp-2">{description}</BodyLong>
|
||||
)}
|
||||
<div
|
||||
className={cl("mt-auto flex h-fit justify-between text-deepblue-700", {
|
||||
"pt-5": description,
|
||||
})}
|
||||
className={cl(
|
||||
"mt-auto flex h-fit items-end justify-between gap-2 text-deepblue-700",
|
||||
{
|
||||
"pt-5": description,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<div className="flex flex-wrap gap-x-3">
|
||||
{undertema && (
|
||||
<div className="flex items-center gap-05 text-teal-700">
|
||||
<TagFillIcon aria-hidden fontSize="1rem" />
|
||||
@@ -74,4 +78,10 @@ function GpArticleCard({
|
||||
);
|
||||
}
|
||||
|
||||
export default GpArticleCard;
|
||||
export default function Component(props: GpArticleCardProps) {
|
||||
return (
|
||||
<ErrorBoundary boundaryName="GpArticleCard">
|
||||
<GpArticleCard {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { HGrid } from "@navikt/ds-react";
|
||||
import { AkselTheme } from "@/sb-util";
|
||||
import GpFrontpageCard from "./GpFrontpageCard";
|
||||
|
||||
const meta = {
|
||||
title: "Layout/god-praksis/cards/FrontpageCard",
|
||||
component: GpFrontpageCard,
|
||||
tags: ["autodocs"],
|
||||
decorators: [AkselTheme],
|
||||
} satisfies Meta<typeof GpFrontpageCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const title = {
|
||||
short: "Design",
|
||||
medium: "Brukerinnsikt",
|
||||
long: "Produktutviklingsmodellen",
|
||||
};
|
||||
|
||||
export const CompactSingle: Story = {
|
||||
args: {
|
||||
href: "gp/brukeropplevelse",
|
||||
children: title.long,
|
||||
},
|
||||
};
|
||||
|
||||
export const CompactMultiple = {
|
||||
render: (props) => {
|
||||
return (
|
||||
<HGrid gap="4" columns={{ xs: 1, md: 2 }}>
|
||||
<GpFrontpageCard {...props} href="test">
|
||||
{title.short}
|
||||
</GpFrontpageCard>
|
||||
<GpFrontpageCard {...props} href="test">
|
||||
{title.short}
|
||||
</GpFrontpageCard>
|
||||
<GpFrontpageCard {...props} href="test">
|
||||
{title.medium}
|
||||
</GpFrontpageCard>
|
||||
<GpFrontpageCard {...props} href="test">
|
||||
{title.long}
|
||||
</GpFrontpageCard>
|
||||
</HGrid>
|
||||
);
|
||||
},
|
||||
args: {
|
||||
innholdstype: "Testing",
|
||||
undertema: "WCAG",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
|
||||
import Image from "next/legacy/image";
|
||||
import NextLink from "next/link";
|
||||
import ErrorBoundary from "@/error-boundary";
|
||||
import { FallbackPictogram } from "@/layout/god-praksis-page/FallbackPictogram";
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { urlFor } from "@/sanity/interface";
|
||||
|
||||
type GpFrontpageCardProps = {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
image?: SanityImageSource;
|
||||
};
|
||||
|
||||
const GpFrontpageCard = ({ image, children, href }: GpFrontpageCardProps) => {
|
||||
return (
|
||||
<li className="flex items-center gap-2 px-2 py-4 sm:gap-4 sm:px-6">
|
||||
<div className="relative h-8 w-8 shrink-0 sm:h-12 sm:w-12">
|
||||
{image ? (
|
||||
<Image
|
||||
src={urlFor(image).auto("format").url()}
|
||||
decoding="sync"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
aria-hidden
|
||||
priority
|
||||
/>
|
||||
) : (
|
||||
<FallbackPictogram />
|
||||
)}
|
||||
</div>
|
||||
<NextLink
|
||||
href={href}
|
||||
passHref
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation("card", e.currentTarget.getAttribute("href"))
|
||||
}
|
||||
className="navds-heading--small navds-link navds-heading flex-wrap break-all text-deepblue-700 no-underline hover:underline focus:outline-none"
|
||||
>
|
||||
{children}
|
||||
</NextLink>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Component(props: GpFrontpageCardProps) {
|
||||
return (
|
||||
<ErrorBoundary boundaryName="GpFrontpageCard">
|
||||
<GpFrontpageCard {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -41,3 +41,24 @@ export const Multiple = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Compact = {
|
||||
render: () => {
|
||||
return (
|
||||
<HStack gap="4">
|
||||
<GpHeroCard href="test" compact>
|
||||
Brukerinnsikt
|
||||
</GpHeroCard>
|
||||
<GpHeroCard href="test" compact>
|
||||
Universell Utforming
|
||||
</GpHeroCard>
|
||||
<GpHeroCard href="test" compact>
|
||||
Innsiktsarbeid
|
||||
</GpHeroCard>
|
||||
<GpHeroCard href="test" compact>
|
||||
nav.no
|
||||
</GpHeroCard>
|
||||
</HStack>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { HTMLAttributes, forwardRef } from "react";
|
||||
import { BodyShort, Heading } from "@navikt/ds-react";
|
||||
import { FallbackPictogram } from "@/layout/god-praksis-page/FallbackPictogram";
|
||||
import { urlFor } from "@/sanity/interface";
|
||||
import ErrorBoundary from "@/web/ErrorBoundary";
|
||||
|
||||
type GpHeroCardProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -21,13 +22,19 @@ const GpHeroCard = forwardRef<HTMLAnchorElement, GpHeroCardProps>(
|
||||
<Link
|
||||
ref={ref}
|
||||
href={`/${href}`}
|
||||
className="group flex gap-2 rounded-lg bg-surface-default py-2 pl-2 pr-3 shadow-xsmall outline-none hover:shadow-small focus-visible:shadow-focus aria-[current]:bg-teal-800 aria-[current]:focus-visible:shadow-focus-gap md:gap-3 md:py-3 md:pl-3 md:pr-6"
|
||||
className={cl(
|
||||
"group flex gap-2 rounded-lg bg-surface-default shadow-xsmall outline-none hover:shadow-small focus-visible:shadow-focus aria-[current]:bg-teal-800 aria-[current]:focus-visible:shadow-focus-gap md:gap-3",
|
||||
"py-2 pl-2 pr-3 md:pl-3 md:pr-6",
|
||||
{
|
||||
"md:py-2.5": compact,
|
||||
"md:py-3": !compact,
|
||||
},
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
className={cl("relative my-auto shrink-0", {
|
||||
"size-8 md:size-12": !compact,
|
||||
"size-6": compact,
|
||||
className={cl("relative my-auto size-8 shrink-0", {
|
||||
"md:size-12": !compact,
|
||||
})}
|
||||
>
|
||||
{image ? (
|
||||
@@ -44,7 +51,7 @@ const GpHeroCard = forwardRef<HTMLAnchorElement, GpHeroCardProps>(
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-05">
|
||||
<div className="grid items-center gap-05">
|
||||
<Heading
|
||||
size="small"
|
||||
as="span"
|
||||
@@ -68,4 +75,10 @@ const GpHeroCard = forwardRef<HTMLAnchorElement, GpHeroCardProps>(
|
||||
},
|
||||
);
|
||||
|
||||
export default GpHeroCard;
|
||||
export default function Component(props: GpHeroCardProps) {
|
||||
return (
|
||||
<ErrorBoundary boundaryName="GpHeroCard">
|
||||
<GpHeroCard {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
+12
-13
@@ -36,10 +36,7 @@ export function GpChipRow({ type, entries }: GpChipRowProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
/* TODO: After release of new GP structure this could be removed since all articles will required proper data, thus always having chips */
|
||||
className="hidden has-[.chiplist]:grid"
|
||||
>
|
||||
<div>
|
||||
<Label
|
||||
as="h2"
|
||||
className={cl("flex items-center gap-1 text-aksel-heading", {
|
||||
@@ -54,17 +51,19 @@ export function GpChipRow({ type, entries }: GpChipRowProps) {
|
||||
|
||||
<div className="relative mt-2">
|
||||
<ul id={id} className={cl("flex flex-wrap gap-2 p-1", styles.chips)}>
|
||||
<GpChip
|
||||
type={type}
|
||||
disabled={false}
|
||||
onClick={reset}
|
||||
pressed={!safeString(query?.[type])}
|
||||
>
|
||||
{`Alle (${entries.reduce((acc, [, count]) => acc + count, 0)})`}
|
||||
</GpChip>
|
||||
<li>
|
||||
<GpChip
|
||||
type={type}
|
||||
disabled={false}
|
||||
onClick={reset}
|
||||
pressed={!safeString(query?.[type])}
|
||||
>
|
||||
{`Alle (${entries.reduce((acc, [, count]) => acc + count, 0)})`}
|
||||
</GpChip>
|
||||
</li>
|
||||
{entries.map(([entryName, count]) => {
|
||||
return (
|
||||
<li key={entryName} className="chiplist">
|
||||
<li key={entryName}>
|
||||
<GpChip
|
||||
type={type}
|
||||
disabled={count === 0}
|
||||
|
||||
@@ -22,8 +22,9 @@ function Cube({ variant = "light" }: CubeProps) {
|
||||
viewBox="0 0 390 560"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden
|
||||
>
|
||||
<g clipPath="url(#clip0_4792_2769)">
|
||||
<g>
|
||||
<path
|
||||
d="M414.265 -22.7451L296.355 95.1649L414.089 95.1649L531.999 -22.7451L414.265 -22.7451Z"
|
||||
stroke="#5EEAD4"
|
||||
@@ -49,11 +50,6 @@ function Cube({ variant = "light" }: CubeProps) {
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4792_2769">
|
||||
<rect width="390" height="580" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -109,7 +109,7 @@ function TemaHero({ tema, heroNav }: GpTemaHeroProps) {
|
||||
shadow="medium"
|
||||
style={inlineStyles}
|
||||
role="dialog"
|
||||
aria-labelledby="tema-selector-title"
|
||||
aria-label="Temanavigasjon"
|
||||
aria-modal="false"
|
||||
ref={(el) => {
|
||||
setDialogHeight(el?.getBoundingClientRect().height || 0);
|
||||
|
||||
+2
-2
@@ -12,14 +12,14 @@ export function HeroCardList({ heroNav, setOpen, currentSlug }: HeroListProps) {
|
||||
<nav aria-label="Temavelger" className="relative z-10 mt-5">
|
||||
<ul className="flex flex-col flex-wrap gap-2 md:flex-row lg:gap-4">
|
||||
<li>
|
||||
<GpHeroCard href="gp" compact>
|
||||
<GpHeroCard href="god-praksis" compact>
|
||||
Alle tema
|
||||
</GpHeroCard>
|
||||
</li>
|
||||
{heroNav.map((tema, idx) => (
|
||||
<li key={tema.slug + idx}>
|
||||
<GpHeroCard
|
||||
href={`gp/${tema.slug}`}
|
||||
href={`god-praksis/${tema.slug}`}
|
||||
image={
|
||||
currentSlug === tema.slug ? tema.imageInverted : tema.image
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import NextLink from "next/link";
|
||||
import { BodyShort, Detail, Heading } from "@navikt/ds-react";
|
||||
import ErrorBoundary from "@/error-boundary";
|
||||
import { useFormatedDate } from "@/hooks/useFormatedDate";
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import {
|
||||
AkselGodPraksisDocT,
|
||||
ResolveContributorsSingleT,
|
||||
ResolveSlugT,
|
||||
ResolveTemaT,
|
||||
} from "@/types";
|
||||
import { abbrName } from "@/utils";
|
||||
|
||||
type ArtikkelCardProps = ResolveContributorsSingleT<
|
||||
ResolveTemaT<ResolveSlugT<AkselGodPraksisDocT>>
|
||||
> & {
|
||||
source?: string;
|
||||
variant: string;
|
||||
level?: "2" | "3";
|
||||
};
|
||||
|
||||
const ArtikkelCard = ({
|
||||
slug,
|
||||
source,
|
||||
heading,
|
||||
ingress,
|
||||
contributor,
|
||||
_updatedAt,
|
||||
publishedAt,
|
||||
variant = "god-praksis",
|
||||
tema,
|
||||
level = "2",
|
||||
...rest
|
||||
}: ArtikkelCardProps) => {
|
||||
const date = useFormatedDate(
|
||||
(rest as any)?.updateInfo?.lastVerified ?? publishedAt ?? _updatedAt,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group relative rounded-lg bg-surface-default p-3 pb-16 shadow-xsmall focus-within:ring-[3px] focus-within:ring-border-focus hover:shadow-small sm:p-5 sm:pb-16">
|
||||
<NextLink
|
||||
href={{
|
||||
pathname: `/${slug}`,
|
||||
query: {
|
||||
...(source ? { tema: source } : {}),
|
||||
},
|
||||
}}
|
||||
passHref
|
||||
className="after:absolute after:inset-0 after:z-10 after:rounded-lg focus:outline-none"
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation(
|
||||
"artikkel-kort",
|
||||
e.currentTarget.getAttribute("href"),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Heading
|
||||
level={level}
|
||||
size="small"
|
||||
className="text-deepblue-700 underline group-hover:no-underline"
|
||||
>
|
||||
{heading}
|
||||
</Heading>
|
||||
</NextLink>
|
||||
{ingress && <BodyShort className="mt-2">{ingress}</BodyShort>}
|
||||
{variant === "god-praksis" ? (
|
||||
<span className="absolute bottom-5 flex gap-2">
|
||||
{contributor && (
|
||||
<Detail as="span">{abbrName(contributor.title)}</Detail>
|
||||
)}
|
||||
{contributor && (
|
||||
<Detail as="span" className="text-text-subtle">
|
||||
—
|
||||
</Detail>
|
||||
)}
|
||||
<Detail as="span" className="text-text-subtle">
|
||||
{date}
|
||||
</Detail>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{tema?.[0] && (
|
||||
<Detail
|
||||
as="span"
|
||||
uppercase
|
||||
className="absolute bottom-5 text-text-subtle"
|
||||
>
|
||||
{tema[0]}
|
||||
</Detail>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Component(props: ArtikkelCardProps) {
|
||||
return (
|
||||
<ErrorBoundary boundaryName="ArtikkelCard">
|
||||
<ArtikkelCard {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import cl from "clsx";
|
||||
import Image from "next/legacy/image";
|
||||
import NextLink from "next/link";
|
||||
import { BodyShort } from "@navikt/ds-react";
|
||||
import ErrorBoundary from "@/error-boundary";
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { urlFor } from "@/sanity/interface";
|
||||
import { AkselTemaT } from "@/types";
|
||||
|
||||
type GodPraksisCardProps = {
|
||||
node: AkselTemaT;
|
||||
};
|
||||
|
||||
const GodPraksisCard = ({ node }: GodPraksisCardProps) => {
|
||||
if (
|
||||
!node?.pictogram ||
|
||||
!node?.slug?.current ||
|
||||
!node?.title ||
|
||||
!node?.refCount
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="group relative grid rounded-lg bg-surface-default p-6 shadow-xsmall focus-within:ring-[3px] focus-within:ring-border-focus hover:shadow-small">
|
||||
<div className="relative mb-4 h-16 w-16">
|
||||
<Image
|
||||
src={urlFor(node?.pictogram)
|
||||
.auto("format")
|
||||
.url()}
|
||||
decoding="sync"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
aria-hidden
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<NextLink
|
||||
href={`/god-praksis/${node.slug.current}`}
|
||||
passHref
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation("card", e.currentTarget.getAttribute("href"))
|
||||
}
|
||||
className="navds-heading--medium navds-heading mb-4 text-deepblue-700 underline after:absolute after:inset-0 after:rounded-lg focus:outline-none group-hover:no-underline"
|
||||
>
|
||||
{node.title}
|
||||
</NextLink>
|
||||
<BodyShort className={cl("mb-2 lg:mb-6")}>{node.oppsummering}</BodyShort>
|
||||
<BodyShort size="small" className="text-text-subtle">
|
||||
{node.refCount === 1 ? "1 ARTIKKEL" : `${node.refCount} ARTIKLER`}
|
||||
</BodyShort>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Component(props: GodPraksisCardProps) {
|
||||
return (
|
||||
<ErrorBoundary boundaryName="GodPraksisCard">
|
||||
<GodPraksisCard {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import Image from "next/legacy/image";
|
||||
import NextLink from "next/link";
|
||||
import ErrorBoundary from "@/error-boundary";
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { urlFor } from "@/sanity/interface";
|
||||
import { AkselTemaT } from "@/types";
|
||||
|
||||
type GodPraksisCardSimpleProps = {
|
||||
node: Partial<AkselTemaT>;
|
||||
};
|
||||
|
||||
const GodPraksisCardSimple = ({ node }: GodPraksisCardSimpleProps) => {
|
||||
if (!node?.pictogram || !node?.slug?.current || !node?.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="flex items-center gap-2 px-2 py-4 sm:gap-4 sm:px-6">
|
||||
<div className="relative h-8 w-8 shrink-0 sm:h-12 sm:w-12">
|
||||
<Image
|
||||
src={urlFor(node?.pictogram)
|
||||
.auto("format")
|
||||
.url()}
|
||||
decoding="sync"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
aria-hidden
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<NextLink
|
||||
href={`/god-praksis/${node.slug.current}`}
|
||||
passHref
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation("card", e.currentTarget.getAttribute("href"))
|
||||
}
|
||||
className="navds-heading--small navds-link navds-heading flex-wrap break-all text-deepblue-700 no-underline hover:underline focus:outline-none"
|
||||
>
|
||||
{node.title}
|
||||
</NextLink>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Component(props: GodPraksisCardSimpleProps) {
|
||||
return (
|
||||
<ErrorBoundary boundaryName="GodPraksisCardSimple">
|
||||
<GodPraksisCardSimple {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,9 @@ export type NextPageT<T> = {
|
||||
slug?: string;
|
||||
preview: boolean;
|
||||
id: string;
|
||||
/**
|
||||
* @deprecated Should be removed since it's not used anymore.
|
||||
*/
|
||||
title: string;
|
||||
};
|
||||
notFound: boolean;
|
||||
|
||||
@@ -13,25 +13,13 @@ export type ResolveContributorsT<T> = Omit<T, "contributors"> & {
|
||||
contributors: { title: string }[];
|
||||
};
|
||||
|
||||
export type ResolveContributorsSingleT<T> = Omit<T, "contributors"> & {
|
||||
contributor: { title: string };
|
||||
};
|
||||
|
||||
export type ResolveTemaT<T> = Omit<T, "tema"> & {
|
||||
tema: string[];
|
||||
};
|
||||
|
||||
export type ResolveRelatedArticlesT<T> = Omit<T, "relevante_artikler"> & {
|
||||
relevante_artikler?: ResolveContributorsSingleT<
|
||||
ResolveSlugT<
|
||||
ResolveTemaT<
|
||||
| AkselBloggDocT
|
||||
| AkselGodPraksisDocT
|
||||
| AkselKomponentDocT
|
||||
| AkselGrunnleggendeDocT
|
||||
>
|
||||
>
|
||||
>[];
|
||||
relevante_artikler?: {
|
||||
heading: string;
|
||||
ingress: string;
|
||||
slug: string;
|
||||
innholdstype: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type DocumentT<T> = {
|
||||
@@ -202,24 +190,6 @@ export interface AkselGodPraksisLandingPageDocT
|
||||
seo?: ArticleT["seo"];
|
||||
}
|
||||
|
||||
export interface AkselTemaT extends DocumentT<"aksel_tema"> {
|
||||
title: string;
|
||||
oppsummering?: string;
|
||||
beskrivelse?: any;
|
||||
refCount?: number;
|
||||
slug: { current: string };
|
||||
ansvarlig?: { _ref: string };
|
||||
seksjoner: ArrayObjectT<{
|
||||
title: string;
|
||||
beskrivelse: any;
|
||||
sider: ResolveContributorsSingleT<
|
||||
ResolveTemaT<ResolveSlugT<AkselGodPraksisDocT>>
|
||||
>[];
|
||||
}>[];
|
||||
pictogram: any;
|
||||
seo?: ArticleT["seo"];
|
||||
}
|
||||
|
||||
export interface AccordionT {
|
||||
list: ArrayObjectT<{
|
||||
title: string;
|
||||
|
||||
@@ -229,7 +229,7 @@ function SanitizedBlock({
|
||||
id={value?._key}
|
||||
level="2"
|
||||
size="large"
|
||||
className="mb-4 mt-12 max-w-text scroll-mt-20 text-deepblue-800 first-of-type:mt-0 focus:outline-none"
|
||||
className="mb-4 mt-12 max-w-text scroll-mt-20 text-deepblue-800 first-of-type:mt-0 focus:outline-none dark:text-text-on-inverted"
|
||||
>
|
||||
{children}
|
||||
</Heading>
|
||||
@@ -237,7 +237,7 @@ function SanitizedBlock({
|
||||
case "h3":
|
||||
return (
|
||||
<Heading
|
||||
className="mt-8 max-w-text scroll-mt-20 text-deepblue-800 focus:outline-none"
|
||||
className="mt-8 max-w-text scroll-mt-20 text-deepblue-800 focus:outline-none dark:text-text-on-inverted"
|
||||
spacing
|
||||
level="3"
|
||||
size="medium"
|
||||
@@ -250,7 +250,7 @@ function SanitizedBlock({
|
||||
case "h4":
|
||||
return (
|
||||
<Heading
|
||||
className="mt-6 max-w-text text-deepblue-800"
|
||||
className="mt-6 max-w-text text-deepblue-800 dark:text-text-on-inverted"
|
||||
spacing
|
||||
level="4"
|
||||
size="small"
|
||||
@@ -264,7 +264,7 @@ function SanitizedBlock({
|
||||
return (
|
||||
<BodyLong
|
||||
spacing
|
||||
className="last:mb-0 group-[.aksel-block-ingress]/ingress:text-xl"
|
||||
className="last:mb-0 group-[.aksel-block-ingress]/ingress:text-xl dark:text-text-on-inverted"
|
||||
>
|
||||
{children}
|
||||
</BodyLong>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"/produktbloggen",
|
||||
"/komponenter/core/alert",
|
||||
"/god-praksis/innholdsarbeid",
|
||||
"/god-praksis/brukerinnsikt",
|
||||
"/god-praksis/universell-utforming",
|
||||
"/god-praksis/navno",
|
||||
"/god-praksis/artikler/innholdsstrategi-for-nav-no"
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
import { at, defineMigration, patch, unset } from "sanity/migrate";
|
||||
|
||||
export default defineMigration({
|
||||
title: "editor-id-to-mail",
|
||||
documentTypes: ["editor"],
|
||||
|
||||
async *migrate(documents) {
|
||||
for await (const document of documents()) {
|
||||
yield patch(document._id, [
|
||||
at("anon_navn", unset()),
|
||||
at("user_id", unset()),
|
||||
at("roller", unset()),
|
||||
at("anonym", unset()),
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -41,7 +41,7 @@
|
||||
"@sanity/image-url": "1.0.2",
|
||||
"@sanity/locale-nb-no": "^1.1.0",
|
||||
"@sanity/table": "1.1.2",
|
||||
"@sanity/vision": "3.36.3",
|
||||
"@sanity/vision": "3.37.2",
|
||||
"@slack/web-api": "^7.0.2",
|
||||
"boring-avatars": "1.10.1",
|
||||
"clsx": "^2.1.0",
|
||||
@@ -63,7 +63,7 @@
|
||||
"react-responsive-masonry": "^2.1.7",
|
||||
"react-rx": "^2.1.3",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sanity": "3.36.3",
|
||||
"sanity": "3.37.2",
|
||||
"sanity-plugin-asset-source-unsplash": "^1.1.2",
|
||||
"sanity-plugin-media": "^2.2.5",
|
||||
"sanity-plugin-utils": "^1.6.4",
|
||||
@@ -80,8 +80,8 @@
|
||||
"@next/bundle-analyzer": "14.1.1-canary.57",
|
||||
"@next/eslint-plugin-next": "14.1.1-canary.57",
|
||||
"@playwright/test": "1.35.1",
|
||||
"@sanity/block-tools": "3.36.3",
|
||||
"@sanity/schema": "3.36.3",
|
||||
"@sanity/block-tools": "3.37.2",
|
||||
"@sanity/schema": "3.37.2",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
|
||||
@@ -52,21 +52,19 @@ export default async function preview(
|
||||
}
|
||||
|
||||
// Check if the article with the given `slug` exists
|
||||
const { article, godpraksis, gp } = await previewClient.fetch(
|
||||
const { article, godpraksis } = await previewClient.fetch(
|
||||
`{
|
||||
"article": *[slug.current == $slug][0].slug.current,
|
||||
"godpraksis": *[slug.current == $godPraksis && _type == "aksel_tema"][0].slug.current,
|
||||
"gp": *[slug.current == $gp && _type == "gp.tema"][0].slug.current
|
||||
"godpraksis": *[slug.current == $godpraksis && _type == "gp.tema"][0].slug.current
|
||||
}`,
|
||||
{
|
||||
slug,
|
||||
godPraksis: slug.replace("god-praksis/", ""),
|
||||
gp: slug.replace("gp/", ""),
|
||||
godpraksis: slug.replace("god-praksis/", ""),
|
||||
},
|
||||
);
|
||||
|
||||
// If the slug doesn't exist prevent preview mode from being enabled
|
||||
if (!article && !godpraksis && !gp) {
|
||||
if (!article && !godpraksis) {
|
||||
return res.status(401).json({ message: "Invalid slug" });
|
||||
}
|
||||
|
||||
@@ -74,8 +72,6 @@ export default async function preview(
|
||||
return redirectToPreview(res, `/${article}`);
|
||||
} else if (godpraksis) {
|
||||
return redirectToPreview(res, `/god-praksis/${godpraksis}`);
|
||||
} else if (gp) {
|
||||
return redirectToPreview(res, `/gp/${gp}`);
|
||||
}
|
||||
|
||||
// Redirect to the path from the fetched article
|
||||
|
||||
@@ -36,6 +36,11 @@ const client = new WebClient(process.env.SLACK_BOT_TOKEN);
|
||||
|
||||
export default authProtectedApi(sendSlackbotFeedback);
|
||||
|
||||
type MembersArray = NonNullable<UsersListResponse["members"]>;
|
||||
type Member = MembersArray[0] & {
|
||||
id: NonNullable<MembersArray[0]["id"]>;
|
||||
};
|
||||
|
||||
async function sendSlackbotFeedback(
|
||||
request: NextApiRequest,
|
||||
response: NextApiResponse,
|
||||
@@ -70,7 +75,8 @@ async function sendSlackbotFeedback(
|
||||
"id": _id,
|
||||
"title": heading,
|
||||
"editors": contributors[]->email,
|
||||
"slug": slug.current
|
||||
"slug": slug.current,
|
||||
"contacts": undertema[]->tema->contacts[]->email
|
||||
}`,
|
||||
{
|
||||
id: validation.data.body.document_id,
|
||||
@@ -102,7 +108,27 @@ async function sendSlackbotFeedback(
|
||||
const slackProfileForEditors = document.editors
|
||||
.filter(Boolean)
|
||||
.map((email: string) => findUserByEmail(email, slackMembers.members))
|
||||
.filter(Boolean) as Exclude<UsersListResponse["members"], undefined>;
|
||||
.filter(Boolean)
|
||||
.filter((x) => !!x.id) as Member[];
|
||||
|
||||
/**
|
||||
* We use contacts found on tema connected to article and find their matching slack profiles
|
||||
*/
|
||||
const slackProfileForTemaContacts =
|
||||
(document.contacts
|
||||
?.filter(Boolean)
|
||||
.map((email: string) => findUserByEmail(email, slackMembers.members))
|
||||
.filter(Boolean)
|
||||
.filter((x) => !!x.id) as Member[]) ?? [];
|
||||
|
||||
/**
|
||||
* We want to make sure we avoid sending the message to the same user multiple times
|
||||
*/
|
||||
const ids = [...slackProfileForEditors, ...slackProfileForTemaContacts].map(
|
||||
(member) => member.id,
|
||||
);
|
||||
|
||||
const recieverSlackIdList = [...new Set(ids)];
|
||||
|
||||
let postMessageError = false;
|
||||
|
||||
@@ -119,21 +145,19 @@ async function sendSlackbotFeedback(
|
||||
title: document.title,
|
||||
},
|
||||
feedback: validation.data.body.feedback,
|
||||
recievers: slackProfileForEditors
|
||||
.map((x) => x.id)
|
||||
.filter(Boolean) as string[],
|
||||
recievers: recieverSlackIdList,
|
||||
sender: {
|
||||
email: user.email,
|
||||
slackId: senderSlackUser?.id,
|
||||
},
|
||||
}),
|
||||
...(slackProfileForEditors.length === 0
|
||||
...(recieverSlackIdList.length === 0
|
||||
? [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: ":exclamation: Denne artikkelen har ingen redaktører. @here",
|
||||
text: ":exclamation: Denne artikkelen har ingen redaktører eller temakontater. @here",
|
||||
},
|
||||
} satisfies SectionBlock,
|
||||
]
|
||||
@@ -147,13 +171,10 @@ async function sendSlackbotFeedback(
|
||||
);
|
||||
});
|
||||
|
||||
for (const editor of slackProfileForEditors) {
|
||||
if (!editor.id) {
|
||||
continue;
|
||||
}
|
||||
for (const slackId of recieverSlackIdList) {
|
||||
await client.chat
|
||||
.postMessage({
|
||||
channel: editor.id,
|
||||
channel: slackId,
|
||||
text: `Tilbakemelding: ${validation.data.body.feedback}`,
|
||||
blocks: slackBlock({
|
||||
article: {
|
||||
@@ -162,9 +183,7 @@ async function sendSlackbotFeedback(
|
||||
title: document.title,
|
||||
},
|
||||
feedback: validation.data.body.feedback,
|
||||
recievers: slackProfileForEditors
|
||||
.map((x) => x.id)
|
||||
.filter(Boolean) as string[],
|
||||
recievers: recieverSlackIdList,
|
||||
sender: {
|
||||
email: user.email,
|
||||
slackId: senderSlackUser?.id,
|
||||
@@ -281,7 +300,7 @@ function slackBlock({ feedback, article, recievers, sender }: SlackBlockT) {
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "\n\nMedvirkende (disse fikk tilbakemeldingen):\n",
|
||||
text: "\n\nDisse fikk tilbakemeldingen (medvirkende og temakontakter):\n",
|
||||
style: {
|
||||
bold: true,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,8 @@ Alle linjer som inneholder `examples/withDsExample` + alt under `// EXAMPLES DO
|
||||
|
||||
Demo-komponent må ha navn `Example` for at CodeSandbox-knapp skal fungere riktig.
|
||||
|
||||
`withDsExample` tar imot parametere, e.g. `{variant: 'static'}` for å sette bredde på komponent til "page width", eller `{variant: 'full'}` for full bredde.
|
||||
|
||||
### Args
|
||||
|
||||
For å håndtere sortering, kan man sette en index i `args` som da brukes for å bedre sortere eksemplene.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { FormSummary } from "@navikt/ds-react";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">
|
||||
Personal information
|
||||
</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#">Edit</FormSummary.EditLink>
|
||||
</FormSummary.Header>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Name</FormSummary.Label>
|
||||
<FormSummary.Value>John Doe</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example, { variant: "static" });
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 2,
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { FormSummary, Link } from "@navikt/ds-react";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Adresse</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
Gate 123
|
||||
<br />
|
||||
1234 Sted
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Fødselsnummer</FormSummary.Label>
|
||||
<FormSummary.Value>12345678910</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Barn nr. 1</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Kari Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kjønn</FormSummary.Label>
|
||||
<FormSummary.Value>Jente</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Alder</FormSummary.Label>
|
||||
<FormSummary.Value>6</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kontaktinformasjon</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<Link>Fyll ut kontaktinformasjon</Link>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Hvordan vil du bli varslet?</FormSummary.Label>
|
||||
<FormSummary.Value>E-post</FormSummary.Value>
|
||||
<FormSummary.Value>SMS</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example, { variant: "static" });
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 0,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { FormSummary } from "@navikt/ds-react";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Barn nr. 1</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Kari Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kjønn</FormSummary.Label>
|
||||
<FormSummary.Value>Jente</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Alder</FormSummary.Label>
|
||||
<FormSummary.Value>6</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Barn nr. 2</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Per Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kjønn</FormSummary.Label>
|
||||
<FormSummary.Value>Gutt</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Alder</FormSummary.Label>
|
||||
<FormSummary.Value>7</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example, { variant: "static" });
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 1,
|
||||
desc: "Man kan gruppere svar ved å neste FormSummary.Answers innenfor FormSummary.Answer.",
|
||||
};
|
||||
@@ -35,5 +35,5 @@ export const Demo = {
|
||||
|
||||
export const args = {
|
||||
index: 3,
|
||||
desc: "Full lar Bleed bryte ut av parent og dekke hele skjermbredden. Dette kan være nyttig for 'Heroes' eller lignende elementer som ønsker å bryte ut av parent-layout",
|
||||
desc: "I noen tilfeller må du også sette 'overflow-x: hidden;' på body for å unngå horisontal scrollbar. Full lar Bleed bryte ut av parent og dekke hele skjermbredden. Dette kan være nyttig for 'Heroes' eller lignende elementer som ønsker å bryte ut av parent-layout",
|
||||
};
|
||||
@@ -1,243 +1,151 @@
|
||||
import cl from "clsx";
|
||||
import Image from "next/legacy/image";
|
||||
import { GetStaticPaths } from "next/types";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { Detail, Heading, Label } from "@navikt/ds-react";
|
||||
import ArtikkelCard from "@/cms/cards/ArtikkelCard";
|
||||
import { groq } from "next-sanity";
|
||||
import { GetServerSideProps } from "next/types";
|
||||
import { useMemo } from "react";
|
||||
import { Box, Page, VStack } from "@navikt/ds-react";
|
||||
import Footer from "@/layout/footer/Footer";
|
||||
import { ArticleSections } from "@/layout/god-praksis-page/ArticleSections";
|
||||
import { GpChipNavigation } from "@/layout/god-praksis-page/chipnavigation/GpChipNavigation";
|
||||
import TemaHero from "@/layout/god-praksis-page/hero/tema-hero/TemaHero";
|
||||
import {
|
||||
GpSlugQueryResponse,
|
||||
ParsedGPArticle,
|
||||
} from "@/layout/god-praksis-page/interface";
|
||||
import Header from "@/layout/header/Header";
|
||||
import { SanityBlockContent } from "@/sanity-block";
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import { getAkselTema, urlFor } from "@/sanity/interface";
|
||||
import { contributorsSingle, destructureBlocks } from "@/sanity/queries";
|
||||
import { AkselTemaT, NextPageT } from "@/types";
|
||||
import { abbrName } from "@/utils";
|
||||
import { AkselCubeStatic } from "@/web/aksel-cube/AkselCube";
|
||||
import { NextPageT } from "@/types";
|
||||
import { dateStr } from "@/utils";
|
||||
import { SEO } from "@/web/seo/SEO";
|
||||
import NotFotfund from "../404";
|
||||
|
||||
type PageProps = NextPageT<{
|
||||
tema: Omit<AkselTemaT, "ansvarlig"> & {
|
||||
ansvarlig?: { title: string; roller: string[] };
|
||||
};
|
||||
}>;
|
||||
|
||||
export const query = `{
|
||||
"tema": *[_type == "aksel_tema" && slug.current == $slug] | order(_updatedAt desc)[0]{
|
||||
const sanityQuery = groq`
|
||||
{
|
||||
"articles": *[_type == "aksel_artikkel" && defined(undertema) && $slug in undertema[]->tema->slug.current] | order(publishedAt desc) {
|
||||
_id,
|
||||
heading,
|
||||
publishedAt,
|
||||
"description": ingress,
|
||||
"undertema": undertema[]->{title, "temaTitle": tema->title},
|
||||
"innholdstype": innholdstype->title,
|
||||
"slug": slug.current,
|
||||
},
|
||||
"tema": *[_type == "gp.tema" && slug.current == $slug][0]{
|
||||
...,
|
||||
"ansvarlig": ansvarlig->{title, roller},
|
||||
seksjoner[]{
|
||||
...,
|
||||
beskrivelse[]{
|
||||
...,
|
||||
${destructureBlocks}
|
||||
},
|
||||
sider[]->{
|
||||
_id,
|
||||
heading,
|
||||
_createdAt,
|
||||
_updatedAt,
|
||||
publishedAt,
|
||||
updateInfo,
|
||||
"slug": slug.current,
|
||||
"tema": tema[]->title,
|
||||
ingress,
|
||||
"contributor": ${contributorsSingle}
|
||||
}
|
||||
},
|
||||
"pictogram": pictogram.asset-> {
|
||||
url,
|
||||
altText,
|
||||
"slug": slug.current,
|
||||
"undertema": *[_type == "gp.tema.undertema" && tema->slug.current == $slug]{
|
||||
title,
|
||||
description
|
||||
},
|
||||
"image": pictogram,
|
||||
},
|
||||
"heroNav": *[_type == "gp.tema" && count(*[_type=="aksel_artikkel"
|
||||
&& (^._id in undertema[]->tema._ref)]) > 0] | order(lower(title)){
|
||||
title,
|
||||
"slug": slug.current,
|
||||
"image": pictogram,
|
||||
"imageInverted": pictogramInverted,
|
||||
}
|
||||
}`;
|
||||
}
|
||||
`;
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
return {
|
||||
paths: await getAkselTema().then((paths) =>
|
||||
paths.map(({ path }) => ({
|
||||
params: {
|
||||
slug: path,
|
||||
},
|
||||
})),
|
||||
),
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
type PageProps = NextPageT<GpSlugQueryResponse>;
|
||||
|
||||
export const getStaticProps = async ({
|
||||
params: { slug },
|
||||
preview = false,
|
||||
}: {
|
||||
params: { slug: string };
|
||||
preview?: boolean;
|
||||
}): Promise<PageProps> => {
|
||||
const { tema } = await getClient().fetch(query, {
|
||||
slug,
|
||||
});
|
||||
export const getServerSideProps: GetServerSideProps = async (
|
||||
ctx,
|
||||
): Promise<PageProps> => {
|
||||
const slug = ctx.params?.slug as string;
|
||||
const preview = !!ctx.preview;
|
||||
|
||||
const { heroNav, tema, articles } =
|
||||
await getClient().fetch<GpSlugQueryResponse>(sanityQuery, {
|
||||
slug,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
tema,
|
||||
heroNav,
|
||||
// Avoids having to format date clientside
|
||||
articles: await Promise.all(
|
||||
articles.map(async (a) => ({
|
||||
...a,
|
||||
publishedAt: await dateStr(a.publishedAt),
|
||||
})),
|
||||
),
|
||||
slug,
|
||||
preview,
|
||||
id: tema?._id ?? null,
|
||||
id: tema?._id ?? "",
|
||||
title: tema?.title ?? "",
|
||||
},
|
||||
notFound: !tema && !preview,
|
||||
revalidate: 60,
|
||||
notFound: !tema || articles.length === 0,
|
||||
};
|
||||
};
|
||||
|
||||
const Page = ({ tema: page }: PageProps["props"]) => {
|
||||
if (!page || !page.seksjoner || page.seksjoner.length === 0) {
|
||||
return <NotFotfund />;
|
||||
}
|
||||
const GpPage = (props: PageProps["props"]) => {
|
||||
const articles = useMemo(
|
||||
() =>
|
||||
props.articles
|
||||
.map((article) => {
|
||||
const undertema = article.undertema.find(
|
||||
(ut) => ut?.temaTitle === props.tema.title,
|
||||
)?.title;
|
||||
|
||||
const hasAnsvarlig = !!page?.ansvarlig?.title;
|
||||
|
||||
const hasPages = !!page.seksjoner.find((x) => !!x.sider);
|
||||
return {
|
||||
...article,
|
||||
undertema: undertema ?? "",
|
||||
} satisfies ParsedGPArticle;
|
||||
})
|
||||
.filter((article) => !!article.undertema)
|
||||
/* Makes sure sections always appear in the same order */
|
||||
.sort((a, b) => a.undertema.localeCompare(b.undertema)),
|
||||
[props.articles, props.tema.title],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title={page?.title}
|
||||
description={page?.seo?.meta}
|
||||
image={page?.seo?.image}
|
||||
title={props.tema?.title ?? ""}
|
||||
description={props.tema?.seo?.meta ?? props.tema.description}
|
||||
image={props.tema?.seo?.image}
|
||||
/>
|
||||
|
||||
<div className="bg-surface-subtle">
|
||||
<Page
|
||||
footer={<Footer />}
|
||||
footerPosition="belowFold"
|
||||
className="bg-surface-subtle"
|
||||
>
|
||||
<Header variant="subtle" />
|
||||
<main
|
||||
<Box
|
||||
paddingBlock="10"
|
||||
as="main"
|
||||
tabIndex={-1}
|
||||
id="hovedinnhold"
|
||||
className="relative overflow-hidden focus:outline-none"
|
||||
className="animate-popUpPage focus:outline-none"
|
||||
key={props.tema?.title}
|
||||
>
|
||||
<AkselCubeStatic className="text-deepblue-300 opacity-5 " />
|
||||
<div className=" pt-20 text-center">
|
||||
<div className="dynamic-wrapper px-4 pb-6 text-center">
|
||||
<Image
|
||||
src={urlFor(page.pictogram.url).auto("format").url()}
|
||||
decoding="sync"
|
||||
width="72"
|
||||
height="72"
|
||||
layout="fixed"
|
||||
priority
|
||||
aria-hidden
|
||||
alt={page.pictogram?.altText}
|
||||
/>
|
||||
<Heading
|
||||
level="1"
|
||||
size="xlarge"
|
||||
className="mt-8 hidden md:block md:text-5xl"
|
||||
>
|
||||
{page.title}
|
||||
</Heading>
|
||||
<Heading level="1" size="large" className="mt-8 block md:hidden">
|
||||
{page.title}
|
||||
</Heading>
|
||||
<Page.Block width="xl" gutters>
|
||||
<VStack gap="10">
|
||||
<TemaHero tema={props.tema} heroNav={props.heroNav} />
|
||||
<Box paddingInline={{ xs: "4", lg: "10" }}>
|
||||
<GpChipNavigation articles={articles} />
|
||||
|
||||
<div className="mt-4 flex flex-col items-center justify-between gap-8">
|
||||
<SanityBlockContent
|
||||
blocks={page.beskrivelse}
|
||||
className="override-text-700 max-w-prose xl:mb-8"
|
||||
isIngress
|
||||
/>
|
||||
<div
|
||||
className={cl(
|
||||
"max-w relative z-10 mb-2 h-fit sm:w-96 xl:mt-[10px]",
|
||||
{ invisible: !hasAnsvarlig },
|
||||
)}
|
||||
aria-hidden={!hasAnsvarlig}
|
||||
>
|
||||
<Detail as="div" className="mb-2" uppercase>
|
||||
Ansvarlig for tema
|
||||
</Detail>
|
||||
<div className="grid">
|
||||
<div>
|
||||
<Label as="div" className="text-xlarge">
|
||||
{page?.ansvarlig?.title
|
||||
? abbrName(page?.ansvarlig?.title)
|
||||
: ""}
|
||||
</Label>
|
||||
{page?.ansvarlig?.roller &&
|
||||
page.ansvarlig.roller.length > 0 ? (
|
||||
<div className="mt-[2px] text-medium">
|
||||
{page?.ansvarlig?.roller.join(", ")}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative px-4 pb-24 pt-8 md:pt-16 xl:pt-8 ">
|
||||
{hasPages ? (
|
||||
<div className="dynamic-wrapper grid gap-20">
|
||||
{page.seksjoner.map((seksjon) =>
|
||||
seksjon?.sider ? (
|
||||
<div key={seksjon._key}>
|
||||
<Heading level="2" size="medium">
|
||||
{seksjon.title}
|
||||
</Heading>
|
||||
{seksjon.beskrivelse && (
|
||||
<div className="max-w-prose">
|
||||
<SanityBlockContent blocks={seksjon.beskrivelse} />
|
||||
</div>
|
||||
)}
|
||||
<div className="card-grid-3-1 mt-6">
|
||||
{seksjon?.sider?.map((x) => (
|
||||
<ArtikkelCard
|
||||
{...x}
|
||||
level="3"
|
||||
source={page?.slug?.current}
|
||||
key={x._id}
|
||||
variant="god-praksis"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="dynamic-wrapper mb-20">
|
||||
<Heading level="2" size="medium">
|
||||
Fant ingen artikler her enda...
|
||||
</Heading>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<VStack gap="12" className="mt-10">
|
||||
<ArticleSections
|
||||
articles={articles}
|
||||
undertemaList={props.tema.undertema}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Page.Block>
|
||||
</Box>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WithPreview = lazy(() => import("@/preview"));
|
||||
|
||||
/**
|
||||
* To avoid infinite loop rendering caused by `count` in query, we avoid showing preview for this page.
|
||||
*/
|
||||
const Wrapper = (props: any) => {
|
||||
if (props?.preview) {
|
||||
return (
|
||||
<Suspense fallback={<Page {...props} />}>
|
||||
<WithPreview
|
||||
comp={Page}
|
||||
query={query}
|
||||
props={props}
|
||||
params={{
|
||||
slug: props?.slug,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <Page {...props} />;
|
||||
return <GpPage {...props} />;
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,46 +1,50 @@
|
||||
import NextLink from "next/link";
|
||||
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
|
||||
import { differenceInMonths } from "date-fns";
|
||||
import { GetServerSideProps } from "next/types";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { ChevronRightIcon } from "@navikt/aksel-icons";
|
||||
import { BodyLong, BodyShort, Detail, Heading, Label } from "@navikt/ds-react";
|
||||
import { UserStateT } from "@/auth/auth.types";
|
||||
import { getAuthUserState } from "@/auth/getUserState";
|
||||
import ArtikkelCard from "@/cms/cards/ArtikkelCard";
|
||||
import Footer from "@/layout/footer/Footer";
|
||||
import GpArticleCard from "@/layout/god-praksis-page/cards/GpArticleCard";
|
||||
import { GpTemaLink } from "@/layout/god-praksis-page/chipnavigation/GpTemaLink";
|
||||
import Header from "@/layout/header/Header";
|
||||
import { SanityBlockContent } from "@/sanity-block";
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import {
|
||||
contributorsAll,
|
||||
contributorsSingle,
|
||||
destructureBlocks,
|
||||
} from "@/sanity/queries";
|
||||
import { contributorsAll, destructureBlocks } from "@/sanity/queries";
|
||||
import {
|
||||
AkselGodPraksisDocT,
|
||||
NextPageT,
|
||||
ResolveContributorsT,
|
||||
ResolveRelatedArticlesT,
|
||||
ResolveSlugT,
|
||||
ResolveTemaT,
|
||||
TableOfContentsT,
|
||||
} from "@/types";
|
||||
import { abbrName, dateStr, generateTableOfContents } from "@/utils";
|
||||
import { Feedback } from "@/web/Feedback/Feedback";
|
||||
import OutdatedAlert from "@/web/OutdatedAlert";
|
||||
import { SEO } from "@/web/seo/SEO";
|
||||
import TableOfContents from "@/web/toc/TableOfContents";
|
||||
import NotFotfund from "../../404";
|
||||
import NotFound from "../../404";
|
||||
|
||||
type PageProps = NextPageT<{
|
||||
page: ResolveContributorsT<
|
||||
ResolveTemaT<ResolveSlugT<ResolveRelatedArticlesT<AkselGodPraksisDocT>>>
|
||||
>;
|
||||
ResolveSlugT<ResolveRelatedArticlesT<AkselGodPraksisDocT>>
|
||||
> & {
|
||||
innholdstype: string;
|
||||
undertema: {
|
||||
title: string;
|
||||
tema: { slug: string; title: string; image: SanityImageSource };
|
||||
}[];
|
||||
};
|
||||
publishDate: string;
|
||||
verifiedDate: string;
|
||||
outdated: boolean;
|
||||
toc: TableOfContentsT;
|
||||
userState: UserStateT;
|
||||
}>;
|
||||
|
||||
export const query = `{
|
||||
const query = `{
|
||||
"page": *[slug.current == $slug] | order(_updatedAt desc)[0]
|
||||
{
|
||||
...,
|
||||
@@ -49,19 +53,21 @@ export const query = `{
|
||||
...,
|
||||
${destructureBlocks}
|
||||
},
|
||||
tema[]->{title, slug, seo},
|
||||
"innholdstype": innholdstype->title,
|
||||
"undertema": undertema[]->{
|
||||
title,
|
||||
"tema": tema->{
|
||||
title,
|
||||
"slug": slug.current,
|
||||
"image": seo.image
|
||||
}
|
||||
},
|
||||
${contributorsAll},
|
||||
relevante_artikler[]->{
|
||||
_id,
|
||||
heading,
|
||||
_createdAt,
|
||||
_updatedAt,
|
||||
publishedAt,
|
||||
updateInfo,
|
||||
"slug": slug.current,
|
||||
"tema": tema[]->tag,
|
||||
ingress,
|
||||
"contributor": ${contributorsSingle},
|
||||
"slug": slug.current,
|
||||
"innholdstype": innholdstype->title,
|
||||
}
|
||||
}
|
||||
}`;
|
||||
@@ -79,6 +85,9 @@ export const getServerSideProps: GetServerSideProps = async (
|
||||
slug: `god-praksis/artikler/${slug}`,
|
||||
});
|
||||
|
||||
const verifiedDate =
|
||||
page?.updateInfo?.lastVerified ?? page?.publishedAt ?? page?._updatedAt;
|
||||
|
||||
return {
|
||||
props: {
|
||||
page,
|
||||
@@ -86,9 +95,8 @@ export const getServerSideProps: GetServerSideProps = async (
|
||||
preview: isPreview,
|
||||
id: page?._id ?? "",
|
||||
title: page?.heading ?? "",
|
||||
verifiedDate: await dateStr(
|
||||
page?.updateInfo?.lastVerified ?? page?.publishedAt ?? page?._updatedAt,
|
||||
),
|
||||
verifiedDate: await dateStr(verifiedDate),
|
||||
outdated: differenceInMonths(new Date(), new Date(verifiedDate)) >= 12,
|
||||
publishDate: await dateStr(page?.publishedAt ?? page?._updatedAt),
|
||||
toc: generateTableOfContents({
|
||||
content: page?.content,
|
||||
@@ -104,11 +112,12 @@ const Page = ({
|
||||
page: data,
|
||||
publishDate,
|
||||
verifiedDate,
|
||||
outdated,
|
||||
toc,
|
||||
userState,
|
||||
}: PageProps["props"]) => {
|
||||
if (!data) {
|
||||
return <NotFotfund />;
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
if (!data.content || !data.heading) {
|
||||
@@ -122,8 +131,6 @@ const Page = ({
|
||||
|
||||
const authors = (data?.contributors as any)?.map((x) => x?.title) ?? [];
|
||||
|
||||
const hasTema = "tema" in data && data.tema && data?.tema.length > 0;
|
||||
|
||||
const aside = data?.relevante_artikler &&
|
||||
data.relevante_artikler.length > 0 && (
|
||||
<aside
|
||||
@@ -143,26 +150,28 @@ const Page = ({
|
||||
: `Relevante artikler`}
|
||||
</Heading>
|
||||
<div className="card-grid-3-1 mt-6 px-4">
|
||||
{data.relevante_artikler.map((x: any) =>
|
||||
x && x?._id ? (
|
||||
<ArtikkelCard level="3" {...x} key={x._id} />
|
||||
) : null,
|
||||
)}
|
||||
{data.relevante_artikler.map((x) => (
|
||||
<GpArticleCard
|
||||
key={x.heading}
|
||||
href={x.slug}
|
||||
description={x.ingress}
|
||||
innholdstype={x.innholdstype}
|
||||
>
|
||||
{x.heading}
|
||||
</GpArticleCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
const filteredTema =
|
||||
hasTema && data?.tema?.filter((x: any) => x?.title && x?.slug);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title={data?.heading}
|
||||
description={data?.seo?.meta ?? data?.ingress}
|
||||
image={data?.seo?.image ?? (data?.tema?.[0] as any)?.seo?.image}
|
||||
image={data?.seo?.image ?? data.undertema?.[0]?.tema?.image}
|
||||
publishDate={publishDate}
|
||||
canonical={`/${data.slug}`}
|
||||
/>
|
||||
@@ -176,10 +185,18 @@ const Page = ({
|
||||
<div className="mx-auto max-w-aksel px-4 sm:w-[90%]">
|
||||
<article className="pb-16 pt-12 md:pb-32">
|
||||
<div className="mx-auto mb-16 max-w-prose lg:ml-0">
|
||||
{data.innholdstype && (
|
||||
<BodyShort
|
||||
weight="semibold"
|
||||
className="uppercase text-violet-600"
|
||||
>
|
||||
{data.innholdstype}
|
||||
</BodyShort>
|
||||
)}
|
||||
<Heading
|
||||
level="1"
|
||||
size="large"
|
||||
className="text-wrap-balance mt-4 text-deepblue-800 md:text-5xl"
|
||||
className="text-wrap-balance mt-1 text-deepblue-800 md:text-5xl"
|
||||
>
|
||||
{data.heading}
|
||||
</Heading>
|
||||
@@ -192,38 +209,26 @@ const Page = ({
|
||||
</BodyLong>
|
||||
)}
|
||||
|
||||
<div className="mt-6 inline-flex flex-wrap items-center gap-2 text-base">
|
||||
<Detail uppercase as="span">
|
||||
{verifiedDate}
|
||||
<div className="mt-5 grid">
|
||||
<Detail as="time" textColor="subtle">
|
||||
{`Oppdatert ${verifiedDate}`}
|
||||
</Detail>
|
||||
<Detail as="time" textColor="subtle">
|
||||
{`Publisert ${publishDate}`}
|
||||
</Detail>
|
||||
{authors?.length > 0 && (
|
||||
<>
|
||||
<span className="h-2 w-2 rotate-45 rounded-[1px] bg-deepblue-700 opacity-25" />
|
||||
<BodyShort
|
||||
size="small"
|
||||
as="div"
|
||||
className="flex flex-wrap gap-1"
|
||||
>
|
||||
<address className="not-italic">{authors?.[0]}</address>
|
||||
</BodyShort>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{hasTema && filteredTema && (
|
||||
<div className="mt-8 flex flex-wrap gap-2">
|
||||
{filteredTema.map(({ title, slug }: any) => (
|
||||
<span key={title}>
|
||||
<BodyShort
|
||||
href={`/god-praksis/${slug.current}`}
|
||||
key={title}
|
||||
size="small"
|
||||
as={NextLink}
|
||||
className="flex min-h-8 items-center justify-center gap-[2px] rounded-full bg-surface-neutral-subtle pl-4 pr-1 text-deepblue-800 ring-1 ring-inset ring-border-subtle hover:bg-surface-neutral-subtle-hover focus:outline-none focus-visible:shadow-focus"
|
||||
>
|
||||
{title}
|
||||
<ChevronRightIcon aria-hidden fontSize="1.25rem" />
|
||||
</BodyShort>
|
||||
</span>
|
||||
|
||||
{data.undertema && data.undertema.length > 0 && (
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{data.undertema.map(({ title, tema }) => (
|
||||
<GpTemaLink
|
||||
key={title}
|
||||
href={`/god-praksis/${
|
||||
tema.slug
|
||||
}?undertema=${encodeURIComponent(title)}`}
|
||||
>
|
||||
{tema.title}
|
||||
</GpTemaLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -231,11 +236,12 @@ const Page = ({
|
||||
<div className="relative mx-auto mt-4 max-w-prose lg:ml-0 lg:grid lg:max-w-none lg:grid-flow-row-dense lg:grid-cols-3 lg:items-start lg:gap-x-12">
|
||||
<TableOfContents toc={toc} variant="subtle" />
|
||||
<div className="max-w-prose lg:col-span-2 lg:col-start-1">
|
||||
{outdated && <OutdatedAlert />}
|
||||
<SanityBlockContent blocks={data?.content ?? []} />
|
||||
<div className="mt-12">
|
||||
{authors?.length > 0 && (
|
||||
<Label className="mb-2 text-deepblue-700" as="p">
|
||||
Bidragsytere
|
||||
Medvirkende
|
||||
</Label>
|
||||
)}
|
||||
{authors?.length > 0 && (
|
||||
@@ -251,9 +257,6 @@ const Page = ({
|
||||
))}
|
||||
</BodyShort>
|
||||
)}
|
||||
<BodyShort as="span" className="text-text-subtle">
|
||||
Publisert: {publishDate}
|
||||
</BodyShort>
|
||||
{userState && <Feedback userState={userState} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,149 +1,234 @@
|
||||
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
|
||||
import cl from "clsx";
|
||||
import { groq } from "next-sanity";
|
||||
import NextLink from "next/link";
|
||||
import { GetStaticProps } from "next/types";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { SparklesIcon } from "@navikt/aksel-icons";
|
||||
import { Heading } from "@navikt/ds-react";
|
||||
import ArtikkelCard from "@/cms/cards/ArtikkelCard";
|
||||
import GodPraksisCard from "@/cms/cards/GodPraksisCard";
|
||||
import {
|
||||
BodyLong,
|
||||
Box,
|
||||
Heading,
|
||||
Link,
|
||||
Page,
|
||||
Stack,
|
||||
VStack,
|
||||
} from "@navikt/ds-react";
|
||||
import Footer from "@/layout/footer/Footer";
|
||||
import { GpCardGrid } from "@/layout/god-praksis-page/ArticleSections";
|
||||
import GpArticleCard from "@/layout/god-praksis-page/cards/GpArticleCard";
|
||||
import GpHeroCard from "@/layout/god-praksis-page/cards/GpHeroCard";
|
||||
import IntroHero from "@/layout/god-praksis-page/hero/intro-hero/IntroHero";
|
||||
import Header from "@/layout/header/Header";
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { SanityBlockContent } from "@/sanity-block";
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import { destructureBlocks } from "@/sanity/queries";
|
||||
import {
|
||||
AkselGodPraksisDocT,
|
||||
AkselGodPraksisLandingPageDocT,
|
||||
AkselTemaT,
|
||||
NextPageT,
|
||||
ResolveContributorsT,
|
||||
ResolveSlugT,
|
||||
ResolveTemaT,
|
||||
} from "@/types";
|
||||
import { AkselCubeStatic } from "@/web/aksel-cube/AkselCube";
|
||||
import { AkselGodPraksisLandingPageDocT, NextPageT } from "@/types";
|
||||
import { AnimatedChevron } from "@/web/AnimatedChevron";
|
||||
import { SEO } from "@/web/seo/SEO";
|
||||
|
||||
type PageProps = NextPageT<{
|
||||
type GpTemaList = {
|
||||
page: AkselGodPraksisLandingPageDocT;
|
||||
temaer: AkselTemaT[];
|
||||
resent: ResolveTemaT<
|
||||
ResolveContributorsT<ResolveSlugT<AkselGodPraksisDocT>>
|
||||
>[];
|
||||
}>;
|
||||
tema: {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
refCount: number;
|
||||
pictogram: SanityImageSource;
|
||||
articles: {
|
||||
heading: string;
|
||||
slug: string;
|
||||
undertema: { title: string; temaTitle: string }[];
|
||||
innholdstype: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export const query = `*[_type == "godpraksis_landingsside"][0]{
|
||||
"page": {
|
||||
type PageProps = NextPageT<GpTemaList>;
|
||||
|
||||
export const query = groq`
|
||||
{
|
||||
"page": *[_type == "godpraksis_landingsside"][0]{
|
||||
...,
|
||||
intro[]{
|
||||
...,
|
||||
${destructureBlocks}
|
||||
}
|
||||
},
|
||||
"temaer": *[_type == "aksel_tema" && defined(seksjoner[].sider[])]{
|
||||
...,
|
||||
"refCount": count(*[_type == "aksel_artikkel" && !(_id in path("drafts.**")) && references(^._id)])
|
||||
},
|
||||
"resent": *[_type == "aksel_artikkel" && defined(publishedAt)] | order(publishedAt desc)[0...9]{
|
||||
_id,
|
||||
heading,
|
||||
_createdAt,
|
||||
_updatedAt,
|
||||
publishedAt,
|
||||
"tema": *[_type == "gp.tema"] | order(lower(title)){
|
||||
title,
|
||||
description,
|
||||
pictogram,
|
||||
"slug": slug.current,
|
||||
"tema": tema[]->title,
|
||||
ingress,
|
||||
"refCount": count(*[_type=="aksel_artikkel"
|
||||
&& (^._id in undertema[]->tema._ref)]),
|
||||
"articles": *[_type=="aksel_artikkel"
|
||||
&& (^._id in undertema[]->tema._ref)] {
|
||||
heading,
|
||||
"slug": slug.current,
|
||||
"undertema": undertema[]->{title, "temaTitle": tema->title},
|
||||
"innholdstype": innholdstype->title,
|
||||
"views": *[_type == "article_views" && article_ref._ref == ^._id][0].views_month
|
||||
} | order(coalesce(views, -1) desc)[0...4]{
|
||||
heading,
|
||||
slug,
|
||||
undertema,
|
||||
innholdstype
|
||||
},
|
||||
}
|
||||
}`;
|
||||
}
|
||||
`;
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({
|
||||
preview = false,
|
||||
}): Promise<PageProps> => {
|
||||
const { temaer, page, resent } = await getClient().fetch(query);
|
||||
const { tema, page } = await getClient().fetch<GpTemaList>(query);
|
||||
|
||||
return {
|
||||
props: {
|
||||
page,
|
||||
temaer,
|
||||
resent,
|
||||
slug: "/god-praksis",
|
||||
tema,
|
||||
preview,
|
||||
title: "Forside God praksis",
|
||||
id: page?._id ?? "",
|
||||
id: "godpraksis_landingsside_id1",
|
||||
title: "God praksis forside",
|
||||
},
|
||||
notFound: !temaer,
|
||||
notFound: false,
|
||||
revalidate: 60,
|
||||
};
|
||||
};
|
||||
|
||||
const Page = ({ temaer, page, resent }: PageProps["props"]) => {
|
||||
const filteredTemas = temaer.filter((x) => x.refCount && x.refCount > 0);
|
||||
const GpPage = (props: PageProps["props"]) => {
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title="God praksis"
|
||||
description={page?.seo?.meta}
|
||||
image={page?.seo?.image}
|
||||
description={props.page?.seo?.meta}
|
||||
image={props.page?.seo?.image}
|
||||
/>
|
||||
|
||||
<div className="relative overflow-hidden bg-surface-subtle">
|
||||
<Header variant="transparent" />
|
||||
<main tabIndex={-1} id="hovedinnhold" className=" focus:outline-none">
|
||||
<div className="mx-auto mb-40 grid w-full max-w-screen-2xl px-4 pt-20 sm:px-6">
|
||||
<Heading
|
||||
level="1"
|
||||
size="xlarge"
|
||||
className="mb-4 text-5xl text-deepblue-800"
|
||||
>
|
||||
God praksis
|
||||
</Heading>
|
||||
{page.intro && <SanityBlockContent isIngress blocks={page.intro} />}
|
||||
<AkselCubeStatic className="text-deepblue-300 opacity-5 " />
|
||||
<div>
|
||||
<ul className="card-grid-3-1 mt-20 ">
|
||||
{filteredTemas.map((t) => (
|
||||
<GodPraksisCard key={t._id} node={t} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="mt-24">
|
||||
<Heading
|
||||
level="2"
|
||||
size="medium"
|
||||
className="flex items-center gap-2 text-deepblue-800"
|
||||
>
|
||||
<SparklesIcon aria-hidden className="shrink-0" /> Nyeste
|
||||
artikler
|
||||
</Heading>
|
||||
<div className="card-grid-3-1 my-6">
|
||||
{resent.map((art: any) => (
|
||||
<ArtikkelCard
|
||||
level="3"
|
||||
variant="tema"
|
||||
{...art}
|
||||
key={art._id}
|
||||
<Page
|
||||
footer={<Footer />}
|
||||
footerPosition="belowFold"
|
||||
className="bg-surface-subtle"
|
||||
>
|
||||
<Header variant="subtle" />
|
||||
<Box
|
||||
paddingBlock="10"
|
||||
as="main"
|
||||
tabIndex={-1}
|
||||
id="hovedinnhold"
|
||||
className="animate-popUpPage focus:outline-none"
|
||||
>
|
||||
<Page.Block width="xl" gutters>
|
||||
<VStack gap="10">
|
||||
<IntroHero title="God praksis">
|
||||
{props.page.intro && (
|
||||
<SanityBlockContent
|
||||
isIngress
|
||||
blocks={props.page.intro}
|
||||
className="mt-4"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)}
|
||||
<nav aria-label="Temavelger">
|
||||
<Stack
|
||||
className="mt-6"
|
||||
gap={{ xs: "4", md: "6" }}
|
||||
wrap
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
as="ul"
|
||||
>
|
||||
{props.tema
|
||||
.filter((x) => x.refCount > 0)
|
||||
.map((tema) => (
|
||||
<li key={tema.slug}>
|
||||
<GpHeroCard
|
||||
articleCount={tema.refCount}
|
||||
href={`god-praksis/${tema.slug}`}
|
||||
image={tema.pictogram}
|
||||
>
|
||||
{tema.title}
|
||||
</GpHeroCard>
|
||||
</li>
|
||||
))}
|
||||
</Stack>
|
||||
</nav>
|
||||
</IntroHero>
|
||||
<Box paddingInline={{ xs: "4", lg: "10" }}>
|
||||
<VStack gap="12">
|
||||
{props.tema
|
||||
.filter((x) => x.refCount > 0)
|
||||
.map((tema) => {
|
||||
return (
|
||||
<section
|
||||
key={tema.slug}
|
||||
aria-label={`Tema ${tema.title}`}
|
||||
>
|
||||
<Heading
|
||||
level="2"
|
||||
size="medium"
|
||||
className={cl("text-aksel-heading", {
|
||||
"mb-4": !tema.description,
|
||||
"mb-2": tema.description,
|
||||
})}
|
||||
>
|
||||
{tema.title}
|
||||
</Heading>
|
||||
{tema.description && (
|
||||
<BodyLong className="mb-6">
|
||||
{tema.description}
|
||||
</BodyLong>
|
||||
)}
|
||||
<GpCardGrid>
|
||||
{tema.articles.map((article) => (
|
||||
<li key={article.slug}>
|
||||
<GpArticleCard
|
||||
href={article.slug}
|
||||
innholdstype={article.innholdstype}
|
||||
undertema={
|
||||
article.undertema.find(
|
||||
(ut) => ut?.temaTitle === tema.title,
|
||||
)?.title
|
||||
}
|
||||
>
|
||||
{article.heading}
|
||||
</GpArticleCard>
|
||||
</li>
|
||||
))}
|
||||
</GpCardGrid>
|
||||
|
||||
<Link
|
||||
href={`/god-praksis/${tema.slug}`}
|
||||
as={NextLink}
|
||||
className="group mt-4 w-fit text-deepblue-700"
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation(
|
||||
"gp-se-alle-link",
|
||||
e.currentTarget.getAttribute("href"),
|
||||
)
|
||||
}
|
||||
>
|
||||
<h3 className="flex items-center">
|
||||
{`Alt fra ${tema.title} `}
|
||||
<AnimatedChevron scale="inline" />
|
||||
</h3>
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Page.Block>
|
||||
</Box>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WithPreview = lazy(() => import("@/preview"));
|
||||
|
||||
/**
|
||||
* To avoid infinite loop rendering caused by `count` in query, we avoid showing preview for this page.
|
||||
*/
|
||||
const Wrapper = (props: any) => {
|
||||
if (props?.preview) {
|
||||
return (
|
||||
<Suspense fallback={<Page {...props} />}>
|
||||
<WithPreview comp={Page} query={query} props={props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <Page {...props} />;
|
||||
return <GpPage {...props} />;
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,170 +0,0 @@
|
||||
import { groq } from "next-sanity";
|
||||
import { GetServerSideProps } from "next/types";
|
||||
import { Suspense, lazy, useEffect, useMemo } from "react";
|
||||
import { Box, Page, VStack } from "@navikt/ds-react";
|
||||
import Footer from "@/layout/footer/Footer";
|
||||
import { ArticleSections } from "@/layout/god-praksis-page/ArticleSections";
|
||||
import { GpChipNavigation } from "@/layout/god-praksis-page/chipnavigation/GpChipNavigation";
|
||||
import TemaHero from "@/layout/god-praksis-page/hero/tema-hero/TemaHero";
|
||||
import {
|
||||
GpSlugQueryResponse,
|
||||
ParsedGPArticle,
|
||||
} from "@/layout/god-praksis-page/interface";
|
||||
import Header from "@/layout/header/Header";
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import { NextPageT } from "@/types";
|
||||
import { dateStr } from "@/utils";
|
||||
import { SEO } from "@/web/seo/SEO";
|
||||
|
||||
const sanityQuery = groq`
|
||||
{
|
||||
"articles": *[_type == "aksel_artikkel" && defined(undertema) && $slug in undertema[]->tema->slug.current] | order(publishedAt desc) {
|
||||
_id,
|
||||
heading,
|
||||
publishedAt,
|
||||
"description": ingress,
|
||||
"undertema": undertema[]->{title, "temaTitle": tema->title},
|
||||
"innholdstype": innholdstype->title,
|
||||
"slug": slug.current,
|
||||
},
|
||||
"tema": *[_type == "gp.tema" && slug.current == $slug][0]{
|
||||
...,
|
||||
"slug": slug.current,
|
||||
"undertema": *[_type == "gp.tema.undertema" && tema->slug.current == $slug]{
|
||||
title,
|
||||
description
|
||||
},
|
||||
"image": pictogram,
|
||||
},
|
||||
"heroNav": *[_type == "gp.tema" && count(*[_type=="aksel_artikkel"
|
||||
&& (^._id in undertema[]->tema._ref)]) > 0] | order(lower(title)){
|
||||
title,
|
||||
"slug": slug.current,
|
||||
"image": pictogram,
|
||||
"imageInverted": pictogramInverted,
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type PageProps = NextPageT<GpSlugQueryResponse>;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (
|
||||
ctx,
|
||||
): Promise<PageProps> => {
|
||||
const slug = ctx.params?.slug as string;
|
||||
const preview = !!ctx.preview;
|
||||
|
||||
const { heroNav, tema, articles } =
|
||||
await getClient().fetch<GpSlugQueryResponse>(sanityQuery, {
|
||||
slug,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
tema,
|
||||
heroNav,
|
||||
// Avoids having to format date clientside
|
||||
articles: await Promise.all(
|
||||
articles.map(async (a) => ({
|
||||
...a,
|
||||
publishedAt: await dateStr(a.publishedAt),
|
||||
})),
|
||||
),
|
||||
slug,
|
||||
preview,
|
||||
id: tema._id ?? "",
|
||||
title: tema.title ?? "",
|
||||
},
|
||||
notFound: !tema || articles.length === 0,
|
||||
};
|
||||
};
|
||||
|
||||
const GpPage = (props: PageProps["props"]) => {
|
||||
useEffect(() => {
|
||||
window.location.host === "aksel.nav.no" &&
|
||||
window.location.replace(`http://aksel.nav.no/404`);
|
||||
}, []);
|
||||
|
||||
const articles = useMemo(
|
||||
() =>
|
||||
props.articles
|
||||
.map((article) => {
|
||||
const undertema = article.undertema.find(
|
||||
(ut) => ut?.temaTitle === props.tema.title,
|
||||
)?.title;
|
||||
|
||||
return {
|
||||
...article,
|
||||
undertema: undertema ?? "",
|
||||
} satisfies ParsedGPArticle;
|
||||
})
|
||||
.filter((article) => !!article.undertema)
|
||||
/* Makes sure sections always appear in the same order */
|
||||
.sort((a, b) => a.undertema.localeCompare(b.undertema)),
|
||||
[props.articles, props.tema.title],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title={props.tema?.title ?? ""}
|
||||
description={props.tema?.seo?.meta ?? props.tema.description}
|
||||
image={props.tema?.seo?.image}
|
||||
/>
|
||||
<Page
|
||||
footer={<Footer />}
|
||||
footerPosition="belowFold"
|
||||
className="bg-surface-subtle"
|
||||
>
|
||||
<Header variant="subtle" />
|
||||
<Box
|
||||
paddingBlock="10"
|
||||
as="main"
|
||||
tabIndex={-1}
|
||||
id="hovedinnhold"
|
||||
className="animate-popUpPage focus:outline-none"
|
||||
key={props.tema?.title}
|
||||
>
|
||||
<Page.Block width="xl" gutters>
|
||||
<VStack gap="10">
|
||||
<TemaHero tema={props.tema} heroNav={props.heroNav} />
|
||||
<Box paddingInline={{ xs: "4", lg: "10" }}>
|
||||
<GpChipNavigation articles={articles} />
|
||||
|
||||
<VStack gap="12" className="mt-10">
|
||||
<ArticleSections
|
||||
articles={articles}
|
||||
undertemaList={props.tema.undertema}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Page.Block>
|
||||
</Box>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WithPreview = lazy(() => import("@/preview"));
|
||||
|
||||
const Wrapper = (props: any) => {
|
||||
if (props?.preview) {
|
||||
return (
|
||||
<Suspense fallback={<GpPage {...props} />}>
|
||||
<WithPreview
|
||||
comp={GpPage}
|
||||
query={sanityQuery}
|
||||
props={props}
|
||||
params={{
|
||||
slug: props?.slug,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <GpPage {...props} />;
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,316 +0,0 @@
|
||||
import { differenceInMonths } from "date-fns";
|
||||
import { GetServerSideProps } from "next/types";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { BodyLong, BodyShort, Detail, Heading, Label } from "@navikt/ds-react";
|
||||
import { UserStateT } from "@/auth/auth.types";
|
||||
import { getAuthUserState } from "@/auth/getUserState";
|
||||
import ArtikkelCard from "@/cms/cards/ArtikkelCard";
|
||||
import Footer from "@/layout/footer/Footer";
|
||||
import { GpTemaLink } from "@/layout/god-praksis-page/chipnavigation/GpTemaLink";
|
||||
import Header from "@/layout/header/Header";
|
||||
import { SanityBlockContent } from "@/sanity-block";
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import {
|
||||
contributorsAll,
|
||||
contributorsSingle,
|
||||
destructureBlocks,
|
||||
} from "@/sanity/queries";
|
||||
import {
|
||||
AkselGodPraksisDocT,
|
||||
NextPageT,
|
||||
ResolveContributorsT,
|
||||
ResolveRelatedArticlesT,
|
||||
ResolveSlugT,
|
||||
TableOfContentsT,
|
||||
} from "@/types";
|
||||
import { abbrName, dateStr, generateTableOfContents } from "@/utils";
|
||||
import { Feedback } from "@/web/Feedback/Feedback";
|
||||
import OutdatedAlert from "@/web/OutdatedAlert";
|
||||
import { SEO } from "@/web/seo/SEO";
|
||||
import TableOfContents from "@/web/toc/TableOfContents";
|
||||
import NotFound from "../../404";
|
||||
|
||||
type PageProps = NextPageT<{
|
||||
page: ResolveContributorsT<
|
||||
ResolveSlugT<ResolveRelatedArticlesT<AkselGodPraksisDocT>>
|
||||
> & {
|
||||
innholdstype: string;
|
||||
undertema: { title: string; tema: { slug: string; title: string } }[];
|
||||
};
|
||||
publishDate: string;
|
||||
verifiedDate: string;
|
||||
outdated: boolean;
|
||||
toc: TableOfContentsT;
|
||||
userState: UserStateT;
|
||||
}>;
|
||||
|
||||
const query = `{
|
||||
"page": *[slug.current == $slug] | order(_updatedAt desc)[0]
|
||||
{
|
||||
...,
|
||||
"slug": slug.current,
|
||||
content[]{
|
||||
...,
|
||||
${destructureBlocks}
|
||||
},
|
||||
"innholdstype": innholdstype->title,
|
||||
"undertema": undertema[]->{
|
||||
title,
|
||||
"tema": tema->{
|
||||
title,
|
||||
"slug": slug.current,
|
||||
}
|
||||
},
|
||||
${contributorsAll},
|
||||
relevante_artikler[]->{
|
||||
_id,
|
||||
heading,
|
||||
_createdAt,
|
||||
_updatedAt,
|
||||
publishedAt,
|
||||
updateInfo,
|
||||
"slug": slug.current,
|
||||
"tema": tema[]->tag,
|
||||
ingress,
|
||||
"contributor": ${contributorsSingle},
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (
|
||||
context,
|
||||
): Promise<PageProps> => {
|
||||
const userState = await getAuthUserState(context.req.headers);
|
||||
|
||||
const isPreview = context.preview ?? false;
|
||||
|
||||
const slug = context.params?.slug as string;
|
||||
|
||||
const { page } = await getClient().fetch(query, {
|
||||
slug: `god-praksis/artikler/${slug}`,
|
||||
});
|
||||
|
||||
const verifiedDate =
|
||||
page?.updateInfo?.lastVerified ?? page?.publishedAt ?? page?._updatedAt;
|
||||
|
||||
return {
|
||||
props: {
|
||||
page,
|
||||
slug,
|
||||
preview: isPreview,
|
||||
id: page?._id ?? "",
|
||||
title: page?.heading ?? "",
|
||||
verifiedDate: await dateStr(verifiedDate),
|
||||
outdated: differenceInMonths(new Date(), new Date(verifiedDate)) >= 12,
|
||||
publishDate: await dateStr(page?.publishedAt ?? page?._updatedAt),
|
||||
toc: generateTableOfContents({
|
||||
content: page?.content,
|
||||
type: "aksel_artikkel",
|
||||
}),
|
||||
userState,
|
||||
},
|
||||
notFound: !page && !isPreview,
|
||||
};
|
||||
};
|
||||
|
||||
const Page = ({
|
||||
page: data,
|
||||
publishDate,
|
||||
verifiedDate,
|
||||
outdated,
|
||||
toc,
|
||||
userState,
|
||||
}: PageProps["props"]) => {
|
||||
if (!data) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
if (!data.content || !data.heading) {
|
||||
console.warn(
|
||||
`Artikkelen har ikke ${
|
||||
!data.content ? "innhold" : "overskrift"
|
||||
}, så den kan ikke vises.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const authors = (data?.contributors as any)?.map((x) => x?.title) ?? [];
|
||||
|
||||
const aside = data?.relevante_artikler &&
|
||||
data.relevante_artikler.length > 0 && (
|
||||
<aside
|
||||
className="overflow-x-clip py-8"
|
||||
aria-labelledby="relevante-artikler-aside"
|
||||
>
|
||||
<div className="relativept-12 pb-16">
|
||||
<div className="dynamic-wrapper">
|
||||
<Heading
|
||||
level="2"
|
||||
size="medium"
|
||||
className="px-4 text-deepblue-700"
|
||||
id="relevante-artikler-aside"
|
||||
>
|
||||
{data?.relevante_artikler?.length === 1
|
||||
? `Les også`
|
||||
: `Relevante artikler`}
|
||||
</Heading>
|
||||
<div className="card-grid-3-1 mt-6 px-4">
|
||||
{data.relevante_artikler.map((x: any) =>
|
||||
x && x?._id ? (
|
||||
<ArtikkelCard level="3" {...x} key={x._id} />
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title={data?.heading}
|
||||
description={data?.seo?.meta ?? data?.ingress}
|
||||
image={data?.seo?.image ?? (data?.tema?.[0] as any)?.seo?.image}
|
||||
publishDate={publishDate}
|
||||
canonical={`/${data.slug}`}
|
||||
/>
|
||||
|
||||
<Header variant="subtle" />
|
||||
<main
|
||||
tabIndex={-1}
|
||||
id="hovedinnhold"
|
||||
className="aksel-artikkel group/aksel bg-surface-subtle pt-4 focus:outline-none"
|
||||
>
|
||||
<div className="mx-auto max-w-aksel px-4 sm:w-[90%]">
|
||||
<article className="pb-16 pt-12 md:pb-32">
|
||||
<div className="mx-auto mb-16 max-w-prose lg:ml-0">
|
||||
{data.innholdstype && (
|
||||
<BodyShort
|
||||
weight="semibold"
|
||||
className="uppercase text-violet-600"
|
||||
>
|
||||
{data.innholdstype}
|
||||
</BodyShort>
|
||||
)}
|
||||
<Heading
|
||||
level="1"
|
||||
size="large"
|
||||
className="text-wrap-balance mt-1 text-deepblue-800 md:text-5xl"
|
||||
>
|
||||
{data.heading}
|
||||
</Heading>
|
||||
{data?.ingress && (
|
||||
<BodyLong
|
||||
size="large"
|
||||
className="override-text-700 mt-4 text-2xl"
|
||||
>
|
||||
{data?.ingress}
|
||||
</BodyLong>
|
||||
)}
|
||||
|
||||
<div className="mt-6 inline-flex flex-wrap items-center gap-2 text-base">
|
||||
<Detail uppercase as="span">
|
||||
{verifiedDate}
|
||||
</Detail>
|
||||
{authors?.length > 0 && (
|
||||
<>
|
||||
<span className="h-2 w-2 rotate-45 rounded-[1px] bg-deepblue-700 opacity-25" />
|
||||
<BodyShort
|
||||
size="small"
|
||||
as="div"
|
||||
className="flex flex-wrap gap-1"
|
||||
>
|
||||
<address className="not-italic">{authors?.[0]}</address>
|
||||
</BodyShort>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{data.undertema && data.undertema.length > 0 && (
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{data.undertema.map(({ title, tema }) => (
|
||||
<GpTemaLink
|
||||
key={title}
|
||||
href={`/gp/${tema.slug}?undertema=${encodeURIComponent(
|
||||
title,
|
||||
)}`}
|
||||
>
|
||||
{tema.title}
|
||||
</GpTemaLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative mx-auto mt-4 max-w-prose lg:ml-0 lg:grid lg:max-w-none lg:grid-flow-row-dense lg:grid-cols-3 lg:items-start lg:gap-x-12">
|
||||
<TableOfContents toc={toc} variant="subtle" />
|
||||
<div className="max-w-prose lg:col-span-2 lg:col-start-1">
|
||||
{outdated && <OutdatedAlert />}
|
||||
<SanityBlockContent blocks={data?.content ?? []} />
|
||||
<div className="mt-12">
|
||||
{authors?.length > 0 && (
|
||||
<Label className="mb-2 text-deepblue-700" as="p">
|
||||
Bidragsytere
|
||||
</Label>
|
||||
)}
|
||||
{authors?.length > 0 && (
|
||||
<BodyShort
|
||||
as="div"
|
||||
className="mb-1 flex flex-wrap gap-1 text-text-subtle"
|
||||
>
|
||||
{authors.map(abbrName).map((x, y) => (
|
||||
<address className="not-italic" key={x}>
|
||||
{x}
|
||||
{y !== authors.length - 1 && ", "}
|
||||
</address>
|
||||
))}
|
||||
</BodyShort>
|
||||
)}
|
||||
<BodyShort as="span" className="text-text-subtle">
|
||||
Publisert: {publishDate}
|
||||
</BodyShort>
|
||||
{userState && <Feedback userState={userState} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{aside}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WithPreview = lazy(() => import("@/preview"));
|
||||
|
||||
const Wrapper = (props: any) => {
|
||||
if (props?.preview) {
|
||||
return (
|
||||
<Suspense fallback={<Page {...props} />}>
|
||||
<WithPreview
|
||||
comp={Page}
|
||||
query={query}
|
||||
props={props}
|
||||
params={{
|
||||
slug: `god-praksis/artikler/${props?.slug}`,
|
||||
}}
|
||||
resolvers={[
|
||||
{
|
||||
key: "toc",
|
||||
dataKeys: ["page.content"],
|
||||
cb: (v) =>
|
||||
generateTableOfContents({
|
||||
content: v[0],
|
||||
type: "aksel_artikkel",
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <Page {...props} />;
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,249 +0,0 @@
|
||||
import cl from "clsx";
|
||||
import { groq } from "next-sanity";
|
||||
import NextLink from "next/link";
|
||||
import { GetStaticProps } from "next/types";
|
||||
import { Suspense, lazy, useEffect } from "react";
|
||||
import {
|
||||
BodyLong,
|
||||
Box,
|
||||
Heading,
|
||||
Link,
|
||||
Page,
|
||||
Stack,
|
||||
VStack,
|
||||
} from "@navikt/ds-react";
|
||||
import Footer from "@/layout/footer/Footer";
|
||||
import { GpCardGrid } from "@/layout/god-praksis-page/ArticleSections";
|
||||
import GpArticleCard from "@/layout/god-praksis-page/cards/GpArticleCard";
|
||||
import GpHeroCard from "@/layout/god-praksis-page/cards/GpHeroCard";
|
||||
import IntroHero from "@/layout/god-praksis-page/hero/intro-hero/IntroHero";
|
||||
import Header from "@/layout/header/Header";
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { SanityBlockContent } from "@/sanity-block";
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import { destructureBlocks } from "@/sanity/queries";
|
||||
import { AkselGodPraksisLandingPageDocT, NextPageT } from "@/types";
|
||||
import { AnimatedChevron } from "@/web/AnimatedChevron";
|
||||
import { SEO } from "@/web/seo/SEO";
|
||||
|
||||
type GpTemaList = {
|
||||
page: AkselGodPraksisLandingPageDocT;
|
||||
tema: {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
refCount: number;
|
||||
pictogram: any;
|
||||
articles: {
|
||||
heading: string;
|
||||
slug: string;
|
||||
undertema: { title: string; temaTitle: string }[];
|
||||
innholdstype: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
type PageProps = NextPageT<GpTemaList>;
|
||||
|
||||
export const query = groq`
|
||||
{
|
||||
"page": *[_type == "godpraksis_landingsside"][0]{
|
||||
...,
|
||||
intro[]{
|
||||
...,
|
||||
${destructureBlocks}
|
||||
}
|
||||
},
|
||||
"tema": *[_type == "gp.tema"] | order(lower(title)){
|
||||
title,
|
||||
description,
|
||||
pictogram,
|
||||
"slug": slug.current,
|
||||
"refCount": count(*[_type=="aksel_artikkel"
|
||||
&& (^._id in undertema[]->tema._ref)]),
|
||||
"articles": *[_type=="aksel_artikkel"
|
||||
&& (^._id in undertema[]->tema._ref)] {
|
||||
heading,
|
||||
"slug": slug.current,
|
||||
"undertema": undertema[]->{title, "temaTitle": tema->title},
|
||||
"innholdstype": innholdstype->title,
|
||||
"views": *[_type == "article_views" && article_ref._ref == ^._id][0].views_month
|
||||
} | order(coalesce(views, -1) desc)[0...4]{
|
||||
heading,
|
||||
slug,
|
||||
undertema,
|
||||
innholdstype
|
||||
},
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({
|
||||
preview = false,
|
||||
}): Promise<PageProps> => {
|
||||
const { tema, page } = await getClient().fetch<GpTemaList>(query);
|
||||
|
||||
return {
|
||||
props: {
|
||||
page,
|
||||
tema,
|
||||
preview,
|
||||
id: "godpraksis_landingsside_id1",
|
||||
title: "God praksis forside",
|
||||
},
|
||||
notFound: false,
|
||||
revalidate: 60,
|
||||
};
|
||||
};
|
||||
|
||||
const GpPage = (props: PageProps["props"]) => {
|
||||
useEffect(() => {
|
||||
window.location.host === "aksel.nav.no" &&
|
||||
window.location.replace(`http://aksel.nav.no/404`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title="God praksis"
|
||||
description={props.page?.seo?.meta}
|
||||
image={props.page?.seo?.image}
|
||||
/>
|
||||
|
||||
<Page
|
||||
footer={<Footer />}
|
||||
footerPosition="belowFold"
|
||||
className="bg-surface-subtle"
|
||||
>
|
||||
<Header variant="subtle" />
|
||||
<Box
|
||||
paddingBlock="10"
|
||||
as="main"
|
||||
tabIndex={-1}
|
||||
id="hovedinnhold"
|
||||
className="animate-popUpPage focus:outline-none"
|
||||
>
|
||||
<Page.Block width="xl" gutters>
|
||||
<VStack gap="10">
|
||||
<IntroHero title="God praksis">
|
||||
{props.page.intro && (
|
||||
<SanityBlockContent
|
||||
isIngress
|
||||
blocks={props.page.intro}
|
||||
className="mt-4"
|
||||
/>
|
||||
)}
|
||||
<nav aria-label="Temavelger">
|
||||
<Stack
|
||||
className="mt-6"
|
||||
gap={{ xs: "4", md: "6" }}
|
||||
wrap
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
as="ul"
|
||||
>
|
||||
{props.tema
|
||||
.filter((x) => x.refCount > 0)
|
||||
.map((tema) => (
|
||||
<li key={tema.slug}>
|
||||
<GpHeroCard
|
||||
articleCount={tema.refCount}
|
||||
href={`gp/${tema.slug}`}
|
||||
image={tema.pictogram}
|
||||
>
|
||||
{tema.title}
|
||||
</GpHeroCard>
|
||||
</li>
|
||||
))}
|
||||
</Stack>
|
||||
</nav>
|
||||
</IntroHero>
|
||||
<Box paddingInline={{ xs: "4", lg: "10" }}>
|
||||
<VStack gap="12">
|
||||
{props.tema
|
||||
.filter((x) => x.refCount > 0)
|
||||
.map((tema) => {
|
||||
return (
|
||||
<section
|
||||
key={tema.slug}
|
||||
aria-label={`Tema ${tema.title}`}
|
||||
>
|
||||
<Heading
|
||||
level="2"
|
||||
size="medium"
|
||||
className={cl("text-aksel-heading", {
|
||||
"mb-4": !tema.description,
|
||||
"mb-2": tema.description,
|
||||
})}
|
||||
>
|
||||
{tema.title}
|
||||
</Heading>
|
||||
{tema.description && (
|
||||
<BodyLong className="mb-6">
|
||||
{tema.description}
|
||||
</BodyLong>
|
||||
)}
|
||||
<GpCardGrid>
|
||||
{tema.articles.map((article) => (
|
||||
<li key={article.slug}>
|
||||
<GpArticleCard
|
||||
href={`${article.slug}`.replace(
|
||||
"god-praksis",
|
||||
"gp",
|
||||
)}
|
||||
innholdstype={article.innholdstype}
|
||||
undertema={
|
||||
article.undertema.find(
|
||||
(ut) => ut?.temaTitle === tema.title,
|
||||
)?.title
|
||||
}
|
||||
>
|
||||
{article.heading}
|
||||
</GpArticleCard>
|
||||
</li>
|
||||
))}
|
||||
</GpCardGrid>
|
||||
|
||||
<Link
|
||||
href={`/gp/${tema.slug}`}
|
||||
as={NextLink}
|
||||
className="group mt-4 w-fit text-deepblue-700"
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation(
|
||||
"gp-se-alle-link",
|
||||
e.currentTarget.getAttribute("href"),
|
||||
)
|
||||
}
|
||||
>
|
||||
<h3 className="flex items-center">
|
||||
{`Alt fra ${tema.title} `}
|
||||
<AnimatedChevron scale="inline" />
|
||||
</h3>
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Page.Block>
|
||||
</Box>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WithPreview = lazy(() => import("@/preview"));
|
||||
|
||||
const Wrapper = (props: any) => {
|
||||
if (props?.preview) {
|
||||
return (
|
||||
<Suspense fallback={<GpPage {...props} />}>
|
||||
<WithPreview comp={GpPage} query={query} props={props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <GpPage {...props} />;
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,4 +1,6 @@
|
||||
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
|
||||
import cl from "clsx";
|
||||
import { groq } from "next-sanity";
|
||||
import { GetStaticProps } from "next/types";
|
||||
import { Suspense, lazy, useState } from "react";
|
||||
import {
|
||||
@@ -18,22 +20,26 @@ import {
|
||||
VStack,
|
||||
useClientLayoutEffect,
|
||||
} from "@navikt/ds-react";
|
||||
import GodPraksisCardSimple from "@/cms/cards/GodPraksisCardSimple";
|
||||
import FrontpageBlock, {
|
||||
BlocksT,
|
||||
} from "@/cms/frontpage-blocks/FrontpageBlocks";
|
||||
import Footer from "@/layout/footer/Footer";
|
||||
import GpFrontpageCard from "@/layout/god-praksis-page/cards/GpFrontpageCard";
|
||||
import Header from "@/layout/header/Header";
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import { contributorsAll } from "@/sanity/queries";
|
||||
import { AkselTemaT, NextPageT } from "@/types";
|
||||
import { NextPageT } from "@/types";
|
||||
import { userPrefersReducedMotion } from "@/utils";
|
||||
import { IntroCards } from "@/web/IntroCards";
|
||||
import { AkselCubeAnimated } from "@/web/aksel-cube/AkselCube";
|
||||
import { SEO } from "@/web/seo/SEO";
|
||||
|
||||
type PageProps = NextPageT<{
|
||||
tema: AkselTemaT[];
|
||||
tema: {
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
pictogram: SanityImageSource;
|
||||
}[];
|
||||
page: {
|
||||
title: string;
|
||||
god_praksis_intro: string;
|
||||
@@ -42,11 +48,18 @@ type PageProps = NextPageT<{
|
||||
blocks?: BlocksT[];
|
||||
}>;
|
||||
|
||||
const query = `*[_type == "aksel_forside"][0]{
|
||||
/**
|
||||
* Using `count` with references in groq query causes infinite loop when previewing
|
||||
*/
|
||||
const query = groq`*[_type == "aksel_forside"][0]{
|
||||
"page": {
|
||||
...,
|
||||
},
|
||||
"tema": *[_type == "aksel_tema" && defined(seksjoner[].sider[])],
|
||||
"tema": select(
|
||||
$preview == "true" => *[_type == "gp.tema"] | order(lower(title)),
|
||||
$preview != "true" => *[_type == "gp.tema" && count(*[_type=="aksel_artikkel"
|
||||
&& (^._id in undertema[]->tema._ref)]) > 0] | order(lower(title))
|
||||
),
|
||||
blocks[]{
|
||||
...,
|
||||
_type == "nytt_fra_aksel"=>{
|
||||
@@ -54,7 +67,7 @@ const query = `*[_type == "aksel_forside"][0]{
|
||||
...,
|
||||
"content": null,
|
||||
${contributorsAll},
|
||||
"tema": tema[]->title,
|
||||
"tema": undertema[]->tema->title,
|
||||
},
|
||||
"curatedResent": {
|
||||
"bloggposts": *[_type == "aksel_blogg" && !(_id in ^.highlights[]._ref)] | order(_createdAt desc)[0...2]{
|
||||
@@ -78,7 +91,7 @@ const query = `*[_type == "aksel_forside"][0]{
|
||||
_updatedAt,
|
||||
publishedAt,
|
||||
"slug": slug.current,
|
||||
"tema": tema[]->title,
|
||||
"tema": undertema[]->tema->title,
|
||||
ingress,
|
||||
seo,
|
||||
${contributorsAll}
|
||||
@@ -91,7 +104,7 @@ const query = `*[_type == "aksel_forside"][0]{
|
||||
_updatedAt,
|
||||
publishedAt,
|
||||
"slug": slug.current,
|
||||
"tema": tema[]->title,
|
||||
"tema": undertema[]->tema->title,
|
||||
ingress,
|
||||
seo,
|
||||
${contributorsAll}
|
||||
@@ -130,21 +143,9 @@ export const getStaticProps: GetStaticProps = async ({
|
||||
preview: "false",
|
||||
});
|
||||
|
||||
const validateTema = tema
|
||||
.filter(
|
||||
(t) =>
|
||||
t?.title &&
|
||||
t?.slug &&
|
||||
t?.pictogram &&
|
||||
t?.seksjoner.some(
|
||||
(seksjon) => seksjon?.sider.some((side: any) => side?._ref),
|
||||
),
|
||||
)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
props: {
|
||||
tema: validateTema,
|
||||
tema,
|
||||
page,
|
||||
blocks,
|
||||
slug: "/",
|
||||
@@ -290,7 +291,13 @@ const Forside = ({ page, tema, blocks }: PageProps["props"]) => {
|
||||
|
||||
<ul className="grid gap-x-8 md:grid-cols-2 xl:grid-cols-3">
|
||||
{tema.map((t) => (
|
||||
<GodPraksisCardSimple key={t._id} node={t} />
|
||||
<GpFrontpageCard
|
||||
key={t.title}
|
||||
href={`/god-praksis/${t.slug.current}`}
|
||||
image={t.pictogram}
|
||||
>
|
||||
{t.title}
|
||||
</GpFrontpageCard>
|
||||
))}
|
||||
</ul>
|
||||
</VStack>
|
||||
|
||||
@@ -77,6 +77,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 1,
|
||||
title: "Enkel",
|
||||
|
||||
@@ -111,6 +111,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 0,
|
||||
title: "Komplett",
|
||||
|
||||
@@ -91,6 +91,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 2,
|
||||
title: "Med CTA til side",
|
||||
|
||||
@@ -92,6 +92,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 3,
|
||||
title: "Med tilbakemeldingsfunksjon",
|
||||
|
||||
@@ -100,6 +100,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 4,
|
||||
title: "Flerspråk",
|
||||
|
||||
@@ -32,6 +32,11 @@ const Example = () => {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 5,
|
||||
title: "Uten dekoratøren",
|
||||
|
||||
@@ -108,6 +108,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 1,
|
||||
title: "Enkel",
|
||||
|
||||
@@ -139,6 +139,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 0,
|
||||
title: "Komplett",
|
||||
|
||||
@@ -115,6 +115,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 2,
|
||||
title: "Med CTA til side",
|
||||
|
||||
@@ -117,6 +117,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 4,
|
||||
title: "Med feil-id",
|
||||
|
||||
@@ -130,6 +130,11 @@ function useDekorator() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 5,
|
||||
title: "Flerspråk",
|
||||
|
||||
@@ -93,6 +93,11 @@ function Example() {
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default Example;
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args = {
|
||||
index: 6,
|
||||
title: "Uten Dekoratør",
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function sitemapPages(
|
||||
let client = token ? noCdnClient(token) : getClient();
|
||||
client = client.config({ perspective: "published" });
|
||||
const artikler = await getDocuments("all", token);
|
||||
const temaer = await getAkselTema(token);
|
||||
const temaer = await getGpTema(token);
|
||||
|
||||
const pages = await client.fetch(
|
||||
`{
|
||||
@@ -67,24 +67,6 @@ export async function getGpTema(
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - Deprecate when getGpTema gets implemented in production
|
||||
*/
|
||||
export async function getAkselTema(
|
||||
token?: string,
|
||||
): Promise<{ path: string; lastmod: string }[]> {
|
||||
const client = token ? noCdnClient(token) : getClient();
|
||||
const tags: { slug: { current: string }; _updatedAt: string }[] =
|
||||
await client.fetch(
|
||||
`*[_type == "aksel_tema" && count(*[references(^._id)]) > 0]{slug,_updatedAt}`,
|
||||
);
|
||||
return tags.map((x) => ({
|
||||
path: x?.slug.current,
|
||||
lastmod: x?._updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getDocuments(
|
||||
source: (typeof allArticleDocuments)[number] | "all",
|
||||
token?: string,
|
||||
|
||||
@@ -168,8 +168,6 @@ ${defaultBlock},
|
||||
|
||||
export const contributorsAll = `contributors[]->{title}`;
|
||||
|
||||
export const contributorsSingle = `contributors[0]->{title}`;
|
||||
|
||||
export const sidebarQuery = `"sidebar": *[_type == $type && defined(kategori)] {
|
||||
heading,
|
||||
"slug": slug.current,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// MediaTipInput.tsx
|
||||
import { SanityDocument, set, useFormValue } from "sanity";
|
||||
import { useListeningQuery } from "sanity-plugin-utils";
|
||||
import { Chips, Label, VStack } from "@navikt/ds-react";
|
||||
|
||||
export function TaxonomyInput(props) {
|
||||
const innholdstype = useFormValue(["gp_taxonomy", "innholdstype"]) as {
|
||||
_ref: string;
|
||||
} | null;
|
||||
|
||||
const { data: innholdstyper } = useListeningQuery<SanityDocument[]>(
|
||||
`*[_type == $type]`,
|
||||
{
|
||||
params: { type: "gp.innholdstype" },
|
||||
initialValue: [],
|
||||
},
|
||||
);
|
||||
|
||||
if (!innholdstyper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack gap="4">
|
||||
{innholdstyper?.length > 0 && (
|
||||
<VStack gap="2">
|
||||
<Label as="p" size="small">
|
||||
Innholdstype
|
||||
</Label>
|
||||
<Chips>
|
||||
{innholdstyper.map((type) => (
|
||||
<Chips.Toggle
|
||||
selected={innholdstype?._ref === type._id}
|
||||
onClick={() => {
|
||||
props.onChange(
|
||||
set({ _ref: type._id, _type: "reference" }, [
|
||||
"innholdstype",
|
||||
]),
|
||||
);
|
||||
}}
|
||||
checkmark={false}
|
||||
variant="neutral"
|
||||
key={type._id}
|
||||
>
|
||||
{(type?.title as string) ?? ""}
|
||||
</Chips.Toggle>
|
||||
))}
|
||||
</Chips>
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { definePlugin } from "sanity";
|
||||
import { structureTool } from "sanity/structure";
|
||||
import { publicationFlow } from "../publication-flow";
|
||||
import innholdsType from "./documents/innholdstype";
|
||||
import tema from "./documents/tema";
|
||||
import undertema from "./documents/undertema";
|
||||
import { defaultDocumentNode, structure } from "./structure";
|
||||
|
||||
export const GP_DOCUMENT_NAMES = [tema.name, undertema.name, innholdsType.name];
|
||||
|
||||
export const godPraksisTaxonomy = definePlugin(() => {
|
||||
return {
|
||||
name: "gp.taxonomy",
|
||||
schema: {
|
||||
templates: [
|
||||
{
|
||||
id: "gp.tema.undertema.by.tema",
|
||||
title: "Undertema",
|
||||
schemaType: "gp.tema.undertema",
|
||||
parameters: [{ name: "id", type: "string" }],
|
||||
value: async (params) => {
|
||||
return {
|
||||
tema: { _type: "reference", _ref: params.id },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gp.artikkel.by.undertema",
|
||||
title: "God praksis aritkkel med undertema",
|
||||
schemaType: "gp.artikkel",
|
||||
parameters: [{ name: "undertema_id", type: "string" }],
|
||||
value: (params) => ({
|
||||
undertema: [{ _type: "reference", _ref: params.undertema_id }],
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "gp.artikkel.by.innholdstype",
|
||||
title: "God praksis aritkkel med innholdstype",
|
||||
schemaType: "gp.artikkel",
|
||||
parameters: [{ name: "id", type: "string" }],
|
||||
value: (params) => ({
|
||||
innholdstype: { _type: "reference", _ref: params.id },
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
structureTool({
|
||||
name: "aksel_taxonomy",
|
||||
title: "Aksel Taxonomy",
|
||||
structure,
|
||||
defaultDocumentNode,
|
||||
}),
|
||||
/* Needs to be redefined here to avoid duplicate actions for documents */
|
||||
publicationFlow(),
|
||||
],
|
||||
};
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import { CurrentUser } from "sanity";
|
||||
import {
|
||||
DefaultDocumentNodeResolver,
|
||||
StructureResolver,
|
||||
} from "sanity/structure";
|
||||
import { SANITY_API_VERSION } from "@/sanity/config";
|
||||
|
||||
const adminOrDev = (user: CurrentUser | null) =>
|
||||
user?.roles.find((role) =>
|
||||
["developer", "administrator"].includes(role.name),
|
||||
) ?? false;
|
||||
|
||||
export const structure: StructureResolver = (S, { currentUser }) => {
|
||||
if (!adminOrDev(currentUser)) {
|
||||
return S.list().title("Ingen tilgang");
|
||||
}
|
||||
|
||||
return S.list()
|
||||
.title("Aksel Manager")
|
||||
.items([
|
||||
S.documentTypeListItem("gp.tema").title("Tema"),
|
||||
S.divider(),
|
||||
S.listItem({
|
||||
id: "tema_view",
|
||||
title: "Undertema",
|
||||
schemaType: "gp.tema.undertema",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.tema")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("gp.tema.undertema")
|
||||
.title("Undertema")
|
||||
.filter("_type == $type && tema._ref == $id")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "gp.tema.undertema", id })
|
||||
.initialValueTemplates([
|
||||
S.initialValueTemplateItem("gp.tema.undertema.by.tema", {
|
||||
id,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.listItem({
|
||||
id: "article_undertema_view",
|
||||
title: "Artikler",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.tema")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("gp.tema.undertema")
|
||||
.title("Undertema")
|
||||
.filter("_type == $type && tema._ref == $id")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "gp.tema.undertema", id })
|
||||
.child((undertema_id) => {
|
||||
return S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && $id in undertema[]._ref")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel", id: undertema_id });
|
||||
});
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.listItem({
|
||||
id: "article_no_undertema_view",
|
||||
title: "Uten undertema",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && !defined(undertema)")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel" })
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.divider(),
|
||||
S.documentTypeListItem("gp.innholdstype").title("Innholdstype"),
|
||||
S.listItem({
|
||||
id: "article_innholdstype_view",
|
||||
title: "Artikler",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.innholdstype")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && $id == innholdstype._ref")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel", id });
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.listItem({
|
||||
id: "article_no_innholdstype_view",
|
||||
title: "Uten innholdstype",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && !defined(innholdstype)")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel" })
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
|
||||
/* S.listItem({
|
||||
id: "artikkel_view",
|
||||
title: "Artikler (tema)",
|
||||
schemaType: "gp.artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.tema")
|
||||
.child((tema_id) =>
|
||||
S.documentTypeList("gp.tema.undertema")
|
||||
.title("Undertema")
|
||||
.filter("_type == $type && tema._ref == $tema_id")
|
||||
.params({ type: "gp.tema.undertema", tema_id })
|
||||
.initialValueTemplates([])
|
||||
.child((undertema_id) =>
|
||||
S.documentTypeList("gp.artikkel")
|
||||
.title("Artikler")
|
||||
.filter(
|
||||
"_type == $type && $undertema_id in undertema[]._ref"
|
||||
)
|
||||
.params({ type: "gp.artikkel", undertema_id })
|
||||
.initialValueTemplates([
|
||||
S.initialValueTemplateItem("gp.artikkel.by.undertema", {
|
||||
undertema_id,
|
||||
}),
|
||||
])
|
||||
)
|
||||
)
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.listItem({
|
||||
id: "artikkel_view_innholdstype",
|
||||
title: "Artikler (innholdstype)",
|
||||
schemaType: "gp.artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.innholdstype")
|
||||
.child((id) =>
|
||||
S.documentTypeList("gp.artikkel")
|
||||
.title("Innholdstype")
|
||||
.filter("_type == $type && innholdstype._ref == $id")
|
||||
.params({ type: "gp.artikkel", id })
|
||||
.initialValueTemplates([
|
||||
S.initialValueTemplateItem("gp.artikkel.by.innholdstype", {
|
||||
id,
|
||||
}),
|
||||
])
|
||||
)
|
||||
.initialValueTemplates([]),
|
||||
}), */
|
||||
]);
|
||||
};
|
||||
|
||||
// set default document node here — so that if users want concepts
|
||||
// and schemes elsewhere in desk, they'll get the right views.
|
||||
export const defaultDocumentNode: DefaultDocumentNodeResolver = (
|
||||
S,
|
||||
{ schemaType },
|
||||
) => {
|
||||
// Conditionally return a different configuration based on the schema type
|
||||
switch (schemaType) {
|
||||
case "gp.tema":
|
||||
return S.document().views([S.view.form()]);
|
||||
case "gp.tema.undertema":
|
||||
return S.document().views([S.view.form()]);
|
||||
case "gp.innholdstype":
|
||||
return S.document().views([S.view.form()]);
|
||||
case "gp.artikkel":
|
||||
return S.document().views([S.view.form()]);
|
||||
default:
|
||||
S.view.form();
|
||||
}
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import { defineArrayMember, defineField } from "sanity";
|
||||
import { SANITY_API_VERSION } from "@/sanity/config";
|
||||
import { TaxonomyInput } from "./TaxonomyInput";
|
||||
|
||||
export const gpTaxonomyObject = {
|
||||
title: "God praksis struktur (versjon 2)",
|
||||
name: "gp_taxonomy",
|
||||
type: "object",
|
||||
group: "staging",
|
||||
components: {
|
||||
input: TaxonomyInput,
|
||||
},
|
||||
fields: [
|
||||
defineField({
|
||||
name: "innholdstype",
|
||||
title: "Innholdstype",
|
||||
type: "reference",
|
||||
to: [{ type: "gp.innholdstype" }],
|
||||
options: {
|
||||
disableNew: true,
|
||||
},
|
||||
|
||||
/* Add required after update */
|
||||
/* validation: (Rule) => Rule.required(), */
|
||||
}),
|
||||
defineField({
|
||||
name: "undertema",
|
||||
title: "Undertema",
|
||||
type: "array",
|
||||
|
||||
/* Add required after update */
|
||||
/* validation: (Rule) => Rule.required(), */
|
||||
/* */
|
||||
of: [
|
||||
defineArrayMember({
|
||||
title: "Undertema",
|
||||
type: "reference",
|
||||
to: [{ type: "gp.tema.undertema" }],
|
||||
options: {
|
||||
disableNew: true,
|
||||
filter: async ({ getClient, parent }) => {
|
||||
const undertema = (
|
||||
parent as {
|
||||
_key: string;
|
||||
_ref?: string;
|
||||
_type: "reference";
|
||||
}[]
|
||||
)
|
||||
.filter((x) => !!x._ref)
|
||||
.map((x) => x._ref);
|
||||
|
||||
const client = getClient({ apiVersion: SANITY_API_VERSION });
|
||||
const temaIds = await client.fetch("*[_id in $ids].tema._ref", {
|
||||
ids: undertema,
|
||||
});
|
||||
return {
|
||||
filter: "!(tema._ref in $temaIds)",
|
||||
params: { temaIds },
|
||||
};
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -68,15 +68,11 @@ export const publicationFlowConfig = definePlugin<PublicationFlowOptions>(
|
||||
({ hasQualityControl, hasPublishedAt }) => ({
|
||||
name: "publication-flow",
|
||||
document: {
|
||||
actions: (prev, { schemaType, currentUser }) => {
|
||||
actions: (prev, { schemaType }) => {
|
||||
if (hasQualityControl.some((e) => e === schemaType)) {
|
||||
return getCustomActions(prev);
|
||||
} else if (hasPublishedAt.some((e) => e === schemaType)) {
|
||||
return withCustomPublishAction(prev);
|
||||
} else if (schemaType === "aksel_feedback") {
|
||||
return currentUser?.roles.find((x) => x.name === "developer")
|
||||
? prev
|
||||
: [prev[0]];
|
||||
}
|
||||
|
||||
return prev;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { StructureBuilder } from "sanity/structure";
|
||||
import {
|
||||
CircleSlashIcon,
|
||||
EyeIcon,
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
} from "@navikt/aksel-icons";
|
||||
import { SANITY_API_VERSION } from "@/sanity/config";
|
||||
|
||||
export function adminStructure(S: StructureBuilder) {
|
||||
const adminOrDev = S.context.currentUser?.roles.find((x) =>
|
||||
["developer", "administrator"].includes(x.name),
|
||||
);
|
||||
if (!adminOrDev) {
|
||||
return S.divider();
|
||||
}
|
||||
|
||||
return S.listItem()
|
||||
.title("Admin")
|
||||
.icon(CircleSlashIcon)
|
||||
.child(
|
||||
S.list()
|
||||
.title("Admin")
|
||||
.items([
|
||||
S.documentListItem()
|
||||
.title(`Forside`)
|
||||
.schemaType(`aksel_forside`)
|
||||
.icon(ImageIcon)
|
||||
.id(`aksel_forside_dokument`),
|
||||
|
||||
S.listItem()
|
||||
.title("Standalone-sider")
|
||||
.child(
|
||||
S.documentList()
|
||||
.title("Sider")
|
||||
.filter(`_type == 'aksel_standalone'`)
|
||||
.apiVersion(SANITY_API_VERSION),
|
||||
),
|
||||
S.listItem()
|
||||
.title("Redirects")
|
||||
.child(
|
||||
S.documentList()
|
||||
.title("Redirects")
|
||||
.filter(`_type == 'redirect'`)
|
||||
.apiVersion(SANITY_API_VERSION),
|
||||
),
|
||||
S.listItem()
|
||||
.title("Eksempler/Templates")
|
||||
.child(
|
||||
S.documentList()
|
||||
.title("Eksempler")
|
||||
.filter(`_type == 'kode_eksempler_fil'`)
|
||||
.apiVersion(SANITY_API_VERSION),
|
||||
),
|
||||
S.listItem()
|
||||
.title("Token-grupper Designsystemet")
|
||||
.child(
|
||||
S.documentList()
|
||||
.title("Grupper")
|
||||
.filter(`_type == 'token_kategori'`)
|
||||
.apiVersion(SANITY_API_VERSION),
|
||||
),
|
||||
S.listItem()
|
||||
.title("Props Designsystemet")
|
||||
.child(
|
||||
S.documentList()
|
||||
.title("Props")
|
||||
.filter(`_type == 'ds_props'`)
|
||||
.apiVersion(SANITY_API_VERSION),
|
||||
),
|
||||
S.documentListItem()
|
||||
.title(`Skrivehjelp`)
|
||||
.schemaType(`skrivehjelp`)
|
||||
.icon(FileTextIcon)
|
||||
.id(`skrivehjelp`),
|
||||
S.documentListItem()
|
||||
.title(`Publiseringsflyt`)
|
||||
.schemaType(`publication_flow`)
|
||||
.icon(FileTextIcon)
|
||||
.id(`publication_flow`),
|
||||
S.listItem()
|
||||
.title("Artikkelvisninger")
|
||||
.icon(EyeIcon)
|
||||
.child(
|
||||
S.documentList()
|
||||
.title("Artikkelvisninger")
|
||||
.filter(`_type == 'article_views'`)
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.menuItems([
|
||||
...(S.documentTypeList("article_views").getMenuItems() ?? []),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -1,67 +1,189 @@
|
||||
import { StructureBuilder } from "sanity/structure";
|
||||
import { FileXMarkIcon, TagIcon } from "@navikt/aksel-icons";
|
||||
import { HouseIcon, PencilBoardIcon } from "@navikt/aksel-icons";
|
||||
import { SANITY_API_VERSION } from "@/sanity/config";
|
||||
import {
|
||||
editorIsContributorFilter,
|
||||
listMyDraftArticles,
|
||||
listPublishedArticles,
|
||||
} from "./structure.util";
|
||||
|
||||
/* documentStore is in Alpha, so avoid using for now */
|
||||
export const GodPraksisPanes = async (getClient, S: StructureBuilder) => {
|
||||
let tema = await getClient({ apiVersion: SANITY_API_VERSION }).fetch(
|
||||
`*[_type == "aksel_tema"]{title,seksjoner, _id}`,
|
||||
);
|
||||
tema = tema
|
||||
.map((x) => ({
|
||||
title: x.title,
|
||||
sider:
|
||||
x.seksjoner?.reduce((b, n) => [...b, ...(n?.sider ?? [])], []) || [],
|
||||
_id: x._id,
|
||||
}))
|
||||
.filter(
|
||||
(x) =>
|
||||
!(
|
||||
x._id.startsWith("drafts") &&
|
||||
tema.find((y) => y._id === x._id.replace("drafts.", ""))
|
||||
),
|
||||
export function gpStructure(S: StructureBuilder) {
|
||||
return S.listItem()
|
||||
.title("God praksis")
|
||||
.icon(PencilBoardIcon)
|
||||
.child(
|
||||
S.list()
|
||||
.title("God Praksis")
|
||||
.items([
|
||||
S.documentListItem()
|
||||
.title(`Landingsside`)
|
||||
.icon(HouseIcon)
|
||||
.schemaType(`godpraksis_landingsside`)
|
||||
.id(`godpraksis_landingsside_id1`),
|
||||
S.divider(),
|
||||
...godPraksisPanes(S),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
const ids = await getClient({ apiVersion: SANITY_API_VERSION }).fetch(
|
||||
`*[_type == "aksel_artikkel"]{_id, tema}`,
|
||||
);
|
||||
|
||||
function godPraksisPanes(S: StructureBuilder) {
|
||||
return [
|
||||
S.listItem()
|
||||
.title(`Temasider (${tema.length})`)
|
||||
.icon(TagIcon)
|
||||
.child(S.documentTypeList("aksel_tema")),
|
||||
listPublishedArticles(S, "aksel_artikkel"),
|
||||
listMyDraftArticles(S, "aksel_artikkel"),
|
||||
|
||||
S.listItem({
|
||||
id: "my_gp_outdated",
|
||||
title: "Mine artikler som trenger oppdatering",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: (_, { structureContext }) => {
|
||||
const mail = structureContext.currentUser?.email;
|
||||
|
||||
return S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter(
|
||||
`_type == $type && (dateTime(updateInfo.lastVerified + "T00:00:00Z") < dateTime(now()) - 60*60*24*365) && ${editorIsContributorFilter}`,
|
||||
)
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel", mail })
|
||||
.initialValueTemplates([])
|
||||
.defaultOrdering([
|
||||
{ field: "updateInfo.lastVerified", direction: "asc" },
|
||||
]);
|
||||
},
|
||||
}),
|
||||
S.divider(),
|
||||
...tema.map(({ title, _id }) =>
|
||||
S.listItem()
|
||||
.title(
|
||||
`${title} (${
|
||||
ids.filter(
|
||||
(x) =>
|
||||
x?.tema &&
|
||||
x?.tema?.find?.((y) => y._ref === _id.replace("drafts.", "")),
|
||||
).length ?? 0
|
||||
})`,
|
||||
)
|
||||
.child(
|
||||
S.documentList()
|
||||
.title(`${title}`)
|
||||
.filter(`_type == 'aksel_artikkel' && $undertema in tema[]._ref`)
|
||||
.params({ undertema: _id.replace("drafts.", "") })
|
||||
.apiVersion(SANITY_API_VERSION),
|
||||
/* .menuItems([...S.documentTypeList("aksel_artikkel").getMenuItems()]) */
|
||||
),
|
||||
),
|
||||
S.documentTypeListItem("gp.tema").title("Tema"),
|
||||
S.listItem({
|
||||
id: "tema_view",
|
||||
title: "Undertema",
|
||||
schemaType: "gp.tema.undertema",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.tema")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("gp.tema.undertema")
|
||||
.title("Undertema")
|
||||
.filter("_type == $type && tema._ref == $id")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "gp.tema.undertema", id })
|
||||
.initialValueTemplates([
|
||||
S.initialValueTemplateItem("gp.tema.undertema.by.tema", {
|
||||
id,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.documentTypeListItem("gp.innholdstype").title("Innholdstyper"),
|
||||
S.divider(),
|
||||
S.listItem()
|
||||
.title(`Artikler uten tema (${ids.filter((x) => !x?.tema).length ?? 0})`)
|
||||
.icon(FileXMarkIcon)
|
||||
.child(
|
||||
S.documentList()
|
||||
.title(`Artikler uten tema`)
|
||||
.filter(`_type == 'aksel_artikkel' && !defined(tema)`)
|
||||
.apiVersion(SANITY_API_VERSION),
|
||||
/* .menuItems([...S.documentTypeList("aksel_artikkel").getMenuItems()]) */
|
||||
),
|
||||
S.listItem({
|
||||
id: "article_tema_complete_view",
|
||||
title: "Tema -> Artikler",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.tema")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && $id in undertema[]->tema._ref")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel", id });
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.listItem({
|
||||
id: "article_undertema_view",
|
||||
title: "Tema -> Undertema -> Artikler",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.tema")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("gp.tema.undertema")
|
||||
.title("Undertema")
|
||||
.filter("_type == $type && tema._ref == $id")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "gp.tema.undertema", id })
|
||||
.child((undertema_id) => {
|
||||
return S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && $id in undertema[]._ref")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel", id: undertema_id })
|
||||
.initialValueTemplates([
|
||||
S.initialValueTemplateItem("gp.artikkel.by.undertema", {
|
||||
undertema_id,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.initialValueTemplates([]);
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.listItem({
|
||||
id: "article_innholdstype_view",
|
||||
title: "Innholdstype -> Artikler",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.innholdstype")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && $id == innholdstype._ref")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel", id })
|
||||
.initialValueTemplates([
|
||||
S.initialValueTemplateItem("gp.artikkel.by.innholdstype", {
|
||||
id,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.divider(),
|
||||
S.listItem({
|
||||
id: "article_no_undertema_view",
|
||||
title: "Uten undertema",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && !defined(undertema)")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel" })
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
|
||||
S.listItem({
|
||||
id: "article_no_innholdstype_view",
|
||||
title: "Uten innholdstype",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter("_type == $type && !defined(innholdstype)")
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel" })
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
S.listItem({
|
||||
id: "article_gp_outdated_tema",
|
||||
title: "Trenger oppdatering",
|
||||
schemaType: "aksel_artikkel",
|
||||
child: () =>
|
||||
S.documentTypeList("gp.tema")
|
||||
.child((id) => {
|
||||
return S.documentTypeList("aksel_artikkel")
|
||||
.title("Artikler")
|
||||
.filter(
|
||||
`_type == $type && $id in undertema[]->tema._ref && (dateTime(updateInfo.lastVerified + "T00:00:00Z") < dateTime(now()) - 60*60*24*365)`,
|
||||
)
|
||||
.apiVersion(SANITY_API_VERSION)
|
||||
.params({ type: "aksel_artikkel", id })
|
||||
.initialValueTemplates([])
|
||||
.defaultOrdering([
|
||||
{ field: "updateInfo.lastVerified", direction: "asc" },
|
||||
]);
|
||||
})
|
||||
.initialValueTemplates([]),
|
||||
}),
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { StructureBuilder } from "sanity/structure";
|
||||
import { HouseIcon, TokenIcon } from "@navikt/aksel-icons";
|
||||
import { grunnleggendeKategorier } from "@/sanity/config";
|
||||
import { Panes } from "./panes";
|
||||
import { listDraftArticles, listOutdatedArticles } from "./structure.util";
|
||||
|
||||
export function grunnleggendeStructure(S: StructureBuilder) {
|
||||
return S.listItem()
|
||||
.title("Grunnleggende")
|
||||
.icon(TokenIcon)
|
||||
.child(
|
||||
S.list()
|
||||
.title("Grunnleggende")
|
||||
.items([
|
||||
S.documentListItem()
|
||||
.title(`Landingsside`)
|
||||
.icon(HouseIcon)
|
||||
.schemaType(`grunnleggende_landingsside`)
|
||||
.id(`grunnleggende_landingsside_id1`),
|
||||
S.divider(),
|
||||
listDraftArticles(S, "ds_artikkel"),
|
||||
listOutdatedArticles(S, "ds_artikkel", 180),
|
||||
|
||||
S.divider(),
|
||||
...Panes("ds_artikkel", [...grunnleggendeKategorier], S),
|
||||
]),
|
||||
);
|
||||
}
|
||||
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user