started on icons
This commit is contained in:
+18
-16
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import {HookRouter, navigate, usePath, useRoutes} from "hookrouter";
|
||||
import {Tabs} from "./primitives/Layout";
|
||||
import {HSColorPicker} from "./primitives/Forms";
|
||||
import {ColorElement} from "./primitives/Elements";
|
||||
import {IconElement} from "./primitives/Elements";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import {LuciferIcon} from "./models/Icons";
|
||||
|
||||
const routeObj: HookRouter.RouteObject = {
|
||||
"/": () => (
|
||||
@@ -12,22 +14,22 @@ const routeObj: HookRouter.RouteObject = {
|
||||
),
|
||||
"/devices": () => (
|
||||
<div>
|
||||
<ColorElement name="Grønn" value={{h: 125, s: 0.5}}/>
|
||||
<ColorElement name="Kelvin 1k" value={{kelvin: 1000}}/>
|
||||
<ColorElement name="Kelvin 2k" value={{kelvin: 2000}}/>
|
||||
<ColorElement name="Kelvin 3k" value={{kelvin: 3000}}/>
|
||||
<ColorElement name="Kelvin 4k" value={{kelvin: 4000}}/>
|
||||
<ColorElement name="Kelvin 5k" value={{kelvin: 5000}}/>
|
||||
<ColorElement name="Kelvin 6k" value={{kelvin: 6000}}/>
|
||||
<ColorElement name="Kelvin 7k" value={{kelvin: 7000}}/>
|
||||
<ColorElement name="Kelvin 8k" value={{kelvin: 8000}}/>
|
||||
<ColorElement name="Kelvin 9k" value={{kelvin: 9000}}/>
|
||||
<ColorElement name="Kelvin 10k" value={{kelvin: 10000}}/>
|
||||
<ColorElement name="Mer... (HS)" value="hs-gradient"/>
|
||||
<ColorElement name="Mer... (K)" value="k-gradient"/>
|
||||
<IconElement icon={LuciferIcon.Hexagon} caption="Grønn" color={{h: 125, s: 0.5}}/>
|
||||
<IconElement icon={LuciferIcon.Square} caption="Kelvin 1k" color={{kelvin: 1000}}/>
|
||||
<IconElement icon={LuciferIcon.Bulb} caption="Kelvin 2k" color={{kelvin: 2000}}/>
|
||||
<IconElement icon={LuciferIcon.BulbGroup} caption="Kelvin 3k" color={{kelvin: 3000}}/>
|
||||
<IconElement icon={LuciferIcon.Bridge} caption="Kelvin 4k" color={{kelvin: 4000}}/>
|
||||
<IconElement caption="Kelvin 5k" color={{kelvin: 5000}}/>
|
||||
<IconElement caption="Kelvin 6k" color={{kelvin: 6000}}/>
|
||||
<IconElement caption="Kelvin 7k" color={{kelvin: 7000}}/>
|
||||
<IconElement caption="Kelvin 8k" color={{kelvin: 8000}}/>
|
||||
<IconElement caption="Kelvin 9k" color={{kelvin: 9000}}/>
|
||||
<IconElement caption="Kelvin 10k" color={{kelvin: 10000}}/>
|
||||
<IconElement caption="Mer" color="hs-gradient"/>
|
||||
<IconElement caption="Mer" color="k-gradient"/>
|
||||
</div>
|
||||
),
|
||||
"/settings": () => <div>3</div>,
|
||||
"/settings": () => <SettingsPage/>,
|
||||
}
|
||||
|
||||
const routeList = ["/", "/devices", "/settings"];
|
||||
@@ -43,7 +45,7 @@ function App() {
|
||||
<Tabs tabNames={tabNames}
|
||||
index={routeList.indexOf(path)}
|
||||
onChange={i => navigate(routeList[i])}
|
||||
boldIndex={0}
|
||||
large boldIndex={0}
|
||||
/>
|
||||
{route || <div>B</div>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {ColorPreset} from "../models/Colors";
|
||||
import {getRequest, postRequest} from "../helpers/rest";
|
||||
|
||||
export function listColorPresets(): Promise<ColorPreset[]> {
|
||||
return getRequest("/color-presets");
|
||||
}
|
||||
|
||||
export function addColorPreset(name: string, colorString: string) {
|
||||
return postRequest("/color-presets", {name, colorString});
|
||||
}
|
||||
@@ -1,5 +1,56 @@
|
||||
import React from "react";
|
||||
import React, {createContext, useCallback, useEffect, useState} from "react";
|
||||
import {ColorPreset} from "../models/Colors";
|
||||
import {DataTarget} from "../models/Shared";
|
||||
import {unimplemented} from "../helpers/misc";
|
||||
import {listColorPresets} from "../actions/ColorPresets";
|
||||
|
||||
interface DataContextValue {
|
||||
colorPresets: ColorPreset[]
|
||||
|
||||
ready(target?: DataTarget): boolean
|
||||
|
||||
refresh(target?: DataTarget): void
|
||||
}
|
||||
|
||||
const DataContext = createContext<DataContextValue>({
|
||||
colorPresets: [],
|
||||
ready: unimplemented,
|
||||
refresh: unimplemented,
|
||||
});
|
||||
|
||||
export const DataContextProvider: React.FC = ({children}) => {
|
||||
const [colorPresets, setColorPresets] = useState<ColorPreset[]>([]);
|
||||
|
||||
const [readyMap, setReadyMap] = useState<{ [key: string]: boolean }>({
|
||||
[DataTarget.Default]: false,
|
||||
[DataTarget.ColorPresets]: false,
|
||||
});
|
||||
|
||||
const ready = useCallback((target: DataTarget = DataTarget.Default) => {
|
||||
return readyMap[target] || false;
|
||||
}, [readyMap]);
|
||||
|
||||
const refresh = useCallback((target: DataTarget = DataTarget.Default) => {
|
||||
const refreshing: Promise<any>[] = [];
|
||||
if ([DataTarget.Default, DataTarget.ColorPresets].includes(target)) {
|
||||
refreshing.push(listColorPresets().then(setColorPresets));
|
||||
}
|
||||
|
||||
Promise.all(refreshing).then(() => setReadyMap(prev => ({...prev, [target]: true})));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={{
|
||||
colorPresets,
|
||||
ready, refresh,
|
||||
}}>
|
||||
{children}
|
||||
</DataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataContext;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-ignore
|
||||
import kelvinToRgb from "kelvin-to-rgb";
|
||||
import {ColorValue} from "../models/Colors";
|
||||
import iro from "@jaames/iro";
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function unimplemented<T>(): T {
|
||||
throw new Error("unimplemented");
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import Axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from "axios";
|
||||
|
||||
interface Response<T> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export const authError = new Error("unauthenticated");
|
||||
|
||||
export async function getRequest<T>(url: string): Promise<T> {
|
||||
const cors = url.includes("//");
|
||||
|
||||
return sendRequest<T>({
|
||||
method: "GET",
|
||||
url: cors ? url : `/api${url}`,
|
||||
responseType: "json",
|
||||
}, cors);
|
||||
}
|
||||
|
||||
export function postRequest<T>(url: string, data: object = {}): Promise<T> {
|
||||
return sendRequest<T>({
|
||||
method: "POST",
|
||||
url: `/api${url}`,
|
||||
responseType: "json",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function putRequest<T>(url: string, data: object): Promise<T> {
|
||||
return sendRequest<T>({
|
||||
method: "PUT",
|
||||
url: `/api${url}`,
|
||||
responseType: "json",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteRequest<T>(url: string): Promise<T> {
|
||||
return sendRequest<T>({
|
||||
method: "DELETE",
|
||||
url: `/api${url}`,
|
||||
responseType: "json",
|
||||
});
|
||||
}
|
||||
|
||||
async function sendRequest<T>(config: AxiosRequestConfig, cors: boolean = false): Promise<T> {
|
||||
try {
|
||||
const res = await Axios(config);
|
||||
|
||||
return await handleResponse(res, cors);
|
||||
} catch (e) {
|
||||
if (e === authError) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return await handleResponse((e as AxiosError).response as AxiosResponse);
|
||||
}
|
||||
}
|
||||
|
||||
function handleResponse<T>(response: AxiosResponse, object: boolean = false): Promise<T> {
|
||||
if (object) {
|
||||
return Promise.resolve(response.data);
|
||||
} else {
|
||||
const obj: Response<T> | undefined = response?.data;
|
||||
|
||||
if (![200, 201].includes(response.status)) {
|
||||
if (response.status === 403) {
|
||||
return Promise.reject(authError);
|
||||
} else {
|
||||
return Promise.reject(obj?.message || "Ukjent feil");
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj) {
|
||||
return Promise.reject("Ingen data mottatt");
|
||||
}
|
||||
|
||||
return Promise.resolve(obj.data);
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -3,10 +3,13 @@ import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import {DataContextProvider} from "./contexts/DataContext";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<DataContextProvider>
|
||||
<App/>
|
||||
</DataContextProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import bridgeIcon from "@iconify-icons/mdi/bridge";
|
||||
import hexagonIcon from "@iconify-icons/mdi/hexagon";
|
||||
import lightBulbIcon from "@iconify-icons/mdi/lightbulb";
|
||||
import lightBulbGroupIcon from "@iconify-icons/mdi/lightbulb-group";
|
||||
import squareIcon from "@iconify-icons/mdi/square-rounded";
|
||||
|
||||
// To add a new icon, follow the instructions on this page:
|
||||
// https://iconify.design/icon-sets/mdi/
|
||||
|
||||
export enum LuciferIcon {
|
||||
Bridge = "Bridge",
|
||||
Bulb = "Bulb",
|
||||
BulbGroup = "BulbGroup",
|
||||
Hexagon = "Hexagon",
|
||||
Square = "Square",
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
[LuciferIcon.Bridge]: bridgeIcon,
|
||||
[LuciferIcon.Bulb]: lightBulbIcon,
|
||||
[LuciferIcon.BulbGroup]: lightBulbGroupIcon,
|
||||
[LuciferIcon.Hexagon]: hexagonIcon,
|
||||
[LuciferIcon.Square]: squareIcon,
|
||||
}
|
||||
|
||||
export function toIconify(icon: LuciferIcon): object {
|
||||
return iconMap[icon];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum DataTarget {
|
||||
Default = "",
|
||||
ColorPresets = "ColorPresets",
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {Page} from "../primitives/Layout";
|
||||
import DataContext from "../contexts/DataContext";
|
||||
import {IconElement} from "../primitives/Elements";
|
||||
import {addColorPreset} from "../actions/ColorPresets";
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
const {colorPresets, refresh} = useContext(DataContext);
|
||||
|
||||
useEffect(() => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page title="Oppsett">
|
||||
<h1>Definerte farger</h1>
|
||||
{colorPresets.map(cp => (
|
||||
<IconElement key={cp.id} {...cp} caption={cp.name}/>
|
||||
))}
|
||||
<button>Add</button>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, {CSSProperties, useMemo} from "react";
|
||||
import {ColorValue} from "../models/Colors";
|
||||
// @ts-ignore
|
||||
import kelvinToRgb from "kelvin-to-rgb";
|
||||
import {kelvinToRgbHex} from "../helpers/kelvin";
|
||||
import {Icon} from "@iconify/react";
|
||||
|
||||
import "./Elements.sass";
|
||||
import {kelvinToColorValue, kelvinToRgbHex} from "../helpers/kelvin";
|
||||
import {LuciferIcon, toIconify} from "../models/Icons";
|
||||
|
||||
function Element({children}: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
@@ -13,35 +13,41 @@ function Element({children}: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
|
||||
interface ColorElementProps {
|
||||
name: string
|
||||
value: ColorValue | "hs-gradient" | "k-gradient"
|
||||
caption: string
|
||||
icon?: LuciferIcon
|
||||
color?: ColorValue | "hs-gradient" | "k-gradient"
|
||||
}
|
||||
|
||||
export function ColorElement({name, value}: ColorElementProps) {
|
||||
export function IconElement({caption, color, icon}: ColorElementProps) {
|
||||
const style: CSSProperties = useMemo(() => {
|
||||
const s: CSSProperties = {};
|
||||
const s: CSSProperties = {
|
||||
backgroundClip: "text",
|
||||
// @ts-ignore
|
||||
textFillColor: "transparent",
|
||||
};
|
||||
|
||||
if (value === "hs-gradient") {
|
||||
s.backgroundImage = "linear-gradient(to bottom right, red, yellow, green, blue, violet)";
|
||||
} else if (value === "k-gradient") {
|
||||
s.backgroundImage = "linear-gradient(to bottom right, #ff9329, #ffd6aa, #fffaf4, #fff, #c9e2ff, #40a3ff)";
|
||||
} else if (value.kelvin !== undefined && value.kelvin > 0) {
|
||||
s.backgroundColor = kelvinToRgbHex(value.kelvin);
|
||||
} else {
|
||||
const hue = value.h || 0;
|
||||
const sat = Math.floor((value.s || 0) * 100);
|
||||
if (color !== undefined) {
|
||||
if (color === "hs-gradient") {
|
||||
s.backgroundImage = "linear-gradient(to bottom right, red, yellow, green, blue, violet)";
|
||||
} else if (color === "k-gradient") {
|
||||
s.backgroundImage = "linear-gradient(to bottom right, #ff9329, #ffd6aa, #fffaf4, #fff, #c9e2ff, #40a3ff)";
|
||||
} else if (color.kelvin !== undefined && color.kelvin > 0) {
|
||||
s.color = kelvinToRgbHex(color.kelvin);
|
||||
} else {
|
||||
const hue = color.h || 0;
|
||||
const sat = Math.floor((color.s || 0) * 100);
|
||||
|
||||
s.backgroundColor = `hsl(${hue}, ${sat}%, 50%)`;
|
||||
s.color = `hsl(${hue}, ${sat}%, 50%)`;
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}, [value]);
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<Element>
|
||||
<div className="ColorElement-block"
|
||||
style={style}
|
||||
/>
|
||||
{name}
|
||||
<Icon icon={toIconify(icon || LuciferIcon.Square)} className="ColorElement-block" style={style}/>
|
||||
{caption}
|
||||
</Element>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
.Tabs-container
|
||||
background-color: $color-foreground-dark
|
||||
|
||||
&.Tabs-large
|
||||
text-align: center
|
||||
font-size: 175%
|
||||
|
||||
.Tabs-element
|
||||
display: inline-block
|
||||
padding: 0.25em 0.7ch
|
||||
@@ -14,4 +18,8 @@
|
||||
|
||||
.Tabs-bold
|
||||
font-family: 'Bitstream Vera Serif', 'Lucida Fax', serif
|
||||
font-weight: 600
|
||||
font-weight: 600
|
||||
|
||||
.Page
|
||||
max-width: 800px
|
||||
margin: 0.25em auto
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useEffect} from "react";
|
||||
import React, {PropsWithChildren, useCallback, useEffect} from "react";
|
||||
import "./Layout.sass";
|
||||
|
||||
interface TabsProps {
|
||||
@@ -6,9 +6,10 @@ interface TabsProps {
|
||||
index: number
|
||||
onChange: (newIndex: number) => void
|
||||
boldIndex?: number
|
||||
large?: boolean
|
||||
}
|
||||
|
||||
export function Tabs({tabNames, index, onChange, boldIndex}: TabsProps) {
|
||||
export function Tabs({tabNames, index, onChange, large, boldIndex}: TabsProps) {
|
||||
useEffect(() => {
|
||||
if (index < 0) {
|
||||
onChange(0);
|
||||
@@ -32,14 +33,47 @@ export function Tabs({tabNames, index, onChange, boldIndex}: TabsProps) {
|
||||
}, [index, boldIndex])
|
||||
|
||||
return (
|
||||
<div className="Tabs-container">
|
||||
{tabNames.map((name, i) => (
|
||||
<div className={tabClass(i)}
|
||||
onClick={() => onChange(i)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
<div className={`Tabs-container ${large ? "Tabs-large" : ""}`}>
|
||||
<div className="Tabs-inner">
|
||||
{tabNames.map((name, i) => (
|
||||
<div className={tabClass(i)}
|
||||
onClick={() => onChange(i)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function Page({title, children}: PropsWithChildren<PageProps>) {
|
||||
useEffect(() => {
|
||||
window.document.title = title ? `${title} - Lucifer` : "Lucifer";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="Page">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DialogProps {
|
||||
title: string
|
||||
buttons?: {
|
||||
text: string
|
||||
onClick?: (() => void)
|
||||
}[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function Dialog({}: PropsWithChildren<DialogProps>) {
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user