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
17 changes: 17 additions & 0 deletions src/components/block.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import { render } from "astro:content";
import { getBlock } from "../lib/blocks";

interface Props {
slug: string;
class?: string;
}

const { slug, class: className } = Astro.props;
const { Content } = await render(await getBlock(slug));
---

{/* Not elements.tsx: its links all open in a new tab and its lists and headings are article-sized. */}
<div class:list={[className, "[&_a]:text-accent [&_a:hover]:underline [&_strong]:font-semibold [&_strong]:text-ink [&_em]:font-medium [&_em]:not-italic [&_em]:text-ink"]}>
<Content />
</div>
62 changes: 20 additions & 42 deletions src/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,16 @@
import { SiLinkedin, SiYoutube, SiFlickr } from "react-icons/si";
import React from "react";
import { site } from "../lib/site";

const BASE = import.meta.env.BASE_URL.replace(/\/$/, '');

const footerLinks = [
{ href: `${BASE}/about`, label: "About" },
{ href: `${BASE}/research-support`, label: "Research Support" },
{ href: `${BASE}/services`, label: "Services" },
{ href: `${BASE}/funding-and-projects`, label: "Funding & Projects" },
{ href: `${BASE}/news`, label: "News" },
{ href: `${BASE}/events`, label: "Events" },
{ href: `${BASE}/training`, label: "Training" },
{ href: `${BASE}/accessibility`, label: "Accessibility" },
];
const { navigation, careers, partners, funders, fundingNote } = site.footer;
const { supportEmail } = site.contact;

const socials = [
{ name: "LinkedIn", href: "https://www.linkedin.com/company/elixir-norway", icon: SiLinkedin },
{ name: "YouTube", href: "https://www.youtube.com/channel/UCvwFIw5HomylguGOGxR8B8w", icon: SiYoutube },
{ name: "Flickr", href: "https://www.flickr.com/photos/elixir-europe/", icon: SiFlickr },
];

const partners = [
{ href: "https://uib.no/", src: `${BASE}/assets/logos/orgs/uib.svg`, alt: "University of Bergen" },
{ href: "https://uio.no/", src: `${BASE}/assets/logos/orgs/uio.svg`, alt: "University of Oslo" },
{ href: "https://uit.no/", src: `${BASE}/assets/logos/orgs/uit.svg`, alt: "UiT The Arctic University of Norway" },
{ href: "https://ntnu.no/", src: `${BASE}/assets/logos/orgs/ntnu.svg`, alt: "NTNU" },
{ href: "https://nmbu.no/", src: `${BASE}/assets/logos/orgs/nmbu.svg`, alt: "NMBU" },
];

const funders = [
{ href: "https://forskningsradet.no/", src: `${BASE}/assets/logos/orgs/nfr.svg`, alt: "Research Council of Norway" },
{ href: "https://www.nordforsk.org", src: `${BASE}/assets/logos/orgs/nordforsk.svg`, alt: "NordForsk" },
{ href: "https://research-and-innovation.ec.europa.eu/", src: `${BASE}/assets/logos/orgs/eu.svg`, alt: "Co-funded by the European Union" },
{ name: "LinkedIn", href: site.footer.socials.linkedin, icon: SiLinkedin },
{ name: "YouTube", href: site.footer.socials.youtube, icon: SiYoutube },
{ name: "Flickr", href: site.footer.socials.flickr, icon: SiFlickr },
];

