mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Stricters Typescript
This commit is contained in:
parent
7dd6efb832
commit
ef2d56327d
|
|
@ -33,7 +33,7 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
|||
if (!query) return [];
|
||||
|
||||
const fuse = new Fuse(options, {
|
||||
keys: [...Object.keys(options[0])],
|
||||
keys: [...Object.keys(options[0] ?? {})],
|
||||
});
|
||||
return fuse
|
||||
.search(query)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function Dialog({
|
||||
children,
|
||||
|
|
@ -46,6 +47,7 @@ function useDOMSync(isOpen: boolean) {
|
|||
if (!dialog.open && !isOpen) return;
|
||||
|
||||
const html = document.getElementsByTagName("html")[0];
|
||||
invariant(html);
|
||||
|
||||
if (isOpen) {
|
||||
dialog.showModal();
|
||||
|
|
|
|||
|
|
@ -29,4 +29,6 @@ export function Label({ valueLimits, children, htmlFor }: LabelProps) {
|
|||
function lengthWarning(valueLimits: NonNullable<LabelProps["valueLimits"]>) {
|
||||
if (valueLimits.current >= valueLimits.max) return "error";
|
||||
if (valueLimits.current / valueLimits.max >= 0.9) return "warning";
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ export function Popover({
|
|||
// @ts-expect-error Popper docs: https://popper.js.org/react-popper/v2/
|
||||
ref={setPopperElement}
|
||||
className="popover-content"
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
style={styles["popper"]}
|
||||
{...attributes["popper"]}
|
||||
>
|
||||
{children}
|
||||
</HeadlessPopover.Panel>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from "clsx";
|
||||
import randomColor from "randomcolor";
|
||||
import { useState } from "react";
|
||||
import { atOrError } from "~/utils/arrays";
|
||||
|
||||
interface HSL {
|
||||
h: number;
|
||||
|
|
@ -92,13 +93,19 @@ class Color {
|
|||
|
||||
multiply(matrix: number[]) {
|
||||
const newR = this.clamp(
|
||||
this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]
|
||||
this.r * atOrError(matrix, 0) +
|
||||
this.g * atOrError(matrix, 1) +
|
||||
this.b * atOrError(matrix, 2)
|
||||
);
|
||||
const newG = this.clamp(
|
||||
this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]
|
||||
this.r * atOrError(matrix, 3) +
|
||||
this.g * atOrError(matrix, 4) +
|
||||
this.b * atOrError(matrix, 5)
|
||||
);
|
||||
const newB = this.clamp(
|
||||
this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]
|
||||
this.r * atOrError(matrix, 6) +
|
||||
this.g * atOrError(matrix, 7) +
|
||||
this.b * atOrError(matrix, 8)
|
||||
);
|
||||
this.r = newR;
|
||||
this.g = newG;
|
||||
|
|
@ -240,15 +247,15 @@ class Solver {
|
|||
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];
|
||||
highArgs[i] = atOrError(values, i) + ck * deltas[i];
|
||||
lowArgs[i] = atOrError(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 ak = atOrError(a, i) / Math.pow(A + k + 1, alpha);
|
||||
values[i] = fix(atOrError(values, i) - ak * g, i);
|
||||
}
|
||||
|
||||
const loss = this.loss(values);
|
||||
|
|
@ -287,12 +294,12 @@ class Solver {
|
|||
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);
|
||||
color.invert(atOrError(filters, 0) / 100);
|
||||
color.sepia(atOrError(filters, 1) / 100);
|
||||
color.saturate(atOrError(filters, 2) / 100);
|
||||
color.hueRotate(atOrError(filters, 3) * 3.6);
|
||||
color.brightness(atOrError(filters, 4) / 100);
|
||||
color.contrast(atOrError(filters, 5) / 100);
|
||||
|
||||
const colorHSL = color.hsl();
|
||||
return (
|
||||
|
|
@ -307,7 +314,7 @@ class Solver {
|
|||
|
||||
css(filters: number[]) {
|
||||
function fmt(idx: number, multiplier = 1) {
|
||||
return Math.round(filters[idx] * multiplier);
|
||||
return Math.round(atOrError(filters, idx) * multiplier);
|
||||
}
|
||||
return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(
|
||||
2
|
||||
|
|
@ -328,9 +335,9 @@ function hexToRgb(hex: string): RGB {
|
|||
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),
|
||||
parseInt(atOrError(result, 1), 16),
|
||||
parseInt(atOrError(result, 2), 16),
|
||||
parseInt(atOrError(result, 3), 16),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +347,11 @@ function hexToRgb(hex: string): RGB {
|
|||
function getFilters(hex: string) {
|
||||
let rgb = [255, 255, 255];
|
||||
rgb = hexToRgb(hex);
|
||||
const color = new Color(rgb[0], rgb[1], rgb[2]);
|
||||
const color = new Color(
|
||||
atOrError(rgb, 0),
|
||||
atOrError(rgb, 1),
|
||||
atOrError(rgb, 2)
|
||||
);
|
||||
const solver = new Solver(color);
|
||||
const result = solver.solve();
|
||||
return result.filter;
|
||||
|
|
|
|||
|
|
@ -41,17 +41,20 @@ export class DiscordStrategy extends OAuth2Strategy<
|
|||
scope: string;
|
||||
|
||||
constructor() {
|
||||
invariant(process.env.DISCORD_CLIENT_ID);
|
||||
invariant(process.env.DISCORD_CLIENT_SECRET);
|
||||
invariant(process.env.BASE_URL);
|
||||
invariant(process.env["DISCORD_CLIENT_ID"]);
|
||||
invariant(process.env["DISCORD_CLIENT_SECRET"]);
|
||||
invariant(process.env["BASE_URL"]);
|
||||
|
||||
super(
|
||||
{
|
||||
authorizationURL: "https://discord.com/api/oauth2/authorize",
|
||||
tokenURL: "https://discord.com/api/oauth2/token",
|
||||
clientID: process.env.DISCORD_CLIENT_ID,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||
callbackURL: new URL("/auth/callback", process.env.BASE_URL).toString(),
|
||||
clientID: process.env["DISCORD_CLIENT_ID"],
|
||||
clientSecret: process.env["DISCORD_CLIENT_SECRET"],
|
||||
callbackURL: new URL(
|
||||
"/auth/callback",
|
||||
process.env["BASE_URL"]
|
||||
).toString(),
|
||||
},
|
||||
async ({ accessToken }) => {
|
||||
const authHeader = ["Authorization", `Bearer ${accessToken}`];
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
invariant(process.env.SESSION_SECRET);
|
||||
invariant(process.env["SESSION_SECRET"]);
|
||||
export const sessionStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "_session",
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secrets: [process.env.SESSION_SECRET],
|
||||
secrets: [process.env["SESSION_SECRET"]],
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { MonthYear } from "~/core/plus";
|
||||
import { atOrError } from "~/utils/arrays";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { sql } from "../sql";
|
||||
import type { PlusSuggestion, User, UserWithPlusTier } from "../types";
|
||||
|
|
@ -68,7 +69,7 @@ export interface FindVisibleForUserSuggestedUserInfo {
|
|||
| "discordDiscriminator"
|
||||
| "discordAvatar"
|
||||
>;
|
||||
suggestions: (Pick<PlusSuggestion, "id" | "text"> & {
|
||||
suggestions: (Pick<PlusSuggestion, "id" | "text" | "createdAt"> & {
|
||||
createdAtText: string;
|
||||
author: Pick<
|
||||
User,
|
||||
|
|
@ -91,10 +92,12 @@ export function findVisibleForUser(
|
|||
|
||||
function mapFindVisibleForUserRowsToResult(rows: any[]): FindVisibleForUser {
|
||||
return rows.reduce((result: FindVisibleForUser, row) => {
|
||||
if (!result[row.tier]) result[row.tier] = [];
|
||||
const usersOfTier = result[row.tier] ?? [];
|
||||
result[row.tier] = usersOfTier;
|
||||
|
||||
const suggestionInfo = {
|
||||
id: row.id,
|
||||
createdAt: row.createdAt,
|
||||
createdAtText: databaseTimestampToDate(row.createdAt).toLocaleString(
|
||||
"en-US",
|
||||
{
|
||||
|
|
@ -113,14 +116,14 @@ function mapFindVisibleForUserRowsToResult(rows: any[]): FindVisibleForUser {
|
|||
},
|
||||
};
|
||||
|
||||
const existingSuggestion = result[row.tier].find(
|
||||
const existingSuggestion = usersOfTier.find(
|
||||
(suggestion) => suggestion.suggestedUser.id === row.suggestedId
|
||||
);
|
||||
|
||||
if (existingSuggestion) {
|
||||
existingSuggestion.suggestions.push(suggestionInfo);
|
||||
} else {
|
||||
result[row.tier].push({
|
||||
usersOfTier.push({
|
||||
suggestedUser: {
|
||||
id: row.suggestedId,
|
||||
discordId: row.suggestedDiscordId,
|
||||
|
|
@ -143,7 +146,9 @@ function sortNewestPlayersToBeSuggestedFirst(
|
|||
Object.entries(suggestions).map(([tier, suggestions]) => [
|
||||
tier,
|
||||
suggestions.sort(
|
||||
(a, b) => b.suggestions[0].createdAt - a.suggestions[0].createdAt
|
||||
(a, b) =>
|
||||
atOrError(b.suggestions, 0).createdAt -
|
||||
atOrError(a.suggestions, 0).createdAt
|
||||
),
|
||||
])
|
||||
);
|
||||
|
|
|
|||
|
|
@ -98,14 +98,13 @@ function groupPlusVotingResults(
|
|||
> = {};
|
||||
|
||||
for (const row of rows) {
|
||||
if (!grouped[row.tier]) {
|
||||
grouped[row.tier] = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
};
|
||||
}
|
||||
const playersOfTier = grouped[row.tier] ?? {
|
||||
passed: [],
|
||||
failed: [],
|
||||
};
|
||||
grouped[row.tier] = playersOfTier;
|
||||
|
||||
grouped[row.tier][row.passedVoting ? "passed" : "failed"].push({
|
||||
playersOfTier[row.passedVoting ? "passed" : "failed"].push({
|
||||
id: row.id,
|
||||
discordAvatar: row.discordAvatar,
|
||||
discordDiscriminator: row.discordDiscriminator,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import Database from "better-sqlite3";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
invariant(process.env.DB_PATH, "DB_PATH env variable must be set");
|
||||
export const sql = new Database(process.env.DB_PATH);
|
||||
invariant(process.env["DB_PATH"], "DB_PATH env variable must be set");
|
||||
export const sql = new Database(process.env["DB_PATH"]);
|
||||
|
||||
sql.pragma("journal_mode = WAL");
|
||||
sql.pragma("foreign_keys = ON");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useMatches } from "@remix-run/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
|
||||
export const useUser = () => {
|
||||
const [root] = useMatches();
|
||||
invariant(root);
|
||||
|
||||
return (root.data as RootLoaderData).user;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -193,7 +193,10 @@ function hasUserSuggestedThisMonth({
|
|||
}: Pick<CanSuggestNewUserFEArgs, "user" | "suggestions">) {
|
||||
return Object.values(suggestions)
|
||||
.flat()
|
||||
.some(({ suggestions }) => suggestions[0].author.id === user?.id);
|
||||
.some(
|
||||
({ suggestions }) =>
|
||||
suggestions[0] && suggestions[0].author.id === user?.id
|
||||
);
|
||||
}
|
||||
|
||||
export function canVoteFE() {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export default function PlusSuggestionsPage() {
|
|||
tierVisible && data.suggestions[tierVisible]
|
||||
? data.suggestions[tierVisible]
|
||||
: [];
|
||||
invariant(visibleSuggestions);
|
||||
|
||||
// xxx: looks strange when suggestedforinfo and suggestions both show https://cdn.discordapp.com/attachments/816458257714511872/984195125913731102/unknown.png
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ export default function PlusCommentModalPage() {
|
|||
const params = useParams();
|
||||
const data = atOrError(matches, -2).data as PlusSuggestionsLoaderData;
|
||||
|
||||
const targetUserId = Number(params.userId);
|
||||
const tierSuggestedTo = String(params.tier);
|
||||
const targetUserId = Number(params["userId"]);
|
||||
const tierSuggestedTo = String(params["tier"]);
|
||||
|
||||
const userBeingCommented = data.suggestions?.[tierSuggestedTo]?.find(
|
||||
(u) => u.suggestedUser.id === targetUserId
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { upcomingVoting } from "~/core/plus";
|
|||
import { db } from "~/db";
|
||||
import type { UserWithPlusTier } from "~/db/types";
|
||||
import { ErrorMessage } from "~/components/ErrorMessage";
|
||||
import { atOrError } from "~/utils/arrays";
|
||||
|
||||
const commentActionSchema = z.object({
|
||||
tier: z.preprocess(actualNumber, z.number().min(1).max(3)),
|
||||
|
|
@ -80,7 +81,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
export default function PlusNewSuggestionModalPage() {
|
||||
const user = useUser();
|
||||
const matches = useMatches();
|
||||
const data = matches.at(-2)!.data as PlusSuggestionsLoaderData;
|
||||
const data = atOrError(matches, -2).data as PlusSuggestionsLoaderData;
|
||||
const [selectedUser, setSelectedUser] = React.useState<{
|
||||
/** User id */
|
||||
value: string;
|
||||
|
|
@ -93,16 +94,17 @@ export default function PlusNewSuggestionModalPage() {
|
|||
|
||||
return tier >= user.plusTier;
|
||||
});
|
||||
const [targetPlusTier, setTargetPlusTier] = React.useState<number>(
|
||||
tierOptions[0]
|
||||
);
|
||||
const [targetPlusTier, setTargetPlusTier] = React.useState<
|
||||
number | undefined
|
||||
>(tierOptions[0]);
|
||||
|
||||
if (
|
||||
!data.suggestions ||
|
||||
!canSuggestNewUserFE({
|
||||
user,
|
||||
suggestions: data.suggestions,
|
||||
})
|
||||
}) ||
|
||||
!targetPlusTier
|
||||
) {
|
||||
return <Redirect to={PLUS_SUGGESTIONS_PAGE} />;
|
||||
}
|
||||
|
|
@ -187,6 +189,8 @@ function getSelectedUserErrorMessage({
|
|||
if (playerAlreadySuggested({ targetPlusTier, suggestions, suggested })) {
|
||||
return `This user was already suggested to +${targetPlusTier}`;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: better UX - allow going over but prevent submit like Twitter
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { ActionFunction, LinksFunction } from "@remix-run/node";
|
|||
import { Form, useMatches, useTransition } from "@remix-run/react";
|
||||
import { countries } from "countries-list";
|
||||
import * as React from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Label } from "~/components/Label";
|
||||
|
|
@ -47,6 +48,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
|
||||
export default function UserEditPage() {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
const transition = useTransition();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { LinksFunction } from "@remix-run/node";
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { SocialLink } from "~/components/u/SocialLink";
|
||||
import styles from "~/styles/u.css";
|
||||
|
|
@ -11,6 +12,7 @@ export const links: LinksFunction = () => {
|
|||
|
||||
export default function UserInfoPage() {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@
|
|||
},
|
||||
"noEmit": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": false
|
||||
"allowJs": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"useUnknownInCatchVariables": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user