index.tsx raw

   1  import { Label } from '@/components/ui/label'
   2  import { PRIMARY_COLORS, TPrimaryColor } from '@/constants'
   3  import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
   4  import { cn } from '@/lib/utils'
   5  import { useScreenSize } from '@/providers/ScreenSizeProvider'
   6  import { useTheme } from '@/providers/ThemeProvider'
   7  import { useUserPreferences } from '@/providers/UserPreferencesProvider'
   8  import { Columns2, LayoutList, List, Monitor, Moon, PanelLeft, Sun } from 'lucide-react'
   9  import { forwardRef } from 'react'
  10  import { useTranslation } from 'react-i18next'
  11  
  12  const THEMES = [
  13    { key: 'system', label: 'System', icon: <Monitor className="size-5" /> },
  14    { key: 'light', label: 'Light', icon: <Sun className="size-5" /> },
  15    { key: 'dark', label: 'Dark', icon: <Moon className="size-5" /> },
  16  ] as const
  17  
  18  const LAYOUTS = [
  19    { key: false, label: 'Two-column', icon: <Columns2 className="size-5" /> },
  20    { key: true, label: 'Single-column', icon: <PanelLeft className="size-5" /> }
  21  ] as const
  22  
  23  const NOTIFICATION_STYLES = [
  24    { key: 'detailed', label: 'Detailed', icon: <LayoutList className="size-5" /> },
  25    { key: 'compact', label: 'Compact', icon: <List className="size-5" /> }
  26  ] as const
  27  
  28  const AppearanceSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
  29    const { t } = useTranslation()
  30    const { isSmallScreen } = useScreenSize()
  31    const { themeSetting, setThemeSetting, primaryColor, setPrimaryColor } = useTheme()
  32    const {
  33      enableSingleColumnLayout,
  34      updateEnableSingleColumnLayout,
  35      notificationListStyle,
  36      updateNotificationListStyle
  37    } = useUserPreferences()
  38  
  39    return (
  40      <SecondaryPageLayout ref={ref} index={index} title={t('Appearance')}>
  41        <div className="space-y-4 my-3">
  42          <div className="flex flex-col gap-2 px-4">
  43            <Label className="text-base">{t('Theme')}</Label>
  44            <div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
  45              {THEMES.map(({ key, label, icon }) => (
  46                <OptionButton
  47                  key={key}
  48                  isSelected={themeSetting === key}
  49                  icon={icon}
  50                  label={t(label)}
  51                  onClick={() => setThemeSetting(key)}
  52                />
  53              ))}
  54            </div>
  55          </div>
  56          {!isSmallScreen && (
  57            <div className="flex flex-col gap-2 px-4">
  58              <Label className="text-base">{t('Layout')}</Label>
  59              <div className="grid grid-cols-2 gap-4 w-full">
  60                {LAYOUTS.map(({ key, label, icon }) => (
  61                  <OptionButton
  62                    key={key.toString()}
  63                    isSelected={enableSingleColumnLayout === key}
  64                    icon={icon}
  65                    label={t(label)}
  66                    onClick={() => updateEnableSingleColumnLayout(key)}
  67                  />
  68                ))}
  69              </div>
  70            </div>
  71          )}
  72          <div className="flex flex-col gap-2 px-4">
  73            <Label className="text-base">{t('Notification list style')}</Label>
  74            <div className="grid grid-cols-2 gap-4 w-full">
  75              {NOTIFICATION_STYLES.map(({ key, label, icon }) => (
  76                <OptionButton
  77                  key={key}
  78                  isSelected={notificationListStyle === key}
  79                  icon={icon}
  80                  label={t(label)}
  81                  onClick={() => updateNotificationListStyle(key)}
  82                />
  83              ))}
  84            </div>
  85          </div>
  86          <div className="flex flex-col gap-2 px-4">
  87            <Label className="text-base">{t('Primary color')}</Label>
  88            <div className="grid grid-cols-4 gap-4 w-full">
  89              {Object.entries(PRIMARY_COLORS).map(([key, config]) => (
  90                <OptionButton
  91                  key={key}
  92                  isSelected={primaryColor === key}
  93                  icon={
  94                    <div
  95                      className="size-8 rounded-full shadow-md"
  96                      style={{
  97                        backgroundColor: `hsl(${config.light.primary})`
  98                      }}
  99                    />
 100                  }
 101                  label={t(config.name)}
 102                  onClick={() => setPrimaryColor(key as TPrimaryColor)}
 103                />
 104              ))}
 105            </div>
 106          </div>
 107        </div>
 108      </SecondaryPageLayout>
 109    )
 110  })
 111  AppearanceSettingsPage.displayName = 'AppearanceSettingsPage'
 112  export default AppearanceSettingsPage
 113  
 114  const OptionButton = ({
 115    isSelected,
 116    onClick,
 117    icon,
 118    label
 119  }: {
 120    isSelected: boolean
 121    onClick: () => void
 122    icon: React.ReactNode
 123    label: string
 124  }) => {
 125    return (
 126      <button
 127        onClick={onClick}
 128        className={cn(
 129          'flex flex-col items-center gap-2 py-4 rounded-lg border-2 transition-all',
 130          isSelected ? 'border-primary' : 'border-border hover:border-muted-foreground/40'
 131        )}
 132      >
 133        <div className="flex items-center justify-center w-8 h-8">{icon}</div>
 134        <span className="text-xs font-medium">{label}</span>
 135      </button>
 136    )
 137  }
 138