Merge pull request #188 from brecert/theme_shop

This commit is contained in:
Paul Makles 2021-09-09 21:58:13 +01:00 committed by GitHub
commit 99116981ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 416 additions and 19 deletions

View file

@ -278,14 +278,18 @@ export const PRESETS: Record<string, Theme> = {
const keys = Object.keys(PRESETS.dark); const keys = Object.keys(PRESETS.dark);
const GlobalTheme = createGlobalStyle<{ theme: Theme }>` const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
:root { :root {
${(props) => ${(props) => generateVariables(props.theme)}
(Object.keys(props.theme) as Variables[]).map((key) => {
if (!keys.includes(key)) return;
return `--${key}: ${props.theme[key]};`;
})}
} }
`; `;
export const generateVariables = (theme: Theme) => {
const mergedTheme = { ...PRESETS[theme.light ? 'light' : 'dark'], ...theme }
return (Object.keys(mergedTheme) as Variables[]).map((key) => {
if (!keys.includes(key)) return;
return `--${key}: ${mergedTheme[key]};`;
})
}
// Load the default default them and apply extras later // Load the default default them and apply extras later
export const ThemeContext = createContext<Theme>(PRESETS["dark"]); export const ThemeContext = createContext<Theme>(PRESETS["dark"]);

4
src/env.d.ts vendored
View file

@ -2,3 +2,7 @@ interface ImportMetaEnv {
VITE_API_URL: string; VITE_API_URL: string;
VITE_THEMES_URL: string; VITE_THEMES_URL: string;
} }
interface ImportMeta {
env: ImportMetaEnv
}

View file

@ -16,6 +16,7 @@ import {
User, User,
Megaphone, Megaphone,
Speaker, Speaker,
Store,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { Route, Switch, useHistory } from "react-router-dom"; import { Route, Switch, useHistory } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js"; import { LIBRARY_VERSION } from "revolt.js";
@ -48,6 +49,8 @@ import { Notifications } from "./panes/Notifications";
import { Profile } from "./panes/Profile"; import { Profile } from "./panes/Profile";
import { Sessions } from "./panes/Sessions"; import { Sessions } from "./panes/Sessions";
import { Sync } from "./panes/Sync"; import { Sync } from "./panes/Sync";
import { ThemeShop } from "./panes/ThemeShop";
import { isExperimentEnabled } from "../../redux/reducers/experiments";
export default function Settings() { export default function Settings() {
const history = useHistory(); const history = useHistory();
@ -123,12 +126,19 @@ export default function Settings() {
title: <Text id="app.settings.pages.experiments.title" />, title: <Text id="app.settings.pages.experiments.title" />,
}, },
{ {
divider: true, divider: !isExperimentEnabled('theme_shop'),
category: "revolt", category: "revolt",
id: "bots", id: "bots",
icon: <Bot size={20} />, icon: <Bot size={20} />,
title: <Text id="app.settings.pages.bots.title" />, title: <Text id="app.settings.pages.bots.title" />,
}, },
{
hidden: !isExperimentEnabled('theme_shop'),
divider: true,
id: "theme_shop",
icon: <Store size={20} />,
title: <Text id="app.settings.pages.theme_shop.title" />,
},
{ {
id: "feedback", id: "feedback",
icon: <Megaphone size={20} />, icon: <Megaphone size={20} />,
@ -169,6 +179,9 @@ export default function Settings() {
<Route path="/settings/bots"> <Route path="/settings/bots">
<MyBots /> <MyBots />
</Route> </Route>
{isExperimentEnabled('theme_shop') && <Route path="/settings/theme_shop">
<ThemeShop />
</Route>}
<Route path="/settings/feedback"> <Route path="/settings/feedback">
<Feedback /> <Feedback />
</Route> </Route>

View file

@ -0,0 +1,179 @@
<svg xmlns="http://www.w3.org/2000/svg" id="preview" viewBox="0 0 323 202" fill="none" preserveAspectRatio="xMidYMid meet">
<defs>
<g id="author">
<circle r="9.25" cy="10" cx="10" fill="#CFCFCF" />
<rect height="7" width="29.5" y="0.75" x="26" rx="1.5" fill="var(--foreground)"/>
<rect height="7" width="24.5" y="0.75" x="59.5" rx="1.5" fill="var(--tertiary-foreground)"/>
</g>
</defs>
<!-- backgrounds -->
<rect x="0" y="0" width="100%" height="100%" fill="var(--background)" />
<path d="M27 14C27 11.7909 28.7909 10 31 10H90V202H31C28.7909 202 27 200.209 27 198V14Z" fill="var(--secondary-background)"/>
<rect x="90" y="10" width="233" height="192" fill="var(--primary-background)"/>
<rect x="90" y="10" width="233" height="18" fill="var(--primary-header)"/>
<rect x="97" y="16" width="30" height="6" rx="2" fill="var(--foreground)"/>
<!-- bottom message -->
<use x="96.5" y="144.5" href="#author" />
<rect height="5.75" width="21" y="158.25" x="122.5" rx="1.5" fill="var(--foreground)"/>
<rect height="5.25" width="40" y="158.25" x="147" rx="1.5" fill="var(--foreground)"/>
<rect height="5.25" width="47.5" y="158.25" x="190.5" rx="1.5" fill="var(--foreground)"/>
<path opacity="0.5" d="M97 131.868H262.242" stroke="var(--tertiary-foreground)" stroke-width="0.85" stroke-linecap="round"/>
<!-- middle message -->
<use x="96.5" y="80.5" href="#author" />
<rect height="5.25" width="70" y="94.5" x="122.5" rx="1.5" fill="var(--accent)"/>
<rect height="5.25" width="20" y="106.75" x="128.5" rx="1.5" fill="var(--accent)"/>
<rect height="5.25" width="89.25" y="115" x="128.5" rx="1.5" fill="var(--secondary-foreground)"/>
<path d="M122.954 105.913V120.621" stroke="#BFBFBF" stroke-width="0.86514" stroke-linecap="round"/>
<!-- top message -->
<use x="96.5" y="37.25" href="#author" />
<rect height="5.25" width="82.25" y="51.25" x="122.5" rx="1.5" fill="var(--foreground)"/>
<rect height="5.25" width="58.75" y="51.25" x="208.25" rx="1.5" fill="var(--foreground)"/>
<rect height="5.25" width="25" y="62.25" x="122.5" rx="1.5" fill="var(--foreground)"/>
<rect height="5.25" width="43.25" y="62.25" x="151" rx="1.5" fill="var(--foreground)"/>
<!-- message box -->
<rect x="90" y="184" width="233" height="18" fill="var(--message-box)"/>
<!-- window buttons -->
<circle cx="317" cy="5" r="2" fill="#C4C4C4"/>
<circle cx="310" cy="5" r="2" fill="#C4C4C4"/>
<circle cx="303" cy="5" r="2" fill="#C4C4C4"/>
<!-- guild separator -->
<line x1="4.5" y1="34.5" x2="21.5" y2="34.5" stroke="#C0C0C0" stroke-linecap="round"/>
<!-- sidebar information -->
<rect x="30" y="16" width="36" height="6" rx="2" fill="var(--foreground)" opacity="0.9"/>
<rect x="30" y="35" width="26" height="4" rx="2" fill="var(--foreground)"/>
<rect x="39" y="46" width="32" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<rect x="39" y="70" width="29" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<rect x="39" y="58" width="13" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<rect x="55" y="58" width="22" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<rect x="30" y="83" width="26" height="4" rx="2" fill="var(--foreground)"/>
<rect x="39" y="94" width="32" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<rect x="39" y="118" width="29" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<rect x="39" y="106" width="13" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<rect x="55" y="106" width="22" height="4" rx="2" fill="var(--tertiary-foreground)"/>
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="10" width="18" height="18">
<circle cx="13" cy="19" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask0)">
<circle cx="13" cy="19" r="9" fill="url(#paint0_linear)"/>
<circle cx="11.9199" cy="22.24" r="3.6" fill="#F9FAFB"/>
<path d="M4 22.6H6.88L9.04 21.52L11.2 20.8L12.64 21.52L14.44 21.16L16.24 21.52L16.96 21.88L19.12 21.52L20.56 21.88L22 21.16V29.08H16.6H11.92H4V24.04V22.6Z" fill="#C42626"/>
<path d="M6.88 22.6H4V24.04L6.88 22.6Z" fill="#882C2F"/>
<path d="M14.44 21.16L12.64 21.52L11.2 24.04L11.92 29.08H16.6L15.88 27.64L16.24 22.96L16.96 21.88L16.24 21.52L14.44 21.16Z" fill="#AF373B"/>
</g>
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="42" width="18" height="18">
<circle cx="13" cy="51" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask1)">
<circle cx="13" cy="51" r="9" fill="#D6D4D5"/>
<path d="M13.612 53.8162C19.048 49.6124 22.0251 53.8162 22.0251 53.8162V60.4402H5.89715L7.49195 53.988C7.9966 53.5705 8.17595 58.02 13.612 53.8162Z" fill="url(#paint1_linear)"/>
<path d="M4.54004 54.0601C4.54004 54.0601 8.10873 49.2615 12.388 54.9961C16.3012 60.2399 20.3146 56.741 20.668 55.8518V55.6801C20.7021 55.7083 20.7011 55.7686 20.668 55.8518V60.684H4.54004V54.0601Z" fill="url(#paint2_linear)"/>
<path d="M21.568 47.1119C21.568 48.2254 20.6654 49.1279 19.552 49.1279C18.4386 49.1279 17.536 48.2254 17.536 47.1119C17.536 45.9985 18.4386 45.0959 19.552 45.0959C20.6654 45.0959 21.568 45.9985 21.568 47.1119Z" fill="#E76563"/>
<path d="M19.12 49.0559H19.984V49.4879H19.12V49.0559Z" fill="#E76563"/>
<rect x="19.12" y="49.344" width="0.864" height="0.072" fill="white"/>
<path d="M19.264 49.488H19.336V49.776H19.264V49.488Z" fill="#4F65B6"/>
<path d="M19.48 49.488H19.624V49.776H19.48V49.488Z" fill="#4F65B6"/>
<path d="M19.768 49.488H19.84V49.776H19.768V49.488Z" fill="#4F65B6"/>
<path d="M19.048 49.776H20.056L19.984 50.28H19.12L19.048 49.776Z" fill="#4F65B6"/>
</g>
<mask id="mask2" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="65" width="18" height="18">
<circle cx="13" cy="74" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask2)">
<circle cx="13" cy="74" r="9" fill="url(#paint3_linear)"/>
<path d="M11.056 79.184L13.936 75.764V80.516L11.992 80.336L11.056 79.184Z" fill="#2E2816"/>
<path d="M5.97998 76.2679L13.72 80.3719L13.792 85.0159L5.97998 82.3519L5.15198 79.1839L5.97998 76.2679Z" fill="url(#paint4_linear)"/>
<path d="M4.75598 78.0319L5.97998 76.2679L5.22398 79.4719L4.93598 78.5359L4.75598 78.0319Z" fill="#7EA6A6"/>
<path d="M19.12 68.708L21.64 70.544L24.484 76.124L22.468 79.94L19.12 68.708Z" fill="#EDEDED"/>
<path d="M12.964 79.976L13.864 80.444L13.936 84.008L13 83.972L12.964 79.976Z" fill="#878787" fill-opacity="0.5"/>
<path d="M13.468 75.584L19.12 68.708L23.008 79.112L13.72 85.736L13.468 75.584Z" fill="url(#paint5_linear)"/>
</g>
<mask id="mask3" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="88" width="18" height="18">
<circle cx="13" cy="97" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask3)">
<circle cx="13" cy="97" r="9" fill="url(#paint6_linear)"/>
<path d="M4.252 89.404C4.252 89.404 11.236 87.2439 16.024 92.284C20.812 97.324 19.732 106.396 19.732 106.396H4.252L3.604 97.936L4.252 89.404Z" fill="url(#paint7_linear)"/>
<path d="M14.404 106.396C12.208 111.508 19.732 106.396 19.732 106.396C19.732 106.396 20.488 100.348 18.508 95.956C16.528 91.564 13.72 90.448 13.72 90.448C13.72 90.448 16.6 101.284 14.404 106.396Z" fill="url(#paint8_linear)"/>
</g>
<mask id="mask4" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="110" width="18" height="18">
<circle cx="13" cy="119" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask4)">
<circle cx="13" cy="119" r="9" fill="url(#paint9_linear)"/>
<path d="M3.35205 122.708L22.2161 122.708" stroke="#8181B1" stroke-width="0.144"/>
<path d="M3.28003 121.376L22.144 121.376" stroke="#A2A2BE" stroke-width="0.144"/>
<path d="M3.56799 119.936L22.432 119.936" stroke="#ADADBD" stroke-width="0.216"/>
<path d="M3.78406 118.496L22.6481 118.496" stroke="#BBBBCD" stroke-width="0.216"/>
<line x1="3.35205" y1="124.004" x2="22.2161" y2="124.004" stroke="#8181B1" stroke-width="0.072"/>
<line x1="3.35205" y1="125.3" x2="22.2161" y2="125.3" stroke="#8181B1" stroke-width="0.072"/>
<path d="M13.144 122.816L13.828 123.824C13.828 123.824 13.936 124.256 13.828 124.328C13.72 124.4 13.288 123.824 13.18 123.5C13.072 123.176 13.144 122.816 13.144 122.816Z" fill="#E6E7F4"/>
<path d="M13.828 124.328V123.824C13.828 123.824 15.304 123.608 16.708 122.816C18.112 122.024 18.364 120.944 18.364 120.944C18.364 120.944 18.292 121.952 16.924 123.032C15.556 124.112 13.828 124.328 13.828 124.328Z" fill="#E6E7F4"/>
<path d="M18.364 120.944C15.448 120.764 13.144 122.826 13.144 122.826L13.828 123.834C13.828 123.834 17.644 123.248 18.364 120.944Z" fill="white"/>
<path d="M18.256 121.016C15.6818 120.86 13.252 122.816 13.252 122.816L13.864 123.716C13.864 123.716 17.6204 123.017 18.256 121.016Z" fill="url(#paint10_linear)"/>
</g>
<!-- notification icon -->
<circle cx="20" cy="45" r="3.5" fill="#EF3B3B" stroke="var(--background)"/>
<defs>
<linearGradient id="paint0_linear" x1="13" y1="10" x2="13" y2="28" gradientUnits="userSpaceOnUse">
<stop stop-color="#AAB6BD"/>
<stop offset="1" stop-color="#D4DDE1"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="14.908" y1="54.744" x2="22.7559" y2="61.08" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F65B6"/>
<stop offset="1" stop-color="#C6D0F1"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="8.39204" y1="54.276" x2="21.496" y2="60.324" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F65B6"/>
<stop offset="1" stop-color="#C6D0F1"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="9.292" y1="65.432" x2="18.22" y2="82.388" gradientUnits="userSpaceOnUse">
<stop stop-color="#009092"/>
<stop offset="1" stop-color="#79C6C8"/>
</linearGradient>
<linearGradient id="paint4_linear" x1="9.39998" y1="76.2679" x2="9.39998" y2="85.0519" gradientUnits="userSpaceOnUse">
<stop stop-color="#CBCBCB"/>
<stop offset="1" stop-color="#FAFAFA"/>
</linearGradient>
<linearGradient id="paint5_linear" x1="18.238" y1="68.708" x2="18.238" y2="85.736" gradientUnits="userSpaceOnUse">
<stop stop-color="#95ABA9"/>
<stop offset="1" stop-color="#DCDCDC"/>
</linearGradient>
<linearGradient id="paint6_linear" x1="10.876" y1="87.604" x2="17.86" y2="105.136" gradientUnits="userSpaceOnUse">
<stop stop-color="#41486A"/>
<stop offset="1" stop-color="#3B3F5C"/>
</linearGradient>
<linearGradient id="paint7_linear" x1="7.312" y1="91.168" x2="19.84" y2="107.224" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C7799"/>
<stop offset="0.9999" stop-color="#39AEBF"/>
<stop offset="1" stop-color="#4C7799" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint8_linear" x1="16.636" y1="96.568" x2="12.388" y2="87.316" gradientUnits="userSpaceOnUse">
<stop stop-color="#DD4878"/>
<stop offset="1" stop-color="#D7E1E8"/>
</linearGradient>
<linearGradient id="paint9_linear" x1="9.94" y1="109.244" x2="13.756" y2="125.912" gradientUnits="userSpaceOnUse">
<stop stop-color="#847DAF"/>
<stop offset="1" stop-color="#4547AE"/>
</linearGradient>
<linearGradient id="paint10_linear" x1="15.484" y1="122.024" x2="16.924" y2="123.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#DFDFE1"/>
<stop offset="1" stop-color="#F5F4FB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,5 +1,5 @@
import { Reset, Import } from "@styled-icons/boxicons-regular"; import { Reset, Import } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid"; import { Pencil, Store } from "@styled-icons/boxicons-solid";
// @ts-expect-error shade-blend-color does not have typings. // @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color"; import pSBC from "shade-blend-color";
@ -8,12 +8,16 @@ import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks"; import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import CategoryButton from "../../../components/ui/fluent/CategoryButton";
import { debounce } from "../../../lib/debounce"; import { debounce } from "../../../lib/debounce";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { EmojiPacks, Settings } from "../../../redux/reducers/settings"; import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import { import {
DEFAULT_FONT, DEFAULT_FONT,
DEFAULT_MONO_FONT, DEFAULT_MONO_FONT,
@ -42,6 +46,8 @@ import mutantSVG from "../assets/mutant_emoji.svg";
import notoSVG from "../assets/noto_emoji.svg"; import notoSVG from "../assets/noto_emoji.svg";
import openmojiSVG from "../assets/openmoji_emoji.svg"; import openmojiSVG from "../assets/openmoji_emoji.svg";
import twemojiSVG from "../assets/twemoji_emoji.svg"; import twemojiSVG from "../assets/twemoji_emoji.svg";
import { Link } from "react-router-dom";
import { isExperimentEnabled } from "../../../redux/reducers/experiments";
interface Props { interface Props {
settings: Settings; settings: Settings;
@ -131,15 +137,12 @@ export function Component(props: Props) {
</h4> </h4>
</div> </div>
</div> </div>
{/*<Checkbox
checked={props.settings.theme?.ligatures === true} {isExperimentEnabled('theme_shop') && <Link to="/settings/theme_shop">
onChange={() => <CategoryButton icon={<Store size={24} />} action="chevron" hover>
setTheme({ <Text id="app.settings.pages.theme_shop.title" />
ligatures: !props.settings.theme?.ligatures, </CategoryButton>
}) </Link>}
}>
Use the system theme
</Checkbox>*/}
<h3> <h3>
<Text id="app.settings.pages.appearance.accent_selector" /> <Text id="app.settings.pages.appearance.accent_selector" />

View file

@ -7,6 +7,7 @@ import {
AVAILABLE_EXPERIMENTS, AVAILABLE_EXPERIMENTS,
ExperimentOptions, ExperimentOptions,
EXPERIMENTS, EXPERIMENTS,
isExperimentEnabled,
} from "../../../redux/reducers/experiments"; } from "../../../redux/reducers/experiments";
import Checkbox from "../../../components/ui/Checkbox"; import Checkbox from "../../../components/ui/Checkbox";
@ -24,7 +25,7 @@ export function Component(props: Props) {
{AVAILABLE_EXPERIMENTS.map((key) => ( {AVAILABLE_EXPERIMENTS.map((key) => (
<Checkbox <Checkbox
key={key} key={key}
checked={(props.options?.enabled ?? []).indexOf(key) > -1} checked={isExperimentEnabled(key, props.options)}
onChange={(enabled) => onChange={(enabled) =>
dispatch({ dispatch({
type: enabled type: enabled

View file

@ -0,0 +1,178 @@
import { useEffect, useState } from "preact/hooks"
import styled from "styled-components"
import Tip from "../../../components/ui/Tip"
import { Theme, generateVariables } from '../../../context/Theme'
import { dispatch } from "../../../redux"
export const fetchManifest = (): Promise<Manifest> =>
fetch(`${import.meta.env.VITE_THEMES_URL}/manifest.json`).then(res => res.json())
export const fetchTheme = (slug: string): Promise<Theme> =>
fetch(`${import.meta.env.VITE_THEMES_URL}/theme_${slug}.json`).then(res => res.json())
interface ThemeMetadata {
name: string,
creator: string,
description: string
}
type Manifest = {
generated: string,
themes: Record<string, ThemeMetadata>
}
// TODO: ability to preview / display the settings set like in the appearance pane
const ThemeInfo = styled.article`
display: grid;
grid:
"preview name creator" min-content
"preview desc desc" 1fr
/ 200px 1fr 1fr;
gap: 0.5rem 1rem;
padding: 1rem;
border-radius: var(--border-radius);
background: var(--secondary-background);
&[data-loaded] {
.preview {
opacity: 1;
}
}
.preview {
grid-area: preview;
aspect-ratio: 323 / 202;
background-color: var(--secondary-background);
border-radius: calc(var(--border-radius) / 2);
// prep style for later
outline: 3px solid transparent;
// hide random svg parts, crop border on firefox
overflow: hidden;
// hide until loaded
opacity: 0;
// style button
border: 0;
margin: 0;
padding: 0;
transition: 0.25s opacity, 0.25s outline;
> * {
grid-area: 1 / 1;
}
svg {
height: 100%;
width: 100%;
object-fit: contain;
}
&:hover, &:active, &:focus-visible {
outline: 3px solid var(--tertiary-background);
}
}
.name {
grid-area: name;
margin: 0;
}
.creator {
grid-area: creator;
justify-self: end;
font-size: 0.75rem;
}
.description {
grid-area: desc;
}
`
const ThemeList = styled.div`
display: grid;
gap: 1rem;
`
import previewPath from '../assets/preview.svg'
const ThemedSVG = styled.svg<{ theme: Theme }>`
${props => props.theme && generateVariables(props.theme)}
`
type ThemePreviewProps = Omit<JSX.HTMLAttributes<SVGSVGElement>, "as"> & {
slug?: string,
theme?: Theme
onThemeLoaded?: (theme: Theme) => void
};
const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => {
return <ThemedSVG {...props} theme={theme} width="323" height="202" aria-hidden="true" data-loaded={!!theme}>
<use href={`${previewPath}#preview`} width="100%" height="100%" />
</ThemedSVG >
}
const ThemeShopRoot = styled.div`
display: grid;
gap: 1rem;
`
export function ThemeShop() {
// setThemeList is for adding more / lazy loading in the future
const [themeList, setThemeList] = useState<[string, ThemeMetadata][] | null>(null);
const [themeData, setThemeData] = useState<Record<string, Theme>>({});
async function fetchThemeList() {
const manifest = await fetchManifest()
setThemeList(Object.entries(manifest.themes))
}
async function getTheme(slug: string) {
const theme = await fetchTheme(slug);
setThemeData(data => ({ ...data, [slug]: theme }))
}
useEffect(() => {
fetchThemeList()
}, [])
useEffect(() => {
themeList?.forEach(([slug]) => {
getTheme(slug)
})
}, [themeList])
return (<ThemeShopRoot>
<Tip warning>This section is under construction.</Tip>
<ThemeList>
{themeList?.map(([slug, theme]) => (
<ThemeInfo key={slug} data-loaded={Reflect.has(themeData, slug)}>
<h2 class="name">{theme.name}</h2>
{/* Maybe id's of the users should be included as well / instead? */}
<div class="creator">by {theme.creator}</div>
<div class="description">{theme.description}</div>
<button
class="preview"
onClick={() => dispatch({
type: "SETTINGS_SET_THEME",
theme: {
custom: themeData[slug],
}
})}
>
<ThemePreview
slug={slug}
theme={themeData[slug]}
/>
</button>
</ThemeInfo>
))}
</ThemeList>
</ThemeShopRoot>)
}

View file

@ -1,5 +1,9 @@
export type Experiments = "search"; import { getState } from "..";
export const AVAILABLE_EXPERIMENTS: Experiments[] = [];
export type Experiments = "search" | "theme_shop";
export const AVAILABLE_EXPERIMENTS: Experiments[] = ["theme_shop"];
export const EXPERIMENTS: { export const EXPERIMENTS: {
[key in Experiments]: { title: string; description: string }; [key in Experiments]: { title: string; description: string };
} = { } = {
@ -7,6 +11,10 @@ export const EXPERIMENTS: {
title: "Search", title: "Search",
description: "Allows you to search for messages in channels.", description: "Allows you to search for messages in channels.",
}, },
theme_shop: {
title: "Theme Shop",
description: "Allows you to access and set user submitted themes.",
},
}; };
export interface ExperimentOptions { export interface ExperimentOptions {
@ -50,3 +58,10 @@ export function experiments(
return state; return state;
} }
} }
export function isExperimentEnabled(
name: Experiments,
experiments: ExperimentOptions = getState().experiments,
) {
return experiments.enabled?.includes(name) ?? false;
}