207 lines
5.4 KiB
TypeScript
207 lines
5.4 KiB
TypeScript
|
|
import type { User, Team } from "@prisma/client";
|
||
|
|
import type { LinksFunction, LoaderFunction, ActionFunction } from "remix";
|
||
|
|
import {
|
||
|
|
Link,
|
||
|
|
useLoaderData,
|
||
|
|
useActionData,
|
||
|
|
Form,
|
||
|
|
redirect,
|
||
|
|
useCatch,
|
||
|
|
json,
|
||
|
|
} from "remix";
|
||
|
|
import { getUser, requireUserId } from "~/utils/session.server";
|
||
|
|
import { db } from "~/utils/db.server";
|
||
|
|
import Check from "~/icons/Check";
|
||
|
|
|
||
|
|
export const links: LinksFunction = () => {
|
||
|
|
return [];
|
||
|
|
};
|
||
|
|
|
||
|
|
type LoaderData = {
|
||
|
|
user: (User & { team: Team & { members: User[] } }) | null;
|
||
|
|
};
|
||
|
|
|
||
|
|
type ActionData = {
|
||
|
|
formError?: string;
|
||
|
|
formSuccess?: string;
|
||
|
|
fieldErrors?: {
|
||
|
|
theme: string | undefined;
|
||
|
|
};
|
||
|
|
fields?: {
|
||
|
|
theme: string;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
export const loader: LoaderFunction = async ({ request }) => {
|
||
|
|
const user = await getUser(request);
|
||
|
|
if (!user?.id) {
|
||
|
|
return redirect("/login");
|
||
|
|
}
|
||
|
|
|
||
|
|
const data: LoaderData = {
|
||
|
|
user,
|
||
|
|
};
|
||
|
|
return data;
|
||
|
|
};
|
||
|
|
|
||
|
|
const themes = [
|
||
|
|
"light",
|
||
|
|
"dark",
|
||
|
|
"cupcake",
|
||
|
|
"bumblebee",
|
||
|
|
"emerald",
|
||
|
|
"corporate",
|
||
|
|
"synthwave",
|
||
|
|
"retro",
|
||
|
|
"cyberpunk",
|
||
|
|
"valentine",
|
||
|
|
];
|
||
|
|
|
||
|
|
const badRequest = (data: ActionData) => json(data, { status: 400 });
|
||
|
|
const success = (data: ActionData) => json(data, { status: 200 });
|
||
|
|
|
||
|
|
const validateTheme = (theme: unknown) => {
|
||
|
|
if (typeof theme !== "string" || !themes.includes(theme)) {
|
||
|
|
return `That theme is not valid`;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
export const action: ActionFunction = async ({ request }) => {
|
||
|
|
const userId = await requireUserId(request);
|
||
|
|
const user = await getUser(request);
|
||
|
|
const form = await request.formData();
|
||
|
|
const theme = form.get("theme");
|
||
|
|
if (typeof theme !== "string" || user === null) {
|
||
|
|
return badRequest({
|
||
|
|
formError: `Form not submitted correctly.`,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const fieldErrors = {
|
||
|
|
theme: validateTheme(theme),
|
||
|
|
};
|
||
|
|
const fields = { theme };
|
||
|
|
if (Object.values(fieldErrors).some(Boolean)) {
|
||
|
|
return badRequest({ fieldErrors, fields });
|
||
|
|
}
|
||
|
|
|
||
|
|
const updatedUser = await db.user.update({
|
||
|
|
where: {
|
||
|
|
id: userId,
|
||
|
|
},
|
||
|
|
data: {
|
||
|
|
theme,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
if (!updatedUser) {
|
||
|
|
return badRequest({
|
||
|
|
formError: `Something went wrong trying to update user.`,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return success({ formSuccess: "Preferences updated successfully." });
|
||
|
|
};
|
||
|
|
|
||
|
|
export default function AccountPreferencesRoute() {
|
||
|
|
const data = useLoaderData<LoaderData>();
|
||
|
|
const actionData = useActionData<ActionData>();
|
||
|
|
const activeTheme = data.user?.theme || "dark";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div className="tabs tabs-boxed my-6 mr-auto">
|
||
|
|
<Link to="/account/preferences" className="tab lg:tab-lg tab-active">
|
||
|
|
Preferences
|
||
|
|
</Link>
|
||
|
|
<Link to="/account/manage" className="tab lg:tab-lg">
|
||
|
|
Manage
|
||
|
|
</Link>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Form autoComplete="off" method="post">
|
||
|
|
<div className="p-6 card bordered">
|
||
|
|
<h3 id="theme" className="mb-4">
|
||
|
|
Theme
|
||
|
|
</h3>
|
||
|
|
{themes.map((theme) => (
|
||
|
|
<div className="form-control" key={theme}>
|
||
|
|
<label className="cursor-pointer label">
|
||
|
|
<span className="label-text">{theme}</span>
|
||
|
|
<input
|
||
|
|
type="radio"
|
||
|
|
name="theme"
|
||
|
|
defaultChecked={activeTheme === theme}
|
||
|
|
onChange={(e) => {
|
||
|
|
if (e.target.checked)
|
||
|
|
document
|
||
|
|
?.querySelector("html")
|
||
|
|
?.setAttribute("data-theme", theme);
|
||
|
|
}}
|
||
|
|
className="radio"
|
||
|
|
aria-labelledby="#theme"
|
||
|
|
value={theme}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{actionData?.fieldErrors?.theme && (
|
||
|
|
<div className="alert alert-error mt-2" role="alert">
|
||
|
|
<div className="flex-1">
|
||
|
|
<svg
|
||
|
|
xmlns="http://www.w3.org/2000/svg"
|
||
|
|
fill="none"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
className="w-6 h-6 mx-2 stroke-current"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2"
|
||
|
|
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||
|
|
></path>
|
||
|
|
</svg>
|
||
|
|
<label id="password-error">
|
||
|
|
{actionData?.fieldErrors.theme}
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{actionData?.formSuccess && (
|
||
|
|
<div className="alert alert-success mt-2" role="alert">
|
||
|
|
<div className="flex-1">
|
||
|
|
<Check className="w-6 h-6 mx-2 stroke-current" />
|
||
|
|
<label id="teamid-error">{actionData?.formSuccess}</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-center align-center mt-6">
|
||
|
|
<button type="submit" className="btn btn-primary">
|
||
|
|
Save
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</Form>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function CatchBoundary() {
|
||
|
|
const caught = useCatch();
|
||
|
|
|
||
|
|
if (caught.status === 401) {
|
||
|
|
return (
|
||
|
|
<div className="error-container">
|
||
|
|
<p>You must be logged in to set your preferences.</p>
|
||
|
|
<Link to="/login">Login</Link>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ErrorBoundary() {
|
||
|
|
return (
|
||
|
|
<div className="error-container">
|
||
|
|
Something unexpected went wrong. Sorry about that.
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|