Add dark mode support
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite build --watch --mode development",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"prettier": "prettier -w src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.5.1",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createContext } from "preact";
|
||||
import { useContext } from "preact/compat";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { fetchUserInfo } from "../../api";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
type TColorScheme = "light" | "dark";
|
||||
|
||||
type TColorSchemeContext = {
|
||||
colorScheme: TColorScheme;
|
||||
};
|
||||
|
||||
const ColorSchemeContext = createContext({ colorScheme: "light" } as TColorSchemeContext);
|
||||
|
||||
export const ColorSchemeProvider = ({ children }) => {
|
||||
const { data } = useQuery({ ...fetchUserInfo() });
|
||||
const [activeScheme, setActiveScheme] = useState<TColorScheme>("light");
|
||||
|
||||
useEffect(() => {
|
||||
const preference = data?.preferences?.dashboard_color_scheme || "system";
|
||||
|
||||
if (preference === "system") {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = () => {
|
||||
setActiveScheme(mediaQuery.matches ? "dark" : "light");
|
||||
};
|
||||
|
||||
handleChange();
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
} else {
|
||||
setActiveScheme(preference as TColorScheme);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<ColorSchemeContext.Provider value={{ colorScheme: activeScheme }}>
|
||||
{children}
|
||||
</ColorSchemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useColorScheme = () => useContext(ColorSchemeContext);
|
||||
@@ -1,59 +1,72 @@
|
||||
import { Sidebar } from "../Sidebar";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import { Welcome } from "../Welcome";
|
||||
import { Guild } from "../Guild";
|
||||
import { FlashProvider } from "./FlashProvider";
|
||||
import { TimezoneProvider } from "./TimezoneProvider";
|
||||
import { User } from "../User";
|
||||
import { GuildReminders } from "../Guild/GuildReminders";
|
||||
import { GuildTodos } from "../Guild/GuildTodos";
|
||||
import {Sidebar} from "../Sidebar";
|
||||
import {QueryClient, QueryClientProvider} from "react-query";
|
||||
import {Route, Router, Switch} from "wouter";
|
||||
import {Welcome} from "../Welcome";
|
||||
import {Guild} from "../Guild";
|
||||
import {FlashProvider} from "./FlashProvider";
|
||||
import {TimezoneProvider} from "./TimezoneProvider";
|
||||
import {User} from "../User";
|
||||
import {GuildReminders} from "../Guild/GuildReminders";
|
||||
import {GuildTodos} from "../Guild/GuildTodos";
|
||||
import {ColorSchemeProvider, useColorScheme} from "./ColorSchemeProvider";
|
||||
import {useEffect} from "preact/hooks";
|
||||
|
||||
const InnerApp = () => {
|
||||
const {colorScheme} = useColorScheme();
|
||||
|
||||
useEffect(() => {
|
||||
const body = document.querySelector("body");
|
||||
body.className = body.className.replace(/scheme-\w+/g, "");
|
||||
body.classList.add(`scheme-${colorScheme}`);
|
||||
}, [colorScheme]);
|
||||
|
||||
return (
|
||||
<Router base={"/dashboard"}>
|
||||
<div class={`columns is-gapless dashboard-frame scheme-${colorScheme}`}>
|
||||
<Sidebar/>
|
||||
<div class="column is-main-content">
|
||||
<div style={{margin: "0 12px 12px 12px"}}>
|
||||
<Switch>
|
||||
<Route path={"/@me/reminders"} component={User}></Route>
|
||||
<Route
|
||||
path={"/:guild/reminders"}
|
||||
component={() => (
|
||||
<Guild>
|
||||
<GuildReminders/>
|
||||
</Guild>
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path={"/:guild/todos"}
|
||||
component={() => (
|
||||
<Guild>
|
||||
<GuildTodos/>
|
||||
</Guild>
|
||||
)}
|
||||
></Route>
|
||||
<Route>
|
||||
<Welcome/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export function App() {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
let scheme = "light";
|
||||
// if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
// scheme = "dark";
|
||||
// }
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TimezoneProvider>
|
||||
<FlashProvider>
|
||||
<Router base={"/dashboard"}>
|
||||
<div class={`columns is-gapless dashboard-frame scheme-${scheme}`}>
|
||||
<Sidebar />
|
||||
<div class="column is-main-content">
|
||||
<div style={{ margin: "0 12px 12px 12px" }}>
|
||||
<Switch>
|
||||
<Route path={"/@me/reminders"} component={User}></Route>
|
||||
<Route
|
||||
path={"/:guild/reminders"}
|
||||
component={() => (
|
||||
<Guild>
|
||||
<GuildReminders />
|
||||
</Guild>
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path={"/:guild/todos"}
|
||||
component={() => (
|
||||
<Guild>
|
||||
<GuildTodos />
|
||||
</Guild>
|
||||
)}
|
||||
></Route>
|
||||
<Route>
|
||||
<Welcome />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</FlashProvider>
|
||||
</TimezoneProvider>
|
||||
<ColorSchemeProvider>
|
||||
<TimezoneProvider>
|
||||
<FlashProvider>
|
||||
<InnerApp/>
|
||||
</FlashProvider>
|
||||
</TimezoneProvider>
|
||||
</ColorSchemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
> * {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {JSX} from "preact";
|
||||
import {createPortal} from "preact/compat";
|
||||
import { JSX } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
|
||||
type Props = {
|
||||
setModalOpen: (open: boolean) => never;
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
children: string | JSX.Element | JSX.Element[] | (() => JSX.Element);
|
||||
};
|
||||
|
||||
export const Modal = ({setModalOpen, title, onSubmit, onSubmitText, children}: Props) => {
|
||||
export const Modal = ({ setModalOpen, title, onSubmit, onSubmitText, children }: Props) => {
|
||||
const body = document.querySelector("body");
|
||||
|
||||
return createPortal(
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
border-radius: 8px;
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 0 5px 0 rgba(0,0,0,0.75);
|
||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
|
||||
|
||||
.highlight {
|
||||
background-color: #35373c;
|
||||
|
||||
@@ -66,7 +66,7 @@ aside.menu {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1023px),print {
|
||||
@media screen and (max-width: 1023px), print {
|
||||
.columns:not(.is-desktop) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ import { useRef, useState } from "preact/hooks";
|
||||
import { ICON_FLASH_TIME } from "../../consts";
|
||||
import { useFlash } from "../App/FlashContext";
|
||||
|
||||
enum ColorScheme {
|
||||
System = "system",
|
||||
Dark = "dark",
|
||||
Light = "light",
|
||||
}
|
||||
|
||||
export const UserPreferences = () => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
@@ -96,6 +102,40 @@ const PreferencesModal = ({ setModalOpen }) => {
|
||||
</label>
|
||||
</div>
|
||||
<br></br>
|
||||
<div style={{ display: "flex", flexDirection: "row", alignContent: "center" }}>
|
||||
<label>
|
||||
<div class={"is-inline-block"} style={{ marginRight: "6px" }}>
|
||||
Dashboard Color Scheme:
|
||||
</div>
|
||||
<div class={"control"}>
|
||||
<div class={"is-inline-block select"}>
|
||||
{isLoading && <i class={"fa fa-spinner"} />}
|
||||
{isSuccess && (
|
||||
<select
|
||||
class={"channel-selector"}
|
||||
value={
|
||||
updatedSettings.dashboard_color_scheme === undefined
|
||||
? data.preferences.dashboard_color_scheme
|
||||
: updatedSettings.dashboard_color_scheme
|
||||
}
|
||||
onChange={(e) =>
|
||||
setUpdatedSettings((s) => ({
|
||||
...s,
|
||||
dashboard_color_scheme: (e.target as HTMLSelectElement)
|
||||
.value as ColorScheme,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value={ColorScheme.System}>System default</option>
|
||||
<option value={ColorScheme.Light}>Light</option>
|
||||
<option value={ColorScheme.Dark}>Dark</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<br></br>
|
||||
<div class="has-text-centered">
|
||||
<button
|
||||
class="button is-success is-outlined"
|
||||
|
||||
@@ -2,58 +2,280 @@
|
||||
|
||||
/* override styles for when the div is collapsed */
|
||||
div.reminderContent.is-collapsed .column.discord-frame {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .column.settings {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .reminder-settings {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .button-row {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .button-row-edit {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .reminder-topbar {
|
||||
padding-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .invert-collapses {
|
||||
display: inline-flex;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
div.reminderContent .invert-collapses {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed input[name="name"] {
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .hide-box {
|
||||
display: inline-flex;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .hide-box i {
|
||||
transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.button.is-success:not(.is-outlined) {
|
||||
color: white;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button.is-outlined.is-success:not(.is-focused, :hover, :focus) {
|
||||
background-color: white;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@import "vars";
|
||||
|
||||
.scheme-light {
|
||||
background-color: $primary-background-light;
|
||||
color: $primary-text-light;
|
||||
|
||||
.column.is-main-content {
|
||||
background-color: $primary-background-light;
|
||||
}
|
||||
|
||||
.reminderContent {
|
||||
background-color: $secondary-background-light;
|
||||
}
|
||||
}
|
||||
|
||||
.scheme-dark {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
|
||||
.column.is-main-content {
|
||||
background-color: $secondary-background-dark;
|
||||
}
|
||||
|
||||
.reminderContent {
|
||||
background-color: $primary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
.label,
|
||||
.help,
|
||||
strong {
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
.input,
|
||||
.textarea {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
|
||||
&::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
.button.is-outlined.is-success:not(.is-focused, :hover, :focus) {
|
||||
background-color: $primary-background-dark;
|
||||
color: #48c78e;
|
||||
border-color: #48c78e;
|
||||
}
|
||||
|
||||
.button.is-outlined.is-danger:not(.is-focused, :hover, :focus) {
|
||||
background-color: $primary-background-dark;
|
||||
}
|
||||
|
||||
.button.is-outlined.is-warning:not(.is-focused, :hover, :focus) {
|
||||
background-color: $primary-background-dark;
|
||||
}
|
||||
|
||||
.button:not(.is-success, .is-danger, .is-warning, .is-light) {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
.button.is-light {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Modal
|
||||
.modal-card-head,
|
||||
.modal-card-foot {
|
||||
background-color: $contrast-background-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
.modal-card-head .modal-card-title {
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
.modal-card-body {
|
||||
background-color: $primary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Navbar
|
||||
.navbar {
|
||||
background-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
.navbar-item,
|
||||
.navbar-burger {
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Select dropdown arrow
|
||||
.select:not(.is-multiple):not(.is-loading)::after {
|
||||
border-color: $primary-text-dark;
|
||||
}
|
||||
|
||||
.select select {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
// Interval selector inputs
|
||||
.interval-group input {
|
||||
background-color: $secondary-background-dark !important;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Notification
|
||||
.notification {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Links
|
||||
a:not(.menu a):not(.button) {
|
||||
color: #7eaef1;
|
||||
}
|
||||
|
||||
// Content area
|
||||
.content {
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Checkbox
|
||||
.checkbox {
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Message input
|
||||
.message-input {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Box
|
||||
.box {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Panel
|
||||
.panel {
|
||||
background-color: $secondary-background-dark;
|
||||
|
||||
.panel-heading {
|
||||
background-color: $contrast-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
color: $primary-text-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
hr {
|
||||
background-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
// Tag
|
||||
.tag:not(.is-success, .is-danger, .is-warning, .is-info) {
|
||||
background-color: $contrast-background-dark;
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Todo
|
||||
.todo {
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// Page links
|
||||
.page-links .button {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
// Loading overlay
|
||||
.load-screen {
|
||||
background-color: rgba(36, 36, 36, 0.8);
|
||||
color: $primary-text-dark;
|
||||
}
|
||||
|
||||
// File/attachment input
|
||||
.file-cta {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
background-color: $secondary-background-dark;
|
||||
color: $primary-text-dark;
|
||||
border-color: $contrast-background-dark;
|
||||
}
|
||||
|
||||
// Date/time inputs color scheme
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
// Modal background overlay
|
||||
.modal-background {
|
||||
background-color: rgba(10, 10, 10, 0.86);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user