Skip to content
Merged
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
"@astrojs/tailwind": "^6.0.0",
"@headlessui/react": "^2.1.2",
"@heroicons/react": "^2.1.3",
"@react-three/drei": "9.117.0",
"@react-three/fiber": "8.17.10",
"@tailwindcss/forms": "^0.5.7",
"animate.css": "^4.1.1",
"astro": "^5.0.0",
"dayjs": "^1.11.12",
"dompurify": "^3.2.4",
"framer-motion": "^12.35.2",
"leaflet": "^1.9.4",
"lodash": "^4.17.21",
"react": "^18.2.0",
Expand All @@ -40,6 +43,7 @@
"sass": "^1.77.8",
"slugify": "^1.6.6",
"tailwindcss": "^3.4.3",
"three": "^0.183.2",
"typescript": "^5.4.5"
},
"devDependencies": {
Expand Down
649 changes: 649 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions src/components/about-hero-visual.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react';
import { motion, useReducedMotion } from 'framer-motion';

interface Chip {
label: string;
size: 'lg' | 'md' | 'sm';
x: number;
y: number;
color?: string;
rotate?: number;
}

const CHIPS: Chip[] = [
{ label: 'UiB Bergen', size: 'lg', x: 12, y: 6, color: '#dc3545', rotate: -2 },
{ label: 'UiO Oslo', size: 'lg', x: 60, y: 2, color: '#c8102e', rotate: 1 },
{ label: 'UiT Tromsø', size: 'lg', x: 2, y: 45, color: '#003349', rotate: -1 },
{ label: 'NTNU Trondheim', size: 'lg', x: 48, y: 80, color: '#00509e', rotate: 2 },
{ label: 'NMBU Ås', size: 'lg', x: 70, y: 42, color: '#005f3b', rotate: -1 },
{ label: 'Services', size: 'md', x: 50, y: 26 },
{ label: 'Training', size: 'md', x: 0, y: 76 },
{ label: 'Helpdesk', size: 'md', x: 72, y: 66, rotate: -1 },
{ label: 'NeLS', size: 'md', x: 34, y: 52, rotate: 1 },
{ label: 'Storage', size: 'sm', x: 28, y: 28, rotate: 2 },
{ label: 'Sensitive Data', size: 'sm', x: 16, y: 66, rotate: -2 },
{ label: 'Bioinformatics', size: 'sm', x: 56, y: 56, rotate: 1 },
];

const sizeClasses = {
lg: 'px-6 py-3 text-base font-bold',
md: 'px-5 py-2.5 text-sm font-semibold',
sm: 'px-4 py-2 text-xs font-semibold',
};

const BLOBS = [
{ x: 20, y: 25, size: 220, color: 'from-brand-primary/20 to-brand-secondary/10', delay: 0 },
{ x: 65, y: 60, size: 180, color: 'from-brand-secondary/15 to-orange-300/10', delay: 2 },
{ x: 40, y: 75, size: 140, color: 'from-blue-400/10 to-brand-primary/15', delay: 4 },
];

export default function AboutHeroVisual() {
const shouldReduceMotion = useReducedMotion();

return (
<div
className="relative w-full aspect-[4/3] max-w-lg mx-auto lg:max-w-none"
style={{ mask: 'linear-gradient(to right, transparent, black 15%, black 85%, transparent), linear-gradient(to bottom, transparent, black 15%, black 85%, transparent)', maskComposite: 'intersect', WebkitMask: 'linear-gradient(to right, transparent, black 15%, black 85%, transparent), linear-gradient(to bottom, transparent, black 15%, black 85%, transparent)', WebkitMaskComposite: 'source-in' }}
role="img"
aria-label="ELIXIR Norway network: five universities and key services"
>
{/* Gradient blobs — background depth */}
{BLOBS.map((blob, i) => (
<motion.div
key={i}
className={`absolute rounded-full bg-gradient-to-br ${blob.color} blur-3xl`}
style={{
left: `${blob.x}%`,
top: `${blob.y}%`,
width: blob.size,
height: blob.size,
transform: 'translate(-50%, -50%)',
}}
animate={shouldReduceMotion ? {} : {
x: [0, 20, -15, 10, 0],
y: [0, -15, 10, -20, 0],
scale: [1, 1.1, 0.95, 1.05, 1],
}}
transition={{
duration: 12 + i * 3,
repeat: Infinity,
ease: 'easeInOut',
delay: blob.delay,
}}
aria-hidden="true"
/>
))}

{/* Subtle dot grid */}
<svg className="absolute inset-0 w-full h-full opacity-[0.04] dark:opacity-[0.06]" aria-hidden="true">
<defs>
<pattern id="dots" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="1" fill="currentColor" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#dots)" />
</svg>

{/* Floating chips */}
{CHIPS.map((chip, i) => {
const hasColor = !!chip.color;
return (
<motion.div
key={chip.label}
className={`absolute rounded-full select-none whitespace-nowrap ${sizeClasses[chip.size]} ${
hasColor
? 'text-white shadow-lg'
: 'bg-white/90 dark:bg-white/[0.1] text-brand-primary dark:text-gray-200 border border-gray-200/60 dark:border-gray-700/30 backdrop-blur-md shadow-sm'
}`}
style={{
left: `${chip.x}%`,
top: `${chip.y}%`,
...(hasColor ? {
backgroundColor: chip.color,
boxShadow: `0 8px 24px -4px ${chip.color}40`,
} : {}),
}}
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.5, rotate: (chip.rotate || 0) * 3 }}
animate={shouldReduceMotion ? { rotate: chip.rotate || 0 } : {
opacity: 1,
scale: 1,
rotate: chip.rotate || 0,
y: [0, -(5 + i % 4 * 3), 0, (4 + i % 3 * 2), 0],
x: [0, (3 + i % 3 * 2), 0, -(2 + i % 2 * 3), 0],
}}
transition={shouldReduceMotion ? {} : {
opacity: { duration: 0.5, delay: 0.05 + i * 0.06 },
scale: { duration: 0.5, delay: 0.05 + i * 0.06, type: 'spring', stiffness: 200 },
rotate: { duration: 0.5, delay: 0.05 + i * 0.06 },
y: { duration: 6 + i * 0.7, repeat: Infinity, ease: 'easeInOut', delay: i * 0.3 },
x: { duration: 7 + i * 0.8, repeat: Infinity, ease: 'easeInOut', delay: i * 0.5 },
}}
>
{chip.label}
</motion.div>
);
})}
</div>
);
}
94 changes: 54 additions & 40 deletions src/components/callout.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,68 @@
import React from 'react';
import { FaExclamationCircle } from "react-icons/fa";
import { MdCancel, MdCheck, MdInfo } from "react-icons/md";

