diff --git a/README.md b/README.md index d7e83bf4..be0a232e 100644 --- a/README.md +++ b/README.md @@ -58,14 +58,44 @@ Please do not hesitate to ask a question, report a bug or add a suggestion. or s ## Development -Please use the develop branch. Create an .env file with the necessary env variables +Please use the develop branch. Create an `.env` file in the root directory with the following variables: ```bash -$ git clone --branch develop git@github.com:medyo/hackertab.dev.git +VITE_API_URL=https://api.hackertab.dev/ +VITE_BUILD_TARGET=web # or extension +VITE_BUILD_PLATFORM=chrome # optional, used for extension builds (chrome or firefox) +VITE_FIREBASE_API_KEY= # optional for local dev, required for auth features +VITE_AMPLITUDE_URL= # optional +VITE_AMPLITUDE_KEY= # optional +VITE_SENTRY_DSN= # optional +``` + +### Setup + +Make sure you are using Node.js version 18. + +```bash +$ git clone https://github.com/medyo/hackertab.dev.git $ cd hackertab.dev $ yarn $ yarn start -$ # Then visit http://localhost:3000 +``` + +Then visit [http://localhost:5173](http://localhost:5173) (or the port shown in your terminal). + +## 🚀 Build + +To build the project for different targets: + +```bash +# Web build +$ yarn build:web + +# Chrome extension +$ yarn build:chrome + +# Firefox extension +$ yarn build:firefox ``` ## Maintainers diff --git a/src/components/Elements/Button/Button.css b/src/components/Elements/Button/Button.css index be5b5d51..e460f0ea 100644 --- a/src/components/Elements/Button/Button.css +++ b/src/components/Elements/Button/Button.css @@ -85,13 +85,4 @@ color: var(--button-hover-text-color); } } - - &.dark-focus { - background-color: var(--dark-mode-background-color); - color: var(--dark-mode-text-color); - - &:hover { - opacity: 0.9; - } - } } diff --git a/src/components/Elements/Button/CircleButton.tsx b/src/components/Elements/Button/CircleButton.tsx index 71d112dc..5ab5a036 100644 --- a/src/components/Elements/Button/CircleButton.tsx +++ b/src/components/Elements/Button/CircleButton.tsx @@ -9,7 +9,6 @@ const sizes = { const variants = { primary: 'primary', - darkfocus: 'dark-focus', } type CircleButtonProps = { diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 8e798cb4..84587808 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -1,9 +1,8 @@ import { clsx } from 'clsx' import { useCallback, useEffect, useState } from 'react' -import { BsFillBookmarksFill, BsFillGearFill, BsMoonFill } from 'react-icons/bs' +import { BsFillBookmarksFill, BsFillGearFill } from 'react-icons/bs' import { CgTab } from 'react-icons/cg' import { FaCrown } from 'react-icons/fa' -import { IoMdSunny } from 'react-icons/io' import { MdDoDisturbOff } from 'react-icons/md' import { RiDashboardHorizontalFill } from 'react-icons/ri' import { TfiLayoutColumn4Alt } from 'react-icons/tfi' @@ -15,12 +14,7 @@ import { UserTags } from 'src/components/Elements/UserTags' import { useAuth } from 'src/features/auth' import { Changelog } from 'src/features/changelog' import { useRemoteConfigStore } from 'src/features/remoteConfig' -import { - identifyUserTheme, - trackDNDDisable, - trackDisplayTypeChange, - trackThemeSelect, -} from 'src/lib/analytics' +import { trackDNDDisable, trackDisplayTypeChange } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { lazyImport } from 'src/utils/lazyImport' import { Button, CircleButton } from '../Elements' @@ -30,36 +24,32 @@ const { DonateModal } = lazyImport(() => import('src/features/donate'), 'DonateM export const Header = () => { const { openAuthModal, user, isConnected, isConnecting } = useAuth() - const [themeIcon, setThemeIcon] = useState() const [openDonateModal, setOpenDonateModal] = useState(false) const { paywall } = useRemoteConfigStore() - const { theme, setTheme, setDNDDuration, isDNDModeActive, layout, setLayout } = - useUserPreferences() + const { theme, setDNDDuration, isDNDModeActive, layout, setLayout } = useUserPreferences() const navigate = useNavigate() const location = useLocation() useEffect(() => { - document.documentElement.classList.add(theme) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + const applyResolvedTheme = (resolved: 'dark' | 'light') => { + document.documentElement.classList.remove('dark', 'light') + document.documentElement.classList.add(resolved) + } - useEffect(() => { - if (theme === 'light') { - document.documentElement.classList.replace('dark', theme) - setThemeIcon() - } else if (theme === 'dark') { - document.documentElement.classList.replace('light', theme) - setThemeIcon() + if (theme === 'system') { + const mq = window.matchMedia('(prefers-color-scheme: dark)') + const updateSystemTheme = (matches: boolean) => { + applyResolvedTheme(matches ? 'dark' : 'light') + } + updateSystemTheme(mq.matches) + const handler = (e: MediaQueryListEvent) => updateSystemTheme(e.matches) + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + } else { + applyResolvedTheme(theme) } }, [theme]) - const onThemeChange = useCallback(() => { - const newTheme = theme === 'dark' ? 'light' : 'dark' - setTheme(newTheme) - trackThemeSelect(newTheme) - identifyUserTheme(newTheme) - }, [theme, setTheme]) - const onLayoutChange = useCallback(() => { const newLayout = layout === 'cards' ? 'grid' : 'cards' trackDisplayTypeChange(newLayout) @@ -111,9 +101,6 @@ export const Header = () => { {layout === 'cards' ? : } - - {themeIcon} - navigate('/settings/bookmarks')}> diff --git a/src/features/settings/components/GeneralSettings/GeneralSettings.tsx b/src/features/settings/components/GeneralSettings/GeneralSettings.tsx index c0e3bf11..7a6d4b06 100644 --- a/src/features/settings/components/GeneralSettings/GeneralSettings.tsx +++ b/src/features/settings/components/GeneralSettings/GeneralSettings.tsx @@ -1,4 +1,7 @@ import React from 'react' +import { BsMoonFill } from 'react-icons/bs' +import { IoMdSunny } from 'react-icons/io' +import { MdMonitor } from 'react-icons/md' import Toggle from 'react-toggle' import 'react-toggle/style.css' import { Footer } from 'src/components/Layout' @@ -12,6 +15,7 @@ import { trackThemeSelect, } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' +import { Theme } from 'src/types' import { DeleteAccount } from '../UserSettings/DeleteAccount' import { UserInfo } from '../UserSettings/UserInfo' import { CardsNumberSettings } from './CardsNumberSettings' @@ -19,6 +23,12 @@ import { DNDSettings } from './DNDSettings' import './generalSettings.css' import { LayoutSettings } from './LayoutSettings' +const themeIcons = { + light: , + dark: , + system: , +} + export const GeneralSettings = () => { const { openLinksNewTab, @@ -47,8 +57,7 @@ export const GeneralSettings = () => { setListingMode(value) } - const onDarkModeChange = () => { - const newTheme = theme === 'dark' ? 'light' : 'dark' + const onThemeChange = (newTheme: Theme) => { setTheme(newTheme) trackThemeSelect(newTheme) identifyUserTheme(newTheme) @@ -70,9 +79,23 @@ export const GeneralSettings = () => {
-

