Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions portal/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import createMDX from "@next/mdx";

/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],

async redirects() {
return [
images: {
remotePatterns: [
{
source: "/",
destination: "/komponenter",
permanent: false,
protocol: "https",
hostname: "cdn.sanity.io",
},
];
],
},
};

Expand Down
14 changes: 13 additions & 1 deletion portal/sanity.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { dataset, projectId } from "@/sanity/env";
import { schemaTypes } from "@/sanity/schemas";
import { codeInput } from "@sanity/code-input";
import { CogIcon, ComponentIcon } from "@sanity/icons";
import { CogIcon, ComponentIcon, HomeIcon } from "@sanity/icons";
import { nbNOLocale } from "@sanity/locale-nb-no";
import { table } from "@sanity/table";
import { visionTool } from "@sanity/vision";
Expand All @@ -21,11 +21,23 @@ export default defineConfig({
S.list()
.title("Innhold")
.items([
S.listItem()
.title("Forside")
.icon(HomeIcon)
.child(
S.document()
.schemaType("jokul_frontpage")
.documentId("jokul_frontpage"),
),
S.divider(),
...S.documentTypeListItems().filter(
(listItem) =>
!["jokul_story"].includes(
listItem.getId() || "",
) &&
!["jokul_frontpage"].includes(
listItem.getId() || "",
) &&
!["jokul_siteData"].includes(
listItem.getId() || "",
),
Expand Down
6 changes: 4 additions & 2 deletions portal/src/app/(frontend)/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ html {
body {
--max-width: 1440px;
--min-width: 320px;
--portal-inline-padding: var(--jkl-spacing-16);
background-color: var(--jkl-background-container-high);
min-height: 100dvh;
}
Expand All @@ -28,11 +29,12 @@ body {

@media (width >=940px) {
--portal-header-height: #{jkl.rem(90px)};
--portal-inline-padding: var(--jkl-spacing-64);

padding: 0;

main {
padding: var(--jkl-spacing-40) var(--jkl-spacing-64);
padding: var(--jkl-spacing-40) var(--portal-inline-padding);
}
}
}
Expand Down Expand Up @@ -67,7 +69,7 @@ samp {

main {
display: grid;
padding: var(--jkl-spacing-16);
padding: var(--portal-inline-padding);
}

article header {
Expand Down
52 changes: 50 additions & 2 deletions portal/src/app/(frontend)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
export default function FrontPage() {
return <h1>Wheee!</h1>;
import {
FrontPageImageStrip,
FrontPageLogo,
FrontPageNewsSection,
FrontPagePortalSections,
} from "@/components/frontpage";
import styles from "@/components/frontpage/frontpagePage.module.scss";
import { sanityFetch } from "@/sanity/lib/live";
import { frontpageQuery } from "@/sanity/queries/frontpage";
import type { FrontpageQueryResult } from "@/sanity/types";

const DEFAULT_HERO_TEXT = "Jøkul Designsystem";

const getHeroText = (frontpage?: FrontpageQueryResult["frontpage"]) => {
if (frontpage?.hero?.useCustomText && frontpage.hero.text) {
return frontpage.hero.text;
}

return DEFAULT_HERO_TEXT;
};

export default async function FrontPage() {
const { data } = await sanityFetch({
query: frontpageQuery,
requestTag: "frontpage",
tags: [
"jokul_frontpage",
"jokul_component",
"jokul_fundamentals",
"jokul_blog_post",
],
});
const frontpageData = data as FrontpageQueryResult | null;
const frontpage = frontpageData?.frontpage || null;
const latestUpdatedDocuments = frontpageData?.latestUpdatedDocuments || [];
Comment on lines +23 to +35
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data as FrontpageQueryResult omgår typesikkerhet og kan skjule mismatch mellom GROQ-query og genererte typer. Andre sider bruker sanityFetch uten type-assertion (f.eks. portal/src/app/(frontend)/blog/page.tsx), så vurder å la sanityFetch/defineQuery inferere typen direkte eller angi generisk type i fetch-kallet.

Copilot uses AI. Check for mistakes.

return (
<div className={styles.frontPage}>
<section>
<h1 className="jkl-sr-only">Jøkul Designsystem</h1>
<FrontPageLogo text={getHeroText(frontpage)} delay={100} />
</section>
<FrontPagePortalSections links={frontpage?.portalLinks} />
<FrontPageImageStrip images={frontpage?.gridImages} />
<FrontPageNewsSection
featuredDocument={frontpage?.highlightedEntry}
documents={latestUpdatedDocuments}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { client } from "@/sanity/lib/client";
import type { FrontpageQueryResult } from "@/sanity/types";
import { Flex } from "@fremtind/jokul/flex";
import { createImageUrlBuilder } from "@sanity/image-url";
import styles from "./frontPageImageStrip.module.scss";

const builder = createImageUrlBuilder(client);

type FrontPageImage = NonNullable<
NonNullable<FrontpageQueryResult["frontpage"]>["gridImages"]
>[number];

type FrontPageImageStripProps = {
images?: FrontPageImage[] | null;
};

export const FrontPageImageStrip = ({ images }: FrontPageImageStripProps) => {
const resolvedImages =
images?.flatMap((image) => {
if (!image.asset?._ref) {
return [];
}

return [
{
key: image.asset._ref,
src: builder.image(image).width(1600).auto("format").url(),
},
];
}) || [];

if (!resolvedImages.length) {
return null;
}

const marqueeSeedImages = Array.from(
{
length: Math.ceil(
Math.max(resolvedImages.length, 6) /
resolvedImages.length,
),
},
() => resolvedImages,
)
.flat()
.slice(0, Math.max(resolvedImages.length, 6));
const animationDuration = `${Math.max(marqueeSeedImages.length * 12, 24)}s`;
const marqueeImages = [...marqueeSeedImages, ...marqueeSeedImages];

return (
<section className={styles.imageStripViewport} aria-hidden="true" inert>
<Flex
alignItems="center"
gap="s"
className={styles.imageStripTrack}
style={{ animationDuration }}
>
{marqueeImages.map((image, index) => (
<img
key={`${image.key}-${index}`}
src={image.src}
alt=""
loading="lazy"
/>
))}
</Flex>
</section>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@use "@fremtind/jokul/styles/core/jkl";

@keyframes image-strip-marquee {
from {
transform: translateX(0);
}

to {
transform: translateX(-50%);
}
}

.imageStripViewport {
--image-strip-item-width: 33svw;
width: 100vw;
margin-inline: calc(50% - 50vw);
padding-bottom: jkl.$spacing-xs;
overflow: hidden;

.imageStripTrack {
width: max-content;
will-change: transform;
animation: image-strip-marquee 30s linear infinite;
height: 100%;

@media (prefers-reduced-motion: reduce) {
animation: none;
}

img {
flex: 0 0 var(--image-strip-item-width);
width: var(--image-strip-item-width);
display: block;
height: auto;
border-radius: jkl.$border-radius-m;
background-color: jkl.$color-background-container-high;
}
}
}
24 changes: 24 additions & 0 deletions portal/src/components/frontpage/FrontPageLogo/FrontPageLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Flex } from "@fremtind/jokul/flex";
import { FrontPageLogoText } from "./FrontPageLogoText";
import styles from "./frontPageLogo.module.scss";

interface FrontPageLogoProps {
text: string;
delay: number;
}

export const FrontPageLogo = ({ text, delay }: FrontPageLogoProps) => {
return (
<Flex
as="section"
alignItems="center"
className={styles.logoText}
aria-hidden="true"
>
<div className={styles.logoTextShell}>
<div className={styles.logoTextMeasure}>{text}</div>
<FrontPageLogoText text={text} delay={delay} />
</div>
</Flex>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { useBrowserPreferences } from "@fremtind/jokul/hooks";
import clsx from "clsx";
import { useEffect, useState } from "react";
import styles from "./frontPageLogo.module.scss";

const sanitizeAnimatedText = (text: string) =>
text.replace(/\u200B|\u200C|\u200D|\u2060|\uFEFF/g, "");

interface FrontPageLogoTextProps {
text: string;
delay: number;
}

export const FrontPageLogoText = ({ text, delay }: FrontPageLogoTextProps) => {
const { prefersReducedMotion } = useBrowserPreferences();
const cleanText = sanitizeAnimatedText(text);
const [displayedText, setDisplayedText] = useState("");

useEffect(() => {
if (prefersReducedMotion || !cleanText.length) {
setDisplayedText(cleanText);
return;
}

setDisplayedText("");
let index = 0;
let timeout = 0;

const scheduleNextCharacter = () => {
timeout = window.setTimeout(
() => {
index += 1;
setDisplayedText(cleanText.slice(0, index));

if (index < cleanText.length) {
scheduleNextCharacter();
}
},
Math.max(delay, Math.round(Math.random() * delay + delay / 2)),
);
};

scheduleNextCharacter();

return () => {
window.clearTimeout(timeout);
};
}, [cleanText, delay, prefersReducedMotion]);

return (
<div
className={clsx(styles.logoTextContent, {
[styles.logoTextContentDone]: displayedText === cleanText,
})}
>
{displayedText}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@use "@fremtind/jokul/styles/core/jkl";

.logoText {
font-size: clamp(4rem, 12vw, 11rem);
max-width: 100%;

@include jkl.from-medium-device {
max-width: 85vw;
min-height: min(68vh, 42rem);
}
}

.logoTextShell {
position: relative;
width: 100%;
}

.logoTextMeasure,
.logoTextContent {
padding-top: jkl.$spacing-l;
width: 100%;
}

.logoTextMeasure {
visibility: hidden;
pointer-events: none;
}

.logoTextContent {
position: absolute;
inset: 0;

&::after {
@include jkl.decorative($content: "_");
opacity: 1;
}
}

.logoTextContentDone {
&::after {
@include jkl.motion("exit");
transition-property: opacity;
opacity: 0;
}
}
Loading
Loading