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 app/components/header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import LanguageSelector from "../landing/header/language-selector";
import Download from "./download";
import Home from "./home";
import Menu from "./menu";
Expand All @@ -17,6 +18,9 @@ export default function Header(props: HeaderProps) {
<Home />
<NavBar devices={props.devices} />
<div className="flex gap-2">
<div className="flex pointer-events-auto items-center h-10 w-16 rounded-full bg-white justify-center shadow-md">
<LanguageSelector/>
</div>
<Download devices={props.devices} />
{/* {data?.user?.email ? <Notification /> : null} */}
<Menu />
Expand Down
63 changes: 36 additions & 27 deletions app/components/landing/header/language-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import i18next from "i18next";
import { useState } from "react";
import { useFetcher, useLoaderData } from "react-router";
import { Button } from "~/components/ui/button";
import { type loader } from "~/root";
import i18next from 'i18next'
import { Globe } from 'lucide-react'
import { useEffect } from 'react'
import { useFetcher, useLoaderData } from 'react-router'
import { Button } from '~/components/ui/button'
import { type loader } from '~/root'

export default function LanguageSelector() {
const data = useLoaderData<typeof loader>();
const fetcher = useFetcher();
const [locale, setLocale] = useState(data.locale || "en");
const data = useLoaderData<typeof loader>()
const fetcher = useFetcher()

const toggleLanguage = () => {
const newLocale = locale === "en" ? "de" : "en"; // Toggle between "en" and "de"
setLocale(newLocale);
void i18next.changeLanguage(newLocale); // Change the language in the app
void fetcher.submit(
{ language: newLocale },
{ method: "post", action: "/action/set-language" }, // Persist the new language
);
};
// When loader locale changes (e.g. after login), sync state
useEffect(() => {
if (!data?.locale) return
const updateLang = async () => {
await i18next.changeLanguage(data.locale)
}
void updateLang()
}, [data.locale])

return (
<Button
variant="ghost"
size="icon"
onClick={toggleLanguage}
className="hover:bg-transparent dark:hover:text-white hover:text-black"
>
{locale === "de" ? <p>DE</p> : <p>EN</p>}
</Button>
);
const toggleLanguage = async () => {
const newLocale = (data?.locale ?? 'en') === 'en' ? 'de' : 'en' // Toggle between "en" and "de"
void fetcher.submit(
{ language: newLocale },
{ method: 'post', action: '/action/set-language' }, // Persist the new language
)
await i18next.changeLanguage(newLocale) // Change the language in the app
}

return (
<Button
variant="ghost"
size="icon"
onClick={toggleLanguage}
className="hover:bg-transparent hover:text-black dark:hover:text-white"
>
<Globe />
{(data?.locale ?? 'en') === 'de' ? <p>DE</p> : <p>EN</p>}
</Button>
)
}
54 changes: 45 additions & 9 deletions app/components/map/map.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
import { forwardRef } from "react";
import { type MapProps, type MapRef, NavigationControl, Map as ReactMap } from "react-map-gl";
import type { Map as MapboxMap, AnyLayer, MapboxEvent } from "mapbox-gl";

Check warning on line 1 in app/components/map/map.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Lint

Prefer using inline type specifiers instead of a top-level type-only import
import { forwardRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { type MapProps, type MapRef, NavigationControl, Map as ReactMap } from "react-map-gl";



const Map = forwardRef<MapRef, MapProps>(
(
// take fog and terrain out of props to resolve error
{ children, mapStyle, fog = null, terrain = null, ...props },
{ children, mapStyle, ...props },
ref,
) => {
// get theme from tailwind
const [theme] = "light"; //useTheme();
const [theme] = "light";
const [, i18n] = useTranslation();
const updateMapLanguage = (map: MapboxMap, locale: string) => {
if (!map) return;

const style = map.getStyle();
if (!style || !style.layers) return;

style.layers.forEach((layer: AnyLayer) => {
// Uses type guarding instead of casting to any
if (!("layout" in layer)) return;

const layout = layer.layout;
if (layout && typeof layout === "object" && 'text-field' in layout) {
map.setLayoutProperty(layer.id, 'text-field', [
'coalesce',
['get', `name_${locale}`],
['get', 'name'],
]);
}
});
};

const handleMapLoad = (event: MapboxEvent<undefined>) => {
updateMapLanguage(event.target as MapboxMap, i18n.language);
};

// Update language when it changes
useEffect(() => {
if (ref && typeof ref !== 'function' && ref.current) {
updateMapLanguage(ref.current.getMap(), i18n.language);
}
}, [i18n.language, ref]);

return (
<ReactMap
id="osem"
Expand All @@ -19,9 +54,9 @@
zoom: 2,
}}
mapStyle={
theme === "dark"
mapStyle || (theme === "dark"
? "mapbox://styles/mapbox/dark-v11"
: "mapbox://styles/mapbox/streets-v12"
: "mapbox://styles/mapbox/streets-v12")
}
mapboxAccessToken={ENV.MAPBOX_ACCESS_TOKEN}
pitchWithRotate={false}
Expand All @@ -37,6 +72,7 @@
left: 0,
}}
touchZoomRotate={false}
onLoad={handleMapLoad}
{...props}
>
{children}
Expand All @@ -48,4 +84,4 @@

Map.displayName = "Map";

export default Map;
export default Map;
7 changes: 7 additions & 0 deletions app/lib/set-language.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// app/lib/set-language.server.ts
//used for setting language cookie during login
import { i18nCookie } from "~/cookies";

export async function setLanguageCookie(lang: string) {
return await i18nCookie.serialize(lang);
}
8 changes: 6 additions & 2 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { type supportedLanguages } from "~/i18next-options";
import i18next from "~/i18next.server";
import { type Partner, getDirectusClient } from "~/lib/directus";
import { getLatestDevices } from "~/models/device.server";
import { getUserByUsername } from "~/models/user.server";
import { getUserId, getUserName } from "~/utils/session.server";

const sections = [
Expand Down Expand Up @@ -52,7 +53,7 @@ const sections = [
];

export const loader = async ({ request }: LoaderFunctionArgs) => {
const locale = (await i18next.getLocale(
let locale = (await i18next.getLocale(
request,
)) as (typeof supportedLanguages)[number];
const directus = getDirectusClient();
Expand Down Expand Up @@ -84,7 +85,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
//* Get user Id from session
const userId = await getUserId(request);
const userName = await getUserName(request);

const user = userName ? await getUserByUsername(userName) : null;
if(user){
locale = user.language?.split(/[_-]/)[0].toLowerCase() as (typeof supportedLanguages)[number];
}//update the locale in the index route loader if user is logged in
const stats = await fetch("https://api.opensensemap.org/stats").then(
(res) => {
return res.json();
Expand Down
9 changes: 8 additions & 1 deletion app/routes/explore.login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Checkbox } from "~/components/ui/checkbox";
import { verifyLogin } from "~/models/user.server";
import { safeRedirect, validateEmail } from "~/utils";
import { createUserSession, getUserId } from "~/utils/session.server";
import { setLanguageCookie } from "~/lib/set-language.server";

export async function loader({ request }: LoaderFunctionArgs) {
const userId = await getUserId(request);
Expand Down Expand Up @@ -65,7 +66,10 @@ export async function action({ request }: ActionFunctionArgs) {
}

const user = await verifyLogin(email, password);

const userLocale = user?.language
? user.language.split(/[_-]/)[0].toLowerCase()
: "en";

if (!user) {
return data(
{ errors: { email: "Invalid email or password", password: null } },
Expand All @@ -78,6 +82,9 @@ export async function action({ request }: ActionFunctionArgs) {
userId: user.id,
remember: remember === "on" ? true : false,
redirectTo,
headers: {
"Set-Cookie": await setLanguageCookie(userLocale),
}
});
}

Expand Down
8 changes: 6 additions & 2 deletions app/routes/explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,16 @@ export async function loader({ request }: LoaderFunctionArgs) {

if (user) {
const profile = await getProfileByUserId(user.id);
const userLocale = user.language
? user.language.split(/[_-]/)[0].toLowerCase()
: "en";
return {
devices,
user,
profile,
filteredDevices,
filterParams,
locale,
locale: userLocale,
//phenomena
};
}
Expand All @@ -149,6 +152,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
filterParams,
filteredDevices,
message,
locale
//phenomena,
};
}
Expand All @@ -170,7 +174,7 @@ if (process.env.NODE_ENV === "production") {

export default function Explore() {
// data from our loader
const { devices, user, profile, filterParams, filteredDevices, message } =
const { devices, user, profile, filterParams, filteredDevices, message,locale } =
useLoaderData<typeof loader>();

const mapRef = useRef<MapRef | null>(null);
Expand Down
2 changes: 2 additions & 0 deletions app/utils/session.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ export async function createUserSession({
userId,
remember,
redirectTo,
headers,
}: {
request: Request;
userId: string;
remember: boolean;
redirectTo: string;
headers?: HeadersInit;// added optional headers parameter
}) {
const session = await getUserSession(request);
session.set(USER_SESSION_KEY, userId);
Expand Down
4 changes: 2 additions & 2 deletions tests/routes/api.measurements.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ describe("multiple CSV POST /boxes/:id/data", () => {
"Content-Type": "application/sbx-bytes",
Authorization: mockAccessToken,
},
body: byteSubmitData(sensors),
body: byteSubmitData(sensors) as unknown as BodyInit,
}
);

Expand Down Expand Up @@ -302,7 +302,7 @@ describe("multiple CSV POST /boxes/:id/data", () => {
"Content-Type": "application/sbx-bytes-ts",
Authorization: mockAccessToken,
},
body: byteSubmitData(sensors, true),
body: byteSubmitData(sensors, true) as unknown as BodyInit,
}
);

Expand Down
Loading