diff --git a/app/components/Catcher.tsx b/app/components/Catcher.tsx
index 7f25a3692..dd0dbe749 100644
--- a/app/components/Catcher.tsx
+++ b/app/components/Catcher.tsx
@@ -1,8 +1,8 @@
import { useCatch, useLocation } from "@remix-run/react";
-import { DISCORD_URL } from "~/constants";
import { getLogInUrl } from "~/utils";
import { useUser } from "~/hooks/common";
import { Button } from "./Button";
+import { discordUrl } from "~/utils/urls";
// TODO: some nice art
export function Catcher() {
@@ -18,7 +18,7 @@ export function Catcher() {
{user ? (
If you need assistance you can ask for help on{" "}
-
+
our Discord
diff --git a/app/components/Layout/DrawingSection.tsx b/app/components/Layout/DrawingSection.tsx
new file mode 100644
index 000000000..28aa22749
--- /dev/null
+++ b/app/components/Layout/DrawingSection.tsx
@@ -0,0 +1,373 @@
+import clsx from "clsx";
+import randomColor from "randomcolor";
+import { useState } from "react";
+
+interface HSL {
+ h: number;
+ s: number;
+ l: number;
+}
+
+class Color {
+ public r: number;
+ public g: number;
+ public b: number;
+ constructor(r: number, g: number, b: number) {
+ this.r = this.clamp(r);
+ this.g = this.clamp(g);
+ this.b = this.clamp(b);
+ }
+
+ toString() {
+ return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(
+ this.b
+ )})`;
+ }
+
+ set(r: number, g: number, b: number) {
+ this.r = this.clamp(r);
+ this.g = this.clamp(g);
+ this.b = this.clamp(b);
+ }
+
+ hueRotate(angle = 0) {
+ angle = (angle / 180) * Math.PI;
+ const sin = Math.sin(angle);
+ const cos = Math.cos(angle);
+
+ this.multiply([
+ 0.213 + cos * 0.787 - sin * 0.213,
+ 0.715 - cos * 0.715 - sin * 0.715,
+ 0.072 - cos * 0.072 + sin * 0.928,
+ 0.213 - cos * 0.213 + sin * 0.143,
+ 0.715 + cos * 0.285 + sin * 0.14,
+ 0.072 - cos * 0.072 - sin * 0.283,
+ 0.213 - cos * 0.213 - sin * 0.787,
+ 0.715 - cos * 0.715 + sin * 0.715,
+ 0.072 + cos * 0.928 + sin * 0.072,
+ ]);
+ }
+
+ grayscale(value = 1) {
+ this.multiply([
+ 0.2126 + 0.7874 * (1 - value),
+ 0.7152 - 0.7152 * (1 - value),
+ 0.0722 - 0.0722 * (1 - value),
+ 0.2126 - 0.2126 * (1 - value),
+ 0.7152 + 0.2848 * (1 - value),
+ 0.0722 - 0.0722 * (1 - value),
+ 0.2126 - 0.2126 * (1 - value),
+ 0.7152 - 0.7152 * (1 - value),
+ 0.0722 + 0.9278 * (1 - value),
+ ]);
+ }
+
+ sepia(value = 1) {
+ this.multiply([
+ 0.393 + 0.607 * (1 - value),
+ 0.769 - 0.769 * (1 - value),
+ 0.189 - 0.189 * (1 - value),
+ 0.349 - 0.349 * (1 - value),
+ 0.686 + 0.314 * (1 - value),
+ 0.168 - 0.168 * (1 - value),
+ 0.272 - 0.272 * (1 - value),
+ 0.534 - 0.534 * (1 - value),
+ 0.131 + 0.869 * (1 - value),
+ ]);
+ }
+
+ saturate(value = 1) {
+ this.multiply([
+ 0.213 + 0.787 * value,
+ 0.715 - 0.715 * value,
+ 0.072 - 0.072 * value,
+ 0.213 - 0.213 * value,
+ 0.715 + 0.285 * value,
+ 0.072 - 0.072 * value,
+ 0.213 - 0.213 * value,
+ 0.715 - 0.715 * value,
+ 0.072 + 0.928 * value,
+ ]);
+ }
+
+ multiply(matrix: number[]) {
+ const newR = this.clamp(
+ this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]
+ );
+ const newG = this.clamp(
+ this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]
+ );
+ const newB = this.clamp(
+ this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]
+ );
+ this.r = newR;
+ this.g = newG;
+ this.b = newB;
+ }
+
+ brightness(value = 1) {
+ this.linear(value);
+ }
+ contrast(value = 1) {
+ this.linear(value, -(0.5 * value) + 0.5);
+ }
+
+ linear(slope = 1, intercept = 0) {
+ this.r = this.clamp(this.r * slope + intercept * 255);
+ this.g = this.clamp(this.g * slope + intercept * 255);
+ this.b = this.clamp(this.b * slope + intercept * 255);
+ }
+
+ invert(value = 1) {
+ this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
+ this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
+ this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
+ }
+
+ hsl(): HSL {
+ // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
+ const r = this.r / 255;
+ const g = this.g / 255;
+ const b = this.b / 255;
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+
+ let h = 0;
+ let s = 0;
+ const l = (max + min) / 2;
+
+ if (max === min) {
+ h = s = 0;
+ } else {
+ const d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+
+ case g:
+ h = (b - r) / d + 2;
+ break;
+
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ return {
+ h: h * 100,
+ s: s * 100,
+ l: l * 100,
+ };
+ }
+
+ clamp(value: number): number {
+ if (value > 255) {
+ value = 255;
+ } else if (value < 0) {
+ value = 0;
+ }
+ return value;
+ }
+}
+interface Solution {
+ loss: number;
+ values: number[];
+}
+class Solver {
+ private target: Color;
+ private targetHSL: HSL;
+ private reusedColor: Color;
+ constructor(target: Color) {
+ this.target = target;
+ this.targetHSL = target.hsl();
+ this.reusedColor = new Color(0, 0, 0);
+ }
+
+ solve() {
+ const result = this.solveNarrow(this.solveWide());
+ return {
+ values: result.values,
+ loss: result.loss,
+ filter: this.css(result.values),
+ };
+ }
+
+ solveWide(): Solution {
+ const A = 5;
+ const c = 15;
+ const a = [60, 180, 18000, 600, 1.2, 1.2];
+
+ let best = { loss: Infinity, values: [] as number[] };
+ for (let i = 0; best.loss > 25 && i < 3; i++) {
+ const initial = [50, 20, 3750, 50, 100, 100];
+ const result = this.spsa(A, a, c, initial, 1000);
+ if (result.loss < best.loss) {
+ best = result;
+ }
+ }
+ return best;
+ }
+
+ solveNarrow(wide: Solution) {
+ const A = wide.loss;
+ const c = 2;
+ const A1 = A + 1;
+ const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
+ return this.spsa(A, a, c, wide.values, 500);
+ }
+
+ spsa(
+ A: number,
+ a: number[],
+ c: number,
+ values: number[],
+ iters: number
+ ): Solution {
+ const alpha = 1;
+ const gamma = 0.16666666666666666;
+
+ let best = [] as number[];
+ let bestLoss = Infinity;
+ const deltas = new Array(6);
+ const highArgs = new Array(6);
+ const lowArgs = new Array(6);
+
+ for (let k = 0; k < iters; k++) {
+ const ck = c / Math.pow(k + 1, gamma);
+ for (let i = 0; i < 6; i++) {
+ deltas[i] = Math.random() > 0.5 ? 1 : -1;
+ highArgs[i] = values[i] + ck * deltas[i];
+ lowArgs[i] = values[i] - ck * deltas[i];
+ }
+
+ const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
+ for (let i = 0; i < 6; i++) {
+ const g = (lossDiff / (2 * ck)) * deltas[i];
+ const ak = a[i] / Math.pow(A + k + 1, alpha);
+ values[i] = fix(values[i] - ak * g, i);
+ }
+
+ const loss = this.loss(values);
+ if (loss < bestLoss) {
+ best = values.slice(0);
+ bestLoss = loss;
+ }
+ }
+ return { values: best, loss: bestLoss };
+
+ function fix(value: number, idx: number): number {
+ let max = 100;
+ if (idx === 2 /* saturate */) {
+ max = 7500;
+ } else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
+ max = 200;
+ }
+
+ if (idx === 3 /* hue-rotate */) {
+ if (value > max) {
+ value %= max;
+ } else if (value < 0) {
+ value = max + (value % max);
+ }
+ } else if (value < 0) {
+ value = 0;
+ } else if (value > max) {
+ value = max;
+ }
+ return value;
+ }
+ }
+
+ loss(filters: number[]) {
+ // Argument is array of percentages.
+ const color = this.reusedColor;
+ color.set(0, 0, 0);
+
+ color.invert(filters[0] / 100);
+ color.sepia(filters[1] / 100);
+ color.saturate(filters[2] / 100);
+ color.hueRotate(filters[3] * 3.6);
+ color.brightness(filters[4] / 100);
+ color.contrast(filters[5] / 100);
+
+ const colorHSL = color.hsl();
+ return (
+ Math.abs(color.r - this.target.r) +
+ Math.abs(color.g - this.target.g) +
+ Math.abs(color.b - this.target.b) +
+ Math.abs(colorHSL.h - this.targetHSL.h) +
+ Math.abs(colorHSL.s - this.targetHSL.s) +
+ Math.abs(colorHSL.l - this.targetHSL.l)
+ );
+ }
+
+ css(filters: number[]) {
+ function fmt(idx: number, multiplier = 1) {
+ return Math.round(filters[idx] * multiplier);
+ }
+ return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(
+ 2
+ )}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(
+ 5
+ )}%)`;
+ }
+}
+
+type RGB = [number, number, number];
+function hexToRgb(hex: string): RGB {
+ // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
+ const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+ hex = hex.replace(shorthandRegex, (_m, r: string, g: string, b: string) => {
+ return r + r + g + g + b + b;
+ });
+
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ if (result) {
+ return [
+ parseInt(result[1], 16),
+ parseInt(result[2], 16),
+ parseInt(result[3], 16),
+ ];
+ }
+
+ throw new Error("Error parsing hex: " + hex);
+}
+
+function getFilters(hex: string) {
+ let rgb = [255, 255, 255];
+ rgb = hexToRgb(hex);
+ const color = new Color(rgb[0], rgb[1], rgb[2]);
+ const solver = new Solver(color);
+ const result = solver.solve();
+ return result.filter;
+}
+
+export function DrawingSection({ type }: { type: "girl" | "boy" }) {
+ const [hexCode, setHexCode] = useState(randomColor());
+
+ const handleColorChange = () => setHexCode(randomColor());
+
+ return (
+
+
+
+
+ );
+}
+
+export default DrawingSection;
diff --git a/app/components/Layout/Menu.tsx b/app/components/Layout/Menu.tsx
new file mode 100644
index 000000000..abb7eb40d
--- /dev/null
+++ b/app/components/Layout/Menu.tsx
@@ -0,0 +1,70 @@
+import { Link } from "@remix-run/react";
+import { navItemsGrouped } from "~/constants";
+import { useOnClickOutside } from "~/hooks/common";
+import { layoutIcon } from "~/utils";
+import { discordUrl, gitHubUrl, patreonUrl, twitterUrl } from "~/utils/urls";
+import { Button } from "../Button";
+import { CrossIcon } from "../icons/Cross";
+import { DiscordIcon } from "../icons/Discord";
+import { GitHubIcon } from "../icons/Github";
+import { PatreonIcon } from "../icons/Patreon";
+import { TwitterIcon } from "../icons/Twitter";
+import DrawingSection from "./DrawingSection";
+import * as React from "react";
+
+export function Menu({ close }: { close: () => void }) {
+ const ref = React.useRef(null);
+ useOnClickOutside(ref, close);
+
+ return (
+
+
+
+
+
+
+ sendou.ink
+
+
+
+
+
+
+ {navItemsGrouped
+ .flatMap((group) => group.items)
+ .map((navItem) => (
+
+
+ {navItem.displayName ?? navItem.name}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/app/components/Layout/MobileNav.tsx b/app/components/Layout/MobileMenu.tsx
similarity index 89%
rename from app/components/Layout/MobileNav.tsx
rename to app/components/Layout/MobileMenu.tsx
index 3524af899..9031fdbed 100644
--- a/app/components/Layout/MobileNav.tsx
+++ b/app/components/Layout/MobileMenu.tsx
@@ -1,10 +1,9 @@
import clsx from "clsx";
import { Fragment } from "react";
import { Link } from "@remix-run/react";
-import { navItems } from "~/constants";
-import { SearchInput } from "./SearchInput";
+import { navItemsGrouped } from "~/constants";
-export function MobileNav({
+export function MobileMenu({
expanded,
closeMenu,
}: {
@@ -14,10 +13,10 @@ export function MobileNav({
return (
-
+ {/* */}
- {navItems.map((navGroup) => (
+ {navItemsGrouped.map((navGroup) => (
{navGroup.title}
diff --git a/app/components/Layout/SearchInput.tsx b/app/components/Layout/SearchInput.tsx
index 4acb25337..4dc1eef9d 100644
--- a/app/components/Layout/SearchInput.tsx
+++ b/app/components/Layout/SearchInput.tsx
@@ -1,6 +1,5 @@
-import * as React from "react";
import clsx from "clsx";
-import { SearchIcon } from "../../components/icons/Search";
+import * as React from "react";
export function SearchInput() {
// TODO: search input that searches
@@ -69,7 +68,7 @@ export function DumbSearchInput({
setValue(e.target.value)}
onKeyDown={(event) => {
@@ -78,7 +77,6 @@ export function DumbSearchInput({
}
}}
/>
-
);
}
diff --git a/app/components/Layout/index.tsx b/app/components/Layout/index.tsx
index e3160b3d7..8ffce72cb 100644
--- a/app/components/Layout/index.tsx
+++ b/app/components/Layout/index.tsx
@@ -1,79 +1,76 @@
+import { useMatches } from "@remix-run/react";
import * as React from "react";
-import { useState } from "react";
+import { PAGE_TITLE_KEY } from "~/constants";
+import { useWindowSize } from "~/hooks/common";
import { HamburgerButton } from "./HamburgerButton";
-import { MobileNav } from "./MobileNav";
+import { Menu } from "./Menu";
+import { MobileMenu } from "./MobileMenu";
import { SearchInput } from "./SearchInput";
import { UserItem } from "./UserItem";
-import { Link } from "@remix-run/react";
-import { navItems } from "~/constants";
-import { layoutIcon } from "~/utils";
-import clsx from "clsx";
+
+interface LayoutProps {
+ children: React.ReactNode;
+ menuOpen: boolean;
+ setMenuOpen: (open: boolean) => void;
+}
export const Layout = React.memo(function Layout({
children,
-}: {
- children: React.ReactNode;
-}) {
- const [menuExpanded, setMenuExpanded] = useState(false);
+ menuOpen,
+ setMenuOpen,
+}: LayoutProps) {
+ const matches = useMatches();
+
+ // you can set this page title from any loader
+ // deeper routes take precedence
+ const pageTitle = matches
+ .map((match) => match.data)
+ .filter(Boolean)
+ .reduceRight((acc: Nullable, routeData) => {
+ if (!acc && typeof routeData[PAGE_TITLE_KEY] === "string") {
+ return routeData[PAGE_TITLE_KEY] as string;
+ }
+
+ return acc;
+ }, null);
return (
<>
- setMenuExpanded(false)}
- />
-
-
- {navItems.map((navGroup) => (
-
-
{navGroup.title}
- {navGroup.items.map((navItem) => (
-
-
- {navItem.displayName ?? navItem.name}
-
- ))}
-
- ))}
-
-
-
-
- {new Array(50).fill(null).map((_, i) => (
- BETA
- ))}
-
-
+
{children}
>
);
});
+
+function ScreenWidthSensitiveMenu({
+ menuOpen,
+ setMenuOpen,
+}: Pick) {
+ const { width } = useWindowSize();
+
+ const closeMenu = () => setMenuOpen(false);
+
+ if (typeof width === "undefined") return null;
+
+ // render it on mobile even if menuOpen = false for the sliding animation
+ if (width < 900) {
+ return ;
+ }
+
+ if (!menuOpen) return null;
+
+ return ;
+}
diff --git a/app/components/SubNav.tsx b/app/components/SubNav.tsx
new file mode 100644
index 000000000..4967d8f12
--- /dev/null
+++ b/app/components/SubNav.tsx
@@ -0,0 +1,24 @@
+import { NavLink } from "@remix-run/react";
+import { RemixNavLinkProps } from "@remix-run/react/components";
+import clsx from "clsx";
+import React from "react";
+import { ArrowUpIcon } from "./icons/ArrowUp";
+
+export function SubNav({ children }: { children: React.ReactNode }) {
+ return {children} ;
+}
+
+export function SubNavLink({
+ children,
+ className,
+ ...props
+}: RemixNavLinkProps & {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/app/components/icons/ArrowUp.tsx b/app/components/icons/ArrowUp.tsx
index f191ac646..97abef7fc 100644
--- a/app/components/icons/ArrowUp.tsx
+++ b/app/components/icons/ArrowUp.tsx
@@ -5,7 +5,7 @@ export function ArrowUpIcon({
style,
}: {
className?: string;
- style: CSSProperties;
+ style?: CSSProperties;
}) {
return (
+
+
+ );
+}
diff --git a/app/components/icons/Patreon.tsx b/app/components/icons/Patreon.tsx
new file mode 100644
index 000000000..ef5e9a940
--- /dev/null
+++ b/app/components/icons/Patreon.tsx
@@ -0,0 +1,16 @@
+export function PatreonIcon({ className }: { className?: string }) {
+ return (
+
+
+
+
+ );
+}
diff --git a/app/components/tournament/InfoBanner.tsx b/app/components/tournament/InfoBanner.tsx
index a32c266a6..60d96cc73 100644
--- a/app/components/tournament/InfoBanner.tsx
+++ b/app/components/tournament/InfoBanner.tsx
@@ -1,14 +1,12 @@
-import { Link, useLoaderData, useLocation } from "@remix-run/react";
+import { Link, useLocation, useMatches } from "@remix-run/react";
import { DiscordIcon } from "~/components/icons/Discord";
import { TwitterIcon } from "~/components/icons/Twitter";
import { resolveTournamentFormatString } from "~/core/tournament/bracket";
-import { tournamentHasStarted } from "~/core/tournament/utils";
import { FindTournamentByNameForUrlI } from "~/services/tournament";
-import { getLogInUrl } from "~/utils";
-import { useUser } from "~/hooks/common";
export function InfoBanner() {
- const data = useLoaderData();
+ const [, parentRoute] = useMatches();
+ const data = parentRoute.data as FindTournamentByNameForUrlI;
const location = useLocation();
const urlToTournamentFrontPage = location.pathname
@@ -78,7 +76,6 @@ export function InfoBanner() {
{data.organizer.name}
-
>
@@ -104,57 +101,3 @@ function dayNumber(date: string) {
function dateYYYYMMDD(date: string) {
return new Date(date).toISOString().split("T")[0];
}
-
-function InfoBannerActionButton() {
- const data = useLoaderData();
- const user = useUser();
- const location = useLocation();
-
- const isAlreadyInATeamButNotCaptain = data.teams
- .flatMap((team) => team.members)
- .filter(({ captain }) => !captain)
- .some(({ member }) => member.id === user?.id);
- if (isAlreadyInATeamButNotCaptain) return null;
-
- const alreadyRegistered = data.teams
- .flatMap((team) => team.members)
- .some(({ member }) => member.id === user?.id);
- if (alreadyRegistered) {
- return (
-
- Add players
-
- );
- }
-
- if (tournamentHasStarted(data.brackets)) {
- return null;
- }
-
- if (!user) {
- return (
-
- );
- }
-
- return (
-
- Register
-
- );
-}
diff --git a/app/constants.ts b/app/constants.ts
index 1f0c10216..43c1bd9c9 100644
--- a/app/constants.ts
+++ b/app/constants.ts
@@ -1,7 +1,5 @@
import { Ability } from "@prisma/client";
-export const DISCORD_URL = "https://discord.gg/sendou";
-
export const ADMIN_UUID = "ee2d82dd-624f-4b07-9d8d-ddee1f8fb36f";
export const ADMIN_TEST_DISCORD_ID = "79237403620945920";
@@ -10,6 +8,8 @@ export const NZAP_UUID = "6cd9d01d-b724-498a-b706-eb70edd8a773";
export const NZAP_TEST_DISCORD_ID = "455039198672453645";
export const NZAP_TEST_AVATAR = "f809176af93132c3db5f0a5019e96339";
+export const PAGE_TITLE_KEY = "pageTitle";
+
export const ROOM_PASS_LENGTH = 4;
export const LFG_GROUP_FULL_SIZE = 4;
export const TOURNAMENT_TEAM_ROSTER_MIN_SIZE = 4;
@@ -39,7 +39,7 @@ export const checkInClosesDate = (startTime: string): Date => {
return new Date(new Date(startTime).getTime() - 1000 * 10);
};
-export const navItems: {
+export const navItemsGrouped: {
title: string;
items: {
name: string;
@@ -51,7 +51,7 @@ export const navItems: {
{
title: "builds",
items: [
- { name: "browse", disabled: true },
+ { name: "builds", disabled: true },
{ name: "gear", disabled: true },
{ name: "analyzer", disabled: true },
],
diff --git a/app/hooks/common.ts b/app/hooks/common.ts
index 80a00827b..fb018b8b8 100644
--- a/app/hooks/common.ts
+++ b/app/hooks/common.ts
@@ -95,3 +95,33 @@ export function usePolling(pollingActive = true) {
return lastUpdated;
}
+
+// https://usehooks.com/useWindowSize/
+export function useWindowSize() {
+ // Initialize state with undefined width/height so server and client renders match
+ // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
+ const [windowSize, setWindowSize] = React.useState<{
+ width?: number;
+ height?: number;
+ }>({
+ width: undefined,
+ height: undefined,
+ });
+ React.useEffect(() => {
+ // Handler to call on window resize
+ function handleResize() {
+ // Set window width/height to state
+ setWindowSize({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+ }
+ // Add event listener
+ window.addEventListener("resize", handleResize);
+ // Call handler right away so state gets updated with initial window size
+ handleResize();
+ // Remove event listener on cleanup
+ return () => window.removeEventListener("resize", handleResize);
+ }, []); // Empty array ensures that effect is only run on mount
+ return windowSize;
+}
diff --git a/app/root.tsx b/app/root.tsx
index 193ee6e29..6d5780e8a 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,4 +1,3 @@
-import * as React from "react";
import type { LinksFunction, LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import {
@@ -8,17 +7,18 @@ import {
Outlet,
Scripts,
useCatch,
- useLoaderData,
} from "@remix-run/react";
-import { LoggedInUser, LoggedInUserSchema } from "~/utils/schemas";
-import { Layout } from "./components/Layout";
-import { Catcher } from "./components/Catcher";
-import resetStyles from "~/styles/reset.css";
+import clsx from "clsx";
+import * as React from "react";
+import { io, Socket } from "socket.io-client";
import globalStyles from "~/styles/global.css";
import layoutStyles from "~/styles/layout.css";
-import { DISCORD_URL } from "./constants";
-import { io, Socket } from "socket.io-client";
+import resetStyles from "~/styles/reset.css";
+import { LoggedInUser, LoggedInUserSchema } from "~/utils/schemas";
+import { Catcher } from "./components/Catcher";
+import { Layout } from "./components/Layout";
import { SocketProvider } from "./utils/socketContext";
+import { discordUrl } from "./utils/urls";
export const links: LinksFunction = () => {
return [
@@ -28,14 +28,14 @@ export const links: LinksFunction = () => {
];
};
-export interface EnvironmentVariables {
- FF_ENABLE_CHAT?: "true" | "admin" | string;
-}
+// export interface EnvironmentVariables {
+// FF_ENABLE_CHAT?: "true" | "admin" | string;
+// }
export interface RootLoaderData {
user?: LoggedInUser;
baseURL: string;
- ENV: EnvironmentVariables;
+ // ENV: EnvironmentVariables;
}
export const loader: LoaderFunction = ({ context }) => {
@@ -45,19 +45,20 @@ export const loader: LoaderFunction = ({ context }) => {
return json({
user: data?.user,
baseURL,
- ENV: {
- FF_ENABLE_CHAT: process.env.FF_ENABLE_CHAT,
- },
+ // ENV: {
+ // FF_ENABLE_CHAT: process.env.FF_ENABLE_CHAT,
+ // },
});
};
export const unstable_shouldReload = () => false;
export default function App() {
+ const [menuOpen, setMenuOpen] = React.useState(false);
const [socket, setSocket] = React.useState();
const children = React.useMemo(() => , []);
- const data = useLoaderData();
+ // const data = useLoaderData();
// TODO: for future optimization could only connect socket on sendouq/tournament pages
React.useEffect(() => {
@@ -69,9 +70,11 @@ export default function App() {
}, []);
return (
-
+
- {children}
+
+ {children}
+
);
@@ -80,11 +83,13 @@ export default function App() {
function Document({
children,
title,
- ENV,
-}: {
+ disableBodyScroll = false,
+}: // ENV,
+{
children: React.ReactNode;
title?: string;
- ENV?: EnvironmentVariables;
+ disableBodyScroll?: boolean;
+ // ENV?: EnvironmentVariables;
}) {
return (
@@ -95,9 +100,9 @@ function Document({
-
+
{children}
-
+ /> */}
{process.env.NODE_ENV === "development" && }
@@ -114,11 +119,15 @@ function Document({
}
export function CatchBoundary() {
+ const [menuOpen, setMenuOpen] = React.useState(false);
const caught = useCatch();
return (
-
-
+
+
@@ -126,18 +135,20 @@ export function CatchBoundary() {
}
export function ErrorBoundary({ error }: { error: Error }) {
+ const [menuOpen, setMenuOpen] = React.useState(false);
+
// TODO: do something not hacky with this
const [message, data] = error.message.split(",");
return (
-
-
+
+
Error happened: {message}
{data && data.length > 0 && data !== "null" &&
Message: {data}
}
If you need help or want to report the error so that it can be fixed
- please visit our Discord
+ please visit our Discord
diff --git a/app/routes/beta.tsx b/app/routes/beta.tsx
deleted file mode 100644
index 2760b6abc..000000000
--- a/app/routes/beta.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { DISCORD_URL } from "~/constants";
-
-export default function BetaPage() {
- return (
-
-
Beta of sendou.ink (Splatoon 3)
-
- Hello there! I appreciate you taking time to visit this beta version of
- sendou.ink's Splatoon 3 site. This being a beta there is a few
- things you should consider:
-
-
-
- It's likely the database will be cleared (more than once) before
- beta ends
-
-
- Bugs are expected. Please give feedback on{" "}
- our Discord
-
-
- Follow Twitter for
- announcements about test tournaments and everything else related to
- sendou.ink
-
-
-
- Return to Splatoon 2 sendou.ink
-
-
- );
-}
diff --git a/app/routes/play/match.$id.tsx b/app/routes/play/match.$id.tsx
index 54980ec4e..62e309821 100644
--- a/app/routes/play/match.$id.tsx
+++ b/app/routes/play/match.$id.tsx
@@ -1,7 +1,4 @@
import { Mode } from "@prisma/client";
-import clsx from "clsx";
-import React from "react";
-
import {
ActionFunction,
json,
@@ -10,7 +7,6 @@ import {
MetaFunction,
redirect,
} from "@remix-run/node";
-
import {
Form,
ShouldReloadFunction,
@@ -19,7 +15,8 @@ import {
useParams,
useTransition,
} from "@remix-run/react";
-
+import clsx from "clsx";
+import React from "react";
import invariant from "tiny-invariant";
import { z } from "zod";
import { Button } from "~/components/Button";
@@ -31,7 +28,7 @@ import { DetailedPlayers } from "~/components/play/DetailedPlayers";
import { MapList } from "~/components/play/MapList";
import { MatchTeams } from "~/components/play/MatchTeams";
import { SubmitButton } from "~/components/SubmitButton";
-import { DISCORD_URL, LFG_AMOUNT_OF_STAGES_TO_GENERATE } from "~/constants";
+import { LFG_AMOUNT_OF_STAGES_TO_GENERATE } from "~/constants";
import { isAdmin } from "~/core/common/permissions";
import { requestMatchDetails } from "~/core/lanista";
import {
@@ -56,6 +53,7 @@ import {
} from "~/utils";
import {
chatRoute,
+ discordUrl,
sendouQAddPlayersPage,
sendouQFrontPage,
} from "~/utils/urls";
@@ -434,7 +432,7 @@ export default function LFGMatchPage() {
The score you reported is different from what your opponent
reported. If you think the information below is wrong notify us on
- the #helpdesk channel of our
Discord {" "}
+ the #helpdesk channel of our
Discord {" "}
channel
)}
diff --git a/app/routes/to/$organization.$tournament.tsx b/app/routes/to/$organization.$tournament.tsx
index 9daf02c95..0c8b838bc 100644
--- a/app/routes/to/$organization.$tournament.tsx
+++ b/app/routes/to/$organization.$tournament.tsx
@@ -6,29 +6,22 @@ import {
LoaderFunction,
MetaFunction,
} from "@remix-run/node";
-import {
- NavLink,
- Outlet,
- ShouldReloadFunction,
- useLoaderData,
-} from "@remix-run/react";
+import { Outlet, ShouldReloadFunction, useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
-import { AdminIcon } from "~/components/icons/Admin";
+import { SubNav, SubNavLink } from "~/components/SubNav";
import { CheckinActions } from "~/components/tournament/CheckinActions";
-import { InfoBanner } from "~/components/tournament/InfoBanner";
+import { PAGE_TITLE_KEY } from "~/constants";
import { tournamentHasStarted } from "~/core/tournament/utils";
+import { isTournamentAdmin } from "~/core/tournament/validators";
+import { useUser } from "~/hooks/common";
import {
checkIn,
findTournamentByNameForUrl,
FindTournamentByNameForUrlI,
} from "~/services/tournament";
-import { makeTitle, requireUser } from "~/utils";
-import type { MyCSSProperties } from "~/utils";
-import { useUser } from "~/hooks/common";
-import tournamentStylesUrl from "../../styles/tournament.css";
-import * as React from "react";
-import { isTournamentAdmin } from "~/core/tournament/validators";
+import { makeTitle, MyCSSProperties, requireUser } from "~/utils";
import { chatRoute } from "~/utils/urls";
+import tournamentStylesUrl from "../../styles/tournament.css";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tournamentStylesUrl }];
@@ -57,7 +50,7 @@ export const action: ActionFunction = async ({ request, context }) => {
return new Response(undefined, { status: 200 });
};
-export const loader: LoaderFunction = ({ params }) => {
+export const loader: LoaderFunction = async ({ params }) => {
invariant(
typeof params.organization === "string",
"Expected params.organization to be string"
@@ -67,10 +60,15 @@ export const loader: LoaderFunction = ({ params }) => {
"Expected params.tournament to be string"
);
- return findTournamentByNameForUrl({
+ const tournament = await findTournamentByNameForUrl({
organizationNameForUrl: params.organization,
tournamentNameForUrl: params.tournament,
});
+
+ return {
+ ...tournament,
+ [PAGE_TITLE_KEY]: tournament.name,
+ };
};
export const meta: MetaFunction = (props) => {
@@ -78,8 +76,6 @@ export const meta: MetaFunction = (props) => {
return {
title: makeTitle(data?.name),
- // TODO: description, image?
- //description: data.description ?? undefined,
};
};
@@ -95,7 +91,7 @@ export default function TournamentPage() {
const user = useUser();
const navLinks = (() => {
- const result: { code: string; text: string; icon?: React.ReactNode }[] = [
+ const result: { code: string; text: string }[] = [
{ code: "", text: "Overview" },
{ code: "map-pool", text: "Map Pool" },
{ code: "teams", text: `Teams (${data.teams.length})` },
@@ -119,15 +115,12 @@ export default function TournamentPage() {
if (isTournamentAdmin({ userId: user?.id, organization: data.organizer })) {
result.push({
code: "manage",
- // TODO: figure out a good name
text: "Controls",
- icon: ,
});
if (!tournamentHasStarted(data.brackets)) {
- result.push({ code: "seeds", text: "Seeds", icon: });
+ result.push({ code: "seeds", text: "Seeds" });
}
- if (thereIsABracketToStart)
- result.push({ code: "start", text: "Start", icon: });
+ if (thereIsABracketToStart) result.push({ code: "start", text: "Start" });
}
return result;
@@ -139,31 +132,16 @@ export default function TournamentPage() {
"--tournaments-text-transparent": data.CSSProperties.textTransparent,
};
- const linksContainerStyle: MyCSSProperties = {
- "--tabs-count": navLinks.length,
- };
-
return (
-
-
-
-
-
- {navLinks.map(({ code, text, icon }) => (
-
- ))}
-
-
-
+
+ {navLinks.map((link) => (
+
+ {link.text}
+
+ ))}
+
+
@@ -173,36 +151,57 @@ export default function TournamentPage() {
);
}
-function TournamentNavLink({
- code,
- icon,
- text,
-}: {
- code: string;
- icon: React.ReactNode;
- text: string;
-}) {
- const ref = React.useRef
(null);
+function MyTeamLink() {
+ const data = useLoaderData();
+ const user = useUser();
- React.useEffect(() => {
- if (!ref.current?.className.includes("active")) return;
- ref.current?.scrollIntoView({
- behavior: "smooth",
- inline: "center",
- block: "nearest",
- });
- }, []);
+ const isAlreadyInATeamButNotCaptain = data.teams
+ .flatMap((team) => team.members)
+ .filter(({ captain }) => !captain)
+ .some(({ member }) => member.id === user?.id);
+ if (isAlreadyInATeamButNotCaptain) return null;
+
+ const alreadyRegistered = data.teams
+ .flatMap((team) => team.members)
+ .some(({ member }) => member.id === user?.id);
+ if (alreadyRegistered) {
+ return (
+
+ Add players
+
+ );
+ }
+
+ if (tournamentHasStarted(data.brackets)) {
+ return null;
+ }
+
+ // TODO: prompt user to log in if not logged in
+ // if (!user) {
+ // return (
+ //
+ // );
+ // }
+ if (!user) return null;
return (
-
- {icon} {text}
-
+ Register
+
);
}
diff --git a/app/routes/to/$organization.$tournament/index.tsx b/app/routes/to/$organization.$tournament/index.tsx
index d06cae989..6f918bcf3 100644
--- a/app/routes/to/$organization.$tournament/index.tsx
+++ b/app/routes/to/$organization.$tournament/index.tsx
@@ -1,9 +1,15 @@
import { useMatches } from "@remix-run/react";
+import { InfoBanner } from "~/components/tournament/InfoBanner";
import type { FindTournamentByNameForUrlI } from "~/services/tournament";
export default function DefaultTab() {
const [, parentRoute] = useMatches();
const { description } = parentRoute.data as FindTournamentByNameForUrlI;
- return {description} ;
+ return (
+
+ );
}
diff --git a/app/styles/global.css b/app/styles/global.css
index 6db99fbb2..19c75dd05 100644
--- a/app/styles/global.css
+++ b/app/styles/global.css
@@ -5,6 +5,7 @@
--bg-darker: hsl(237.3deg 42.3% 26.6%);
--bg-lighter: hsl(237.3deg 42.3% 35.6%);
--bg-lighter-transparent: hsla(237.3deg 42.3% 35.6% / 50%);
+ --bg-darker-very-transparent: hsla(237.3deg 42.3% 26.6% / 50%);
--bg-darker-transparent: hsla(237.3deg 42.3% 26.6% / 90%);
--bg-modal-backdrop: hsla(237deg 98% 1% / 70%);
--border: hsl(237.3deg 42.3% 45.6%);
@@ -345,7 +346,6 @@ hr {
top: 0;
right: 0;
width: 70px;
- color: var(--text);
font-size: 80%;
line-height: 50px;
text-align: center;
@@ -447,6 +447,40 @@ hr {
font-weight: var(--bold);
}
+.sub-nav__container {
+ display: flex;
+ width: 100vw;
+ flex-wrap: wrap;
+ justify-content: center;
+ background-color: var(--bg-lighter);
+ background-image: url("/svg/background-pattern.svg");
+ overflow-x: auto;
+}
+
+.sub-nav__link {
+ display: flex;
+ height: 100%;
+ flex-direction: column;
+ align-items: center;
+ padding: var(--s-4);
+ background-color: var(--bg-lighter);
+ color: var(--text);
+ font-size: var(--fonts-xs);
+ font-weight: var(--semi-bold);
+ white-space: nowrap;
+}
+
+.sub-nav__active-icon {
+ height: 1.2rem;
+ margin-block-end: -1rem;
+ margin-block-start: -3px;
+ visibility: hidden;
+}
+
+.sub-nav__link.active > .sub-nav__active-icon {
+ visibility: visible;
+}
+
.popover-content {
z-index: 1;
max-width: 20rem;
@@ -813,6 +847,10 @@ hr {
border-radius: var(--rounded);
}
+.no-scroll {
+ overflow: hidden;
+}
+
.z-10 {
z-index: 10;
}
diff --git a/app/styles/layout.css b/app/styles/layout.css
index ef84d5ed0..1cf728085 100644
--- a/app/styles/layout.css
+++ b/app/styles/layout.css
@@ -10,125 +10,40 @@
grid-template-columns: 1fr 2fr;
}
-.layout__header__logo-container {
- display: grid;
- width: var(--item-size);
- min-width: var(--item-size);
- height: var(--item-size);
- min-height: var(--item-size);
- padding: var(--s-1);
- background-color: var(--bg-lighter);
- background-image: url("/svg/background-pattern.svg");
- border-radius: var(--rounded);
- cursor: pointer;
- justify-self: flex-start;
- place-items: center;
-}
-
-.layout__logo {
- max-width: 100%;
- max-height: 100%;
-}
-
.layout__header__search-container {
display: none;
}
+.layout__header__title-container {
+ color: var(--text-lighter);
+ font-size: var(--fonts-sm);
+ font-weight: var(--bold);
+ text-align: center;
+}
+
.layout__header__right-container {
display: flex;
gap: var(--s-4);
justify-self: flex-end;
}
-.layout__beta__link {
- text-decoration: none;
-}
-
-.layout__beta__banner {
- display: flex;
- width: 100%;
- justify-content: center;
- background-color: var(--theme);
- color: var(--button-text-transparent);
- font-size: var(--fonts-xs);
- font-weight: var(--bold);
- gap: var(--s-8);
- overflow-x: hidden;
- white-space: nowrap;
-}
-
-.layout__nav {
- display: none;
- justify-content: center;
- background-color: var(--bg-lighter);
- background-image: url("/svg/background-pattern.svg");
-}
-
-.layout__nav__items {
- display: inline-flex;
- justify-content: center;
- padding: var(--s-4) var(--s-8);
- background-color: var(--bg-lighter);
- gap: var(--s-12);
- grid-template-columns: repeat(4, 100px);
-}
-
-.layout__nav__items__column {
- display: flex;
- flex-direction: column;
- gap: var(--s-2);
-}
-
-.layout__nav__link {
- display: flex;
- align-items: center;
- color: var(--text);
- font-size: var(--fonts-sm);
- font-weight: var(--bold);
- gap: var(--s-2);
- text-decoration: none;
- text-transform: capitalize;
- transition: 0.2s transform;
-}
-
-.layout__nav__link.disabled {
- filter: grayscale(100%);
-}
-
-.layout__nav__link:hover {
- transform: translateX(2px);
-}
-
-.layout__nav__column__title {
- color: var(--text-lighter);
- font-size: var(--fonts-xxs);
- font-weight: var(--bold);
- text-transform: uppercase;
-}
-
-.layout__nav__link__icon {
- width: 2rem;
- height: 2rem;
-}
-
.layout__main {
max-width: 48rem;
margin: 0 auto;
padding-block-end: var(--s-32);
- padding-block-start: var(--s-8);
padding-inline: var(--s-2);
}
.layout__burger {
display: flex;
- width: 3rem;
- height: 3rem;
+ width: var(--item-size);
+ height: var(--item-size);
flex-direction: column;
align-items: center;
justify-content: center;
- padding: 0.5rem;
+ padding: 0.25rem;
border: 2px solid;
- border-color: var(--bg-lighter);
+ border-color: var(--theme-transparent);
background-color: transparent;
border-radius: var(--rounded);
color: inherit;
@@ -194,7 +109,7 @@
.layout__mobile-nav__top-action {
display: grid;
- padding-top: var(--s-24);
+ padding-top: var(--s-12);
padding-bottom: var(--s-8);
background-color: var(--bg);
gap: var(--s-4);
@@ -282,11 +197,6 @@
letter-spacing: 0.5px;
}
-.layout__search-input__icon {
- height: 1.25rem;
- color: var(--text);
-}
-
.layout__log-in-button {
display: flex;
height: var(--item-size);
@@ -329,23 +239,181 @@
.layout__header__search-container {
display: block;
- }
-
- .layout__nav {
- display: flex;
- }
-
- .layout__burger {
- display: none;
- }
-
- .layout__mobile-nav {
- display: none;
+ max-width: 12rem;
}
}
-@media screen and (min-width: 480px) {
- .mobileNavLink {
- margin: 0 var(--s-24);
+.menu {
+ --small-icons-height: 1.25rem;
+
+ position: fixed;
+ z-index: 999;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: grid;
+ background: radial-gradient(
+ circle,
+ rgb(14 3 40 / 100%) 0%,
+ rgb(28 4 82) 100%
+ );
+ place-items: center;
+}
+
+@supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) {
+ .menu {
+ -webkit-backdrop-filter: blur(15px);
+ backdrop-filter: blur(15px);
+ background: transparent;
}
}
+
+.menu__inner {
+ display: grid;
+ grid-template:
+ ". top-icons ." 1.25rem
+ "img-boy nav-items img-girl" 14rem
+ ". bottom-icons ." 1.25rem
+ / 1.15fr 2fr 1.15fr;
+ place-items: center;
+}
+
+@media screen and (min-width: 1024px) {
+ .menu__inner {
+ grid-template-rows: 1.25rem 18rem 1.25rem;
+ }
+}
+
+.menu__top-extras {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ grid-area: top-icons;
+}
+
+.menu__logo-container {
+ display: flex;
+ height: var(--small-icons-height);
+ align-items: center;
+ color: var(--text);
+ font-size: var(--fonts-xs);
+ font-weight: var(--bold);
+ gap: var(--s-1);
+}
+
+.menu__cross-icon {
+ width: 1.25rem;
+ height: 100%;
+ fill: var(--text);
+}
+
+.menu__nav {
+ display: grid;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+ padding: var(--s-12);
+ background-color: var(--bg-darker-very-transparent);
+ grid-area: nav-items;
+ grid-template-columns: repeat(3, 8rem);
+ row-gap: var(--s-2);
+}
+
+.menu__nav__link {
+ display: flex;
+ align-items: center;
+ color: var(--text);
+ font-size: var(--fonts-xs);
+ font-weight: var(--bold);
+ gap: var(--s-1);
+ text-decoration: none;
+ text-transform: capitalize;
+ transition: 0.2s transform;
+}
+
+.menu__icons-container {
+ display: flex;
+ width: 100%;
+ height: var(--small-icons-height);
+ justify-content: flex-end;
+ gap: var(--s-1);
+ grid-area: bottom-icons;
+ margin-block-start: 1px;
+}
+
+.menu__icons-container > a {
+ display: flex;
+ height: 100%;
+ justify-content: flex-end;
+}
+
+.menu__icon {
+ width: 1.25rem;
+ padding: 0.1rem;
+ fill: var(--text);
+}
+
+.menu__img-container {
+ position: relative;
+ display: flex;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ background-color: hsla(237deg 98% 1% / 25%);
+ border-radius: 0 var(--rounded) var(--rounded) 0;
+ grid-area: img-girl;
+ justify-self: flex-start;
+}
+
+.menu__img-container.boy {
+ border-radius: var(--rounded) 0 0 var(--rounded);
+ grid-area: img-boy;
+ justify-self: flex-end;
+}
+
+@keyframes slide-in-from-top {
+ 0% {
+ transform: translateY(-50%);
+ }
+
+ 100% {
+ transform: translateY(0);
+ }
+}
+
+@keyframes slide-in-from-bottom {
+ 0% {
+ transform: translateY(50%);
+ }
+
+ 100% {
+ transform: translateY(0);
+ }
+}
+
+.menu__img {
+ overflow: hidden;
+ height: 100%;
+ animation: 0.75s ease-out 0s 1 slide-in-from-bottom;
+ object-fit: cover;
+}
+
+.menu__img-bg {
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ height: 100%;
+ animation: 0.75s ease-out 0s 1 slide-in-from-bottom;
+ object-fit: cover;
+}
+
+.menu__img.boy {
+ animation: 0.75s ease-out 0s 1 slide-in-from-top;
+}
+
+.menu__img-bg.boy {
+ animation: 0.75s ease-out 0s 1 slide-in-from-top;
+}
diff --git a/app/styles/tournament-match.css b/app/styles/tournament-match.css
index 12ca1af06..ccd0e920b 100644
--- a/app/styles/tournament-match.css
+++ b/app/styles/tournament-match.css
@@ -14,7 +14,7 @@
.tournament-match-modal__error-msg {
color: var(--theme-error);
font-size: var(--fonts-sm);
-
+
/* Prevent layout shift */
padding-block: 0.16rem;
}
diff --git a/app/styles/tournament.css b/app/styles/tournament.css
index 97d29e7f5..644a00650 100644
--- a/app/styles/tournament.css
+++ b/app/styles/tournament.css
@@ -59,47 +59,6 @@
place-items: center;
}
-.tournament__nav-link {
- all: unset;
- display: flex;
- width: 100%;
- justify-content: center;
- background-color: var(--bg-lighter);
- color: var(--text);
- cursor: pointer;
- font-size: var(--fonts-sm);
- gap: var(--s-1);
- padding-block: var(--s-1-5);
-}
-
-.tournament__nav-link > svg {
- width: 1rem;
-}
-
-.tournament__nav-link:first-of-type {
- border-bottom-left-radius: var(--rounded);
- border-top-left-radius: var(--rounded);
-}
-
-.tournament__nav-link:last-of-type {
- border-bottom-right-radius: var(--rounded);
- border-top-right-radius: var(--rounded);
-}
-
-.tournament__nav-link:hover {
- font-weight: var(--bold);
-}
-
-.tournament__nav-link:focus-visible {
- background-color: var(--bg-lighter-transparent);
-}
-
-.tournament__nav-link.active {
- background-color: transparent;
- color: var(--tournaments-text);
- font-weight: var(--bold);
-}
-
.info-banner {
width: 100%;
padding: var(--s-6);
@@ -189,44 +148,10 @@
inline-size: 2.25rem;
}
-.info-banner__icon-button:active {
- transform: translateY(1px);
-}
-
-.info-banner__icon-button:focus-visible {
- outline: 2px solid var(--tournaments-text-transparent);
-}
-
-.info-banner__action-button {
- all: unset;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border: 1px solid;
- border-color: var(--tournaments-text-transparent);
- border-radius: var(--rounded);
- color: inherit;
- cursor: pointer;
- font-size: var(--fonts-sm);
- font-weight: var(--bold);
- padding-block: var(--s-2);
- padding-inline: var(--s-4);
- text-decoration: none;
-}
-
-.info-banner__action-button:active {
- transform: translateY(1px);
-}
-
-.info-banner__action-button:focus-visible {
- outline: 2px solid var(--tournaments-text-transparent);
-}
-
.tournament__outlet-spacer {
padding-block-start: var(--s-8);
}
-/* separate .css file..? */
.teams-tab {
display: flex;
flex-direction: column;
@@ -307,8 +232,4 @@
border-inline-start: 2px solid var(--theme);
padding-inline-start: var(--s-6);
}
-
- .tournament__nav-link:last-of-type {
- padding-inline-end: 0;
- }
}
diff --git a/app/utils/index.ts b/app/utils/index.ts
index 2c0f0a7f2..00c4c5ae3 100644
--- a/app/utils/index.ts
+++ b/app/utils/index.ts
@@ -4,8 +4,6 @@ import { json } from "@remix-run/node";
import { useLocation } from "@remix-run/react";
import type { Socket } from "socket.io-client";
import { z } from "zod";
-import { ADMIN_UUID, NZAP_UUID } from "~/constants";
-import { EnvironmentVariables } from "~/root";
import { LoggedInUserSchema } from "~/utils/schemas";
export function flipObject<
@@ -156,21 +154,21 @@ export function falsyToNull(value: unknown): unknown {
return null;
}
-export function isFeatureFlagOn({
- flag,
- userId,
-}: {
- flag: keyof EnvironmentVariables;
- userId?: string;
-}) {
- if (typeof window === "undefined") return false;
+// export function isFeatureFlagOn({
+// flag,
+// userId,
+// }: {
+// flag: keyof EnvironmentVariables;
+// userId?: string;
+// }) {
+// if (typeof window === "undefined") return false;
- if (window.ENV[flag] === "admin") {
- return userId === ADMIN_UUID || userId === NZAP_UUID;
- }
+// if (window.ENV[flag] === "admin") {
+// return userId === ADMIN_UUID || userId === NZAP_UUID;
+// }
- return window.ENV[flag] === "true";
-}
+// return window.ENV[flag] === "true";
+// }
export type Serialized = {
[P in keyof T]: T[P] extends Date
@@ -217,7 +215,6 @@ export interface MyCSSProperties extends CSSProperties {
"--brackets-bottom-border-length"?: number;
"--brackets-column-matches"?: number;
"--height-override"?: string;
- "--tabs-count"?: number;
}
/** Minimal information on user to show their name and avatar */
diff --git a/app/utils/urls.ts b/app/utils/urls.ts
index 573d1064a..d411b3ceb 100644
--- a/app/utils/urls.ts
+++ b/app/utils/urls.ts
@@ -56,3 +56,12 @@ export function chatRoute(roomIds?: string[]) {
if (!roomIds || roomIds.length === 0) return "/chat";
return `/chat?${roomIds.map((id) => `id=${id}`).join("&")}`;
}
+
+//
+// Outside of sendou.ink
+//
+
+export const discordUrl = () => "https://discord.gg/sendou";
+export const twitterUrl = () => "https://twitter.com/sendouink";
+export const patreonUrl = () => "https://patreon.com/sendou";
+export const gitHubUrl = () => "https://github.com/Sendouc/sendou.ink";
diff --git a/package-lock.json b/package-lock.json
index 3e431c49a..23fdbb70f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,7 +7,6 @@
"": {
"name": "sendou.ink",
"version": "3.0.0",
- "hasInstallScript": true,
"dependencies": {
"@dnd-kit/core": "^5.0.3",
"@dnd-kit/sortable": "^6.0.1",
@@ -16,6 +15,7 @@
"@popperjs/core": "^2.11.5",
"@prisma/client": "^3.10.0",
"@remix-run/express": "^1.4.0",
+ "@remix-run/node": "^1.4.0",
"@remix-run/react": "^1.4.0",
"clsx": "^1.1.1",
"compression": "^1.7.4",
@@ -31,10 +31,10 @@
"openskill": "^2.1.0",
"passport": "^0.5.2",
"passport-discord": "^0.1.4",
+ "randomcolor": "^0.6.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-popper": "^2.2.5",
- "remix": "^1.4.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.4.1",
"tiny-invariant": "^1.2.0",
@@ -50,6 +50,7 @@
"@types/morgan": "^1.9.3",
"@types/passport": "^1.0.7",
"@types/passport-discord": "^0.1.5",
+ "@types/randomcolor": "^0.5.6",
"@types/react": "^18.0.5",
"@types/react-dom": "^18.0.1",
"@types/uuid": "^8.3.4",
@@ -3046,6 +3047,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/randomcolor": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/@types/randomcolor/-/randomcolor-0.5.6.tgz",
+ "integrity": "sha512-lKkW8DGUQpZldTrwa+HM5rY+7eTyaHOMTsnj9ewt7AAwXuMPwBmkLlfh8+SXdgOhBW9iNI4x4zRjQ/TQZkdycQ==",
+ "dev": true
+ },
"node_modules/@types/range-parser": {
"version": "1.2.4",
"dev": true,
@@ -12907,6 +12914,11 @@
"url": "https://opencollective.com/ramda"
}
},
+ "node_modules/randomcolor": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.6.2.tgz",
+ "integrity": "sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A=="
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"license": "MIT",
@@ -13368,11 +13380,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/remix": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/remix/-/remix-1.4.0.tgz",
- "integrity": "sha512-yCSh9L+voOFGaFKCEw/379F0YNQLxDU3mDveEdxHUsJkqVyJI4qbSY27H5+2aVtcEW6h0qEefCZcXZyBa5A6ww=="
- },
"node_modules/repeat-element": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
@@ -18676,6 +18683,12 @@
"version": "6.9.7",
"dev": true
},
+ "@types/randomcolor": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/@types/randomcolor/-/randomcolor-0.5.6.tgz",
+ "integrity": "sha512-lKkW8DGUQpZldTrwa+HM5rY+7eTyaHOMTsnj9ewt7AAwXuMPwBmkLlfh8+SXdgOhBW9iNI4x4zRjQ/TQZkdycQ==",
+ "dev": true
+ },
"@types/range-parser": {
"version": "1.2.4",
"dev": true
@@ -25327,6 +25340,11 @@
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz",
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA=="
},
+ "randomcolor": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.6.2.tgz",
+ "integrity": "sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A=="
+ },
"range-parser": {
"version": "1.2.1"
},
@@ -25672,11 +25690,6 @@
"unified": "^10.0.0"
}
},
- "remix": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/remix/-/remix-1.4.0.tgz",
- "integrity": "sha512-yCSh9L+voOFGaFKCEw/379F0YNQLxDU3mDveEdxHUsJkqVyJI4qbSY27H5+2aVtcEW6h0qEefCZcXZyBa5A6ww=="
- },
"repeat-element": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
diff --git a/package.json b/package.json
index 755cf63b9..796638af2 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"openskill": "^2.1.0",
"passport": "^0.5.2",
"passport-discord": "^0.1.4",
+ "randomcolor": "^0.6.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-popper": "^2.2.5",
@@ -70,6 +71,7 @@
"@types/morgan": "^1.9.3",
"@types/passport": "^1.0.7",
"@types/passport-discord": "^0.1.5",
+ "@types/randomcolor": "^0.5.6",
"@types/react": "^18.0.5",
"@types/react-dom": "^18.0.1",
"@types/uuid": "^8.3.4",
diff --git a/public/img/layout/browse.webp b/public/img/layout/builds.webp
similarity index 100%
rename from public/img/layout/browse.webp
rename to public/img/layout/builds.webp
diff --git a/public/img/layout/new_boy_bg.png b/public/img/layout/new_boy_bg.png
new file mode 100644
index 000000000..d2af69ff5
Binary files /dev/null and b/public/img/layout/new_boy_bg.png differ
diff --git a/public/img/layout/new_boy_dark.png b/public/img/layout/new_boy_dark.png
new file mode 100644
index 000000000..bb09ddd57
Binary files /dev/null and b/public/img/layout/new_boy_dark.png differ
diff --git a/public/img/layout/new_girl_bg.png b/public/img/layout/new_girl_bg.png
new file mode 100644
index 000000000..a6fac5a6a
Binary files /dev/null and b/public/img/layout/new_girl_bg.png differ
diff --git a/public/img/layout/new_girl_dark.png b/public/img/layout/new_girl_dark.png
new file mode 100644
index 000000000..25686b0f0
Binary files /dev/null and b/public/img/layout/new_girl_dark.png differ
diff --git a/window.d.ts b/window.d.ts
deleted file mode 100644
index ae706bec1..000000000
--- a/window.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { EnvironmentVariables } from "~/root";
-
-declare global {
- interface Window {
- ENV: EnvironmentVariables;
- }
-}