UI improvments
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user