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