mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-27 14:04:01 -05:00
Add custom theme to teams
This commit is contained in:
parent
2b4c6a65ab
commit
ae3349ec2d
|
|
@ -331,6 +331,22 @@ export async function update({
|
|||
return team;
|
||||
}
|
||||
|
||||
export async function updateCustomTheme({
|
||||
id,
|
||||
customTheme,
|
||||
}: {
|
||||
id: number;
|
||||
customTheme: CustomTheme | null;
|
||||
}) {
|
||||
await db
|
||||
.updateTable("AllTeam")
|
||||
.set({
|
||||
customTheme: customTheme ? JSON.stringify(customTheme) : null,
|
||||
})
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function switchMainTeam({
|
||||
userId,
|
||||
teamId,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,23 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
}
|
||||
|
||||
switch (data._action) {
|
||||
case "UPDATE_CUSTOM_THEME": {
|
||||
errorToastIfFalsy(
|
||||
canAddCustomizedColors(team),
|
||||
"Team does not have custom theme access",
|
||||
);
|
||||
|
||||
const customTheme = data.newValue
|
||||
? clampThemeToGamut(data.newValue)
|
||||
: null;
|
||||
|
||||
await TeamRepository.updateCustomTheme({
|
||||
id: team.id,
|
||||
customTheme,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
case "DELETE_TEAM": {
|
||||
await TeamRepository.del(team.id);
|
||||
throw redirect(TEAM_SEARCH_PAGE);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { notFoundIfFalsy } from "~/utils/remix.server";
|
|||
import { teamPage } from "~/utils/urls";
|
||||
import * as TeamRepository from "../TeamRepository.server";
|
||||
import { teamParamsSchema } from "../team-schemas.server";
|
||||
import { isTeamManager } from "../team-utils";
|
||||
import { canAddCustomizedColors, isTeamManager } from "../team-utils";
|
||||
|
||||
export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const user = requireUser();
|
||||
|
|
@ -17,5 +17,9 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
|||
throw redirect(teamPage(customUrl));
|
||||
}
|
||||
|
||||
return { team, customTheme: team.customTheme };
|
||||
return {
|
||||
team,
|
||||
customTheme: canAddCustomizedColors(team) ? team.customTheme : null,
|
||||
canAddCustomizedColors: canAddCustomizedColors(team),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MetaFunction } from "react-router";
|
||||
import { Form, Link, useLoaderData } from "react-router";
|
||||
import { Form, Link, useFetcher, useLoaderData } from "react-router";
|
||||
import { CustomThemeSelector } from "~/components/CustomThemeSelector";
|
||||
import { Divider } from "~/components/Divider";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { FormErrors } from "~/components/FormErrors";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
|
|
@ -12,6 +14,7 @@ import { Main, mainStyles } from "~/components/Main";
|
|||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { TeamGoBackButton } from "~/features/team/components/TeamGoBackButton";
|
||||
import type { ThemeInput } from "~/utils/oklch-gamut";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { uploadImagePage } from "~/utils/urls";
|
||||
import { action } from "../actions/t.$customUrl.edit.server";
|
||||
|
|
@ -31,7 +34,7 @@ export const meta: MetaFunction = (args) => {
|
|||
export default function EditTeamPage() {
|
||||
const { t } = useTranslation(["common", "team"]);
|
||||
const user = useUser();
|
||||
const { team } = useLoaderData<typeof loader>();
|
||||
const { team, canAddCustomizedColors } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
|
|
@ -67,6 +70,14 @@ export default function EditTeamPage() {
|
|||
</SubmitButton>
|
||||
<FormErrors namespace="team" />
|
||||
</Form>
|
||||
{canAddCustomizedColors ? (
|
||||
<>
|
||||
<Divider className={styles.formDivider} smallText>
|
||||
{t("team:forms.customTheme.header")}
|
||||
</Divider>
|
||||
<TeamCustomThemeSelector />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Main>
|
||||
);
|
||||
|
|
@ -235,3 +246,36 @@ function BioTextarea() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamCustomThemeSelector() {
|
||||
const { customTheme, canAddCustomizedColors } =
|
||||
useLoaderData<typeof loader>();
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const handleSave = (themeInput: ThemeInput) => {
|
||||
fetcher.submit(
|
||||
{
|
||||
_action: "UPDATE_CUSTOM_THEME",
|
||||
newValue: themeInput,
|
||||
} as unknown as Parameters<typeof fetcher.submit>[0],
|
||||
{ method: "post", encType: "application/json" },
|
||||
);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
fetcher.submit(
|
||||
{ _action: "UPDATE_CUSTOM_THEME", newValue: null },
|
||||
{ method: "post", encType: "application/json" },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomThemeSelector
|
||||
initialTheme={customTheme}
|
||||
isSupporter={canAddCustomizedColors}
|
||||
isPersonalTheme={false}
|
||||
onSave={handleSave}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ const deleteActionsSchema = z.object({
|
|||
|
||||
export const editTeamSchema = z.union([
|
||||
deleteActionsSchema,
|
||||
z.object({
|
||||
_action: _action("UPDATE_CUSTOM_THEME"),
|
||||
newValue: z.preprocess(
|
||||
(val) => (!val || val === "null" ? null : val),
|
||||
themeInputSchema.nullable(),
|
||||
),
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("EDIT"),
|
||||
name: z.string().min(TEAM.NAME_MIN_LENGTH).max(TEAM.NAME_MAX_LENGTH),
|
||||
|
|
|
|||
|
|
@ -307,6 +307,10 @@
|
|||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.formDivider {
|
||||
margin-block: var(--s-6);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.bannerFlags {
|
||||
display: flex;
|
||||
|
|
|
|||
22
app/root.tsx
22
app/root.tsx
|
|
@ -250,7 +250,7 @@ function Document({
|
|||
lang={locale}
|
||||
dir={i18n.dir()}
|
||||
className={clsx(htmlThemeClass, "scrollbar")}
|
||||
style={customThemeStyle}
|
||||
style={Object.fromEntries(customThemeStyle)}
|
||||
>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
|
|
@ -353,23 +353,25 @@ declare module "react-aria-components" {
|
|||
}
|
||||
}
|
||||
|
||||
function useCustomThemeVars(): React.CSSProperties | undefined {
|
||||
function useCustomThemeVars() {
|
||||
const matches = useMatches();
|
||||
let styles: React.CSSProperties | undefined;
|
||||
const styles: Map<string, number> = new Map();
|
||||
|
||||
for (const match of matches) {
|
||||
const data = match.data as { customTheme?: CustomTheme } | undefined;
|
||||
|
||||
if (data?.customTheme) {
|
||||
const styleObj: Record<string, number> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(data.customTheme)) {
|
||||
// Skips size variables for themes that arent the user's own
|
||||
if (match.id !== "root" && key.includes("--_size")) continue;
|
||||
styleObj[key] = value;
|
||||
}
|
||||
// Skips size and border variables for themes that arent the user's own
|
||||
if (
|
||||
match.id !== "root" &&
|
||||
(key.includes("--_size") || key.includes("--_border"))
|
||||
)
|
||||
continue;
|
||||
if (value === null) continue;
|
||||
|
||||
styles = styleObj as React.CSSProperties;
|
||||
styles.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"forms.info.tag": "Typically used before in-game name to indicate membership of a team (e.g. [TAG] PlayerName)",
|
||||
"forms.errors.duplicateName": "There is already a team with this name",
|
||||
"forms.errors.noOnlySpecialCharacters": "Team name can't be only special characters",
|
||||
"forms.customTheme.header": "Custom Theme",
|
||||
"roster.teamFull": "Team is full",
|
||||
"roster.inviteLink.header": "Share invite link to add members",
|
||||
"roster.members.header": "Members",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user