const Footer = () => {
Expand Down Expand Up @@ -93,7 +72,7 @@ const Footer = () => {
<div className="lg:col-span-2">
<h3 className="text-sm font-semibold text-ink mb-4">Navigation</h3>
<ul className="space-y-2">
{footerLinks.map(({ href, label }) => (
{navigation.map(({ href, label }) => (
<li key={label}>
<a
href={href}
Expand All @@ -111,18 +90,18 @@ const Footer = () => {
<h3 className="text-sm font-semibold text-ink mb-4">Contact</h3>
<ul className="space-y-2 text-sm text-body">
<li>
<a href="mailto:support@elixir.no" className="hover:text-ink decoration-marker decoration-2 underline-offset-4 hover:underline transition-colors">
support@elixir.no
<a href={`mailto:${supportEmail}`} className="hover:text-ink decoration-marker decoration-2 underline-offset-4 hover:underline transition-colors">
{supportEmail}
</a>
</li>
<li>
<a
href="https://elixir-europe.org/about-us/vacancies"
href={careers.href}
target="_blank"
rel="noopener noreferrer"
className="hover:text-ink decoration-marker decoration-2 underline-offset-4 hover:underline transition-colors"
>
Careers at ELIXIR
{careers.label}
</a>
</li>
</ul>
Expand All @@ -132,35 +111,35 @@ const Footer = () => {
<div className="lg:col-span-4">
<h3 className="text-sm font-semibold text-ink mb-4">Partner institutions</h3>
<div className="flex flex-wrap items-center gap-4">
{partners.map(({ href, src, alt }) => (
{partners.map(({ name, href, logo }) => (
<a
key={alt}
key={name}
href={href}
target="_blank"
rel="noopener noreferrer"
className="opacity-80 hover:opacity-100 transition-opacity focus:outline-none focus-visible:ring-2 focus-visible:ring-accent rounded-control"
>
<img
src={src}
alt={alt}
src={logo}
alt={name}
className="h-10 w-auto dark:invert-85"
/>
</a>
))}
</div>
<h3 className="text-sm font-semibold text-ink mt-6 mb-4">Funded by</h3>
<div className="flex flex-wrap items-center gap-4">
{funders.map(({ href, src, alt }) => (
{funders.map(({ name, href, logo }) => (
<a
key={alt}
key={name}
href={href}
target="_blank"
rel="noopener noreferrer"
className="opacity-80 hover:opacity-100 transition-opacity focus:outline-none focus-visible:ring-2 focus-visible:ring-accent rounded-control"
>
<img
src={src}
alt={alt}
src={logo}
alt={name}
className="h-10 w-auto dark:invert-85"
/>
</a>
Expand All @@ -174,8 +153,7 @@ const Footer = () => {
<div className="border-t border-rule">
<div className="mx-auto max-w-7xl px-6 sm:px-8 py-6">
<p className="text-xs text-center text-balance text-muted">
Financed by the Research Council of Norway (grants 208481, 270068, 295932, 350529),
its partner institutions, NordForsk, and co-funded by the European Union.
{fundingNote}
</p>
</div>
</div>
Expand Down
41 changes: 16 additions & 25 deletions src/components/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,22 @@ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
import { PauseIcon, PlayIcon } from '@heroicons/react/20/solid';
import Button from './button';
import HeroVideo from './hero-video';
import type { HeroCopy } from '../lib/site';


const BASE = import.meta.env.BASE_URL.replace(/\/$/, '');

// The first phrase is also the static fallback and completes the sr-only heading.
const WORDS = [
'life science', 'genomics', 'bioinformatics', 'biomedical', 'proteomics',
'AI-ready data', 'scientists across Norway', 'discoveries that matter', 'data you can trust', 'understanding life', 'the next generation',
];
const FLIP_MS = 600;
// Multi-word phrases stay a little longer than single fields so they can be read.
const dwellMs = (word: string) => (word.includes(' ') && word !== WORDS[0] ? 2600 : 2300);
const dwellMs = (word: string, words: string[]) => (word.includes(' ') && word !== words[0] ? 2600 : 2300);

function RotatingWord({ playing }: { playing: boolean }) {
function RotatingWord({ words, playing }: { words: string[]; playing: boolean }) {
const [{ current, leaving }, setFlip] = useState<{ current: number; leaving: number | null }>({ current: 0, leaving: null });

useEffect(() => {
if (!playing) return;
const id = setTimeout(() => {
setFlip(({ current }) => ({ current: (current + 1) % WORDS.length, leaving: current }));
}, FLIP_MS + dwellMs(WORDS[current]));
setFlip(({ current }) => ({ current: (current + 1) % words.length, leaving: current }));
}, FLIP_MS + dwellMs(words[current], words));
return () => clearTimeout(id);
}, [playing, current]);
}, [playing, current, words]);

useEffect(() => {
if (leaving === null) return;
Expand All @@ -36,7 +29,7 @@ function RotatingWord({ playing }: { playing: boolean }) {

return (
<span className="rotor" aria-hidden="true">
{WORDS.map((word, i) => {
{words.map((word, i) => {
const state = i === current
? (leaving === null ? 'is-on' : 'is-on is-in')
: i === leaving ? 'is-out' : '';
Expand All @@ -46,7 +39,7 @@ function RotatingWord({ playing }: { playing: boolean }) {
);
}

export function Hero() {
export function Hero({ copy }: { copy: HeroCopy }) {
const shouldReduceMotion = useReducedMotion();
const [paused, setPaused] = useState(false);
const playing = !paused && !shouldReduceMotion;
Expand All @@ -73,12 +66,12 @@ export function Hero() {
/>
<motion.div {...fadeUp} transition={{ duration: 0.6, delay: 0.1 }}>
<a
href="https://elixir-europe.org"
href={copy.tagline.href}
target="_blank"
rel="noopener noreferrer"
className="hero-legible inline-flex items-center gap-2.5 text-sm font-semibold text-ink hover:underline underline-offset-4 before:h-2 before:w-2 before:rounded-marker before:bg-marker before:content-['']"
>
Part of the European ELIXIR infrastructure
{copy.tagline.label}
<ArrowTopRightOnSquareIcon className="h-3.5 w-3.5 text-muted" aria-hidden="true" />
</a>
</motion.div>
Expand All @@ -88,10 +81,10 @@ export function Hero() {
transition={{ duration: 0.6, delay: 0.2 }}
className="mt-4 sm:mt-5 text-3xl sm:text-4xl md:text-5xl xl:text-6xl font-semibold tracking-[-0.035em] text-ink leading-[1.05] text-balance hero-legible"
>
<span className="sr-only">Research infrastructure for life science</span>
<span aria-hidden="true">Research infrastructure for</span>
<span className="sr-only">{`${copy.headline} ${copy.rotatingWords[0]}`}</span>
<span aria-hidden="true">{copy.headline}</span>
<span className="block">
<RotatingWord playing={playing} />
<RotatingWord words={copy.rotatingWords} playing={playing} />
</span>
</motion.h1>

Expand All @@ -100,18 +93,16 @@ export function Hero() {
transition={{ duration: 0.6, delay: 0.3 }}
className="mt-6 sm:mt-8 text-base sm:text-lg leading-relaxed text-ink max-w-2xl mx-auto hero-legible"
>
ELIXIR Norway supports life science researchers with bioinformatics
services, data management tools, and secure e-infrastructure.
Part of Europe's leading bioinformatics network.
{copy.subline}
</motion.p>

<motion.div
{...fadeUp}
transition={{ duration: 0.6, delay: 0.4 }}
className="mt-8 sm:mt-10 flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-8"
>
<Button href={`${BASE}/services`}>Explore services</Button>
<Button href={`${BASE}/research-support`} variant="link" className="hero-legible">Get support</Button>
<Button href={copy.primaryAction.href}>{copy.primaryAction.label}</Button>
<Button href={copy.secondaryAction.href} variant="link" className="hero-legible">{copy.secondaryAction.label}</Button>
</motion.div>
</div>
</div>
Expand Down
7 changes: 4 additions & 3 deletions src/components/pricing.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { CheckIcon } from '@heroicons/react/24/outline';
import Button from './button';
import type { PricingTier } from '../lib/site';

export default function Pricing({ tiers }) {
export default function Pricing({ tiers, email }: { tiers: PricingTier[]; email: string }) {
return (
<div className="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{tiers.map((tier) => (
<div
key={tier.id}
key={tier.name}
className={`relative flex flex-col rounded-card border bg-surface p-5 ${
tier.mostPopular
? 'border-ink'
Expand Down Expand Up @@ -50,7 +51,7 @@ export default function Pricing({ tiers }) {
</ul>

<Button
href="mailto:support@elixir.no"
href={`mailto:${email}`}
variant={tier.mostPopular ? 'primary' : 'link'}
size="sm"
className="mt-6 self-center"
Expand Down
3 changes: 3 additions & 0 deletions src/content/blocks/research-support-model/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Projects that can be solved within a working day are **free of charge**, enabled by funding from the Research Council of Norway. For more demanding projects, an hourly rate applies and you will need to enter an agreement with ELIXIR Norway.

Projects beyond technical assistance may be set up as a research collaboration, e.g. by providing funding for a percentage of the position of a helpdesk member.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: The Service Agreement
---

For requests requiring more than a working day, an agreement between the client and the platform must be made covering service type and cost. If applicable, the agreement should include a plan for *publication* and *co-authorship*. Regardless of contribution type, we expect to be included in the **acknowledgements** of resulting publications.
5 changes: 5 additions & 0 deletions src/content/blocks/training-domain-specific/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Domain-Specific Courses
---

Specialised training in single-cell sequencing, microbiome analysis, biodiversity studies, genome assembly, and more.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Research Data Management
---

General RDM courses covering data collection, metadata management, sensitive data regulations, and FAIR data handling best practices.
5 changes: 5 additions & 0 deletions src/content/blocks/training-tool-specific/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Tool-Specific Courses
---

Hands-on training for [NeLS](https://nels.elixir.no), [UseGalaxy](https://galaxyproject.org/use/usegalaxy-no/), [Data Stewardship Wizard](https://norway.dsw.elixir-europe.org/wizard/), [FEGA-Norway](https://ega.elixir.no/), and other tools in the ELIXIR ecosystem.
5 changes: 5 additions & 0 deletions src/content/blocks/training-train-the-trainer/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Train-the-Trainer
---

Part of the [ELIXIR-GOBLET TtT programme](https://elixir-europe.org/platforms/training/train-the-trainer). Learn teaching techniques for productive learner engagement and effective course design.
11 changes: 11 additions & 0 deletions src/content/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,22 @@ const services = defineCollection({
}),
});

// Prose that editors change and that needs inline links or emphasis, rendered
// into pages by src/components/block.astro. Short plain strings live in
// src/data/site.json instead.
const blocks = defineCollection({
type: 'content',
schema: z.object({
title: z.string().trim().min(1).optional(),
}).strict(),
});

export const collections = {
news,
services,
events,
about,
'funding-and-projects': fundingAndProjects,
accessibility,
blocks,
};
Loading
Loading