Skip to content

Commit 68f77fa

Browse files
Feat: dark mode (#919)
* dark mode * dark map
1 parent a531679 commit 68f77fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+728
-523
lines changed

web-app/public/locales/en/feeds.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"officialFeed": "Official Feed",
9292
"officialFeedTooltip": "The transit provider has confirmed this feed should be shared with riders. This has been confirmed either by the transit provider providing the feed on their website or from personalized confirmation with the Mobility Database team.",
9393
"officialFeedTooltipShort": "Verified feed: Confirmed by the transit provider or the Mobility Database team for rider use.",
94-
"seeDetailPageProviders": "See detail page to view {providersCount} others",
94+
"seeDetailPageProviders": "See detail page to view {{providersCount}} others",
9595
"openFullQualityReport": "Open Full Quality Report",
9696
"qualityReportUpdated": "Quality report updated",
9797
"officialFeedUpdated": "Official verification updated",

web-app/src/app/Theme.ts

Lines changed: 140 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { type PaletteColor, createTheme } from '@mui/material/styles';
1+
import {
2+
type PaletteColor,
3+
type Theme,
4+
createTheme,
5+
} from '@mui/material/styles';
26
import { type Property } from 'csstype';
37

48
declare module '@mui/material/Typography' {
@@ -16,6 +20,11 @@ declare module '@mui/material/styles/createMixins' {
1620
}
1721
}
1822

23+
export enum ThemeModeEnum {
24+
light = 'light',
25+
dark = 'dark',
26+
}
27+
1928
export const fontFamily = {
2029
primary: '"Mulish"',
2130
secondary: '"IBM Plex Mono"',
@@ -43,87 +52,153 @@ const palette = {
4352
secondary: 'rgba(71, 71, 71, 0.8)',
4453
disabled: 'rgba(0,0,0,0.3)',
4554
},
55+
divider: 'rgba(0, 0, 0, 0.23)',
4656
};
4757

48-
export const theme = createTheme({
49-
palette,
50-
mixins: {
51-
code: {
52-
contrastText: '#f1fa8c',
53-
command: {
54-
fontWeight: 'bold',
55-
color: '#ff79c6',
56-
},
57-
},
58+
const darkPalette = {
59+
primary: {
60+
main: '#96a1ff',
61+
dark: '#4a5dff',
62+
light: '#e7e8ff',
63+
contrastText: '#E3E3E3',
5864
},
59-
typography: {
60-
fontFamily: fontFamily.primary,
65+
secondary: {
66+
main: '#3959fa',
67+
dark: '#002eea',
68+
light: '#989ffc',
69+
contrastText: '#ffffff',
70+
},
71+
background: {
72+
default: '#121212',
73+
paper: '#1E1E1E',
74+
},
75+
text: {
76+
primary: '#E3E3E3',
77+
secondary: 'rgba(255, 255, 255, 0.7)',
78+
disabled: 'rgba(255, 255, 255, 0.3)',
6179
},
62-
components: {
63-
MuiFormLabel: {
64-
styleOverrides: {
65-
root: {
66-
color: '#474747',
80+
divider: 'rgba(255, 255, 255, 0.23)',
81+
};
82+
83+
export const getTheme = (mode: ThemeModeEnum): Theme => {
84+
const isLightMode = mode === ThemeModeEnum.light;
85+
const chosenPalette = !isLightMode ? darkPalette : palette;
86+
return createTheme({
87+
palette: { ...chosenPalette, mode },
88+
mixins: {
89+
code: {
90+
contrastText: '#f1fa8c',
91+
command: {
6792
fontWeight: 'bold',
93+
color: '#ff79c6',
6894
},
6995
},
7096
},
71-
MuiTextField: {
72-
styleOverrides: {
73-
root: {
74-
'&.md-small-input': {
75-
input: { paddingTop: '7px', paddingBottom: '7px' },
97+
typography: {
98+
fontFamily: fontFamily.primary,
99+
},
100+
components: {
101+
MuiInputAdornment: {
102+
styleOverrides: {
103+
root: {
104+
color: 'inherit',
76105
},
77106
},
78107
},
79-
},
80-
MuiSelect: {
81-
styleOverrides: {
82-
root: {
83-
'.MuiSelect-select': { paddingTop: '7px', paddingBottom: '7px' },
108+
MuiFormLabel: {
109+
styleOverrides: {
110+
root: {
111+
color: chosenPalette.text.primary,
112+
fontWeight: 'bold',
113+
},
84114
},
85115
},
86-
},
87-
MuiButton: {
88-
styleOverrides: {
89-
root: {
90-
textTransform: 'none',
91-
boxShadow: 'none',
92-
fontFamily: fontFamily.secondary,
93-
boxSizing: 'border-box',
94-
'&.MuiButton-contained': {
95-
border: '2px solid transparent',
116+
MuiTextField: {
117+
styleOverrides: {
118+
root: {
119+
'&.md-small-input': {
120+
input: { paddingTop: '7px', paddingBottom: '7px' },
121+
},
122+
'.MuiOutlinedInput-root fieldset': {
123+
borderColor: chosenPalette.divider,
124+
},
96125
},
97-
'&.MuiButton-containedPrimary:hover': {
98-
boxShadow: 'none',
99-
backgroundColor: 'transparent',
100-
border: `2px solid ${palette.primary.main}`,
101-
color: palette.primary.main,
102-
},
103-
'&.MuiButton-outlinedPrimary': {
104-
border: `2px solid ${palette.primary.main}`,
105-
padding: '6px 16px',
106-
},
107-
'&.MuiButton-outlinedPrimary:hover': {
108-
backgroundColor: palette.primary.main,
109-
color: palette.primary.contrastText,
126+
},
127+
},
128+
MuiSelect: {
129+
styleOverrides: {
130+
root: {
131+
'.MuiSelect-select': { paddingTop: '7px', paddingBottom: '7px' },
132+
'.MuiSvgIcon-root': { color: chosenPalette.text.primary },
133+
'&.MuiInputBase-root fieldset': {
134+
borderColor: chosenPalette.divider,
135+
},
110136
},
111137
},
112138
},
113-
},
114-
MuiTypography: {
115-
variants: [
116-
{
117-
props: { variant: 'sectionTitle' },
118-
style: {
119-
color: palette.primary?.main,
120-
fontWeight: 'bold',
121-
fontSize: '1.5rem',
122-
marginBottom: '0.5rem',
123-
marginTop: '1rem',
139+
MuiButton: {
140+
styleOverrides: {
141+
root: {
142+
textTransform: 'none',
143+
boxShadow: 'none',
144+
fontFamily: fontFamily.secondary,
145+
boxSizing: 'border-box',
146+
'&.MuiButton-contained': {
147+
border: '2px solid transparent',
148+
color: chosenPalette.background.default,
149+
'&.Mui-disabled': {
150+
backgroundColor: chosenPalette.text.disabled,
151+
},
152+
},
153+
'&.MuiButton-containedPrimary:hover': {
154+
boxShadow: 'none',
155+
backgroundColor: 'transparent',
156+
border: `2px solid ${chosenPalette.primary.main}`,
157+
color: chosenPalette.primary.main,
158+
},
159+
'&.MuiButton-outlinedPrimary': {
160+
border: `2px solid ${chosenPalette.primary.main}`,
161+
padding: '6px 16px',
162+
},
163+
'&.MuiButton-outlinedPrimary:hover': {
164+
backgroundColor: chosenPalette.primary.main,
165+
color: isLightMode
166+
? chosenPalette.primary.contrastText
167+
: chosenPalette.background.default,
168+
},
169+
'&.MuiButton-text.inline': {
170+
fontFamily: fontFamily.primary,
171+
fontSize: 'inherit',
172+
padding: `0 8px`,
173+
lineHeight: 'normal',
174+
verticalAlign: 'baseline',
175+
'&.line-start': {
176+
paddingLeft: 0,
177+
},
178+
'.MuiButton-endIcon': {
179+
marginRight: 0,
180+
svg: {
181+
color: 'inherit',
182+
},
183+
},
184+
},
124185
},
125186
},
126-
],
187+
},
188+
MuiTypography: {
189+
variants: [
190+
{
191+
props: { variant: 'sectionTitle' },
192+
style: {
193+
color: chosenPalette.primary?.main,
194+
fontWeight: 'bold',
195+
fontSize: '1.5rem',
196+
marginBottom: '0.5rem',
197+
marginTop: '1rem',
198+
},
199+
},
200+
],
201+
},
127202
},
128-
},
129-
});
203+
});
204+
};

web-app/src/app/components/ContentBox.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { Box, Typography, type SxProps } from '@mui/material';
2+
import { Box, Typography, useTheme, type SxProps } from '@mui/material';
33

44
export interface ContentBoxProps {
55
title: string;
@@ -13,11 +13,13 @@ export interface ContentBoxProps {
1313
export const ContentBox = (
1414
props: React.PropsWithChildren<ContentBoxProps>,
1515
): JSX.Element => {
16+
const theme = useTheme();
1617
return (
1718
<Box
1819
width={props.width}
1920
sx={{
20-
background: '#FFFFFF',
21+
background: theme.palette.background.default,
22+
color: theme.palette.text.primary,
2123
borderRadius: '6px',
2224
border: `2px solid ${props.outlineColor}`,
2325
p: props.padding ?? 5,

web-app/src/app/components/FeedStatus.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Box, Chip, Tooltip } from '@mui/material';
2-
import { theme } from '../Theme';
1+
import { Box, Chip, Tooltip, useTheme } from '@mui/material';
32
import { useTranslation } from 'react-i18next';
43
import { type TFunction } from 'i18next';
54

@@ -52,6 +51,7 @@ export const FeedStatusIndicator = (
5251
props: React.PropsWithChildren<FeedStatusProps>,
5352
): JSX.Element => {
5453
const { t } = useTranslation('feeds');
54+
const theme = useTheme();
5555
const statusData = getFeedStatusData(props.status, t);
5656
return (
5757
<>

web-app/src/app/components/Footer.tsx

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
import React from 'react';
22
import '../styles/Footer.css';
33
import TwitterIcon from '@mui/icons-material/Twitter';
4-
import { Button, IconButton } from '@mui/material';
4+
import { Button, IconButton, useTheme } from '@mui/material';
55
import { GitHub, LinkedIn, OpenInNew } from '@mui/icons-material';
66
import { MOBILITY_DATA_LINKS } from '../constants/Navigation';
7-
import { fontFamily, theme } from '../Theme';
8-
9-
const SlackSvg = (
10-
<svg
11-
xmlns='http://www.w3.org/2000/svg'
12-
width='24px'
13-
height='24px'
14-
viewBox='0 0 24 24'
15-
>
16-
<path
17-
fill={theme.palette.primary.main}
18-
d='M6 15a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2h2zm1 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2a2 2 0 0 1-2-2zm2-8a2 2 0 0 1-2-2a2 2 0 0 1 2-2a2 2 0 0 1 2 2v2zm0 1a2 2 0 0 1 2 2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2a2 2 0 0 1 2-2zm8 2a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2h-2zm-1 0a2 2 0 0 1-2 2a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2a2 2 0 0 1 2 2zm-2 8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2v-2zm0-1a2 2 0 0 1-2-2a2 2 0 0 1 2-2h5a2 2 0 0 1 2 2a2 2 0 0 1-2 2z'
19-
/>
20-
</svg>
21-
);
7+
import { fontFamily } from '../Theme';
228

239
const Footer: React.FC = () => {
10+
const theme = useTheme();
2411
const navigateTo = (link: string): void => {
2512
window.open(link, '_blank');
2613
};
2714

15+
const SlackSvg = (
16+
<svg
17+
xmlns='http://www.w3.org/2000/svg'
18+
width='24px'
19+
height='24px'
20+
viewBox='0 0 24 24'
21+
>
22+
<path
23+
fill={theme.palette.primary.main}
24+
d='M6 15a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2h2zm1 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2a2 2 0 0 1-2-2zm2-8a2 2 0 0 1-2-2a2 2 0 0 1 2-2a2 2 0 0 1 2 2v2zm0 1a2 2 0 0 1 2 2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2a2 2 0 0 1 2-2zm8 2a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2h-2zm-1 0a2 2 0 0 1-2 2a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2a2 2 0 0 1 2 2zm-2 8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2v-2zm0-1a2 2 0 0 1-2-2a2 2 0 0 1 2-2h5a2 2 0 0 1 2 2a2 2 0 0 1-2 2z'
25+
/>
26+
</svg>
27+
);
28+
2829
return (
2930
<footer className='footer' style={{ fontFamily: fontFamily.secondary }}>
3031
<a
@@ -88,16 +89,23 @@ const Footer: React.FC = () => {
8889
</IconButton>
8990
</div>
9091
<p style={{ margin: 0 }}>Maintained with &#128156; by MobilityData.</p>
91-
<p style={{ margin: 0 }}>
92-
<a href={'/privacy-policy'} target={'_blank'} rel={'noreferrer'}>
93-
Privacy Policy
94-
</a>
95-
</p>
96-
<p style={{ margin: 0 }}>
97-
<a href={'/terms-and-conditions'} target={'_blank'} rel={'noreferrer'}>
98-
Terms and Conditions
99-
</a>
100-
</p>
92+
<Button
93+
variant='text'
94+
href={'/privacy-policy'}
95+
target={'_blank'}
96+
rel={'noreferrer'}
97+
>
98+
Privacy Policy
99+
</Button>
100+
|
101+
<Button
102+
variant='text'
103+
href={'/terms-and-conditions'}
104+
target={'_blank'}
105+
rel={'noreferrer'}
106+
>
107+
Terms and Conditions
108+
</Button>
101109
</footer>
102110
);
103111
};

0 commit comments

Comments
 (0)