Add module detail page with toggle for enabling/disabling modules
- Delete scaffolded static sector route files that conflicted with dynamic routing - Add Supabase migration for module_toggles table with RLS policies - Extend modules-data.ts with longDesc field and getModuleBySlug helper - Create ModuleToggle client component with optimistic upsert to Supabase - Create [sector]/[module] detail page with description, badges, and toggle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { ArrowLeft, BookOpen, BarChart3, FileDown, Cog } from 'lucide-react'
|
||||
import { getModuleBySlug, CATEGORY_LABELS, type ModuleCategory } from '@/lib/modules-data'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ModuleToggle } from '@/components/modules/ModuleToggle'
|
||||
|
||||
const CATEGORY_META: Record<ModuleCategory, {
|
||||
icon: typeof BookOpen
|
||||
color: string
|
||||
bgColor: string
|
||||
borderColor: string
|
||||
}> = {
|
||||
bokforing: {
|
||||
icon: BookOpen,
|
||||
color: 'text-primary',
|
||||
bgColor: 'bg-primary/8',
|
||||
borderColor: 'border-primary/10',
|
||||
},
|
||||
rapport: {
|
||||
icon: BarChart3,
|
||||
color: 'text-accent',
|
||||
bgColor: 'bg-accent/8',
|
||||
borderColor: 'border-accent/10',
|
||||
},
|
||||
import: {
|
||||
icon: FileDown,
|
||||
color: 'text-success',
|
||||
bgColor: 'bg-success/8',
|
||||
borderColor: 'border-success/10',
|
||||
},
|
||||
operativ: {
|
||||
icon: Cog,
|
||||
color: 'text-muted-foreground',
|
||||
bgColor: 'bg-muted/60',
|
||||
borderColor: 'border-border',
|
||||
},
|
||||
}
|
||||
|
||||
export default async function ModuleDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sector: string; module: string }>
|
||||
}) {
|
||||
const { sector: sectorSlug, module: moduleSlug } = await params
|
||||
const result = getModuleBySlug(sectorSlug, moduleSlug)
|
||||
|
||||
if (!result) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { sector, module: mod } = result
|
||||
const SectorIcon = sector.icon
|
||||
const catMeta = CATEGORY_META[mod.cat]
|
||||
const CatIcon = catMeta.icon
|
||||
|
||||
return (
|
||||
<div className="space-y-8 stagger-enter">
|
||||
{/* Navigation */}
|
||||
<div>
|
||||
<Link
|
||||
href={`/modules/${sectorSlug}`}
|
||||
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
{sector.name}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-5">
|
||||
<div className={`flex items-center justify-center w-12 h-12 rounded-xl ${catMeta.bgColor} ring-1 ${catMeta.borderColor}`}>
|
||||
<CatIcon className={`h-6 w-6 ${catMeta.color}`} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="font-display text-2xl font-semibold tracking-tight">
|
||||
{mod.name}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{mod.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
className={`${catMeta.bgColor} ${catMeta.color} border-transparent`}
|
||||
>
|
||||
{CATEGORY_LABELS[mod.cat]}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="gap-1.5">
|
||||
<SectorIcon className="h-3 w-3" strokeWidth={1.5} />
|
||||
{sector.name}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="h-px bg-border" />
|
||||
|
||||
{/* Long description */}
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<div className="rounded-xl border border-border bg-card p-6 space-y-4">
|
||||
{mod.longDesc.split('. Nyckelfunktioner: ').map((part, i) => {
|
||||
if (i === 0) {
|
||||
return (
|
||||
<div key={i}>
|
||||
<h2 className="text-base font-semibold text-foreground mt-0 mb-2">
|
||||
Beskrivning
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed m-0">
|
||||
{part}{mod.longDesc.includes('Nyckelfunktioner') ? '' : ''}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div key={i}>
|
||||
<h2 className="text-base font-semibold text-foreground mt-0 mb-2">
|
||||
Nyckelfunktioner
|
||||
</h2>
|
||||
<ul className="space-y-1.5 m-0 p-0 list-none">
|
||||
{part.split('; ').map((feature, j) => (
|
||||
<li
|
||||
key={j}
|
||||
className="flex items-start gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-primary/40 flex-shrink-0" />
|
||||
{feature.replace(/,\s*$/, '')}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle */}
|
||||
<ModuleToggle sectorSlug={sectorSlug} moduleSlug={moduleSlug} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
UtensilsCrossed,
|
||||
BedDouble,
|
||||
Monitor,
|
||||
HardHat,
|
||||
HeartPulse,
|
||||
Store,
|
||||
ShoppingCart,
|
||||
Scissors,
|
||||
Truck,
|
||||
Scale,
|
||||
GraduationCap,
|
||||
Wheat,
|
||||
Film,
|
||||
Dumbbell,
|
||||
Wrench,
|
||||
UserPlus,
|
||||
Factory,
|
||||
Briefcase,
|
||||
PartyPopper,
|
||||
Building,
|
||||
Layers,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
|
||||
const sectors = [
|
||||
{ name: 'Restaurang & Café', slug: 'restaurang', icon: UtensilsCrossed, description: 'Menyhantering, bordsbokning & kassa' },
|
||||
{ name: 'Hotell & Boende', slug: 'hotell', icon: BedDouble, description: 'Rumsbokning, gäster & städschema' },
|
||||
{ name: 'Tech & IT', slug: 'tech', icon: Monitor, description: 'Projekthantering, tid & SLA' },
|
||||
{ name: 'Bygg & Entreprenad', slug: 'bygg', icon: HardHat, description: 'Projektkalkyler, underentreprenörer' },
|
||||
{ name: 'Hälsa & Sjukvård', slug: 'halsa', icon: HeartPulse, description: 'Patientbokning, journal & recept' },
|
||||
{ name: 'Detaljhandel', slug: 'detaljhandel', icon: Store, description: 'Lager, kassa & leverantörer' },
|
||||
{ name: 'E-handel', slug: 'ehandel', icon: ShoppingCart, description: 'Ordrar, frakt & returer' },
|
||||
{ name: 'Frisör & Skönhet', slug: 'frisor', icon: Scissors, description: 'Tidsbokning, kunder & produkter' },
|
||||
{ name: 'Transport & Logistik', slug: 'transport', icon: Truck, description: 'Flotta, rutter & leveranser' },
|
||||
{ name: 'Juridik & Redovisning', slug: 'juridik', icon: Scale, description: 'Ärenden, tidrapport & klienter' },
|
||||
{ name: 'Utbildning & Förskola', slug: 'utbildning', icon: GraduationCap, description: 'Schema, elever & närvaro' },
|
||||
{ name: 'Jordbruk & Livsmedel', slug: 'jordbruk', icon: Wheat, description: 'Skörd, djurhållning & spårbarhet' },
|
||||
{ name: 'Media & Kommunikation', slug: 'media', icon: Film, description: 'Projekt, innehåll & publicering' },
|
||||
{ name: 'Fitness & Sport', slug: 'fitness', icon: Dumbbell, description: 'Medlemskap, klasser & pass' },
|
||||
{ name: 'Fordon & Verkstad', slug: 'fordon', icon: Wrench, description: 'Arbetsorder, delar & fordon' },
|
||||
{ name: 'Bemanning & HR', slug: 'bemanning', icon: UserPlus, description: 'Kandidater, uppdrag & timmar' },
|
||||
{ name: 'Tillverkning & Industri', slug: 'tillverkning', icon: Factory, description: 'Produktion, lager & kvalitet' },
|
||||
{ name: 'Konsult & Rådgivning', slug: 'konsult', icon: Briefcase, description: 'Uppdrag, tid & debitering' },
|
||||
{ name: 'Event & Underhållning', slug: 'event', icon: PartyPopper, description: 'Evenemang, biljetter & artister' },
|
||||
{ name: 'Fastighetsförvaltning', slug: 'fastighet', icon: Building, description: 'Objekt, hyresgäster & underhåll' },
|
||||
] as const
|
||||
import { Layers } from 'lucide-react'
|
||||
import { sectors } from '@/lib/modules-data'
|
||||
|
||||
export default function ModulesPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Page header */}
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold tracking-tight">
|
||||
Företagsmoduler
|
||||
@@ -60,75 +14,37 @@ export default function ModulesPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Visa alla — inline as first item in the grid */}
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8 gap-1">
|
||||
{/* Visa alla tile */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
<Link
|
||||
href="/modules/alla"
|
||||
className="group flex flex-col items-center gap-2 rounded-xl px-2 py-4 transition-all duration-200 hover:bg-accent/10"
|
||||
className="group flex items-center gap-3.5 rounded-xl border border-accent/20 bg-accent/5 px-4 py-3.5 transition-all duration-200 hover:border-accent/40 hover:bg-accent/10"
|
||||
>
|
||||
<div className="flex items-center justify-center w-11 h-11 rounded-xl bg-accent/12 text-accent transition-all duration-200 group-hover:bg-accent/20 group-hover:scale-110">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-accent/12 text-accent shrink-0 transition-transform duration-200 group-hover:scale-105">
|
||||
<Layers className="h-5 w-5" strokeWidth={1.5} />
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-accent text-center leading-tight">
|
||||
<span className="text-sm font-medium text-accent truncate">
|
||||
Visa alla
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Sector tiles */}
|
||||
{sectors.map((sector) => {
|
||||
const Icon = sector.icon
|
||||
return (
|
||||
<Link
|
||||
key={sector.slug}
|
||||
href={`/modules/${sector.slug}`}
|
||||
className="group relative flex flex-col items-center gap-2 rounded-xl px-2 py-4 transition-all duration-200 hover:bg-secondary/60"
|
||||
className="group flex items-center gap-3.5 rounded-xl border border-border/50 bg-card px-4 py-3.5 transition-all duration-200 hover:border-border hover:bg-secondary/50 hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-center w-11 h-11 rounded-xl bg-muted/60 text-muted-foreground transition-all duration-200 group-hover:bg-primary/10 group-hover:text-primary group-hover:scale-110">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-muted/60 text-muted-foreground shrink-0 transition-all duration-200 group-hover:bg-primary/10 group-hover:text-primary group-hover:scale-105">
|
||||
<Icon className="h-5 w-5" strokeWidth={1.5} />
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-muted-foreground text-center leading-tight transition-colors duration-200 group-hover:text-foreground">
|
||||
<span className="text-sm font-medium text-foreground/80 truncate transition-colors duration-200 group-hover:text-foreground">
|
||||
{sector.name}
|
||||
</span>
|
||||
|
||||
{/* Hover tooltip with description */}
|
||||
<div className="pointer-events-none absolute -bottom-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10">
|
||||
<div className="whitespace-nowrap rounded-md bg-foreground px-2.5 py-1 text-[10px] text-background shadow-lg">
|
||||
{sector.description}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Compact list below for scanning */}
|
||||
<div>
|
||||
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-3">
|
||||
Alla branscher
|
||||
</p>
|
||||
<div className="columns-1 sm:columns-2 lg:columns-3 gap-0">
|
||||
{sectors.map((sector) => {
|
||||
const Icon = sector.icon
|
||||
return (
|
||||
<Link
|
||||
key={sector.slug}
|
||||
href={`/modules/${sector.slug}`}
|
||||
className="group flex items-center gap-2.5 py-2 px-2 -mx-2 rounded-lg transition-colors duration-150 hover:bg-secondary/50 break-inside-avoid"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 text-muted-foreground/60 flex-shrink-0 transition-colors duration-150 group-hover:text-primary" strokeWidth={1.5} />
|
||||
<span className="text-sm text-muted-foreground transition-colors duration-150 group-hover:text-foreground truncate">
|
||||
{sector.name}
|
||||
</span>
|
||||
<span className="hidden sm:inline text-xs text-muted-foreground/50 truncate transition-colors duration-150 group-hover:text-muted-foreground">
|
||||
{sector.description}
|
||||
</span>
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground/30 ml-auto flex-shrink-0 opacity-0 transition-all duration-150 group-hover:opacity-100" />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user