const Callout = ({ variant = 'info', title, children }) => {

const variants = {
info: {
bgColor: 'bg-blue-50 dark:bg-blue-800/25',
textColor: 'text-blue-800 dark:text-blue-200',
icon: MdInfo,
iconColor: 'text-blue-400 dark:text-blue-200',
},
success: {
bgColor: 'bg-green-50',
textColor: 'text-green-800',
icon: MdCheck,
iconColor: 'text-green-400',
},
warn: {
bgColor: 'bg-yellow-50',
textColor: 'text-yellow-800',
icon: FaExclamationCircle,
iconColor: 'text-yellow-400',
},
danger: {
bgColor: 'bg-red-50',
textColor: 'text-red-800',
icon: MdCancel,
iconColor: 'text-red-400',
},
};
const variants = {
info: {
border: 'border-blue-400 dark:border-blue-500',
bg: 'bg-blue-50/50 dark:bg-blue-900/10',
title: 'text-blue-900 dark:text-blue-200',
text: 'text-blue-800 dark:text-blue-300',
icon: (
<svg className="h-5 w-5 text-blue-500 dark:text-blue-400" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
),
},
success: {
border: 'border-green-400 dark:border-green-500',
bg: 'bg-green-50/50 dark:bg-green-900/10',
title: 'text-green-900 dark:text-green-200',
text: 'text-green-800 dark:text-green-300',
icon: (
<svg className="h-5 w-5 text-green-500 dark:text-green-400" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
),
},
warn: {
border: 'border-yellow-400 dark:border-yellow-500',
bg: 'bg-yellow-50/50 dark:bg-yellow-900/10',
title: 'text-yellow-900 dark:text-yellow-200',
text: 'text-yellow-800 dark:text-yellow-300',
icon: (
<svg className="h-5 w-5 text-yellow-500 dark:text-yellow-400" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
),
},
danger: {
border: 'border-red-400 dark:border-red-500',
bg: 'bg-red-50/50 dark:bg-red-900/10',
title: 'text-red-900 dark:text-red-200',
text: 'text-red-800 dark:text-red-300',
icon: (
<svg className="h-5 w-5 text-red-500 dark:text-red-400" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
),
},
};

const { bgColor, textColor, icon: Icon, iconColor } = variants[variant] || variants.info;
const Callout = ({ variant = 'info', title, children }) => {
const v = variants[variant] || variants.info;

return (
<div className={`rounded-md ${bgColor} p-4`}>
<div className="flex">
<div className="flex-shrink-0">
<Icon aria-hidden="true" className={`h-auto w-6 ${iconColor}`}/>
</div>
<div className="ml-3">
{title && <h3 className={`text-lg font-medium ${textColor}`}>{title}</h3>}
<div className={`mt-2 ${textColor} [&:first-child]:mt-0 [&_a]:font-bold [&_p]:text-base [&_p:first-child]:mt-0`}>
<div className={`my-6 rounded-lg border-l-4 ${v.border} ${v.bg} px-5 py-4`}>
<div className="flex items-start gap-3">
<div className="shrink-0 mt-0.5">{v.icon}</div>
<div className="min-w-0">
{title && <p className={`text-base font-semibold ${v.title}`}>{title}</p>}
<div className={`mt-1 text-base leading-relaxed ${v.text} [&_a]:font-semibold [&_a]:underline [&_a]:underline-offset-2 [&_p]:text-base [&_p:first-child]:mt-0`}>
{children}
</div>
</div>
</div>
</div>
);

};

export default Callout;
11 changes: 5 additions & 6 deletions src/components/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ export type CardTypeProps = {

export default function Card({ title, icon: Icon, children, margin = true, className = "" }: CardTypeProps) {
return (
<div
className={`${margin ? "my-8" : ''} overflow-hidden rounded-lg bg-white dark:bg-dark-surface shadow ${className}`}>
<div className="px-4 py-5 sm:px-6 flex gap-x-4 items-start shadow-md">
{Icon && <Icon className="w-24 h-auto"/>}
<h3 className="font-bold text-xl">{title}</h3>
<div className={`${margin ? "my-8" : ''} rounded-xl border border-gray-200/60 dark:border-gray-700/30 bg-white dark:bg-white/[0.03] ${className}`}>
<div className="px-5 py-4 border-b border-gray-200/60 dark:border-gray-700/30 flex gap-x-3 items-center">
{Icon && <Icon className="w-5 h-5 text-brand-secondary shrink-0"/>}
<h3 className="font-semibold text-lg text-brand-primary dark:text-white">{title}</h3>
</div>
<div className="px-4 py-5 sm:p-6 [&_p]:text-base [&_p]:leading-relaxed">
<div className="px-5 py-4 text-sm leading-relaxed text-brand-grey dark:text-gray-300 [&_p]:text-sm [&_p]:leading-relaxed [&_p:first-child]:mt-0 [&_p:last-child]:mb-0">
{children}
</div>
</div>
Expand Down
Loading