started on icons

This commit is contained in:
Stian Fredrik Aune
2021-06-05 18:31:21 +02:00
parent 677d9aa9b1
commit ea3037b03a
15 changed files with 360 additions and 55 deletions
+18 -16
View File
@@ -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>
+10
View File
@@ -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});
}
+52 -1
View File
@@ -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;
-2
View File
@@ -1,5 +1,3 @@
// @ts-ignore
import kelvinToRgb from "kelvin-to-rgb";
import {ColorValue} from "../models/Colors";
import iro from "@jaames/iro";
+3
View File
@@ -0,0 +1,3 @@
export function unimplemented<T>(): T {
throw new Error("unimplemented");
}
+81
View File
@@ -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
View File
@@ -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')
);
+28
View File
@@ -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];
}
+4
View File
@@ -0,0 +1,4 @@
export enum DataTarget {
Default = "",
ColorPresets = "ColorPresets",
}
+24
View File
@@ -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;
+28 -22
View File
@@ -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>
);
}
+9 -1
View File
@@ -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
+44 -10
View File
@@ -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;
}