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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 openSenseMap
Copyright (c) 2026 openSenseMap

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
7 changes: 7 additions & 0 deletions app/components/header/menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Globe,
LogIn,
LogOut,
Puzzle,

Check warning on line 5 in app/components/header/menu/index.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Lint

eslint(no-unused-vars)

Identifier 'Puzzle' is imported but never used.
Menu as MenuIcon,
FileLock2,
Coins,
Expand All @@ -12,6 +12,7 @@
Compass,
ScrollText,
MessagesSquare,
Info,
} from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -103,6 +104,12 @@
<Spinner />
</div>
)}
<Link to="/about">
<DropdownMenuItem className="cursor-pointer">
<Info className="mr-2 h-5 w-5" />
<span>{t('about_label')}</span>
</DropdownMenuItem>
</Link>
{!(matches[1].pathname === '/explore') && (
<Link to="/explore">
<DropdownMenuItem className="cursor-pointer">
Expand Down
5 changes: 3 additions & 2 deletions app/components/landing/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'

export default function Footer() {
const { t } = useTranslation('footer')
const currentYear = new Date().getFullYear();
return (
<footer
id="footer"
Expand All @@ -10,7 +11,7 @@ export default function Footer() {
<hr className="my-6 border-gray-300 sm:mx-auto lg:my-8 dark:border-gray-100" />
<div className="mx-auto w-full max-w-(--breakpoint-xl) p-4 md:flex md:items-center md:justify-between">
<span className="text-sm text-gray-500 sm:text-center dark:text-gray-400">
© 2025{' '}
© {currentYear}{' '}
<a href="https://opensenselab.org/" className="hover:underline">
openSenseLab
</a>
Expand Down Expand Up @@ -72,7 +73,7 @@ export default function Footer() {
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231 5.45-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z" />
</svg>
<span className="sr-only">{t('twitter')}</span>
</a>
Expand Down
2 changes: 1 addition & 1 deletion app/components/landing/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import LanguageSelector from './language-selector'

const links = [
{
name: 'Explore',
name: 'Map',
link: '/explore',
},
{
Expand Down
12 changes: 0 additions & 12 deletions app/components/landing/sections/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,6 @@ export default function Features() {
{t('dataAggregation')}
</div>
</div>
<div className="flex flex-col rounded-sm border-2 px-4 py-2 text-lg">
<div className="flex items-center gap-3">
<Trash className="mr-2 h-4 w-4" />
{t('noDataRetention')}
</div>
</div>
<div className="flex flex-col rounded-sm border-2 px-4 py-2 text-lg">
<div className="flex items-center gap-3">
<Copyleft className="mr-2 h-4 w-4" />
Expand All @@ -48,12 +42,6 @@ export default function Features() {
{t('discoverDevices')}
</div>
</div>
<div className="flex rounded-sm border-2 px-4 py-2 text-lg">
<div className="flex items-center gap-3">
<Scale className="mr-2 h-4 w-4" />
{t('compareDevices')}
</div>
</div>
<div className="flex rounded-sm border-2 px-4 py-2 text-lg">
<div className="flex items-center gap-3">
<Download className="mr-2 h-4 w-4" />
Expand Down
4 changes: 2 additions & 2 deletions app/components/landing/sections/partners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export default function Partners({ data }: PartnersProps) {
}}
className="flex flex-col items-center justify-center"
>
<p>{t('hosted')}</p>
<img src="/img/openSenseLab_logo.png" alt="openSenseLab Logo"></img>
<p>{t('made_by')}</p>
<img src="/img/openSenseLab_Logo.svg" alt="openSenseLab Logo" className='p-2'></img>
</motion.div>
</div>
</div>
Expand Down
263 changes: 4 additions & 259 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,260 +1,5 @@
import { readItems } from '@directus/sdk'
import { useMediaQuery } from '@mantine/hooks'
import { motion } from 'framer-motion'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import {
type LoaderFunctionArgs,
data,
Link,
useLoaderData,
} from 'react-router'
import { type Route } from './+types/_index'
import Footer from '~/components/landing/footer'
import { GlobeComponent } from '~/components/landing/globe.client'
import Header from '~/components/landing/header/header'
import Connect from '~/components/landing/sections/connect'
import Features from '~/components/landing/sections/features'
import Integrations from '~/components/landing/sections/integrations'
import Partners from '~/components/landing/sections/partners'
import PricingPlans from '~/components/landing/sections/pricing-plans'
import Stats from '~/components/landing/stats'
import { getLatestDevices } from '~/db/models/device.server'
import { type SupportedLanguage } from '~/i18next-config'
import { type Partner, getDirectusClient } from '~/lib/directus'
import { getLocale } from '~/middleware/i18next'
import { getUserId, getUserName } from '~/services/session-service.server'
import { redirect } from "react-router";

const sections = [
{
title: 'Features',
description:
'The openSenseMap platform has a lot to offer that makes discoverability and sharing of environmental and sensor data easy.',
component: Features,
},
{
title: 'Connect',
description:
'Connect your devices to the openSenseMap platform and start sharing your environmental data with the world.',
component: Connect,
},
{
title: 'Integrations',
description:
'Integrate your devices with the openSenseMap platform and start sharing your environmental data with the world.',
component: Integrations,
},
{
title: 'Pricing',
description:
'Choose the right pricing plan for your needs and start sharing your environmental data with the world.',
component: PricingPlans,
},
]

export const loader = async ({ context, request }: Route.LoaderArgs) => {
const locale = getLocale(context) as SupportedLanguage

const directus = getDirectusClient()

const useCasesResponse = await directus.request(
readItems('use_cases', {
fields: ['*'],
filter: {
language: { _eq: locale },
},
}),
)

const featuresResponse = await directus.request(
readItems('features', {
fields: ['*'],
filter: {
language: { _eq: locale },
},
}),
)

const partnersResponse = await directus.request(
readItems('partners', {
fields: ['*'],
}),
)

const userId = await getUserId(request)
const userName = await getUserName(request)
const stats = await fetch('https://api.opensensemap.org/stats').then(
(res) => {
return res.json()
},
)

const latestDevices = await getLatestDevices()

return data({
useCases: useCasesResponse,
features: featuresResponse,
partners: partnersResponse,
stats: stats,
header: { userId: userId, userName: userName },
locale: locale,
latestDevices: latestDevices,
})
}

export default function Index() {
const { partners, stats, latestDevices } = useLoaderData<{
partners: Partner[]
stats: number[]
latestDevices: any[]
}>()

const { t } = useTranslation('landing')

const isDesktop = useMediaQuery('(min-width: 768px)')

/**
* Stupid workaround for chromium and webkit that both render double
* scroll bars when using scrollSnapType.
* Simply setting overflow hidden on the html element fixes it and
* the rest of the pages stay untouched from this.
*/
useEffect(() => {
document.documentElement.style.setProperty('overflow', 'hidden')
return () => {
document.documentElement.style.removeProperty('overflow')
}
}, [])

return (
<div
className="max-h-screen bg-white dark:bg-black"
style={{
scrollSnapType: 'y mandatory',
overflowY: 'auto',
}}
>
<header className="z-10">
<Header />
</header>
<main>
<div
id="firstSection"
className="mx-auto flex max-w-7xl flex-col justify-center px-4 sm:px-6 lg:px-8"
style={{
/** for some reasons not really worth debugging tailwind does not apply min-h-[calc(100vh-8rem)], so we have to use element styles here */
minHeight: 'calc(100vh - 8rem)',
scrollSnapAlign: 'end',
}}
>
<div className="flex items-center justify-between px-8">
<div className="md:w-1/2">
<h1 className="text-light-green dark:text-dark-green text-5xl font-bold tracking-tight">
openSenseMap
</h1>
<motion.div
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
transition={{ ease: 'easeInOut', duration: 0.5 }}
>
<p className="mt-6 ml-6 text-lg text-gray-600 dark:text-gray-100">
{t('introduction')}
</p>
</motion.div>
<div className="mt-8 flex items-center justify-around gap-x-6 gap-y-4 text-xl">
<motion.div
initial={{ opacity: 0, y: 100, scale: 0.8 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
duration: 0.5,
delay: 0.2,
type: 'spring',
stiffness: 50,
}}
>
<Link to="/explore" prefetch="intent">
<motion.div
whileHover={{ scale: 1.1 }}
transition={{
type: 'spring',
stiffness: 400,
damping: 10,
}}
>
<button className="border-light-green text-light-green hover:bg-light-green dark:border-dark-green dark:bg-dark-green mt-8 rounded-lg border-t-4 border-r-8 border-b-8 border-l-4 border-solid p-2 transition-all hover:text-white dark:text-white">
{t('explore')}
</button>
</motion.div>
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 100, scale: 0.8 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
duration: 0.5,
delay: 0.5,
type: 'spring',
stiffness: 50,
}}
>
<Link
to="https://www.betterplace.org/de/projects/89947-opensensemap-org-die-freie-karte-fuer-umweltdaten"
target="_blank"
rel="noopener noreferrer"
prefetch="intent"
>
<motion.div
whileHover={{ scale: 1.1 }}
transition={{
type: 'spring',
stiffness: 400,
damping: 10,
}}
>
<button className="border-light-blue text-light-blue hover:bg-light-blue dark:border-dark-blue dark:bg-dark-blue mt-8 rounded-lg border-t-4 border-r-8 border-b-8 border-l-4 border-solid p-2 transition-all hover:scale-105 hover:text-white dark:text-white">
{t('donate')}
</button>
</motion.div>
</Link>
</motion.div>
</div>
</div>
{isDesktop && (
<div className="w-1/3 cursor-pointer">
<GlobeComponent latestDevices={latestDevices} />
</div>
)}
</div>
{isDesktop && (
<div>
<Stats {...stats} />
</div>
)}
</div>
{sections.map((section, _index) => {
const Component = section.component
return (
<div
key={section.title}
className="mx-32 flex h-screen items-center justify-center"
style={{
scrollSnapAlign: 'center',
}}
>
<Component />
</div>
)
})}
<div
className="mx-32 flex h-screen flex-col items-center justify-center"
style={{
scrollSnapAlign: 'center',
}}
>
<Partners data={partners} />
<Footer />
</div>
</main>
</div>
)
}
export function loader() {
return redirect("/explore");
}
Loading
Loading