more ui refactoring & theme controller

This commit is contained in:
Anton Tranelis 2025-03-21 19:09:34 +00:00
parent faebd0afb6
commit 5069b6b32a
19 changed files with 138 additions and 158 deletions

View File

@ -7,6 +7,7 @@ import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth/useAuth'
import { useItems } from '#components/Map/hooks/useItems'
import { ThemeControl } from '#components/Templates/ThemeControl'
import { useAppState, useSetAppState } from './hooks/useAppState'
@ -100,115 +101,7 @@ export default function NavBar({ appName }: { appName: string }) {
</div>
</div>
<div className='tw:dropdown tw:mr-2'>
<div tabIndex={0} role='button' className='tw:btn tw:m-1'>
Theme
<svg
width='12px'
height='12px'
className='tw:inline-block tw:h-2 tw:w-2 tw:fill-current tw:opacity-60'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 2048 2048'
>
<path d='M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z'></path>
</svg>
</div>
<ul
tabIndex={0}
className='tw:dropdown-content tw:bg-base-300 tw:rounded-box tw:z-1 tw:w-52 tw:p-2 tw:shadow-2xl'
>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Default'
value='default'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Dark'
value='dark'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Light'
value='light'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Retro'
value='retro'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Cyberpunk'
value='cyberpunk'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Valentine'
value='valentine'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Aqua'
value='aqua'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Caramellatte'
value='caramellatte'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Abyss'
value='abyss'
/>
</li>
<li>
<input
type='radio'
name='theme-dropdown'
className='theme-controller tw:btn tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start'
aria-label='Silk'
value='silk'
/>
</li>
</ul>
</div>
{appState.showThemeControl && <ThemeControl />}
{isAuthenticated ? (
<div className='tw:flex tw:mr-2'>

View File

@ -45,17 +45,14 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
id='sidenav'
className={`${appState.sideBarOpen ? 'tw:translate-x-0' : 'tw:-translate-x-full'}
${appState.sideBarSlim ? 'tw:w-14' : 'tw:w-48'}
${embedded ? 'tw:mt-0 tw:h-[100dvh]' : 'tw:mt-16 tw:h-[calc(100dvh-64px)]'}
${embedded ? 'tw:mt-5.5 tw:h-[calc(100dvh-22px)]' : 'tw:mt-16 tw:h-[calc(100dvh-64px)]'}
tw:fixed tw:left-0 tw:transition-all tw:duration-300 tw:top-0 tw:z-10035
tw:overflow-hidden tw:shadow-xl tw:dark:bg-zinc-800`}
>
<div
className={`tw:flex tw:flex-col ${embedded ? 'tw:h-full' : 'tw:h-[calc(100dvh-64px)]'}`}
>
<ul
className='tw:menu tw:w-full tw:bg-base-100 tw:text-base-content tw:p-0'
data-te-sidenav-menu-ref
>
<ul className='tw:menu tw:w-full tw:bg-base-100 tw:text-base-content tw:p-0'>
{routes.map((route, k) => {
return (
<li className='' key={k}>
@ -145,7 +142,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
<ChevronRightIcon
className={
'tw:w-5 tw:h-5 tw:mb-4 tw:mr-4 tw:cursor-pointer tw:float-right tw:delay-400 tw:duration-500 tw:transition-all ' +
'tw:w-5 tw:h-5 tw:mb-4 tw:mr-5 tw:mt-2 tw:cursor-pointer tw:float-right tw:delay-400 tw:duration-500 tw:transition-all ' +
(!appState.sideBarSlim ? 'tw:rotate-180' : '')
}
onClick={() => toggleSidebarSlim()}

View File

@ -8,6 +8,7 @@ interface AppState {
assetsApi: AssetsApi
sideBarOpen: boolean
sideBarSlim: boolean
showThemeControl: boolean
}
type UseAppManagerResult = ReturnType<typeof useAppManager>
@ -16,6 +17,7 @@ const initialAppState: AppState = {
assetsApi: {} as AssetsApi,
sideBarOpen: false,
sideBarSlim: false,
showThemeControl: false,
}
const AppContext = createContext<UseAppManagerResult>({

View File

@ -0,0 +1,12 @@
import { useEffect } from 'react'
export const useTheme = (defaultTheme = 'default') => {
useEffect(() => {
const savedTheme = localStorage.getItem('theme')
const initialTheme = savedTheme ? (JSON.parse(savedTheme) as string) : defaultTheme
if (initialTheme !== 'default') {
document.documentElement.setAttribute('data-theme', defaultTheme)
localStorage.setItem('theme', JSON.stringify(initialTheme))
}
}, [defaultTheme])
}

View File

@ -43,7 +43,7 @@ export const LocateControl = () => {
return (
<>
<div className='tw:card tw:h-12 tw:w-12 tw:bg-base-100 tw:shadow-xl tw:items-center tw:justify-center tw:hover:bg-slate-300 tw:hover:cursor-pointer tw:transition-all tw:duration-300 tw:ml-2'>
<div className='tw:card tw:flex-none tw:h-12 tw:w-12 tw:bg-base-100 tw:shadow-xl tw:items-center tw:justify-center tw:hover:bg-slate-300 tw:hover:cursor-pointer tw:transition-all tw:duration-300 tw:ml-2'>
<div
className='tw:card-body tw:card tw:p-2 tw:h-10 tw:w-10 '
onClick={() => {

View File

@ -112,13 +112,13 @@ export const SearchControl = () => {
<div className='tw:w-[calc(100vw-2rem)] tw:max-w-[22rem] '>
<div className='tw:flex tw:flex-row'>
{embedded && <SidebarControl />}
<div className='tw:relative'>
<div className='tw:relative tw:shrink tw:max-w-69 tw:w-full'>
<input
type='text'
placeholder='search ...'
autoComplete='off'
value={value}
className='tw:input tw:input-bordered tw:h-12 tw:grow tw:shadow-xl tw:rounded-box tw:pr-12 tw:w-69'
className='tw:input tw:input-bordered tw:h-12 tw:grow tw:shadow-xl tw:rounded-box tw:pr-12 tw:w-full'
ref={searchInput}
onChange={(e) => setValue(e.target.value)}
onFocus={() => {

View File

@ -1,21 +1,21 @@
import Bars3Icon from '@heroicons/react/16/solid/Bars3Icon'
import { useAppState, useSetAppState } from '#components/AppShell/hooks/useAppState'
// Converts leaflet.locatecontrol to a React Component
export const SidebarControl = () => {
const appState = useAppState()
const setAppState = useSetAppState()
const toggleSidebar = () => {
setAppState({ sideBarOpen: !appState.sideBarOpen })
}
return (
<>
<div className='tw:card tw:bg-base-100 tw:shadow-xl tw:items-center tw:justify-center tw:hover:bg-slate-300 tw:hover:cursor-pointer tw:transition-all tw:duration-300 tw:mr-2 tw:h-12 tw:w-12 '>
<div className='tw:card-body tw:card tw:p-0'>
<button
className='tw:btn tw:btn-square tw:btn-ghost tw:rounded-2xl'
data-te-sidenav-toggle-ref
data-te-target='#sidenav'
aria-controls='#sidenav'
aria-haspopup='true'
>
<Bars3Icon className='tw:inline-block tw:w-5 tw:h-5' />
</button>
</div>
<div
className='tw:card tw:justify-center tw:items-center tw:bg-base-100 tw:flex-none tw:shadow-xl tw:px-0 tw:hover:bg-slate-300 tw:hover:cursor-pointer tw:transition-all tw:duration-300 tw:mr-2 tw:h-12 tw:w-12 '
onClick={() => toggleSidebar()}
>
<Bars3Icon className='tw:inline-block tw:w-5 tw:h-5' />
</div>
</>
)

View File

@ -57,9 +57,7 @@ export function HeaderView({
const [imageLoaded, setImageLoaded] = useState(false)
const avatar =
item.image &&
appState.assetsApi.url + item.image + `${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
const avatar = item.image && appState.assetsApi.url + item.image + '?width=160&heigth=160'
const title = item.name
const subtitle = item.subname
@ -111,7 +109,7 @@ export function HeaderView({
</div>
)}
{subtitle && !hideSubname && (
<div className={`tw:text-xs tw:text-gray-500 ${truncateSubname && 'tw:truncate'}`}>
<div className={`tw:text-xs tw:opacity-50 ${truncateSubname && 'tw:truncate'}`}>
{subtitle}
</div>
)}

View File

@ -111,7 +111,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
<div className='tw:flex tw:-mb-1 tw:flex-row tw:mr-2 tw:mt-1'>
{infoExpanded ? (
<p
className={'tw:italic tw:min-h-[21px] tw:my-0! tw:text-gray-500'}
className={'tw:italic tw:min-h-[21px] tw:my-0! tw:opacity-50'}
>{`${props.item.date_updated && props.item.date_updated !== props.item.date_created ? 'updated' : 'posted'} ${props.item && props.item.user_created && props.item.user_created.first_name ? `by ${props.item.user_created.first_name}` : ''} ${props.item.date_updated ? timeAgo(props.item.date_updated) : timeAgo(props.item.date_created!)}`}</p>
) : (
<p

View File

@ -20,6 +20,8 @@ function UtopiaMap({
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true,
showThemeControl = false,
defaultTheme,
infoText,
donationWidget,
}: UtopiaMapProps) {
@ -39,6 +41,8 @@ function UtopiaMap({
showLayerControl={showLayerControl}
infoText={infoText}
donationWidget={donationWidget}
showThemeControl={showThemeControl}
defaultTheme={defaultTheme}
>
{children}
</UtopiaMapInner>

View File

@ -12,6 +12,8 @@ import MarkerClusterGroup from 'react-leaflet-cluster'
import { Outlet, useLocation } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useSetAppState } from '#components/AppShell/hooks/useAppState'
import { useTheme } from '#components/AppShell/hooks/useTheme'
import { containsUUID } from '#utils/ContainsUUID'
import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
@ -43,6 +45,8 @@ export function UtopiaMapInner({
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true,
showThemeControl = false,
defaultTheme = '',
donationWidget,
}: UtopiaMapProps) {
const selectNewItemPosition = useSelectPosition()
@ -52,6 +56,8 @@ export function UtopiaMapInner({
const setMapClicked = useSetMapClicked()
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null)
useTheme(defaultTheme)
const layers = useLayers()
const addVisibleLayer = useAddVisibleLayer()
const leafletRefs = useLeafletRefs()
@ -64,6 +70,12 @@ export function UtopiaMapInner({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [layers])
const setAppState = useSetAppState()
useEffect(() => {
setAppState({ showThemeControl })
}, [setAppState, showThemeControl])
const init = useRef(false)
useEffect(() => {
if (!init.current) {

View File

@ -30,7 +30,7 @@ export const FormHeader = ({ item, state, setState }) => {
}
className={'tw:-left-6 tw:top-14 tw:-mr-6'}
/>
<div className='tw:grow tw:mr-4'>
<div className='tw:grow tw:mr-4 tw:pt-1'>
<TextInput
placeholder='Name'
defaultValue={item?.name ? item.name : ''}
@ -40,7 +40,8 @@ export const FormHeader = ({ item, state, setState }) => {
name: v,
}))
}
containerStyle='tw:grow tw:input-md'
containerStyle='tw:grow tw:px-4'
inputStyle='tw:input-md'
/>
<TextInput
placeholder='Subtitle'
@ -52,7 +53,8 @@ export const FormHeader = ({ item, state, setState }) => {
subname: v,
}))
}
containerStyle='tw:grow tw:input-sm tw:px-4 tw:mt-1'
containerStyle='tw:grow tw:px-4 tw:mt-1'
inputStyle='tw:input-sm'
/>
</div>
</div>

View File

@ -53,12 +53,12 @@ export const TabsForm = ({
}, [location.search])
return (
<div role='tablist' className='tw:tabs tw:tabs-lifted tw:mt-3'>
<div role='tablist' className='tw:tabs tw:tabs-lift tw:mt-3'>
<input
type='radio'
name='my_tabs_2'
role='tab'
className={'tw:tab tw:[--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'}
className={'tw:tab '}
aria-label='Info'
checked={activeTab === 1 && true}
onChange={() => updateActiveTab(1)}
@ -124,9 +124,7 @@ export const TabsForm = ({
type='radio'
name='my_tabs_2'
role='tab'
className={
'tw:tab tw:min-w-[10em] tw:[--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
}
className={'tw:tab tw:min-w-[10em] '}
aria-label='Offers & Needs'
checked={activeTab === 3 && true}
onChange={() => updateActiveTab(3)}
@ -172,7 +170,7 @@ export const TabsForm = ({
type='radio'
name='my_tabs_2'
role='tab'
className='tw:tab tw:[--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
className='tw:tab '
aria-label='Links'
checked={activeTab === 7 && true}
onChange={() => updateActiveTab(7)}

View File

@ -85,14 +85,12 @@ export const TabsView = ({
}, [location.search])
return (
<div role='tablist' className='tw:tabs tw:tabs-lifted tw:mt-2 tw:mb-2 tw:px-6'>
<div role='tablist' className='tw:tabs tw:tabs-lift tw:mt-2 tw:mb-2 tw:px-6'>
<input
type='radio'
name='my_tabs_2'
role='tab'
className={
'tw:tab tw:font-bold tw:ps-2! tw:pe-2! tw:[--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
}
className={'tw:tab tw:font-bold tw:ps-2! tw:pe-2! '}
aria-label={`${item.layer?.itemType.icon_as_labels && activeTab !== 1 ? '📝' : '📝\u00A0Info'}`}
checked={activeTab === 1 && true}
onChange={() => updateActiveTab(1)}
@ -116,9 +114,7 @@ export const TabsView = ({
type='radio'
name='my_tabs_2'
role='tab'
className={
'tw:tab tw:font-bold tw:ps-2! tw:pe-2! tw:[--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
}
className={'tw:tab tw:font-bold tw:ps-2! tw:pe-2!'}
aria-label={`${item.layer.itemType.icon_as_labels && activeTab !== 2 ? '❤️' : '❤️\u00A0Trust'}`}
checked={activeTab === 2 && true}
onChange={() => updateActiveTab(2)}
@ -199,7 +195,7 @@ export const TabsView = ({
type='radio'
name='my_tabs_2'
role='tab'
className={`tw:tab tw:font-bold tw:ps-2! tw:pe-2! ${!(item.layer.itemType.icon_as_labels && activeTab !== 3) && 'tw:min-w-[10.4em]'} tw:[--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`}
className={`tw:tab tw:font-bold tw:ps-2! tw:pe-2! ${!(item.layer.itemType.icon_as_labels && activeTab !== 3) && 'tw:min-w-[10.4em]'} `}
aria-label={`${item.layer.itemType.icon_as_labels && activeTab !== 3 ? '♻️' : '♻️\u00A0Offers & Needs'}`}
checked={activeTab === 3 && true}
onChange={() => updateActiveTab(3)}
@ -252,7 +248,7 @@ export const TabsView = ({
type='radio'
name='my_tabs_2'
role='tab'
className='tw:tab tw:font-bold tw:ps-2! tw:pe-2! tw:[--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
className='tw:tab tw:font-bold tw:ps-2! tw:pe-2! '
aria-label={`${item.layer.itemType.icon_as_labels && activeTab !== 7 ? '🔗' : '🔗\u00A0Links'}`}
checked={activeTab === 7 && true}
onChange={() => updateActiveTab(7)}

View File

@ -0,0 +1,63 @@
import { useState, useEffect } from 'react'
const themes = [
'default',
'light',
'dark',
'valentine',
'retro',
'aqua',
'cyberpunk',
'caramellatte',
'abyss',
'silk',
]
export const ThemeControl = () => {
const [theme, setTheme] = useState<string>(() => {
const savedTheme = localStorage.getItem('theme')
return savedTheme ? (JSON.parse(savedTheme) as string) : 'default'
})
useEffect(() => {
if (theme !== 'default') {
localStorage.setItem('theme', JSON.stringify(theme))
document.documentElement.setAttribute('data-theme', theme)
}
}, [theme])
return (
<div className='tw:dropdown tw:mr-2'>
<div tabIndex={0} role='button' className='tw:btn tw:m-1'>
Theme
<svg
width='12px'
height='12px'
className='tw:inline-block tw:h-2 tw:w-2 tw:fill-current tw:opacity-60'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 2048 2048'
>
<path d='M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z'></path>
</svg>
</div>
<ul
tabIndex={0}
className='tw:dropdown-content tw:bg-base-300 tw:rounded-box tw:z-1 tw:w-52 tw:p-2 tw:shadow-2xl'
>
{themes.map((t) => (
<li key={t}>
<input
className={`tw:btn ${theme === t ? 'tw:bg-primary' : ''} tw:btn-sm tw:btn-block tw:btn-ghost tw:justify-start`}
type='radio'
name='theme'
value={t}
checked={theme === t}
onChange={() => setTheme(t)}
aria-label={t.toLowerCase()}
/>
</li>
))}
</ul>
</div>
)
}

View File

@ -47,4 +47,9 @@
background-repeat: no-repeat;
background-attachment: fixed;
background-position: 50% 80%;
}
.leaflet-popup-close-button span {
color: var(--color-base-content);
opacity: 50%;
}

View File

@ -77,8 +77,4 @@
.modal-box {
max-height: calc(100dvh - 2em);
}
.tab-content .container {
height: 100%;
}

View File

@ -6,7 +6,7 @@
}
.Toastify__toast {
border-radius: 1rem;
border-radius: var(--radius-box);
--shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-colored: 0 20px 25px -5px var(--shadow-color), 0 8px 10px -6px var(--shadow-color);
box-shadow: var(--ring-offset-shadow, 0 0 #0000), var(--ring-shadow, 0 0 #0000), var(--shadow);

View File

@ -12,6 +12,8 @@ export interface UtopiaMapProps {
showFilterControl?: boolean
showLayerControl?: boolean
showGratitudeControl?: boolean
showThemeControl?: boolean
infoText?: string
donationWidget?: boolean
defaultTheme?: string
}