UI improvments

This commit is contained in:
2026-03-17 14:58:08 +01:00 Verified
parent ff3c179565
commit e3baccbe86
21 changed files with 1068 additions and 594 deletions
+9 -1
View File
@@ -5,6 +5,7 @@
import { Briefcase, Calendar, FileText, Home as HomeIcon, LifeBuoy, Lock, Map as MapIcon } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useRef } from 'react';
const NavItem = ({ to, icon: Icon, label, className = "" }: { to: string, icon: any, label: string, className?: string }) => {
const pathname = usePathname();
@@ -25,8 +26,15 @@ const NavItem = ({ to, icon: Icon, label, className = "" }: { to: string, icon:
};
export const Navigation = () => {
const pathname = usePathname();
const navRef = useRef<HTMLElement>(null);
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'instant' });
}, [pathname]);
return (
<nav className="flex overflow-x-auto scrollbar-hide border-t border-eggshell/10">
<nav ref={navRef} className="flex overflow-x-auto scrollbar-hide border-t border-eggshell/10">
<NavItem to="/" icon={HomeIcon} label="Hem" />
<NavItem to="/schedule" icon={Calendar} label="Schema" />
<NavItem to="/faq" icon={MapIcon} label="FAQ" />
+31
View File
@@ -0,0 +1,31 @@
// app/components/ui/ActionLinkCard.tsx
import { ArrowRight } from 'lucide-react';
import Link from 'next/link';
interface ActionLinkCardProps {
href: string;
title: string;
description: string;
}
export function ActionLinkCard({ href, title, description }: ActionLinkCardProps) {
return (
<Link
href={href}
className="group bg-white/40 backdrop-blur-md border border-white/40 p-5 rounded-3xl shadow-sm hover:shadow-md hover:bg-white/50 transition-all duration-300 flex flex-col gap-2"
>
<div className="flex items-center gap-3">
<div className="bg-white w-10 h-10 rounded-xl flex items-center justify-center shrink-0 text-slate-teal group-hover:bg-seafoam group-hover:text-eggshell transition-colors shadow-sm">
<ArrowRight size={20} className="group-hover:rotate-45 transition-transform" />
</div>
<h3 className="font-black text-ebony text-lg uppercase tracking-wide leading-tight">
{title}
</h3>
</div>
<p className="text-sm text-ebony/70 font-medium leading-relaxed w-full m-0 pl-1">
{description}
</p>
</Link>
);
}
+87
View File
@@ -0,0 +1,87 @@
// app/components/ui/Cards.tsx
import { Archive, CheckCircle, Download, ExternalLink } from 'lucide-react';
import { ReactNode } from 'react';
// --- Types ---
export interface DocumentItem {
title: string;
file: string;
icon: ReactNode;
size: string;
}
export interface LinkItem {
title: string;
url: string;
}
// --- Document Card Component ---
interface DocumentCardProps {
doc: DocumentItem;
isOffline: boolean;
isCached: boolean;
}
export function DocumentCard({ doc, isOffline, isCached }: DocumentCardProps) {
return (
<a
href={doc.file}
target="_blank"
rel="noopener noreferrer"
className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline && !isCached
? 'opacity-50 grayscale cursor-not-allowed pointer-events-none border-transparent bg-eggshell/50'
: 'bg-white/50 hover:bg-white border-slate-teal/10'
}`}
>
<div className="flex items-center text-slate-teal">
<div className={`p-2.5 rounded-xl mr-3 text-eggshell shrink-0 transition-colors ${isCached ? 'bg-moss' : 'bg-slate-teal'}`}>
{doc.icon}
</div>
<div className="flex flex-col">
<span className="font-bold text-sm leading-tight text-ebony">{doc.title}</span>
<div className="flex flex-wrap items-center gap-2 mt-1">
<span className="text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded uppercase tracking-wider">
{doc.size}
</span>
{isCached && (
<span className="flex items-center gap-1 text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded uppercase tracking-wider">
<Archive size={10} /> Nedladdad
</span>
)}
</div>
</div>
</div>
{isCached ? (
<CheckCircle size={18} className="text-moss shrink-0 ml-2" />
) : (
<Download size={18} className="text-slate-teal/50 group-hover:text-slate-teal transition-colors shrink-0 ml-2" />
)}
</a>
);
}
// --- External Link Card Component ---
interface ExternalLinkCardProps {
link: LinkItem;
isOffline: boolean;
}
export function ExternalLinkCard({ link, isOffline }: ExternalLinkCardProps) {
return (
<a
href={isOffline ? '#' : link.url}
target={isOffline ? '_self' : '_blank'}
rel="noopener noreferrer"
className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline
? 'opacity-40 grayscale cursor-not-allowed pointer-events-none bg-eggshell/50 border-transparent'
: 'bg-white/50 hover:bg-white border-slate-teal/10'
}`}
>
<span className="font-bold text-slate-teal text-sm leading-tight pr-4">{link.title}</span>
<div className={`rounded-full p-2 transition-colors shrink-0 ${isOffline ? 'bg-transparent' : 'bg-white group-hover:bg-seafoam'}`}>
<ExternalLink size={16} className={isOffline ? 'text-ebony/40' : 'text-slate-teal group-hover:text-eggshell'} />
</div>
</a>
);
}
+36
View File
@@ -0,0 +1,36 @@
// app/components/ui/FAQItem.tsx
import { ChevronDown, ChevronUp } from 'lucide-react';
interface FAQItemProps {
id: string;
question: string;
answer: string;
isOpen: boolean;
onToggle: (id: string) => void;
}
export function FAQItem({ id, question, answer, isOpen, onToggle }: FAQItemProps) {
return (
<div className={`rounded-xl overflow-hidden transition-colors ${isOpen ? 'bg-white/80 shadow-sm' : 'bg-white/40 hover:bg-white/60'}`}>
<button
className="w-full text-left p-3 flex justify-between items-center focus:outline-none"
onClick={() => onToggle(id)}
>
<span className="font-bold text-slate-teal text-sm pr-4 leading-snug">{question}</span>
<div className={`p-1 rounded-full transition-colors ${isOpen ? 'bg-seafoam text-eggshell' : 'text-seafoam'}`}>
{isOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</div>
</button>
{/* The CSS Grid accordion trick */}
<div className={`grid transition-all duration-300 ease-in-out ${isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'}`}>
<div className="overflow-hidden">
<div className="p-2 pb-3 text-ebony font-medium text-sm leading-relaxed border-t border-ebony/20 mx-4">
{answer}
</div>
</div>
</div>
</div>
);
}
+15
View File
@@ -0,0 +1,15 @@
// app/components/ui/OfflineBadge.tsx
import { WifiOff } from 'lucide-react';
interface OfflineBadgeProps {
className?: string;
}
export function OfflineBadge({ className = "" }: OfflineBadgeProps) {
return (
<div className={`bg-goldenrod/10 text-goldenrod border border-goldenrod/20 px-3 py-1.5 rounded-full flex items-center w-fit text-xs font-black uppercase tracking-widest ${className}`}>
<WifiOff size={14} className="mr-2" /> Offline-läge
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
// app/components/ui/PageHeader.tsx
import { LucideIcon } from 'lucide-react';
import { ReactNode } from 'react';
interface PageHeaderProps {
title: string;
icon: LucideIcon;
description: string;
variant?: 'default' | 'emergency';
actions?: ReactNode;
}
export function PageHeader({
title,
icon: Icon,
description,
variant = 'default',
actions
}: PageHeaderProps) {
// Set colors based on the variant
const isEmergency = variant === 'emergency';
const titleColor = isEmergency ? 'text-emergency uppercase' : 'text-slate-teal';
const iconColor = isEmergency ? 'text-emergency' : 'text-seafoam';
const descColor = isEmergency ? 'text-ebony' : 'text-moss';
return (
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-4 mb-4">
<div>
<h1 className={`text-3xl font-black tracking-tight flex items-center ${titleColor}`}>
<Icon className={`mr-3 shrink-0 ${iconColor}`} size={32} />
{title}
</h1>
<p className={`font-medium mt-2 ml-11 ${descColor} hidden sm:visible`}>
{description}
</p>
</div>
{actions && (
<div className="flex flex-col gap-2 items-start md:items-end ml-11 md:ml-0 mt-2 md:mt-0">
{actions}
</div>
)}
</div>
);
}
+85
View File
@@ -0,0 +1,85 @@
// app/components/ui/SectionCard.tsx
import { LucideIcon } from 'lucide-react';
import { ReactNode } from 'react';
interface SectionCardProps {
title: string;
icon?: LucideIcon;
children: ReactNode;
description?: ReactNode;
variant?: 'default' | 'alert' | 'highlight';
className?: string;
}
export function SectionCard({
title,
icon: Icon,
children,
description,
variant = 'default',
className = ""
}: SectionCardProps) {
// Define base styles for the card container
let cardStyle = "rounded-3xl shadow-sm transition-all duration-300 ";
let headerStyle = "text-xl font-black uppercase tracking-widest mb-4 flex items-center ";
let iconStyle = "mr-3 shrink-0 ";
let descStyle = "text-sm mb-6 leading-relaxed ";
// Apply specific styles based on the chosen variant
switch (variant) {
case 'alert':
cardStyle += "bg-emergency/10 border-2 border-emergency/30 p-6 md:p-8 hover:shadow-md";
headerStyle += "text-emergency";
iconStyle += "text-emergency";
descStyle += "text-emergency/90 font-bold";
break;
case 'highlight':
cardStyle += "bg-goldenrod/30 border border-goldenrod/20 p-5";
headerStyle += "text-sm text-goldenrod mb-1";
iconStyle += "text-goldenrod mt-0.5";
descStyle += "text-ebony font-medium m-0";
break;
case 'default':
default:
cardStyle += "bg-white/40 backdrop-blur-md border border-white/40 p-6 md:p-8 hover:shadow-md";
headerStyle += "text-ebony";
iconStyle += "text-slate-teal";
descStyle += "text-ebony/80 font-medium";
break;
}
return (
<section className={`${cardStyle} ${className}`}>
<div className={variant === 'highlight' ? 'flex items-start' : ''}>
{variant === 'highlight' && Icon && (
<Icon className={iconStyle} size={24} />
)}
<div className={variant === 'highlight' ? 'flex-1' : ''}>
<h2 className={headerStyle}>
{variant !== 'highlight' && Icon && <Icon className={iconStyle} size={24} />}
{title}
</h2>
{description && (
<div className={descStyle}>
{description}
</div>
)}
{variant === 'highlight' ? (
<div className="text-sm font-medium leading-relaxed text-ebony">
{children}
</div>
) : (
children
)}
</div>
</div>
</section>
);
}