Skip to content

Commit

Permalink
feat(ui): add dark theme (solidcouch#128)
Browse files Browse the repository at this point in the history
- add theme switch
- add dark theme styles everywhere
- add new optional environment variable VITE_DARK_MODE_LOGO_STYLE=invert
- replace classnames with clsx
- move redux files to src/redux/
  • Loading branch information
mrkvon committed Jan 3, 2025
1 parent 758b9a4 commit 0fdf80c
Show file tree
Hide file tree
Showing 57 changed files with 360 additions and 156 deletions.
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
VITE_EMAIL_NOTIFICATIONS_IDENTITY: ${{ vars.EMAIL_NOTIFICATIONS_IDENTITY }}
VITE_GEOINDEX: ${{ vars.GEOINDEX }}
VITE_BASE_URL: ${{ vars.BASE_URL }} # base url for clientid.jsonld
VITE_DARK_MODE_LOGO_STYLE: ${{ vars.DARK_MODE_LOGO_STYLE }}

- name: Upload production-ready build files
uses: actions/upload-artifact@v3
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add dark theme

## [0.2.0] - 2025-01-01

### Added
Expand Down
5 changes: 5 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ You'll find [configurable options](#options) for the application here. We use en
- `VITE_BASE_URL` - this is base url for ClientID in ./public/clientid.jsonld, it's disabled in development environment by default (dynamic clientID is used), defaults to http://localhost:5173 in development, and http://localhost:4173 for build
- `VITE_ENABLE_DEV_CLIENT_ID` - enable static ClientID in development environment (see also `BASE_URL` option). If you set this option, you'll only be able to sign in with Solid Pod running on localhost! (dynamic clientID will be used by default)
- `VITE_GEOINDEX` - webId of the geoindex, if available
- `VITE_DARK_MODE_LOGO_STYLE` - change logo colors in dark mode (default undefined - no change)
- `invert` - invert colors of the logo

## Usage

Expand Down Expand Up @@ -45,6 +47,9 @@ Have a look in [deployment workflow](../.github/workflows/deploy.yml) to see how
- `EMAIL_NOTIFICATIONS_TYPE`
- `EMAIL_NOTIFICATIONS_SERVICE`
- `EMAIL_NOTIFICATIONS_IDENTITY`
- `GEOINDEX`
- `BASE_URL`
- `DARK_MODE_LOGO_STYLE`
- see [deployment workflow](../.github/workflows/deploy.yml) for more

## Adding a new option
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@types/ngeohash": "^0.6.8",
"ajv": "^8.13.0",
"ajv-errors": "^3.0.0",
"classnames": "^2.3.2",
"clsx": "^2.1.1",
"cross-fetch": "^4.0.0",
"dayjs": "^1.11.8",
"favicons": "^7.2.0",
Expand Down
19 changes: 11 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Header as PageHeader } from '@/components'
import { Head } from '@/components/Head.tsx'
import { useSetEditableConfig } from '@/config/hooks'
import { useAuth } from '@/hooks/useAuth'
import { usePreviousUriAfterSolidRedirect } from '@/hooks/usePreviousUriAfterSolidRedirect'
import { useTheme } from '@/hooks/useTheme.ts'
import { Content, Header, Layout } from '@/layouts/Layout.tsx'
import { actions } from '@/redux/authSlice.ts'
import { useAppDispatch } from '@/redux/hooks.ts'
import { handleIncomingRedirect } from '@inrupt/solid-client-authn-browser'
import { useEffect } from 'react'
import { Outlet } from 'react-router-dom'
import { Slide, ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { useAppDispatch } from './app/hooks'
import { Header as PageHeader } from './components'
import { Head } from './components/Head.tsx'
import { useSetEditableConfig } from './config/hooks'
import { actions } from './features/auth/authSlice'
import { useAuth } from './hooks/useAuth'
import { usePreviousUriAfterSolidRedirect } from './hooks/usePreviousUriAfterSolidRedirect'
import { Content, Header, Layout } from './layouts/Layout.tsx'

export const App = () => {
// initialize the app, provide layout
Expand All @@ -22,6 +23,8 @@ export const App = () => {
const dispatch = useAppDispatch()
const auth = useAuth()

useTheme()

useEffect(() => {
;(async () => {
const session = await handleIncomingRedirect({
Expand Down
8 changes: 4 additions & 4 deletions src/components/AccommodationForm/AccommodationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styles from '@/pages/MyOffers.module.scss'
import { Accommodation } from '@/types'
import { ajvResolver } from '@hookform/resolvers/ajv'
import { JSONSchemaType } from 'ajv'
import classNames from 'classnames'
import clsx from 'clsx'
import merge from 'lodash/merge'
import { Controller, useForm } from 'react-hook-form'
import { FaExclamationTriangle, FaLocationArrow } from 'react-icons/fa'
Expand Down Expand Up @@ -62,7 +62,7 @@ export const AccommodationForm = ({
<form
onSubmit={handleFormSubmit}
onReset={onCancel}
className={classNames(styles.accommodationForm, styles.accommodation)}
className={clsx(styles.accommodationForm, styles.accommodation)}
data-cy="accommodation-form"
>
<label>
Expand All @@ -75,7 +75,7 @@ export const AccommodationForm = ({
control={control}
name="location"
render={({ field }) => (
<div className={classNames(errors.location && styles.inputError)}>
<div className={clsx(errors.location && styles.inputError)}>
<SelectLocation
value={field.value}
onChange={field.onChange}
Expand All @@ -92,7 +92,7 @@ export const AccommodationForm = ({

<label htmlFor="description">About your hosting</label>
<textarea
className={classNames(errors.description && styles.inputError)}
className={clsx(errors.description && styles.inputError)}
id="description"
placeholder="Tell others about your place and hosting"
{...register('description')}
Expand Down
9 changes: 3 additions & 6 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useFile } from '@/hooks/data/useFile'
import { URI } from '@/types'
import classNames from 'classnames'
import clsx from 'clsx'
import { FaUserCircle } from 'react-icons/fa'
import styles from './Avatar.module.scss'

Expand All @@ -20,15 +20,12 @@ export const Avatar = ({

return photo ? (
<img
className={classNames(styles.photo, square && styles.square, className)}
className={clsx(styles.photo, square && styles.square, className)}
src={photo}
alt=""
style={{ width: `${size * 2}rem`, height: `${size * 2}rem` }}
/>
) : (
<FaUserCircle
className={classNames(styles.photo, className)}
size={size * 32}
/>
<FaUserCircle className={clsx(styles.photo, className)} size={size * 32} />
)
}
27 changes: 19 additions & 8 deletions src/components/Button/Button.module.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
@use 'sass:color';

.button {
--__theme-color: var(--text-color);
--__text-color: var(--background-color);

padding: 0.25rem 0.5rem;
text-align: center;
display: inline-flex;
Expand All @@ -9,30 +12,38 @@
gap: 0.25rem;

&.primary {
border: 2px solid black;
background-color: black;
color: white;
border: 2px solid var(--__theme-color);
background-color: var(--__theme-color);
color: var(--__text-color);
text-decoration: none;

&:hover {
background-color: color.adjust(black, $lightness: 20%, $space: hsl);
background-color: color-mix(
in srgb,
var(--__theme-color) 80%,
transparent
);
}
}

&.secondary {
border: 2px solid black;
color: black;
border: 2px solid var(--__theme-color);
color: var(--__theme-color);
text-decoration: none;

&:hover {
background-color: color.adjust(white, $lightness: -10%, $space: hsl);
background-color: color-mix(
in srgb,
var(--__theme-color) 20%,
transparent
);
}
}

&.tertiary {
text-decoration: underline;
text-decoration-thickness: 2px;
text-decoration-color: black;
text-decoration-color: var(--__theme-color);
}

&.danger {
Expand Down
8 changes: 4 additions & 4 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import classNames from 'classnames'
import clsx from 'clsx'
import { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react'
import { FaExternalLinkAlt } from 'react-icons/fa'
import { Link, LinkProps } from 'react-router-dom'
Expand All @@ -21,7 +21,7 @@ export const Button = ({
}: ButtonHTMLAttributes<HTMLButtonElement> & ButtonProps) => {
return (
<button
className={classNames(
className={clsx(
className,
styles.button,
primary && styles.primary,
Expand All @@ -47,7 +47,7 @@ export const ButtonLink = ({
}: LinkProps & ButtonProps) => {
return (
<Link
className={classNames(
className={clsx(
className,
styles.button,
primary && styles.primary,
Expand Down Expand Up @@ -82,7 +82,7 @@ export const ExternalButtonLink = ({
}: AnchorHTMLAttributes<HTMLAnchorElement> & ButtonProps) => {
return (
<a
className={classNames(
className={clsx(
className,
styles.button,
primary && styles.primary,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Header/Header.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
height: 4rem;
display: flex;
align-items: center;
gap: 0.5rem;
gap: 1.5rem;

.logoContainer {
display: flex;
Expand Down
55 changes: 6 additions & 49 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
import { useAppSelector } from '@/app/hooks'
import { Avatar } from '@/components'
import { Logo } from '@/components/Logo/Logo.tsx'
import { SignOut } from '@/components/SignOut.tsx'
import { ThemeSwitch } from '@/components/ThemeSwitch/ThemeSwitch'
import { useConfig } from '@/config/hooks'
import { selectAuth } from '@/features/auth/authSlice'
import { useReadCommunity } from '@/hooks/data/useCommunity'
import { useProfile } from '@/hooks/data/useProfile'
import { useReadMessagesFromInbox } from '@/hooks/data/useReadThreads'
import { Menu, MenuButton, MenuDivider, MenuItem } from '@szhsin/react-menu'
import '@szhsin/react-menu/dist/index.css'
import '@szhsin/react-menu/dist/transitions/slide.css'
import { selectAuth } from '@/redux/authSlice'
import { useAppSelector } from '@/redux/hooks'
import { Link } from 'react-router-dom'
import styles from './Header.module.scss'
import { MainMenu } from './MainMenu'

export const Header = () => {
const { communityId } = useConfig()
const auth = useAppSelector(selectAuth)

const [profile] = useProfile(auth.webId ?? '', communityId)

const { data: newMessages } = useReadMessagesFromInbox(auth.webId ?? '')

const community = useReadCommunity(communityId)

return (
Expand All @@ -34,42 +25,8 @@ export const Header = () => {
/>
</Link>
<div className={styles.spacer} />
{auth.isLoggedIn === true && (
<Menu
menuButton={
<MenuButton data-cy="menu-button">
<Avatar photo={profile.photo} />
</MenuButton>
}
>
<MenuItem>
<Link to="profile">{profile?.name || 'profile'}</Link>
</MenuItem>
<MenuItem>
<Link to="profile/edit" data-cy="menu-item-edit-profile">
edit profile
</Link>
</MenuItem>
<MenuItem>
<Link to="messages">
messages
{newMessages?.length ? ` (${newMessages.length} new)` : null}
</Link>
</MenuItem>
<MenuItem>
<Link to={`profile/${encodeURIComponent(auth.webId!)}/contacts`}>
contacts
</Link>
</MenuItem>
<MenuItem>
<Link to="host/offers">my hosting</Link>
</MenuItem>
<MenuDivider />
<MenuItem>
<SignOut />
</MenuItem>
</Menu>
)}
<ThemeSwitch />
{auth.isLoggedIn === true && <MainMenu />}
{auth.isLoggedIn === undefined && <>...</>}
</nav>
)
Expand Down
58 changes: 58 additions & 0 deletions src/components/Header/MainMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Avatar } from '@/components'
import { SignOut } from '@/components/SignOut.tsx'
import { useConfig } from '@/config/hooks'
import { useProfile } from '@/hooks/data/useProfile'
import { useReadMessagesFromInbox } from '@/hooks/data/useReadThreads'
import { selectAuth } from '@/redux/authSlice'
import { useAppSelector } from '@/redux/hooks'
import { selectTheme } from '@/redux/uiSlice'
import { Menu, MenuButton, MenuDivider, MenuItem } from '@szhsin/react-menu'
import { Link } from 'react-router-dom'

export const MainMenu = () => {
const { communityId } = useConfig()
const auth = useAppSelector(selectAuth)
const [profile] = useProfile(auth.webId ?? '', communityId)

const { data: newMessages } = useReadMessagesFromInbox(auth.webId ?? '')

const theme = useAppSelector(selectTheme)

return (
<Menu
menuButton={
<MenuButton data-cy="menu-button">
<Avatar photo={profile.photo} />
</MenuButton>
}
theming={theme === 'dark' ? 'dark' : undefined}
>
<MenuItem>
<Link to="profile">{profile?.name || 'profile'}</Link>
</MenuItem>
<MenuItem>
<Link to="profile/edit" data-cy="menu-item-edit-profile">
edit profile
</Link>
</MenuItem>
<MenuItem>
<Link to="messages">
messages
{newMessages?.length ? ` (${newMessages.length} new)` : null}
</Link>
</MenuItem>
<MenuItem>
<Link to={`profile/${encodeURIComponent(auth.webId!)}/contacts`}>
contacts
</Link>
</MenuItem>
<MenuItem>
<Link to="host/offers">my hosting</Link>
</MenuItem>
<MenuDivider />
<MenuItem>
<SignOut />
</MenuItem>
</Menu>
)
}
2 changes: 1 addition & 1 deletion src/components/Interests/Interests.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

.item {
display: inline-block;
border: 1px solid black;
border: 1px solid var(--text-color);
border-radius: 100rem;
padding: 0rem 0.5rem;

Expand Down
Loading

0 comments on commit 0fdf80c

Please sign in to comment.