First Commit. Duplicate bunext-sqlite preset

This commit is contained in:
2026-10-18 16:47:11 +01:00
commit 81e4a8aecc
718 changed files with 41695 additions and 0 deletions
@@ -0,0 +1,44 @@
import userAuth from "@/src/functions/backend/auth/user-auth";
import grabUsers from "@/src/functions/backend/db/users/grab-users";
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
import grabUserPasswordHash from "@/src/functions/backend/auth/grab-user-password-hash";
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
type Params = Parameters<BunextPageServerFn<PagePropsType>>[0];
/**
* Shared server props for the `/admin/settings` sub-pages.
*/
export default async function grabSettingsServerProps({ req }: Params) {
const { user } = await userAuth({ req });
if (!user?.id) {
return {
redirect: {
destination: "/auth/login",
},
};
}
const user_res = await grabUsers({ user_id: user.id });
if (!user_res.singleRes) {
return {
redirect: {
destination: "/admin",
},
};
}
const has_password = Boolean(
await grabUserPasswordHash({ user_id: user.id }),
);
return {
props: {
person: sanitizeUserBio({ user: user_res.singleRes }),
has_password,
},
};
}
@@ -0,0 +1,25 @@
import type { PagePropsType } from "@/src/types";
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
import { SiteData } from "@/src/data/site-data";
type Params = {
label: string;
};
export default function settingsPageMeta({
label,
}: Params): BunextPageModuleMetaFn {
return async (params) => {
const person = (params.serverRes?.props as PagePropsType | undefined)
?.person;
const person_name = person
? `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`
: "";
return {
title: `${label} Settings${person_name ? ` - ${person_name}` : ""} | ${SiteData["SiteName"]}`,
description: `Manage your ${label.toLowerCase()} settings on ${SiteData["SiteName"]}.`,
};
};
}
@@ -0,0 +1,85 @@
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import clientDeleteMediaHandler from "@/src/functions/frontend/client-delete-media-handler";
import clientUploadMediaHandler from "@/src/functions/frontend/client-upload-media-handler";
import type useFormInit from "@/src/hooks/use-form-init";
import type { UserFormObject } from "@/src/types";
import _ from "lodash";
export default async function submitAccountForm({
setLoading,
setStatus,
form,
existing_full,
image,
}: ReturnType<typeof useFormInit<UserFormObject>>) {
try {
const user_id = existing_full?.id;
if (!user_id) {
return;
}
if (!window.confirm("Update your account details?")) {
return;
}
const new_user_data: BUN_SQLITE_BSQLITE_USERS = _.pick<
UserFormObject,
keyof UserFormObject
>(form, ["first_name", "last_name", "email", "username", "bio"]);
setLoading(true);
const update_user_res = await adminCrudHandler({
action: "update",
table: "users",
update_data: new_user_data,
id: user_id,
});
if (!update_user_res.success) {
throw new Error(update_user_res.msg || "Account Update Failed");
}
if (image?.imageBase64Full) {
const delete_media_res = await clientDeleteMediaHandler({
user_id: user_id,
media_paradigm: "user-profile-image",
});
if (!delete_media_res.success) {
throw new Error(
delete_media_res.msg ||
delete_media_res.error ||
`Couldn't delete old media`,
);
}
const upload_image_res = await clientUploadMediaHandler({
user_id: user_id,
media_base_64_data_url: image.imageBase64Full,
media_paradigms: ["user-profile-image"],
media_type: "image",
is_media_primary: true,
update_on_duplicate: true,
reauth: true,
});
if (!upload_image_res.success) {
throw new Error(
upload_image_res.msg || "Couldn't upload Image",
);
}
}
window.location.reload();
} catch (error: any) {
setStatus({
error: true,
msg: error.message || "Account Form Error",
});
setLoading(false);
}
}
@@ -0,0 +1,13 @@
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
import Button from "@moduletrace/twui/layout/Button";
import useFormInit from "@/src/hooks/use-form-init";
export default function AccountFormAction(
_params: ReturnType<typeof useFormInit<BUN_SQLITE_BSQLITE_USERS>>,
) {
return (
<>
<Button title="Save Changes">Save Changes</Button>
</>
);
}
@@ -0,0 +1,53 @@
import Form from "@moduletrace/twui/form/Form";
import LoadingOverlay from "@moduletrace/twui/elements/LoadingOverlay";
import useFormInit from "@/src/hooks/use-form-init";
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
import submitAccountForm from "../(functions)/submit-account-form";
import AccountFormAction from "./account-form-action";
import Tag from "@moduletrace/twui/elements/Tag";
import Row from "@moduletrace/twui/layout/Row";
import UserFormName from "@/src/pages/admin/people/create/(partials)/user-form/user-form-name";
import UserFormEmail from "@/src/pages/admin/people/create/(partials)/user-form/user-form-email";
import UserFormUsername from "@/src/pages/admin/people/create/(partials)/user-form/user-form-username";
import UserFormBio from "@/src/pages/admin/people/create/(partials)/user-form/user-form-bio";
import UserFormImage from "@/src/pages/admin/people/create/(partials)/user-form/user-form-image";
import type { UserFormObject } from "@/src/types";
type Props = {
existing_user?: UserFormObject;
existing_user_full?: BUN_SQLITE_BSQLITE_USERS;
};
export default function AccountForm({ existing_user, existing_user_full }: Props) {
const init = useFormInit<UserFormObject>({
existing: existing_user,
existing_full: existing_user_full,
});
const { loading, status } = init;
return (
<>
<Form
className="w-full gap-6"
onSubmit={() => {
submitAccountForm(init);
}}
>
{status?.error && <Tag color="error">{status.msg}</Tag>}
{loading && <LoadingOverlay />}
<UserFormName {...init} />
<Row className="w-full grid xl:grid-cols-2">
<UserFormEmail {...init} />
<UserFormUsername {...init} />
</Row>
<Row className="w-full grid xl:grid-cols-2 items-stretch">
<UserFormBio {...init} />
<UserFormImage {...init} />
</Row>
<AccountFormAction {...init} />
</Form>
</>
);
}
@@ -0,0 +1,35 @@
import Section from "@moduletrace/twui/layout/Section";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import AccountForm from "../(partials)/account-form";
import _ from "lodash";
import type { UserFormObject } from "@/src/types";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
export default function AccountFormSection() {
const { pageProps } = useContext(AppContext);
const person = pageProps?.person;
if (!person) return null;
return (
<Section>
<AccountForm
existing_user={
_.pick<
BUN_SQLITE_BSQLITE_USERS_JOIN,
keyof BUN_SQLITE_BSQLITE_USERS_JOIN
>(person, [
"first_name",
"last_name",
"email",
"username",
"bio",
]) as UserFormObject
}
existing_user_full={person}
/>
</Section>
);
}
@@ -0,0 +1,43 @@
import AdminHero from "@/src/components/general/admin-hero";
import Tag from "@moduletrace/twui/elements/Tag";
import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
type Props = {
section_title: string;
};
export default function Hero({ section_title }: Props) {
const { pageProps } = useContext(AppContext);
const person = pageProps?.person;
if (!person) return null;
return (
<AdminHero
title={section_title}
description={
<Stack className="gap-4">
{person.user_types?.[0] ? (
<Row className="flex-nowrap">
{person.user_types?.split(",").map((ut, i) => (
<Tag key={i} color="gray" variant="outlined">
{ut}
</Tag>
))}
</Row>
) : null}
{person.bio ? (
<div
dangerouslySetInnerHTML={{ __html: person.bio }}
className="bio-subtitle"
/>
) : null}
</Stack>
}
/>
);
}
+7
View File
@@ -0,0 +1,7 @@
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
import grabSettingsServerProps from "./(functions)/grab-settings-server-props";
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
return await grabSettingsServerProps(params);
};
+14
View File
@@ -0,0 +1,14 @@
import Hero from "./(sections)/hero";
import AccountFormSection from "./(sections)/account-form-section";
import settingsPageMeta from "./(functions)/settings-page-meta";
export default function AdminSettingsPage() {
return (
<>
<Hero section_title="Profile" />
<AccountFormSection />
</>
);
}
export const meta = settingsPageMeta({ label: "Profile" });
@@ -0,0 +1,63 @@
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import type useFormInit from "@/src/hooks/use-form-init";
import type { ApiReqParams, PasswordFormObject } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import validatePassword from "@/src/utils/validate-password";
type Params = ReturnType<typeof useFormInit<PasswordFormObject>> & {
has_password: boolean;
onDone: (params: { has_password: boolean }) => void;
};
export default async function submitPasswordForm({
form,
setLoading,
setStatus,
has_password,
onDone,
}: Params) {
if (has_password && !form.current_password) {
setStatus({ error: true, msg: "Enter your current password." });
return;
}
const password_error = validatePassword({ password: form.new_password });
if (password_error) {
setStatus({ error: true, msg: password_error });
return;
}
if (form.new_password != form.confirm_password) {
setStatus({ error: true, msg: "The passwords don't match." });
return;
}
setLoading(true);
const res = await fetchApi<ApiReqParams, APIResponseObject>(
`/api/admin/settings/update-password`,
{
method: "POST",
body: {
password_update: {
current_password: form.current_password,
new_password: form.new_password,
},
},
},
);
setLoading(false);
if (!res.success) {
setStatus({
error: true,
msg: res.msg || "Couldn't update your password.",
});
return;
}
setStatus({ success: true, msg: res.msg || "Password saved." });
onDone({ has_password: true });
}
@@ -0,0 +1,66 @@
import { useState } from "react";
import Form from "@moduletrace/twui/form/Form";
import LoadingOverlay from "@moduletrace/twui/elements/LoadingOverlay";
import Tag from "@moduletrace/twui/elements/Tag";
import Row from "@moduletrace/twui/layout/Row";
import useFormInit from "@/src/hooks/use-form-init";
import type { PasswordFormObject } from "@/src/types";
import PasswordFormField from "./password-form-field";
import PasswordFormActions from "./password-form-actions";
import submitPasswordForm from "../../(functions)/submit-password-form";
type Props = {
has_password: boolean;
};
export default function PasswordForm({ has_password: initial }: Props) {
const init = useFormInit<PasswordFormObject>();
const { loading, status } = init;
const [has_password, setHasPassword] = useState(initial);
const [form_key, setFormKey] = useState(0);
const onDone = ({ has_password }: { has_password: boolean }) => {
setHasPassword(has_password);
init.setForm({});
setFormKey((prev) => prev + 1);
};
return (
<Form
key={form_key}
className="w-full gap-6"
onSubmit={() => {
submitPasswordForm({ ...init, has_password, onDone });
}}
>
{status?.error && <Tag color="error">{status.msg}</Tag>}
{status?.success && <Tag color="success">{status.msg}</Tag>}
{loading && <LoadingOverlay />}
{has_password ? (
<PasswordFormField
{...init}
field="current_password"
label="Current password"
autoComplete="current-password"
/>
) : null}
<Row className="w-full grid xl:grid-cols-2">
<PasswordFormField
{...init}
field="new_password"
label={has_password ? "New password" : "Password"}
autoComplete="new-password"
/>
<PasswordFormField
{...init}
field="confirm_password"
label="Confirm password"
autoComplete="new-password"
/>
</Row>
<PasswordFormActions has_password={has_password} />
</Form>
);
}
@@ -0,0 +1,15 @@
import Button from "@moduletrace/twui/layout/Button";
type Props = {
has_password: boolean;
};
export default function PasswordFormActions({ has_password }: Props) {
const label = has_password ? "Change password" : "Set password";
return (
<Button title={label} type="submit">
{label}
</Button>
);
}
@@ -0,0 +1,28 @@
import type { AutocompleteOptions } from "@moduletrace/twui/types";
import PasswordInput from "@/src/components/general/password-input";
import useFormInit from "@/src/hooks/use-form-init";
import type { PasswordFormObject } from "@/src/types";
type Props = ReturnType<typeof useFormInit<PasswordFormObject>> & {
field: keyof PasswordFormObject;
label: string;
autoComplete: (typeof AutocompleteOptions)[number];
};
export default function PasswordFormField({
setForm,
field,
label,
autoComplete,
}: Props) {
return (
<PasswordInput
id={field}
label={label}
autoComplete={autoComplete}
onValueChange={(value) => {
setForm((prev) => ({ ...prev, [field]: value }));
}}
/>
);
}
@@ -0,0 +1,29 @@
import Section from "@moduletrace/twui/layout/Section";
import Stack from "@moduletrace/twui/layout/Stack";
import H3 from "@moduletrace/twui/layout/H3";
import Span from "@moduletrace/twui/layout/Span";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import useLoginMethod from "@/src/hooks/use-login-method";
import PasswordForm from "../(partials)/password-form";
export default function PasswordFormSection() {
const { pageProps } = useContext(AppContext);
const method = useLoginMethod();
if (!pageProps?.person || method != "password") return null;
return (
<Section>
<Stack className="w-full gap-6">
<Stack className="gap-1">
<H3 className="mb-0!">Change Password</H3>
<Span variant="faded">
You log in with your password, then a code we email you.
</Span>
</Stack>
<PasswordForm has_password={Boolean(pageProps.has_password)} />
</Stack>
</Section>
);
}
@@ -0,0 +1,16 @@
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
import grabSettingsServerProps from "../(functions)/grab-settings-server-props";
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
if (grabLoginMethod() != "password") {
return {
redirect: {
destination: "/admin/settings",
},
};
}
return await grabSettingsServerProps(params);
};
@@ -0,0 +1,14 @@
import Hero from "../(sections)/hero";
import PasswordFormSection from "./(sections)/password-form-section";
import settingsPageMeta from "../(functions)/settings-page-meta";
export default function AdminSettingsPasswordPage() {
return (
<>
<Hero section_title="Password" />
<PasswordFormSection />
</>
);
}
export const meta = settingsPageMeta({ label: "Password" });