From 34ca290bdd6be2749416fa675dcea5ab02ef5160 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 5 Dec 2022 16:05:51 +0200 Subject: [PATCH] Redesign (#1179) * Remove light mode * Trim header * New front page initial * Get rid of build layout * Breadcrumbs * Desktop side nav * Overhaul colors * Add breadcrumbs * New sub nav style * Front page action buttons * Add back add new build button * Add articles page with icon * Minor Object damage page layout tweaks * Remove one unnecessary render from object damage * Fix wrong link in article page * Profile -> My Page in header * Log in/out buttons in front * Add drawings to front page * Remove unnecessary comment --- app/components/Ability.tsx | 18 +- app/components/Breadcrumbs.tsx | 58 --- app/components/Image.tsx | 5 +- app/components/SubNav.tsx | 4 +- app/components/icons/Globe.tsx | 4 + .../icons/{ArrowUp.tsx => LogIn.tsx} | 17 +- app/components/icons/Moon.tsx | 28 -- app/components/icons/Sun.tsx | 29 -- app/components/icons/SunAndMoon.tsx | 28 -- app/components/layout/HamburgerButton.tsx | 60 --- app/components/layout/LanguageChanger.tsx | 21 +- .../layout/LogInButtonContainer.tsx | 58 +++ app/components/layout/Menu.tsx | 60 --- app/components/layout/SideNav.tsx | 32 ++ app/components/layout/ThemeChanger.tsx | 58 --- app/components/layout/UserItem.tsx | 68 +-- app/components/layout/index.tsx | 108 +++-- app/components/layout/nav-items.json | 90 +++- app/modules/theme/README.md | 3 - app/modules/theme/action.server.ts | 32 -- app/modules/theme/index.ts | 2 - app/modules/theme/provider.tsx | 193 -------- app/modules/theme/session.server.ts | 39 -- app/root.tsx | 60 +-- app/routes/a.$slug.tsx | 30 +- app/routes/a.tsx | 33 +- app/routes/analyzer.tsx | 7 +- app/routes/badges.tsx | 13 +- app/routes/builds.tsx | 39 -- app/routes/builds/$slug.tsx | 43 +- app/routes/builds/index.tsx | 24 +- app/routes/calendar/$id/index.tsx | 16 + app/routes/calendar/index.tsx | 13 +- app/routes/index.tsx | 423 ++++++------------ app/routes/maps.tsx | 13 +- app/routes/object-damage-calculator.tsx | 36 +- app/routes/plans.tsx | 6 + app/routes/plus.tsx | 14 +- app/routes/plus/voting/index.tsx | 2 +- app/routes/theme.ts | 5 - app/routes/u.$identifier.tsx | 5 +- app/routes/u.$identifier/builds/index.tsx | 9 +- app/routes/u.$identifier/edit.tsx | 7 +- app/routes/u.$identifier/index.tsx | 59 ++- app/routes/u.$identifier/results/index.tsx | 7 +- app/styles/builds.css | 8 +- app/styles/common.css | 84 ++-- app/styles/front.css | 257 +++++------ app/styles/layout.css | 236 ++++------ app/styles/object-damage.css | 18 +- app/styles/plans.css | 2 + app/styles/plus-history.css | 2 +- app/styles/plus.css | 6 +- app/styles/u-edit.css | 2 +- app/styles/vars.css | 109 ++--- app/utils/remix.ts | 6 +- app/utils/urls.ts | 7 + package.json | 1 + public/locales/da/common.json | 6 +- public/locales/da/front.json | 16 - public/locales/de/common.json | 4 +- public/locales/de/front.json | 14 - public/locales/en/common.json | 11 +- public/locales/en/front.json | 16 - public/locales/es-ES/common.json | 4 +- public/locales/es-ES/front.json | 12 - public/locales/es-US/common.json | 4 +- public/locales/es-US/front.json | 12 - public/locales/fr/common.json | 4 +- public/locales/fr/front.json | 11 - public/locales/ja/common.json | 4 +- public/locales/ja/front.json | 15 - public/locales/ko/common.json | 4 +- public/locales/ko/front.json | 10 - public/locales/nl/common.json | 4 +- public/locales/nl/front.json | 13 - public/locales/ru/common.json | 6 +- public/locales/ru/front.json | 16 - public/locales/zh/common.json | 6 +- public/locales/zh/front.json | 16 - public/static-assets/img/layout/articles.avif | Bin 0 -> 6868 bytes public/static-assets/img/layout/articles.png | Bin 0 -> 27581 bytes .../img/layout/front-boy-bg.avif | Bin 0 -> 15541 bytes .../static-assets/img/layout/front-boy-bg.png | Bin 0 -> 36391 bytes .../static-assets/img/layout/front-boy.avif | Bin 0 -> 60443 bytes public/static-assets/img/layout/front-boy.png | Bin 0 -> 783956 bytes .../img/layout/front-girl-bg.avif | Bin 0 -> 17059 bytes .../img/layout/front-girl-bg.png | Bin 0 -> 38362 bytes .../static-assets/img/layout/front-girl.avif | Bin 0 -> 50193 bytes .../static-assets/img/layout/front-girl.png | Bin 0 -> 650128 bytes .../static-assets/svg/background-pattern.svg | 6 - .../svg/new-background-pattern.svg | 1 + scripts/hex-to-filter.ts | 353 +++++++++++++++ types/react-i18next.d.ts | 2 - 94 files changed, 1363 insertions(+), 1824 deletions(-) delete mode 100644 app/components/Breadcrumbs.tsx rename app/components/icons/{ArrowUp.tsx => LogIn.tsx} (53%) delete mode 100644 app/components/icons/Moon.tsx delete mode 100644 app/components/icons/Sun.tsx delete mode 100644 app/components/icons/SunAndMoon.tsx delete mode 100644 app/components/layout/HamburgerButton.tsx create mode 100644 app/components/layout/LogInButtonContainer.tsx delete mode 100644 app/components/layout/Menu.tsx create mode 100644 app/components/layout/SideNav.tsx delete mode 100644 app/components/layout/ThemeChanger.tsx delete mode 100644 app/modules/theme/README.md delete mode 100644 app/modules/theme/action.server.ts delete mode 100644 app/modules/theme/index.ts delete mode 100644 app/modules/theme/provider.tsx delete mode 100644 app/modules/theme/session.server.ts delete mode 100644 app/routes/builds.tsx delete mode 100644 app/routes/theme.ts delete mode 100644 public/locales/da/front.json delete mode 100644 public/locales/de/front.json delete mode 100644 public/locales/en/front.json delete mode 100644 public/locales/es-ES/front.json delete mode 100644 public/locales/es-US/front.json delete mode 100644 public/locales/fr/front.json delete mode 100644 public/locales/ja/front.json delete mode 100644 public/locales/ko/front.json delete mode 100644 public/locales/nl/front.json delete mode 100644 public/locales/ru/front.json delete mode 100644 public/locales/zh/front.json create mode 100644 public/static-assets/img/layout/articles.avif create mode 100644 public/static-assets/img/layout/articles.png create mode 100644 public/static-assets/img/layout/front-boy-bg.avif create mode 100644 public/static-assets/img/layout/front-boy-bg.png create mode 100644 public/static-assets/img/layout/front-boy.avif create mode 100644 public/static-assets/img/layout/front-boy.png create mode 100644 public/static-assets/img/layout/front-girl-bg.avif create mode 100644 public/static-assets/img/layout/front-girl-bg.png create mode 100644 public/static-assets/img/layout/front-girl.avif create mode 100644 public/static-assets/img/layout/front-girl.png delete mode 100644 public/static-assets/svg/background-pattern.svg create mode 100644 public/static-assets/svg/new-background-pattern.svg create mode 100644 scripts/hex-to-filter.ts diff --git a/app/components/Ability.tsx b/app/components/Ability.tsx index ba8f0ad76..1f382adba 100644 --- a/app/components/Ability.tsx +++ b/app/components/Ability.tsx @@ -17,6 +17,7 @@ export function Ability({ dropAllowed = false, onClick, onDrop, + className, }: { ability: AbilityWithUnknown; size: keyof typeof sizeMap; @@ -24,6 +25,7 @@ export function Ability({ dropAllowed?: boolean; onClick?: () => void; onDrop?: (event: React.DragEvent) => void; + className?: string; }) { const sizeNumber = sizeMap[size]; @@ -45,12 +47,16 @@ export function Ability({ return ( - matches - .map((match) => { - const handle = match.handle as undefined | SendouRouteHandle; - const name = handle?.breadcrumb?.({ match, t }); - return name ? { path: match.pathname, name } : undefined; - }) - .filter(isDefined), - [matches, t] - ); -} - -export function Breadcrumbs() { - const breadcrumbs = useBreadcrumbs(); - - const showBreadcrumbs = breadcrumbs.length > 0; - - if (!showBreadcrumbs) { - return null; - } - - return ( - - ); -} diff --git a/app/components/Image.tsx b/app/components/Image.tsx index 5956b89f6..c95786a1c 100644 --- a/app/components/Image.tsx +++ b/app/components/Image.tsx @@ -6,21 +6,22 @@ export function Image({ width, height, style, + containerClassName, }: { path: string; alt: string; title?: string; className?: string; + containerClassName?: string; width?: number; height?: number; style?: React.CSSProperties; }) { return ( - + - {children} - + {children} ); } diff --git a/app/components/icons/Globe.tsx b/app/components/icons/Globe.tsx index 7a1ad3e7a..cf5d56399 100644 --- a/app/components/icons/Globe.tsx +++ b/app/components/icons/Globe.tsx @@ -1,9 +1,11 @@ export function GlobeIcon({ className, alt, + size, }: { className?: string; alt: string; + size?: number; }) { return ( {alt !== "" && {alt}} ); diff --git a/app/components/icons/Moon.tsx b/app/components/icons/Moon.tsx deleted file mode 100644 index df73cf2b7..000000000 --- a/app/components/icons/Moon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export function MoonIcon({ - className, - alt, -}: { - className?: string; - alt: string; -}) { - return ( - - {alt !== "" && {alt}} - - - ); -} diff --git a/app/components/icons/Sun.tsx b/app/components/icons/Sun.tsx deleted file mode 100644 index 4d896e2f4..000000000 --- a/app/components/icons/Sun.tsx +++ /dev/null @@ -1,29 +0,0 @@ -export function SunIcon({ - className, - alt, -}: { - className?: string; - alt: string; - title?: string; -}) { - return ( - - {alt !== "" && {alt}} - - - ); -} diff --git a/app/components/icons/SunAndMoon.tsx b/app/components/icons/SunAndMoon.tsx deleted file mode 100644 index 72f0e2869..000000000 --- a/app/components/icons/SunAndMoon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export function SunAndMoonIcon({ - className, - alt, -}: { - className?: string; - alt?: string; -}) { - return ( - - {alt !== "" && {alt}} - - - ); -} diff --git a/app/components/layout/HamburgerButton.tsx b/app/components/layout/HamburgerButton.tsx deleted file mode 100644 index 80f9d46a5..000000000 --- a/app/components/layout/HamburgerButton.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import clsx from "clsx"; - -export function HamburgerButton({ - onClick, - expanded, -}: { - onClick: () => void; - expanded: boolean; -}) { - return ( - - ); -} diff --git a/app/components/layout/LanguageChanger.tsx b/app/components/layout/LanguageChanger.tsx index f82fec053..cf9ed82f4 100644 --- a/app/components/layout/LanguageChanger.tsx +++ b/app/components/layout/LanguageChanger.tsx @@ -1,4 +1,5 @@ import { useSearchParams } from "@remix-run/react"; +import type * as React from "react"; import { useTranslation } from "~/hooks/useTranslation"; import { languages } from "~/modules/i18n"; import { LinkButton } from "../Button"; @@ -16,19 +17,27 @@ const addUniqueParam = ( return paramsCopy; }; -export function LanguageChanger() { +export function LanguageChanger({ + children, + plain = false, +}: { + children?: React.ReactNode; + plain?: boolean; +}) { const { t, i18n } = useTranslation(); const [searchParams] = useSearchParams(); return ( + children ?? ( + + ) } - triggerClassName="layout__header__button" + triggerClassName={plain ? undefined : "layout__header__button"} >
{languages.map((lang) => ( diff --git a/app/components/layout/LogInButtonContainer.tsx b/app/components/layout/LogInButtonContainer.tsx new file mode 100644 index 000000000..17752c915 --- /dev/null +++ b/app/components/layout/LogInButtonContainer.tsx @@ -0,0 +1,58 @@ +import { useSearchParams } from "@remix-run/react"; +import { useTranslation } from "~/hooks/useTranslation"; +import { LOG_IN_URL } from "~/utils/urls"; +import { Button } from "../Button"; +import { Dialog } from "../Dialog"; + +export function LogInButtonContainer({ + children, +}: { + children: React.ReactNode; +}) { + const { t } = useTranslation(); + const [searchParams, setSearchParams] = useSearchParams(); + const authError = searchParams.get("authError"); + const closeAuthErrorDialog = () => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.delete("authError"); + setSearchParams(newSearchParams); + }; + + return ( + <> +
+ {children} +
+ {authError != null && ( + +
+ + +
+
+ )} + + ); +} + +function AuthenticationErrorHelp({ errorCode }: { errorCode: string }) { + const { t } = useTranslation(); + + switch (errorCode) { + case "aborted": + return ( + <> +

{t("auth.errors.aborted")}

+ {t("auth.errors.discordPermissions")} + + ); + case "unknown": + default: + return ( + <> +

{t("auth.errors.failed")}

+ {t("auth.errors.unknown")} + + ); + } +} diff --git a/app/components/layout/Menu.tsx b/app/components/layout/Menu.tsx deleted file mode 100644 index 04a97d5d8..000000000 --- a/app/components/layout/Menu.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import clsx from "clsx"; -import { Link } from "@remix-run/react"; -import navItems from "./nav-items.json"; -import { Image } from "../Image"; -import { useIsMounted } from "~/hooks/useIsMounted"; -import { canPerformAdminActions } from "~/permissions"; -import { useUser } from "~/modules/auth"; -import { useTranslation } from "~/hooks/useTranslation"; -import { navIconUrl } from "~/utils/urls"; - -export function Menu({ - expanded, - closeMenu, -}: { - expanded: boolean; - closeMenu: () => void; -}) { - const user = useUser(); - const isMounted = useIsMounted(); - const { t } = useTranslation(); - - // without this menu is initially visible due to SSR and not knowing user screen width on server (probably) - if (!isMounted) return null; - - const visibleNavItems = navItems.filter((navItem) => { - if (navItem.name === "admin") { - return canPerformAdminActions(user); - } - - return true; - }); - - return ( - - ); -} diff --git a/app/components/layout/SideNav.tsx b/app/components/layout/SideNav.tsx new file mode 100644 index 000000000..02d48f1ca --- /dev/null +++ b/app/components/layout/SideNav.tsx @@ -0,0 +1,32 @@ +import { Link } from "@remix-run/react"; +import navItems from "~/components/layout/nav-items.json"; +import { useTranslation } from "~/hooks/useTranslation"; +import { navIconUrl } from "~/utils/urls"; +import { Image } from "../Image"; + +export function SideNav() { + const { t } = useTranslation(["common"]); + + return ( + + ); +} diff --git a/app/components/layout/ThemeChanger.tsx b/app/components/layout/ThemeChanger.tsx deleted file mode 100644 index 52936af94..000000000 --- a/app/components/layout/ThemeChanger.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useTranslation } from "~/hooks/useTranslation"; -import { Theme, useTheme } from "~/modules/theme"; -import { Button } from "../Button"; -import { MoonIcon } from "../icons/Moon"; -import { SunIcon } from "../icons/Sun"; -import { SunAndMoonIcon } from "../icons/SunAndMoon"; -import { Popover } from "../Popover"; - -const ThemeIcons = { - [Theme.LIGHT]: SunIcon, - [Theme.DARK]: MoonIcon, - auto: SunAndMoonIcon, -}; - -export function ThemeChanger() { - const { userTheme, setUserTheme } = useTheme(); - const { t } = useTranslation(); - - if (!userTheme) { - return null; - } - - const SelectedIcon = ThemeIcons[userTheme]; - - return ( - - } - triggerClassName="layout__header__button" - > -
- {(["auto", Theme.DARK, Theme.LIGHT] as const).map((theme) => { - const Icon = ThemeIcons[theme]; - const selected = userTheme === theme; - return ( - - ); - })} -
-
- ); -} diff --git a/app/components/layout/UserItem.tsx b/app/components/layout/UserItem.tsx index 909ea10f5..f4f29d93b 100644 --- a/app/components/layout/UserItem.tsx +++ b/app/components/layout/UserItem.tsx @@ -1,19 +1,18 @@ -import { Link, useSearchParams } from "@remix-run/react"; +import { Link } from "@remix-run/react"; import { useTranslation } from "~/hooks/useTranslation"; import { useUser } from "~/modules/auth"; -import { LOG_IN_URL, LOG_OUT_URL, userPage } from "~/utils/urls"; +import { LOG_OUT_URL, userPage } from "~/utils/urls"; import { Avatar } from "../Avatar"; import { Button } from "../Button"; -import { Dialog } from "../Dialog"; -import { DiscordIcon } from "../icons/Discord"; +import { LogInIcon } from "../icons/LogIn"; import { LogOutIcon } from "../icons/LogOut"; import { UserIcon } from "../icons/User"; import { Popover } from "../Popover"; +import { LogInButtonContainer } from "./LogInButtonContainer"; export function UserItem() { const { t } = useTranslation(); const user = useUser(); - const [searchParams, setSearchParams] = useSearchParams(); if (user) { return ( @@ -37,11 +36,17 @@ export function UserItem() { variant="outlined" icon={} > - {t("header.profile")} + {t("pages.myPage")}
-
@@ -50,50 +55,11 @@ export function UserItem() { ); } - const authError = searchParams.get("authError"); - const closeAuthErrorDialog = () => { - const newSearchParams = new URLSearchParams(searchParams); - newSearchParams.delete("authError"); - setSearchParams(newSearchParams); - }; - return ( - <> -
- -
- {authError != null && ( - -
- - -
-
- )} - + + + ); } - -function AuthenticationErrorHelp({ errorCode }: { errorCode: string }) { - const { t } = useTranslation(); - - switch (errorCode) { - case "aborted": - return ( - <> -

{t("auth.errors.aborted")}

- {t("auth.errors.discordPermissions")} - - ); - case "unknown": - default: - return ( - <> -

{t("auth.errors.failed")}

- {t("auth.errors.unknown")} - - ); - } -} diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index dd001a272..35da69e97 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -1,36 +1,32 @@ -import { Link, useMatches } from "@remix-run/react"; +import { Link, useLocation, useMatches } from "@remix-run/react"; import * as React from "react"; -import { useTranslation } from "~/hooks/useTranslation"; import type { RootLoaderData } from "~/root"; -import { type SendouRouteHandle } from "~/utils/remix"; -import { LOGO_PATH, navIconUrl } from "~/utils/urls"; -import { Image } from "../Image"; -import { ThemeChanger } from "./ThemeChanger"; +import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix"; import { Footer } from "./Footer"; -import { HamburgerButton } from "./HamburgerButton"; -import { LanguageChanger } from "./LanguageChanger"; -import { Menu } from "./Menu"; -import navItems from "./nav-items.json"; +import { useTranslation } from "~/hooks/useTranslation"; +import { Image } from "../Image"; +import { SideNav } from "./SideNav"; import { UserItem } from "./UserItem"; +import { LanguageChanger } from "./LanguageChanger"; -function useActiveNavItem() { +function useBreadcrumbs() { + const { t } = useTranslation(); const matches = useMatches(); return React.useMemo(() => { - let activeItem: { name: string; url: string } | undefined = undefined; + const result: Array> = []; - // `.reverse()` is mutating! for (const match of [...matches].reverse()) { const handle = match.handle as SendouRouteHandle | undefined; + const resolvedBreadcrumb = handle?.breadcrumb?.({ match, t }); - if (handle?.navItemName) { - activeItem = navItems.find(({ name }) => name === handle.navItemName); - break; + if (resolvedBreadcrumb) { + result.push(resolvedBreadcrumb); } } - return activeItem; - }, [matches]); + return result.flat(); + }, [matches, t]); } export const Layout = React.memo(function Layout({ @@ -42,45 +38,63 @@ export const Layout = React.memo(function Layout({ patrons?: RootLoaderData["patrons"]; isCatchBoundary?: boolean; }) { - const { t } = useTranslation(); - const [menuOpen, setMenuOpen] = React.useState(false); - const activeNavItem = useActiveNavItem(); + const { t } = useTranslation(["common"]); + const location = useLocation(); + const breadcrumbs = useBreadcrumbs(); + + const isFrontPage = location.pathname === "/"; return (
- - sendou.ink logo - +
+ + sendou.ink + + {breadcrumbs.flatMap((breadcrumb) => { + return [ + + / + , + , + ]; + })} + {isFrontPage ? ( + <> +
-
+
+ {t("common:websiteSubtitle")} +
+ + ) : null} +
- {!isCatchBoundary ? : null} - - setMenuOpen(!menuOpen)} - /> + {!isCatchBoundary ? : null}
- setMenuOpen(false)} /> - {activeNavItem && ( -

- - {t(`pages.${activeNavItem.name}` as any)} -

- )} + {!isFrontPage ? : null} {children}
); }); + +function BreadcrumbLink({ data }: { data: Breadcrumb }) { + if (data.type === "IMAGE") { + return ( + + + + ); + } + + return ( + + {data.text} + + ); +} diff --git a/app/components/layout/nav-items.json b/app/components/layout/nav-items.json index 322463aae..08679ec6e 100644 --- a/app/components/layout/nav-items.json +++ b/app/components/layout/nav-items.json @@ -1,27 +1,83 @@ [ - { - "name": "admin", - "url": "admin", - "prefetch": false - }, - { "name": "builds", "url": "builds", "prefetch": true }, - { "name": "analyzer", "url": "analyzer", "prefetch": true }, - { - "name": "object-damage-calculator", - "url": "object-damage-calculator", - "prefetch": true - }, { "name": "plans", "url": "plans", - "prefetch": false + "prefetch": false, + "filters": [ + "invert(46%) sepia(98%) saturate(7459%) hue-rotate(327deg) brightness(102%) contrast(94%)", + "invert(68%) sepia(78%) saturate(364%) hue-rotate(74deg) brightness(100%) contrast(100%)" + ] + }, + { + "name": "maps", + "url": "maps", + "prefetch": false, + "filters": [ + "invert(53%) sepia(87%) saturate(411%) hue-rotate(42deg) brightness(95%) contrast(90%)", + "invert(81%) sepia(25%) saturate(0%) hue-rotate(166deg) brightness(89%) contrast(82%)" + ] + }, + { + "name": "badges", + "url": "badges", + "prefetch": false, + "filters": [ + "invert(92%) sepia(88%) saturate(701%) hue-rotate(327deg) brightness(94%) contrast(93%)", + "invert(35%) sepia(84%) saturate(1474%) hue-rotate(329deg) brightness(105%) contrast(101%)" + ] + }, + { + "name": "calendar", + "url": "calendar", + "prefetch": false, + "filters": [ + "invert(71%) sepia(16%) saturate(7491%) hue-rotate(76deg) brightness(107%) contrast(100%)", + "invert(26%) sepia(59%) saturate(5210%) hue-rotate(323deg) brightness(98%) contrast(101%)" + ] }, - { "name": "calendar", "url": "calendar", "prefetch": false }, - { "name": "maps", "url": "maps", "prefetch": false }, - { "name": "badges", "url": "badges", "prefetch": false }, { "name": "plus", "url": "plus/suggestions", - "prefetch": false + "prefetch": false, + "filters": [ + "invert(68%) sepia(69%) saturate(1761%) hue-rotate(145deg) brightness(101%) contrast(108%)", + "invert(22%) sepia(100%) saturate(2645%) hue-rotate(279deg) brightness(98%) contrast(115%)" + ] + }, + { + "name": "articles", + "url": "a", + "prefetch": true, + "filters": [ + "invert(30%) sepia(73%) saturate(4089%) hue-rotate(326deg) brightness(96%) contrast(104%)", + "invert(55%) sepia(20%) saturate(738%) hue-rotate(224deg) brightness(86%) contrast(88%)" + ] + }, + { + "name": "object-damage-calculator", + "url": "object-damage-calculator", + "prefetch": true, + "filters": [ + "invert(19%) sepia(97%) saturate(6605%) hue-rotate(326deg) brightness(103%) contrast(105%)", + "invert(20%) sepia(99%) saturate(4341%) hue-rotate(220deg) brightness(102%) contrast(104%)" + ] + }, + { + "name": "analyzer", + "url": "analyzer", + "prefetch": true, + "filters": [ + "invert(27%) sepia(69%) saturate(5653%) hue-rotate(220deg) brightness(96%) contrast(85%)", + "invert(23%) sepia(83%) saturate(2378%) hue-rotate(334deg) brightness(87%) contrast(102%)" + ] + }, + { + "name": "builds", + "url": "builds", + "prefetch": true, + "filters": [ + "invert(16%) sepia(80%) saturate(5634%) hue-rotate(272deg) brightness(91%) contrast(105%)", + "invert(71%) sepia(42%) saturate(4219%) hue-rotate(150deg) brightness(99%) contrast(86%)" + ] } ] diff --git a/app/modules/theme/README.md b/app/modules/theme/README.md deleted file mode 100644 index 7aff6875e..000000000 --- a/app/modules/theme/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Implements dark mode for a Remix app. - -Based on https://github.com/remix-run/remix/blob/main/examples/dark-mode diff --git a/app/modules/theme/action.server.ts b/app/modules/theme/action.server.ts deleted file mode 100644 index 140ccc58e..000000000 --- a/app/modules/theme/action.server.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ActionFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; - -import { getThemeSession } from "./session.server"; -import { isTheme } from "./provider"; - -export const action: ActionFunction = async ({ request }) => { - const themeSession = await getThemeSession(request); - const requestText = await request.text(); - const form = new URLSearchParams(requestText); - const theme = form.get("theme"); - - if (theme === "auto") { - return json( - { success: true }, - { headers: { "Set-Cookie": await themeSession.destroy() } } - ); - } - - if (!isTheme(theme)) { - return json({ - success: false, - message: `theme value of ${theme ?? "null"} is not a valid theme`, - }); - } - - themeSession.setTheme(theme); - return json( - { success: true }, - { headers: { "Set-Cookie": await themeSession.commit() } } - ); -}; diff --git a/app/modules/theme/index.ts b/app/modules/theme/index.ts deleted file mode 100644 index 119743922..000000000 --- a/app/modules/theme/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Theme, ThemeHead, ThemeProvider, useTheme } from "./provider"; -export { action } from "./action.server"; diff --git a/app/modules/theme/provider.tsx b/app/modules/theme/provider.tsx deleted file mode 100644 index 33bf2175b..000000000 --- a/app/modules/theme/provider.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { useFetcher } from "@remix-run/react"; -import { type ReactNode, useCallback } from "react"; -import { createContext, useContext, useEffect, useState } from "react"; - -enum Theme { - DARK = "dark", - LIGHT = "light", -} -const themes: Array = Object.values(Theme); - -type ThemeContextType = { - /** The CSS class to attach to the `html` tag */ - htmlThemeClass: Theme | ""; - /** The color scheme to be defined in the meta tag */ - metaColorScheme: "light dark" | "dark light"; - /** - * The Theme setting of the user, as displayed in the theme switcher. - * `null` means there is no theme switcher (static theme on error pages). - */ - userTheme: Theme | "auto" | null; - /** Persists a new `userTheme` setting */ - setUserTheme: (newTheme: Theme | "auto") => void; -}; - -const ThemeContext = createContext(undefined); - -const prefersLightMQ = "(prefers-color-scheme: light)"; -const getPreferredTheme = () => - window.matchMedia(prefersLightMQ).matches ? Theme.LIGHT : Theme.DARK; - -type ThemeProviderProps = { - children: ReactNode; - specifiedTheme: Theme | null; - themeSource: "user-preference" | "static"; -}; - -function ThemeProvider({ - children, - specifiedTheme, - themeSource, -}: ThemeProviderProps) { - const [[theme, isAutoDetected], setThemeState] = useState< - [Theme, false] | [Theme | null, true] - >(() => { - if (themeSource === "static") { - return [specifiedTheme ?? Theme.DARK, false]; - } - - if (specifiedTheme) { - return [specifiedTheme, false]; - } - - /* - If we don't know a preferred user theme, we have to auto-detect it. - - Since the server has no way of doing auto-detection, it returns null, - leading to the `html` class and `color-scheme` values being set to a - default. - - Then, on the client, the `clientThemeCode` will run, correcting those - defaults with the determined correct value. - - Which means, when we later render this component again, hydration will - succeed. Because the output of `getPreferredTheme()` is (very likely) the - same that the `clientThemeCode` determined and added to the html element - shortly before. - */ - - return [typeof document === "undefined" ? null : getPreferredTheme(), true]; - }); - - const persistThemeFetcher = useFetcher(); - const persistTheme = persistThemeFetcher.submit; - - const setUserTheme = useCallback( - (newTheme: Theme | "auto") => { - setThemeState( - newTheme === "auto" ? [getPreferredTheme(), true] : [newTheme, false] - ); - persistTheme( - { theme: newTheme }, - { - action: "theme", - method: "post", - } - ); - }, - [setThemeState, persistTheme] - ); - - useEffect(() => { - if (!isAutoDetected) { - return; - } - - const mediaQuery = window.matchMedia(prefersLightMQ); - const handleChange = () => { - setThemeState([mediaQuery.matches ? Theme.LIGHT : Theme.DARK, true]); - }; - mediaQuery.addEventListener("change", handleChange); - return () => mediaQuery.removeEventListener("change", handleChange); - }, [isAutoDetected]); - - return ( - - {children} - - ); -} - -// this is how I make certain we avoid a flash of the wrong theme. If you select -// a theme, then I'll know what you want in the future and you'll not see this -// script anymore. -const clientThemeCode = ` -;(() => { - const theme = window.matchMedia(${JSON.stringify(prefersLightMQ)}).matches - ? 'light' - : 'dark'; - const cl = document.documentElement.classList; - const themeAlreadyApplied = cl.contains('light') || cl.contains('dark'); - if (themeAlreadyApplied) { - console.warn( - "Script is running but theme is already applied", - ); - } else { - cl.add(theme); - } - const meta = document.querySelector('meta[name=color-scheme]'); - if (meta) { - if (theme === 'dark') { - meta.content = 'dark light'; - } else if (theme === 'light') { - meta.content = 'light dark'; - } - } else { - console.warn( - "No meta tag", - ); - } -})(); -`; - -function ThemeHead() { - const { userTheme, metaColorScheme } = useTheme(); - const [initialUserTheme] = useState(userTheme); - - return ( - <> - {/* - On the server, "theme" might be `null`, so clientThemeCode ensures that - this is correct before hydration. - */} - - {/* - If we know what the theme is from user preference, then we don't need - to do fancy tricks prior to hydration to make things match. - */} - {initialUserTheme === "auto" && ( -