Dark Mode

+

Theme

- +
+ {(['light', 'dark', 'system'] as Theme[]).map((option) => ( + + ))} +
diff --git a/src/features/settings/components/GeneralSettings/generalSettings.css b/src/features/settings/components/GeneralSettings/generalSettings.css index a490f502..cf422090 100644 --- a/src/features/settings/components/GeneralSettings/generalSettings.css +++ b/src/features/settings/components/GeneralSettings/generalSettings.css @@ -5,6 +5,7 @@ align-self: flex-start; color: var(--primary-text-color); } + .settingRow { padding: 10px 0; display: flex; @@ -12,11 +13,13 @@ align-items: center; justify-content: space-between; } + .settingRow .optionIcon { display: flex; align-items: center; column-gap: 8px; } + .settingRow:not(:last-child) { border-bottom: 1px solid var(--card-content-divider); } @@ -34,12 +37,15 @@ cursor: pointer; transition: opacity 0.2s linear; } + .settingContent button:hover { opacity: 0.9; } + .rssButton { background-color: #ee802f; color: white; + &:hover { background-color: #f99147; color: white; @@ -50,16 +56,54 @@ width: 100%; flex: 1; } + .settingHint { font-size: 12px; margin-top: 12px; } + .settingHint a { text-decoration: underline; cursor: pointer; font-weight: 500; color: var(--primary-text-color); } + +/** +Theme selector styles +**/ +.themeSelector { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.themeOption { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 6px 16px; + border-radius: 20px; + border: 1px solid var(--card-border-color); + background-color: transparent; + color: var(--secondary-text-color); + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; + user-select: none; +} + +.themeOption input[type='radio'] { + display: none; +} + +.themeOption.active { + background-color: var(--button-background-color); + border-color: var(--button-background-color); + color: var(--button-text-color); +} + /** Select styles **/ @@ -67,27 +111,34 @@ Select styles background-color: var(--card-background-color) !important; border-color: var(--tag-border-color) !important; } + .hackertab__indicator-separator { background-color: var(--tag-secondary-color) !important; } + .hackertab__indicator { color: var(--tag-secondary-color) !important; } + .hackertab__multi-value { background-color: var(--tag-background-color) !important; border-color: var(--tag-background-color) !important; border-radius: 20px !important; } + .hackertab__multi-value__label { color: var(--tag-text-color) !important; } + .hackertab__menu { background-color: var(--card-background-color) !important; } + .hackertab__option { color: var(--tag-input-background) !important; background-color: var(--card-background-color) !important; } + .hackertab__single-value { color: var(--primary-text-color) !important; } @@ -96,6 +147,7 @@ Select styles background: var(--tag-background-color) !important; color: var(--tag-text-color) !important; } + .hackertab__multi-value__remove { border-radius: 20px !important; color: var(--tag-secondary-color) !important; @@ -115,26 +167,32 @@ Select styles border-radius: 100%; align-self: flex-start; } + .userDetails { display: flex; flex-direction: row; gap: 8px; } + .userInfo { display: flex; flex-direction: column; gap: 8px; } + .userName { font-weight: 600; } + .actions { margin-top: 6px; } + .description { font-size: 0.9em; opacity: 0.9; } + .sub { font-size: 0.9em; opacity: 0.9; @@ -156,12 +214,14 @@ Select styles padding: 0; font-size: 0.9em; } + .icon { width: 1.6em; vertical-align: bottom; position: relative; top: 2px; } + .highlight { font-weight: 900; color: #ff8f1f; @@ -178,6 +238,7 @@ Select styles position: absolute; margin-top: 9px; } + .streaks ul.streaksWeek { position: relative; margin: 0; @@ -197,9 +258,11 @@ Select styles width: 48px; position: relative; } + .streaks .dayWrapper:last-child { width: auto; } + .streaks .dayWrapper .day { border: 1px solid var(--card-content-divider); background-color: var(--card-content-divider); @@ -209,6 +272,7 @@ Select styles position: relative; z-index: 5; } + .streaks .dayWrapper::before { content: ''; display: block; @@ -223,6 +287,7 @@ Select styles border: 2px solid #18bc2d; background-color: #18bc2d; } + .streaks .checked:is(:has(+ .checked))::before { background-color: #18bc2d; } @@ -231,10 +296,12 @@ Select styles .settingContent { margin-top: 6px; } + .settingRow { flex-direction: column; align-items: flex-start; } + .userContent { flex-direction: column; align-items: flex-start; diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 3d571c63..cb729719 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -179,7 +179,7 @@ export const trackSearchEngineSelect = (searchEngineName: string) => { }) } -export const trackThemeSelect = (theme: 'dark' | 'light') => { +export const trackThemeSelect = (theme: 'dark' | 'light' | 'system') => { trackEvent({ object: Objects.THEME, verb: Verbs.SELECT, @@ -459,7 +459,7 @@ export const identifyUserCards = (sources: string[]) => { identifyUserProperty(Attributes.SOURCES, sources) } -export const identifyUserTheme = (theme: 'dark' | 'light') => { +export const identifyUserTheme = (theme: 'dark' | 'light' | 'system') => { identifyUserProperty(Attributes.THEME, theme) } diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index ae70d2eb..72303c6e 100644 --- a/src/lib/firebase.ts +++ b/src/lib/firebase.ts @@ -6,10 +6,21 @@ const firebaseConfig = { apiKey: FIREBASE_API_KEY, } +let firebaseAuth: any + if (!FIREBASE_API_KEY) { - console.warn('Missing Firebase api Key') + console.warn('Missing Firebase api Key, Auth features will not work') + firebaseAuth = { + onAuthStateChanged: (callback: any) => { + callback(null) + return () => {} + }, + signOut: () => Promise.resolve(), + } +} else { + // Initialize Firebase + const app = initializeApp(firebaseConfig) + firebaseAuth = getAuth(app) } -// Initialize Firebase -const app = initializeApp(firebaseConfig) -const firebaseAuth = getAuth(app) + export { firebaseAuth } diff --git a/src/types/index.ts b/src/types/index.ts index c34ba350..0008c6b7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,7 +25,7 @@ export type SearchEngine = { } export type Layout = 'grid' | 'cards' -export type Theme = 'dark' | 'light' +export type Theme = 'dark' | 'light' | 'system' export type ListingMode = 'normal' | 'compact' export type BaseEntry = { @@ -133,7 +133,7 @@ export type CardPropsType = { export type BaseItemPropsType< T extends { id: string - } + }, > = { item: T className?: string