From fb507a956ef1d4e473f645ea9fdc4fe1c67f0a30 Mon Sep 17 00:00:00 2001 From: Luna Seemann Date: Mon, 18 May 2026 21:00:34 +0200 Subject: [PATCH] chore: switch to @mwlica/eslint (#78) * chore: switch to @mwlica/eslint shared config * chore: regenerate bun.lock * fix: lint * Merge branch 'master' into chore/mwlica-eslint --- app/(home)/impressum/page.tsx | 4 +- app/(home)/premium/subscribe.component.tsx | 4 +- app/(home)/status/side.component.tsx | 6 +- app/(home)/team/page.tsx | 4 +- app/(home)/text-to-speech/use-history.ts | 2 +- .../[guildId]/custom-commands/page.tsx | 4 +- app/dashboard/[guildId]/layout.tsx | 62 ++- .../leaderboards/updating.component.tsx | 10 +- app/dashboard/[guildId]/moderation/page.tsx | 11 +- .../notifications/create-reddit.component.tsx | 6 +- .../notifications/style.component.tsx | 10 +- app/dashboard/[guildId]/starboard/page.tsx | 7 +- app/dashboard/[guildId]/style.component.tsx | 14 +- app/docs/[...pathname]/layout.tsx | 4 +- app/profile/billing/page.tsx | 80 +++- app/profile/page.tsx | 5 +- app/profile/rank/card-style.component.tsx | 2 +- .../rank/leaderboard-style.component.tsx | 2 +- app/profile/rank/page.tsx | 2 +- app/profile/text-to-speech/page.tsx | 2 +- bun.lock | 445 ++++++------------ components/copy-to-clipboard.tsx | 2 +- components/dashboard/lists/hook.ts | 8 +- components/discord/message-embed.tsx | 7 +- components/header.tsx | 2 +- components/inputs/image-url-input.tsx | 2 +- components/inputs/multi-select-menu.tsx | 6 +- components/inputs/number-input.tsx | 4 +- components/list.tsx | 2 +- components/markdown/index.tsx | 38 +- components/share.tsx | 4 +- components/ui/audio-player.tsx | 8 +- components/ui/input-base.tsx | 1 + eslint.config.mjs | 265 +---------- lib/cookies.ts | 4 +- package.json | 5 +- tsconfig.eslint.json | 18 + utils/bitfields.ts | 4 +- utils/create-selectable-items.tsx | 4 +- utils/{decimalToRgb.ts => decimal-to-rgb.ts} | 0 utils/{deepMerge.ts => deep-merge.ts} | 2 +- utils/input.ts | 2 +- 42 files changed, 409 insertions(+), 665 deletions(-) create mode 100644 tsconfig.eslint.json rename utils/{decimalToRgb.ts => decimal-to-rgb.ts} (100%) rename utils/{deepMerge.ts => deep-merge.ts} (92%) diff --git a/app/(home)/impressum/page.tsx b/app/(home)/impressum/page.tsx index dcb3927..bd2db9b 100644 --- a/app/(home)/impressum/page.tsx +++ b/app/(home)/impressum/page.tsx @@ -6,6 +6,8 @@ import type { Metadata } from "next"; import Link from "next/link"; import { HiExternalLink } from "react-icons/hi"; +const SEMICOLON_REGEX = /;/g; + export const revalidate = false; const imprint = Buffer.from(process.env.IMPRINT_BASE64!, "base64").toString(); @@ -52,7 +54,7 @@ export default function Home() { .split("\n") .map((item) => (
- {item.replace(/;/g, "")} + {item.replace(SEMICOLON_REGEX, "")}
)) } diff --git a/app/(home)/premium/subscribe.component.tsx b/app/(home)/premium/subscribe.component.tsx index 62904ea..b4ecd6c 100644 --- a/app/(home)/premium/subscribe.component.tsx +++ b/app/(home)/premium/subscribe.component.tsx @@ -10,6 +10,8 @@ import { useSearchParams } from "next/navigation"; import { type HTMLProps, useState } from "react"; import { HiArrowDown, HiArrowUp, HiLightningBolt, HiOutlineInformationCircle } from "react-icons/hi"; +const FIRST_WORD_CHAR_REGEX = /^\w/; + export const MONTHLY_PRICES = [4, 8, 12, 18, 25] as const; export const YEARLY_PRICES = [40, 50, 60, 80, 100] as const; const PERIODS = ["month", "year"] as const; @@ -106,7 +108,7 @@ export function Subscribe({ header }: { header?: boolean; }) { setDonation(nearest - targetBase); }} > - {p.replace(/^\w/, (char) => char.toUpperCase())}ly + {p.replace(FIRST_WORD_CHAR_REGEX, (char) => char.toUpperCase())}ly {p === "year" && ( (""); const clusterId = useMemo( - () => /^\d{15,20}$/.test(guildId) + () => SNOWFLAKE_REGEX.test(guildId) ? getClusterId(guildId || "", status.clusters.length) : null, [guildId, status.clusters.length] @@ -63,7 +65,7 @@ export function Side({ placeholder="Paste your Server Id" value={guildId} setValue={setGuildId} - description={/^\d{15,20}$/.test(guildId) ? `Your guild is on cluster #${clusterId}.` : ""} + description={SNOWFLAKE_REGEX.test(guildId) ? `Your guild is on cluster #${clusterId}.` : ""} />

diff --git a/app/(home)/team/page.tsx b/app/(home)/team/page.tsx index bdab4cf..98fbbea 100644 --- a/app/(home)/team/page.tsx +++ b/app/(home)/team/page.tsx @@ -9,12 +9,14 @@ import { DiscordServer } from "./discord.component"; import { Person } from "./person.component"; import { Repository } from "./repository.component"; +const FIRST_WORD_CHAR_REGEX = /^\w/; + export const revalidate = 3_600; function formatTeamName(team: string) { return team .split("-") - .map((str) => str.replace(/^\w/, (char) => char.toUpperCase())) + .map((str) => str.replace(FIRST_WORD_CHAR_REGEX, (char) => char.toUpperCase())) .join(" "); } diff --git a/app/(home)/text-to-speech/use-history.ts b/app/(home)/text-to-speech/use-history.ts index c908815..71ab99b 100644 --- a/app/(home)/text-to-speech/use-history.ts +++ b/app/(home)/text-to-speech/use-history.ts @@ -79,7 +79,7 @@ export function useHistory() { } }; - loadHistory(); + void loadHistory(); const cache = urlCache.current; return () => { diff --git a/app/dashboard/[guildId]/custom-commands/page.tsx b/app/dashboard/[guildId]/custom-commands/page.tsx index 721811b..478e7ba 100644 --- a/app/dashboard/[guildId]/custom-commands/page.tsx +++ b/app/dashboard/[guildId]/custom-commands/page.tsx @@ -19,6 +19,8 @@ import { useQuery, useQueryClient } from "react-query"; import { CreateTag, Style } from "./create.component"; import { DeleteTag } from "./delete.component"; +const UPPERCASE_LETTER_REGEX = /([A-Z])/g; + export default function Home() { const guild = guildStore((g) => g); const pathname = usePathname(); @@ -241,6 +243,6 @@ export default function Home() { } function convertCamelCaseToSpaced(input: string): string { - const spacedString = input.replace(/([A-Z])/g, " $1"); + const spacedString = input.replace(UPPERCASE_LETTER_REGEX, " $1"); return spacedString.charAt(0).toUpperCase() + spacedString.slice(1); } \ No newline at end of file diff --git a/app/dashboard/[guildId]/layout.tsx b/app/dashboard/[guildId]/layout.tsx index 7e87042..80d564f 100644 --- a/app/dashboard/[guildId]/layout.tsx +++ b/app/dashboard/[guildId]/layout.tsx @@ -198,30 +198,46 @@ export default function RootLayout({ /> - {error ? - - - - } - /> - : - isLoaded ? children : <> - } + + {children} + ); +} + +function DashboardBody({ + error, + isLoaded, + children +}: { + error: string | undefined; + isLoaded: boolean; + children: React.ReactNode; +}) { + if (!error) { + return isLoaded ? children : <>; + } + + return ( + + + + } + /> + ); } \ No newline at end of file diff --git a/app/dashboard/[guildId]/leaderboards/updating.component.tsx b/app/dashboard/[guildId]/leaderboards/updating.component.tsx index 279cbb6..253ed7e 100644 --- a/app/dashboard/[guildId]/leaderboards/updating.component.tsx +++ b/app/dashboard/[guildId]/leaderboards/updating.component.tsx @@ -13,6 +13,8 @@ import { useCookies } from "next-client-cookies"; import { useState } from "react"; import { HiExternalLink, HiPencil, HiTrash } from "react-icons/hi"; +const FIRST_WORD_CHAR_REGEX = /^\w/; + interface Props { guild: Guild; lb: ApiV1GuildsModulesLeaderboardUpdatingPostResponse | undefined; @@ -109,7 +111,7 @@ export default function UpdatingLeaderboardCard({ match.toUpperCase())} leaderboard`} + title={`${type.replace(FIRST_WORD_CHAR_REGEX, (match) => match.toUpperCase())} leaderboard`} isOpen={modal === ModalType.CreateAndEdit && Boolean(guild)} isDisabled={!channelId} onClose={() => { @@ -206,7 +208,7 @@ export default function UpdatingLeaderboardCard({ match.toUpperCase())} leaderboard`} + defaultState={`☕ ${type.replace(FIRST_WORD_CHAR_REGEX, (match) => match.toUpperCase())} leaderboard`} max={256} onSave={(v) => { setEmbed({ @@ -248,7 +250,7 @@ export default function UpdatingLeaderboardCard({ label="Show nickname" items={ ["mention", "username", "nickname", "id"].map((key) => ({ - name: key.replace(/^\w/, (char) => char.toUpperCase()), + name: key.replace(FIRST_WORD_CHAR_REGEX, (char) => char.toUpperCase()), value: key })) } @@ -286,7 +288,7 @@ export default function UpdatingLeaderboardCard({ className="flex flex-col gap-3" buttonName="Delete" variant="destructive" - title={`Delete ${type.replace(/^\w/, (match) => match.toUpperCase())} leaderboard`} + title={`Delete ${type.replace(FIRST_WORD_CHAR_REGEX, (match) => match.toUpperCase())} leaderboard`} isOpen={modal === ModalType.Delete && Boolean(guild)} onClose={() => { setModal(undefined); diff --git a/app/dashboard/[guildId]/moderation/page.tsx b/app/dashboard/[guildId]/moderation/page.tsx index 64ec011..bfb22e3 100644 --- a/app/dashboard/[guildId]/moderation/page.tsx +++ b/app/dashboard/[guildId]/moderation/page.tsx @@ -11,6 +11,11 @@ import { createSelectableItems } from "@/utils/create-selectable-items"; import { ChannelType } from "discord-api-types/v10"; import { useParams } from "next/navigation"; +const UNDERSCORE_GLOBAL_REGEX = /_/g; +const WORD_START_REGEX = /(^| +)\w/g; +const UNDERSCORE_REGEX = /_/; +const COMMA_NEWLINE_REGEX = /,|\n/; + const AUTOMOD_TYPES = Object .values(AutomodType) .filter((type) => typeof type === "string"); @@ -36,8 +41,8 @@ export default function Home() { {AUTOMOD_TYPES.map((type) => ( c.toUpperCase())}`} - description={`Prevent ${type.replace(/_/, " ")} links from being sent.`} + label={`Block ${type.replace(UNDERSCORE_GLOBAL_REGEX, " ").replace(WORD_START_REGEX, (c) => c.toUpperCase())}`} + description={`Prevent ${type.replace(UNDERSCORE_REGEX, " ")} links from being sent.`} endpoint={`${url}/${type}`} k="enabled" defaultState={data.status[type] || false} @@ -89,7 +94,7 @@ export default function Home() { defaultState={data.keywordFilter.join(", ")} max={Infinity} onSave={(value) => { - edit("keywordFilter", (value)?.split(/,|\n/).map((word) => word.trim()) || []); + edit("keywordFilter", (value)?.split(COMMA_NEWLINE_REGEX).map((word) => word.trim()) || []); }} /> ); diff --git a/app/dashboard/[guildId]/notifications/create-reddit.component.tsx b/app/dashboard/[guildId]/notifications/create-reddit.component.tsx index d5d0d2c..0f4ab81 100644 --- a/app/dashboard/[guildId]/notifications/create-reddit.component.tsx +++ b/app/dashboard/[guildId]/notifications/create-reddit.component.tsx @@ -11,10 +11,12 @@ import { useState } from "react"; const URL_CHANNEL_REGEX = /^https?:\/\/((www|m|old|oauth)\.)?reddit\.com\/r\/(?=.{3,21}$)[A-Za-z]\w*\/?$/; const CHANNE_HANDLE = /^((\/)?r\/)?(?=.{3,21}$)[A-Za-z]\w*$/; +const TRAILING_SLASH_REGEX = /\/$/; +const SUBREDDIT_PREFIX_REGEX = /^(\/)?r\//; function validateAccount(input: string) { - if (URL_CHANNEL_REGEX.test(input)) return input.split("/r/")[1].replace(/\/$/, ""); - if (CHANNE_HANDLE.test(input)) return input.replace(/^(\/)?r\//, ""); + if (URL_CHANNEL_REGEX.test(input)) return input.split("/r/")[1].replace(TRAILING_SLASH_REGEX, ""); + if (CHANNE_HANDLE.test(input)) return input.replace(SUBREDDIT_PREFIX_REGEX, ""); return null; } diff --git a/app/dashboard/[guildId]/notifications/style.component.tsx b/app/dashboard/[guildId]/notifications/style.component.tsx index 9232951..215a11b 100644 --- a/app/dashboard/[guildId]/notifications/style.component.tsx +++ b/app/dashboard/[guildId]/notifications/style.component.tsx @@ -14,6 +14,8 @@ import Link from "next/link"; import { useCallback, useMemo, useRef, useState } from "react"; import { HiOutlineUpload, HiPencil, HiSparkles, HiX } from "react-icons/hi"; +const FILTERED_CHARS_REGEX = /@|#|:|```|discord|clyde/i; + const ALLOWED_FILE_TYPES = ["image/png", "image/jpeg", "image/webp"]; const MAX_FILE_SIZE = 8 * 1_024 * 1_024; @@ -124,7 +126,7 @@ function isValidUsername(value: string | null): boolean { if (!value) return false; if (value.length < 2 || value.length > 32) return false; if (["everyone", "here"].includes(value.toLowerCase())) return false; - if (/@|#|:|```|discord|clyde/i.test(value)) return false; + if (FILTERED_CHARS_REGEX.test(value)) return false; return true; } @@ -228,9 +230,9 @@ export function ChangeStyleModal({ return; } - file.arrayBuffer().then((buffer) => { - setAvatar(buffer); - }); + void file + .arrayBuffer() + .then((buffer) => setAvatar(buffer)); }} ref={avatarRef} type="file" diff --git a/app/dashboard/[guildId]/starboard/page.tsx b/app/dashboard/[guildId]/starboard/page.tsx index fe4e402..32297b4 100644 --- a/app/dashboard/[guildId]/starboard/page.tsx +++ b/app/dashboard/[guildId]/starboard/page.tsx @@ -22,6 +22,9 @@ import { HiExternalLink } from "react-icons/hi"; import { useExample } from "./hooks"; +const SEPARATOR_REGEX = /-|_/g; +const SNOWFLAKE_REGEX = /\d{15,20}/; + export default function Home() { const guild = guildStore((g) => g); const params = useParams(); @@ -168,7 +171,7 @@ export default function Home() { width={64} /> ), - name: c.name.replace(/-|_/g, " "), + name: c.name.replace(SEPARATOR_REGEX, " "), value: c.id })) || [] ]} @@ -305,7 +308,7 @@ export default function Home() { } function Emoji({ emoji }: { emoji: string; }) { - if (!/\d{15,20}/.test(emoji)) { + if (!SNOWFLAKE_REGEX.test(emoji)) { return emoji; } diff --git a/app/dashboard/[guildId]/style.component.tsx b/app/dashboard/[guildId]/style.component.tsx index 61f26a2..b29dac1 100644 --- a/app/dashboard/[guildId]/style.component.tsx +++ b/app/dashboard/[guildId]/style.component.tsx @@ -13,6 +13,8 @@ import Image from "next/image"; import { useCallback, useMemo, useRef, useState } from "react"; import { HiOutlineUpload, HiPencil, HiX } from "react-icons/hi"; +const FILTERED_CHARS_REGEX = /@|#|:|```|discord|clyde/i; + const ALLOWED_FILE_TYPES = ["image/jpg", "image/jpeg", "image/png", "image/webp", "image/gif", "image/apng"]; const MAX_FILE_SIZE = 8 * 1_024 * 1_024; @@ -120,7 +122,7 @@ function isValidUsername(value: string | null): boolean { if (!value) return false; if (value.length < 2 || value.length > 32) return false; if (["everyone", "here"].includes(value.toLowerCase())) return false; - if (/@|#|:|```|discord|clyde/i.test(value)) return false; + if (FILTERED_CHARS_REGEX.test(value)) return false; return true; } @@ -298,10 +300,12 @@ function FileUpload({ return; } - file.arrayBuffer().then((buffer) => { - setBuf(buffer); - onUpload(buffer); - }); + void file + .arrayBuffer() + .then((buffer) => { + setBuf(buffer); + onUpload(buffer); + }); }} ref={ref} type="file" diff --git a/app/docs/[...pathname]/layout.tsx b/app/docs/[...pathname]/layout.tsx index 749e72d..151a707 100644 --- a/app/docs/[...pathname]/layout.tsx +++ b/app/docs/[...pathname]/layout.tsx @@ -9,6 +9,8 @@ import Link from "next/link"; import { BsDiscord, BsGithub } from "react-icons/bs"; import { HiUserAdd, HiViewGridAdd } from "react-icons/hi"; +const MARKDOWN_EXT_REGEX = /\.md$/; + interface Props { params: Promise<{ pathname: string[]; }>; children: React.ReactNode; @@ -141,7 +143,7 @@ function NavButton({ page: typeof metadata.pages[0]; active?: boolean; }) { - const file = page.file.replace(/\.md$/, ""); + const file = page.file.replace(MARKDOWN_EXT_REGEX, ""); const icon = page.name.split(" ").shift() || ""; const name = page.name.replace(icon, ""); diff --git a/app/profile/billing/page.tsx b/app/profile/billing/page.tsx index 18e34ec..d7669e9 100644 --- a/app/profile/billing/page.tsx +++ b/app/profile/billing/page.tsx @@ -24,6 +24,8 @@ import { HiCreditCard, HiLightningBolt } from "react-icons/hi"; import { SiDinersclub, SiDiscover, SiJcb, SiMastercard, SiPaypal, SiStripe, SiVisa } from "react-icons/si"; import { useQueryClient } from "react-query"; +const FIRST_WORD_CHAR_REGEX = /^\w/; + export default function Home() { const user = userStore((u) => u); const [changeDonationModalOpen, setChangeDonationModalOpen] = useState( @@ -79,7 +81,7 @@ export default function Home() { )}

- You have all premium features for EUR {totalAmount} / {period.replace(/^\w/, (c) => c.toUpperCase())}! + You have all premium features for EUR {totalAmount} / {period.replace(FIRST_WORD_CHAR_REGEX, (c) => c.toUpperCase())}!

@@ -94,28 +96,13 @@ export default function Home() {

Billing Cycle

- {isLoading || !data ? ( - - ) : data.cancelAtPeriodEnd ? ( -

- The subscription will expire on {formatDate(data.currentPeriodEnd)} and you will not be charged again. -

- ) : ( -

- The subscription will renew on {formatDate(data.currentPeriodEnd)}, for a total of EUR {totalAmount}. -
- You{"'"}re paying EUR {basePrice} Premium and EUR {(data.donationQuantity || 0).toFixed(2)} Donation{(data.donationQuantity || 0) === 1 ? "" : "s"} - {" "} - (). -

- )} +

Payment Method

@@ -159,6 +146,49 @@ export default function Home() { ); } +function BillingCycleContent({ + isLoading, + data, + totalAmount, + basePrice, + setChangeDonationModalOpen +}: { + isLoading: boolean; + data?: ApiV1UsersMeBillingGetResponse; + totalAmount: string; + basePrice: number; + setChangeDonationModalOpen: (open: boolean) => void; +}) { + if (isLoading || !data) { + return ; + } + + if (data.cancelAtPeriodEnd) { + return ( +

+ The subscription will expire on {formatDate(data.currentPeriodEnd)} and you will not be charged again. +

+ ); + } + + return ( +

+ The subscription will renew on {formatDate(data.currentPeriodEnd)}, for a total of EUR {totalAmount}. +
+ You{"'"}re paying EUR {basePrice} Premium and EUR {(data.donationQuantity || 0).toFixed(2)} Donation{(data.donationQuantity || 0) === 1 ? "" : "s"} + {" "} + (). +

+ ); +} + function formatDate(seconds: number) { return new Date(seconds * 1_000).toLocaleDateString(); } @@ -172,7 +202,7 @@ function getPeriodEndsIn(endsAt: number, nowInSeconds: number) { function PortalButton({ data }: { data: ApiV1UsersMeBillingGetResponse; }) { const path = getPortalPath(data); - const label = path?.split("/").pop()?.replace(/^\w/, (c) => c.toUpperCase()) || "Manage"; + const label = path?.split("/").pop()?.replace(FIRST_WORD_CHAR_REGEX, (c) => c.toUpperCase()) || "Manage"; return (