Compare commits

..

No commits in common. "a410143ce74ab71552aabd71b1500b6cb4aa3913" and "84e8efd4e95091cc3f004e8ecc2d9bc7aa667575" have entirely different histories.

9 changed files with 39 additions and 256 deletions

1
.gitignore vendored
View File

@ -14,4 +14,3 @@ src/**/*.bak.*
*.db
*.sqlite
*.sqlite3
next-env.d.ts

View File

@ -1,11 +0,0 @@
# *
User-agent: *
Disallow: /admin/*
# Host
Host: http://localhost:3000
# Sitemaps
Sitemap: http://localhost:3000/sitemap.xml
Sitemap: http://localhost:3000/pages-sitemap.xml
Sitemap: http://localhost:3000/posts-sitemap.xml

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap><loc>http://localhost:3000/pages-sitemap.xml</loc></sitemap>
<sitemap><loc>http://localhost:3000/posts-sitemap.xml</loc></sitemap>
</sitemapindex>

View File

@ -1,31 +1,16 @@
import { getCachedGlobal } from '@/utilities/getGlobals'
import Link from 'next/link'
import React from 'react'
import type { Footer, Page } from '@/payload-types'
import type { Footer } from '@/payload-types'
import { CMSLink } from '@/components/Link'
import { Logo } from '@/components/Logo/Logo'
/** Resolves the logo href from the logoLink group field */
function resolveLogoHref(logoLink: Footer['logoLink']): string {
if (!logoLink) return '/'
if (logoLink.type === 'reference') {
const ref = logoLink.reference
if (ref && typeof ref === 'object' && 'value' in ref) {
const page = ref.value
return typeof page === 'object' && page !== null ? `/${(page as Page).slug}` : '/'
}
return '/'
}
return logoLink.url || '/'
}
export async function Footer() {
const footerData = await getCachedGlobal('footer', 1)() as unknown as Footer
const footerData = await getCachedGlobal("footer", 1)() as unknown as Footer
const columns = footerData?.columns || []
const navItems = footerData?.navItems || []
const hasColumns = columns.length > 0
const logoHref = resolveLogoHref(footerData?.logoLink)
const bottomLeft = (footerData?.bottomLeftText || '© {year} Fiber Direkt. Alla rättigheter förbehållna.').replace('{year}', new Date().getFullYear().toString())
const bottomRight = footerData?.bottomRightText || 'Svenskt datacenter · ISO 27001 · ISO 14001'
@ -36,7 +21,7 @@ export async function Footer() {
<div className="flex flex-col lg:flex-row gap-10 lg:gap-16">
{/* Logo column */}
<div className="flex-shrink-0">
<Link className="flex items-center" href={logoHref}>
<Link className="flex items-center" href="/">
<Logo variant="white" className="max-w-[120px]" />
</Link>
</div>

View File

@ -8,49 +8,6 @@ export const Footer: GlobalConfig = {
read: () => true,
},
fields: [
// ── Logo link ─────────────────────────────────────────────────────────────
{
name: 'logoLink',
type: 'group',
label: 'Logotyplänk',
admin: {
description: 'Vart man hamnar när man klickar på logotypen i footern. Standard är startsidan (/).',
},
fields: [
{
name: 'type',
type: 'radio',
label: 'Länktyp',
options: [
{ label: 'Intern sida', value: 'reference' },
{ label: 'Anpassad URL', value: 'custom' },
],
defaultValue: 'custom',
admin: { layout: 'horizontal' },
},
{
name: 'reference',
type: 'relationship',
label: 'Intern sida',
relationTo: ['pages'] as const,
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
},
{
name: 'url',
type: 'text',
label: 'URL',
defaultValue: '/',
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
description: 'T.ex. / eller /startsida',
},
},
],
},
// ── Sitemap columns ───────────────────────────────────────────────────────
{
name: 'columns',
type: 'array',
@ -90,8 +47,6 @@ export const Footer: GlobalConfig = {
},
],
},
// ── Simple nav (legacy) ───────────────────────────────────────────────────
{
name: 'navItems',
type: 'array',
@ -107,8 +62,6 @@ export const Footer: GlobalConfig = {
],
maxRows: 6,
},
// ── Bottom text ───────────────────────────────────────────────────────────
{
name: 'bottomLeftText',
type: 'text',

View File

@ -4,7 +4,7 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation'
import React, { useEffect, useState } from 'react'
import type { Header, Page } from '@/payload-types'
import type { Header } from '@/payload-types'
import { Logo } from '@/components/Logo/Logo'
import { HeaderNav } from './Nav'
@ -13,20 +13,6 @@ interface HeaderClientProps {
data: Header
}
/** Resolves the logo href from the logoLink group field */
function resolveLogoHref(logoLink: Header['logoLink']): string {
if (!logoLink) return '/'
if (logoLink.type === 'reference') {
const ref = logoLink.reference
if (ref && typeof ref === 'object' && 'value' in ref) {
const page = ref.value
return typeof page === 'object' && page !== null ? `/${(page as Page).slug}` : '/'
}
return '/'
}
return logoLink.url || '/'
}
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
/* Storing the value in a useState to avoid hydration errors */
const [theme, setTheme] = useState<string | null>(null)
@ -43,15 +29,13 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [headerTheme])
const logoHref = resolveLogoHref(data?.logoLink)
return (
<header
className="relative z-20 w-full bg-white"
{...(theme ? { 'data-theme': theme } : {})}
>
<div className="container py-5 flex justify-between items-center">
<Link href={logoHref}>
<Link href="/">
<Logo loading="eager" priority="high" variant="blue" className="max-w-[80px] md:max-w-[100px]" />
</Link>
<HeaderNav data={data} />

View File

@ -1,28 +1,22 @@
'use client'
import React, { useState, useEffect, useRef } from 'react'
import type { Header as HeaderType, Page } from '@/payload-types'
import type { Header as HeaderType } from '@/payload-types'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
// ─── Types ────────────────────────────────────────────────────────────────────
type NavChild = NonNullable<NonNullable<NonNullable<HeaderType['navItems']>[number]['children']>[number]>
type NavItem = NonNullable<HeaderType['navItems']>[number]
// ─── Types ───────────────────────────────────────────────────────────────────
type NavChild = {
label: string
url: string
group?: string | null
}
// ─── Resolve href from reference or custom url ────────────────────────────────
function resolveHref(item: {
type?: string | null
type NavItem = {
label: string
url?: string | null
reference?: { relationTo?: string; value?: number | Page | null } | null
}): string {
if (item.type === 'reference' && item.reference?.value) {
const page = item.reference.value
if (typeof page === 'object' && page !== null) {
return page.slug === 'home' || page.slug === 'startsida' ? '/' : `/${(page as Page).slug}`
}
return '#'
}
return item.url || '#'
megaMenu?: boolean | null
children?: NavChild[] | null
}
// ─── Swedish Flag ─────────────────────────────────────────────────────────────
@ -87,14 +81,14 @@ const MegaMenuPanel: React.FC<{
return (
<>
{/* Blur backdrop */}
{/* Blur backdrop — covers page below, click to close */}
<div
className="fixed inset-0 z-30"
style={{ top, backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)', backgroundColor: 'rgba(14, 35, 56, 0.08)' }}
onClick={onClose}
/>
{/* Panel */}
{/* Panel — same white as header, no top border so they read as one */}
<div
ref={panelRef}
className="fixed left-0 right-0 z-40"
@ -123,7 +117,7 @@ const MegaMenuPanel: React.FC<{
{col.links.map((link, li) => (
<li key={li}>
<Link
href={resolveHref(link)}
href={link.url}
onClick={onClose}
className={`block py-1.5 transition-colors duration-150 hover:text-fd-yellow ${
col.heading
@ -243,7 +237,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
{item.children!.map((child, j) => (
<Link
key={j}
href={resolveHref(child)}
href={child.url}
onClick={() => setOpenDropdown(null)}
className="block px-5 py-2.5 font-joey text-fd-navy hover:bg-fd-yellow/20 transition-colors text-sm"
>
@ -256,7 +250,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
) : (
<Link
key={i}
href={resolveHref(item)}
href={item.url || '#'}
className="font-joey-bold text-fd-navy hover:text-fd-yellow transition-colors text-base"
>
{item.label}
@ -332,7 +326,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
{item.children!.map((child, j) => (
<Link
key={j}
href={resolveHref(child)}
href={child.url}
onClick={closeMobile}
className="block pl-4 py-3 text-fd-yellow font-joey text-lg border-b border-white/5 hover:text-white transition-colors"
>
@ -344,7 +338,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
) : (
<Link
key={i}
href={resolveHref(item)}
href={item.url || '#'}
onClick={closeMobile}
className="py-4 text-white font-joey-medium text-xl border-b border-white/10 hover:text-fd-yellow transition-colors block"
>

View File

@ -7,49 +7,6 @@ export const Header: GlobalConfig = {
read: () => true,
},
fields: [
// ── Logo link ─────────────────────────────────────────────────────────────
{
name: 'logoLink',
type: 'group',
label: 'Logotyplänk',
admin: {
description: 'Vart man hamnar när man klickar på logotypen. Standard är startsidan (/).',
},
fields: [
{
name: 'type',
type: 'radio',
label: 'Länktyp',
options: [
{ label: 'Intern sida', value: 'reference' },
{ label: 'Anpassad URL', value: 'custom' },
],
defaultValue: 'custom',
admin: { layout: 'horizontal' },
},
{
name: 'reference',
type: 'relationship',
label: 'Intern sida',
relationTo: ['pages'] as const,
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
},
{
name: 'url',
type: 'text',
label: 'URL',
defaultValue: '/',
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
description: 'T.ex. / eller /startsida',
},
},
],
},
// ── Nav items ─────────────────────────────────────────────────────────────
{
name: 'navItems',
type: 'array',
@ -65,43 +22,13 @@ export const Header: GlobalConfig = {
name: 'label',
type: 'text',
required: true,
label: 'Etikett',
},
// ── Link type toggle ────────────────────────────────────────────────
{
name: 'type',
type: 'radio',
label: 'Länktyp',
options: [
{ label: 'Intern sida', value: 'reference' },
{ label: 'Anpassad URL', value: 'custom' },
],
defaultValue: 'custom',
admin: {
layout: 'horizontal',
description:
'Lämna tomt / ignoreras om detta objekt har en undermeny.',
},
},
{
name: 'reference',
type: 'relationship',
label: 'Intern sida',
relationTo: ['pages'] as const,
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
label: 'Label',
},
{
name: 'url',
type: 'text',
label: 'URL',
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
description: 'T.ex. /bredband eller https://extern-sida.se',
},
label: 'URL (leave empty if this item has a submenu)',
},
// ── Mega menu toggle ────────────────────────────────────────────────
{
name: 'megaMenu',
type: 'checkbox',
@ -109,18 +36,17 @@ export const Header: GlobalConfig = {
defaultValue: false,
admin: {
description:
'När ikryssad öppnar hover/klick ett fullbredds mega menu istället för en liten dropdown.',
'When checked, hovering/clicking this item opens a full-width mega menu instead of a small dropdown card.',
},
},
// ── Children ────────────────────────────────────────────────────────
{
name: 'children',
type: 'array',
label: 'Undermenylänkar',
label: 'Submenu links',
maxRows: 16,
admin: {
description:
'Lägg till undermenylänkar. Om dessa finns ignoreras förälderlänken. Använd "Grupp" för att skapa kolumner i mega menu.',
'Add submenu links. If these exist, the parent URL is ignored. Use "Group" to create columns in the mega menu.',
initCollapsed: true,
},
fields: [
@ -128,43 +54,21 @@ export const Header: GlobalConfig = {
name: 'label',
type: 'text',
required: true,
label: 'Etikett',
},
{
name: 'type',
type: 'radio',
label: 'Länktyp',
options: [
{ label: 'Intern sida', value: 'reference' },
{ label: 'Anpassad URL', value: 'custom' },
],
defaultValue: 'custom',
admin: { layout: 'horizontal' },
},
{
name: 'reference',
type: 'relationship',
label: 'Intern sida',
relationTo: ['pages'] as const,
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
label: 'Label',
},
{
name: 'url',
type: 'text',
required: true,
label: 'URL',
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
},
},
{
name: 'group',
type: 'text',
label: 'Grupp (valfri)',
label: 'Group (optional)',
admin: {
description:
'Länkar med samma gruppnamn visas tillsammans i en separat kolumn i mega menu. Lämna tomt för huvudkolumnen.',
'Links with the same group name appear together in a separate column in the mega menu. Leave empty for the main column.',
},
},
],

View File

@ -46,8 +46,8 @@ import {
PreviewField,
} from '@payloadcms/plugin-seo/fields'
// ── Slug generator — handles Swedish characters ────────────────────────────────
const generateSlugFromTitle = (value: string): string =>
// ── Slug generator — handles Swedish characters ────────────────────────────
const generateSlug = (value: string): string =>
value
.toLowerCase()
.trim()
@ -131,7 +131,7 @@ export const Pages: CollectionConfig<'pages'> = {
FDVideoBlock,
FDCtaBannerBlock,
FDTestimonialBlock,
FDTeamBlock,
FDTeamBlock
],
required: true,
admin: {
@ -173,7 +173,7 @@ export const Pages: CollectionConfig<'pages'> = {
position: 'sidebar',
},
},
// ── Slug ─────────────────────────────────────────────────────────────────
// ── Slug ──────────────────────────────────────────────────────────────
{
name: 'slug',
type: 'text',
@ -181,34 +181,14 @@ export const Pages: CollectionConfig<'pages'> = {
index: true,
admin: {
position: 'sidebar',
description:
'Sidans URL-slug, t.ex. "om-oss" → fiberdirekt.se/om-oss. Kan alltid redigeras manuellt. Bocka i "Generera slug" nedan för att skriva om automatiskt från titeln.',
description: 'Genereras automatiskt från titeln.',
},
hooks: {
beforeChange: [
({ data, siblingData, value }) => {
// Only auto-generate if the checkbox is explicitly checked
if (siblingData?.generateSlug) {
return generateSlugFromTitle(data?.title || siblingData?.title || '')
}
// Otherwise keep whatever value the editor typed (or the existing value)
return value
},
({ data }) => generateSlug(data?.title || ''),
],
},
},
// ── Auto-generate toggle ──────────────────────────────────────────────────
{
name: 'generateSlug',
type: 'checkbox',
label: 'Generera slug automatiskt från titeln',
defaultValue: true,
admin: {
position: 'sidebar',
description:
'När ikryssad skrivs slugen om från titeln vid varje sparning. Avbocka för att låsa slugen och redigera den manuellt.',
},
},
}, // ← slug object closes here, then straight into ],
],
hooks: {
afterChange: [revalidatePage],