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,73 @@
import _ from "lodash";
import { AppData } from "@/src/data/app-data";
import { useContext, useRef } from "react";
import useStatus from "@moduletrace/twui/hooks/useStatus";
import type { ApiReqParams, UserType } from "@/src/types";
import { AppContext } from "@/src/pages/__root";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import EJSON from "@/src/utils/ejson";
import { useUsersGet } from "@/src/hooks/use-users-get";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
type Props = {
users_type?: UserType | null;
};
export default function useAllPeopleListInit({ users_type }: Props) {
const { pageProps, query } = useContext(AppContext);
const { user_types, url } = pageProps || {};
const limit = AppData["DefaultQueryLimit"];
const can_view_users = checkUserAccess({
user_types,
includes: ["admin"],
});
let location_query = EJSON.parse(url?.searchParams?.q || "") as
| ApiReqParams
| undefined;
if (query?.user_type) {
if (!location_query) {
location_query = {
target_user_type: query.user_type as any,
};
} else {
location_query.target_user_type = query.user_type as any;
}
}
const {
users: people,
count,
page,
setPage,
setUsersQuery,
setUsers: set_people,
ready,
} = useUsersGet({
query: location_query,
});
const selected = useRef(new Set<BUN_SQLITE_BSQLITE_USERS_JOIN>());
const { refresh, setRefresh } = useStatus();
return {
user_types,
limit,
can_view_users,
people,
count,
page,
setPage,
setUsersQuery,
set_people,
selected,
refresh,
setRefresh,
users_type,
location_query,
ready,
};
}
@@ -0,0 +1,114 @@
import Search from "@moduletrace/twui/elements/Search";
import Select from "@moduletrace/twui/form/Select";
import Divider from "@moduletrace/twui/layout/Divider";
import { useRef } from "react";
import _ from "lodash";
import twuiSlugToNormalText from "@moduletrace/twui/utils/slug-to-normal-text";
import type useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
import searchableFieldToQueryField from "@/src/utils/searchable-field-to-query-field";
import type { ApiReqParams } from "@/src/types";
export default function AllPeopleListSearch({
setUsersQuery,
location_query,
}: ReturnType<typeof useAllPeopleListInit>) {
const searchable_fields: (keyof BUN_SQLITE_BSQLITE_USERS_JOIN)[] = [
"first_name",
"last_name",
"email",
"user_types",
];
const initial_search_field = (Object.keys(location_query || {}).filter(
(q) => q.startsWith("search_term"),
)?.[0] ||
searchableFieldToQueryField({
field: searchable_fields[0]!,
})) as keyof ApiReqParams;
const initial_search = String(location_query?.[initial_search_field] || "");
const search_field_ref = useRef(initial_search_field);
const search_value_ref = useRef(initial_search);
const initialized_ref = useRef(false);
function appendQuery() {
setUsersQuery((prev) => {
let nq = _.cloneDeep(prev) || {};
for (let i = 0; i < searchable_fields.length; i++) {
const sf = searchableFieldToQueryField({
field: searchable_fields[i]!,
});
delete nq[sf];
}
if (search_value_ref.current.match(/./)) {
nq[search_field_ref.current] = search_value_ref.current;
}
nq.page = 1;
if (_.isEqual(nq, prev)) {
return prev;
}
return nq;
});
}
return (
<Search
placeholder="Find People"
changeHandler={(value) => {
search_value_ref.current = value || "";
if (!initialized_ref.current) {
initialized_ref.current = true;
if ((value || "") === initial_search) {
return;
}
}
appendQuery();
}}
defaultValue={initial_search}
no_search_button
inputProps={{
suffix: (
<>
<Divider vertical className="-my-4 -mr-2 h-9" />
<Select
options={searchable_fields.map((sf) => ({
value: sf,
default:
searchableFieldToQueryField({
field: sf,
}) == search_field_ref.current,
title: twuiSlugToNormalText(
sf.replace(/user_types/, "user_type"),
),
}))}
className="-my-4 min-w-[80px] xs:min-w-[150px] -mr-2 pl-4"
wrapperWrapperProps={{
className: "-mr-2",
}}
wrapperProps={{
className: "border-none!",
}}
changeHandler={(value) => {
search_field_ref.current =
searchableFieldToQueryField({
field: value,
});
appendQuery();
}}
/>
</>
),
autoFocus: true,
}}
/>
);
}
@@ -0,0 +1,29 @@
import Row from "@moduletrace/twui/layout/Row";
import _ from "lodash";
import Span from "@moduletrace/twui/layout/Span";
import Card from "@moduletrace/twui/elements/Card";
import type { RefObject } from "react";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
type Props = {
selected: RefObject<Set<BUN_SQLITE_BSQLITE_USERS_JOIN>>;
};
export default function AllPeopleListSelections({ selected }: Props) {
const total = Array.from(selected.current.entries()).length;
if (!total) {
return null;
}
return (
<Card className="py-2! px-3! w-full">
<Row>
<Span>
{Array.from(selected.current.entries()).length} items
selected
</Span>
</Row>
</Card>
);
}
@@ -0,0 +1,38 @@
import Row from "@moduletrace/twui/layout/Row";
import _ from "lodash";
import { AppData } from "@/src/data/app-data";
import type useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
import AllPeopleListSearch from "./all-people-list-search";
import AllPeopleListUsersTypeSelect from "./all-people-list-users-type-select";
export default function AllPeopleListTitleSection(
props: ReturnType<typeof useAllPeopleListInit>,
) {
const { users_type, people, page, count } = props;
const limit = AppData["DefaultQueryLimit"];
const range_start = people?.length ? (page - 1) * limit + 1 : 0;
const range_end = people?.length ? range_start + people.length - 1 : 0;
return (
<Row className="flex items-center justify-between gap-4 w-full">
<Row>
<AllPeopleListSearch {...props} />
{users_type ? null : (
<AllPeopleListUsersTypeSelect {...props} />
)}
</Row>
{typeof count === "number" && count > 0 ? (
<p className="text-sm text-muted-foreground">
Showing {range_start}-{range_end} of {count} users
</p>
) : (
<span className="text-sm text-muted-foreground">
No Results
</span>
)}
</Row>
);
}
@@ -0,0 +1,39 @@
import _ from "lodash";
import type useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
import { UserTypes } from "@/src/dict/user-types-dict";
import SearchSelect from "@moduletrace/twui/form/SearchSelect";
export default function AllPeopleListUsersTypeSelect({
setUsersQuery,
location_query,
}: ReturnType<typeof useAllPeopleListInit>) {
const initial_user_type = String(location_query?.target_user_type || "");
const options = [
{ value: "N/A", label: "All Users" },
...UserTypes.map((ut) => ({
value: ut.value,
label: ut.value,
})),
];
return (
<div className="flex flex-col gap-1">
<SearchSelect
options={options}
defaultValues={[initial_user_type]}
changeHandler={(value) => {
setUsersQuery((prev) => {
const new_query = _.cloneDeep(prev) || {};
if (!value || value === "N/A") {
delete new_query?.target_user_type;
} else {
new_query.target_user_type = value as any;
}
return new_query;
});
}}
/>
</div>
);
}
@@ -0,0 +1,105 @@
import Card from "@moduletrace/twui/elements/Card";
import LucideIcon from "@moduletrace/twui/elements/lucide-icon";
import Tag from "@moduletrace/twui/elements/Tag";
import Button from "@moduletrace/twui/layout/Button";
import H3 from "@moduletrace/twui/layout/H3";
import Img from "@moduletrace/twui/layout/Img";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import grabMediaURL from "@/src/functions/backend/media/grab-media-url";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
import { AppContext } from "@/src/pages/__root";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import { useContext } from "react";
import dayjs from "dayjs";
type Props = {
person: BUN_SQLITE_BSQLITE_USERS_JOIN;
};
export default function PeopleListCard({ person }: Props) {
const { pageProps } = useContext(AppContext);
const is_read_only = isReadOnlyUser({
user_types: pageProps?.user_types,
});
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
const person_profile_image = person.profile_media_id
? grabMediaURL({
media: { id: person.profile_media_id },
})
: undefined;
const person_url = `/admin/people/${person.id}`;
const edit_person_url = `/admin/people/${person.id}/edit`;
const date_created = dayjs(Number(person.created_at));
return (
<Card
className="cursor-pointer"
onClick={(e) => {
const target_el = e.target as HTMLElement;
if (target_el.closest(".cancel-link")) {
return;
}
if (e.ctrlKey) {
window.open(person_url, "__blank");
} else {
window.location.pathname = person_url;
}
}}
>
<Row className="w-full justify-between">
<Row>
<Img
alt={`${person_name}`}
src={
person_profile_image?.media_url || "/icons/man.png"
}
size={60}
circle
/>
<Stack className="gap-2">
<H3>{person_name}</H3>
<Row className="gap-2!">
{person.user_types?.split(",").map((ut, i) => (
<Tag key={i} color="gray">
{ut}
</Tag>
))}
</Row>
<Row className="gap-2!">
<LucideIcon
name="Clock"
size={14}
className="opacity-50"
/>
<Span
variant="faded"
className="text-xs"
>
{date_created.toString()}
</Span>
</Row>
</Stack>
</Row>
<Row className="cancel-link">
{is_read_only ? null : (
<Button
title="Edit Person"
href={edit_person_url}
variant="ghost"
color="gray"
>
<LucideIcon name="Edit2" size={18} />
</Button>
)}
</Row>
</Row>
</Card>
);
}
@@ -0,0 +1,40 @@
import Loading from "@moduletrace/twui/elements/Loading";
import Center from "@moduletrace/twui/layout/Center";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import PeopleListCard from "./people-list-card";
import Paper from "@moduletrace/twui/elements/Paper";
import AllPeopleListSelections from "./all-people-list-selections";
import useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
import AllPeopleListSearch from "./all-people-list-search";
export default function PeopleList() {
const init = useAllPeopleListInit({});
const { people } = init;
return (
<Stack className="w-full items-stretch">
<AllPeopleListSearch {...init} />
<AllPeopleListSelections {...init} />
{!people ? (
<Paper>
<Center className="p-20">
<Loading />
</Center>
</Paper>
) : !people[0] ? (
<Paper>
<Center className="p-20">
<Span>No People Found!</Span>
</Center>
</Paper>
) : (
people.map((person, index) => {
return <PeopleListCard person={person} key={index} />;
})
)}
</Stack>
);
}
@@ -0,0 +1,28 @@
import AdminHero from "@/src/components/general/admin-hero";
import Button from "@moduletrace/twui/layout/Button";
import { AppContext } from "@/src/pages/__root";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import { useContext } from "react";
export default function Hero() {
const { pageProps } = useContext(AppContext);
const is_read_only = isReadOnlyUser({ user_types: pageProps?.user_types });
return (
<AdminHero
title="People"
buttons={
<>
{is_read_only ? null : (
<Button
title="Create New Person"
href="/admin/people/create"
>
Create New Person
</Button>
)}
</>
}
/>
);
}
@@ -0,0 +1,13 @@
import Section from "@moduletrace/twui/layout/Section";
import Stack from "@moduletrace/twui/layout/Stack";
import PeopleList from "../(partials)/people-list";
export default function PeopleListSection() {
return (
<Section>
<Stack className="w-full items-stretch">
<PeopleList />
</Stack>
</Section>
);
}
@@ -0,0 +1,60 @@
import AdminHero from "@/src/components/general/admin-hero";
import Tag from "@moduletrace/twui/elements/Tag";
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import { useContext } from "react";
export default function Hero() {
const { pageProps } = useContext(AppContext);
const person = pageProps?.person;
if (!person) return null;
const is_read_only = isReadOnlyUser({
user_types: pageProps?.user_types,
});
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
return (
<AdminHero
title={person_name}
buttons={
<>
{is_read_only ? null : (
<Button
title={`Edit ${person_name} profile`}
href={`/admin/people/${person.id}/edit`}
>
Edit Person
</Button>
)}
</>
}
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>
}
/>
);
}
@@ -0,0 +1,38 @@
import Paper from "@moduletrace/twui/elements/Paper";
import H2 from "@moduletrace/twui/layout/H2";
import Row from "@moduletrace/twui/layout/Row";
import Section from "@moduletrace/twui/layout/Section";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
export default function PersonInfo() {
const { pageProps } = useContext(AppContext);
const person = pageProps?.person;
if (!person) return null;
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
return (
<Section>
<Row className="w-full grid xl:grid-cols-2">
<Paper>
<Stack>
<H2>Biography</H2>
{person.bio ? (
<div
dangerouslySetInnerHTML={{ __html: person.bio }}
className="bio-content"
/>
) : (
<Span>No Bio</Span>
)}
</Stack>
</Paper>
</Row>
</Section>
);
}
@@ -0,0 +1,41 @@
import AdminHero from "@/src/components/general/admin-hero";
import Section from "@moduletrace/twui/layout/Section";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import UserForm from "../../../create/(partials)/user-form";
import _ from "lodash";
import type { UserFormObject, UserType } from "@/src/types";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
export default function EditPersonFormSection() {
const { pageProps } = useContext(AppContext);
const person = pageProps?.person;
if (!person) return null;
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
return (
<Section>
<UserForm
existing_user={{
...(_.pick<
BUN_SQLITE_BSQLITE_USERS_JOIN,
keyof BUN_SQLITE_BSQLITE_USERS_JOIN
>(person, [
"first_name",
"last_name",
"email",
"username",
"bio",
]) as UserFormObject),
user_types: person.user_types?.split(",") as
| UserType[]
| undefined,
}}
existing_user_full={person}
/>
</Section>
);
}
@@ -0,0 +1,15 @@
import AdminHero from "@/src/components/general/admin-hero";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
export default function Hero() {
const { pageProps } = useContext(AppContext);
const person = pageProps?.person;
if (!person) return null;
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
return <AdminHero title={`Edit ${person_name} Profile`} />;
}
@@ -0,0 +1,18 @@
import { server as person_server } from "../index.server";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
const user_types = params.props?.user_types;
if (isReadOnlyUser({ user_types })) {
return {
redirect: {
destination: "/admin/people",
},
};
}
return await person_server(params);
};
@@ -0,0 +1,26 @@
import type { PagePropsType } from "@/src/types";
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
import Hero from "./(sections)/hero";
import EditPersonFormSection from "./(sections)/edit-person-form-section";
import { SiteData } from "@/src/data/site-data";
export default function EditPersonPage() {
return (
<>
<Hero />
<EditPersonFormSection />
</>
);
}
export const meta: BunextPageModuleMetaFn = async (params) => {
const person = (params.serverRes?.props as PagePropsType | undefined)
?.person;
const person_name = `${person?.first_name + (person?.last_name ? ` ${person.last_name}` : "")}`;
return {
title: `Edit ${person_name} profile | ${SiteData["SiteName"]}`,
description: person?.bio || `${SiteData["SiteName"]} Personnel`,
};
};
@@ -0,0 +1,32 @@
import grabUsers from "@/src/functions/backend/db/users/grab-users";
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
import type { PagePropsType, PageQueryObject } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
const query: PageQueryObject = params.query;
if (!query.user_id) {
return {
redirect: {
destination: "/admin",
},
};
}
const user_res = await grabUsers({ query: { user_id: query.user_id } });
if (!user_res.singleRes) {
return {
redirect: {
destination: "/admin",
},
};
}
return {
props: {
person: sanitizeUserBio({ user: user_res.singleRes }),
},
};
};
@@ -0,0 +1,24 @@
import type { PagePropsType } from "@/src/types";
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
import Hero from "./(sections)/hero";
import PersonInfo from "./(sections)/person-info";
import { SiteData } from "@/src/data/site-data";
export default function PersonPage() {
return (
<>
<Hero />
<PersonInfo />
</>
);
}
export const meta: BunextPageModuleMetaFn = async (params) => {
const person = (params.serverRes?.props as PagePropsType | undefined)
?.person;
return {
title: `${person?.first_name + (person?.last_name ? ` ${person.last_name}` : "")} | ${SiteData["SiteName"]}`,
description: person?.bio || `${SiteData["SiteName"]} Personnel`,
};
};
@@ -0,0 +1,133 @@
import type {
BUN_SQLITE_BSQLITE_USER_TYPES,
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 submitUserForm({
setLoading,
setStatus,
form,
existing_full,
existing,
image,
}: ReturnType<typeof useFormInit<UserFormObject>>) {
try {
const confirm_msg = existing_full?.id
? `Update this user?`
: `Create new user?`;
if (!window.confirm(confirm_msg)) {
return;
}
const new_user_data: BUN_SQLITE_BSQLITE_USERS = _.pick<
UserFormObject,
keyof UserFormObject
>(form, ["first_name", "last_name", "email", "username", "bio"]);
setLoading(true);
const create_or_update_user_res = await adminCrudHandler({
action: existing_full?.id ? "update" : "insert",
table: "users",
insert_data: [new_user_data],
update_data: new_user_data,
id: existing_full?.id,
});
const target_user_id =
existing_full?.id || create_or_update_user_res.singleRes?.id;
if (image?.imageBase64Full) {
const is_user_settings =
window.location.pathname.includes("admin/settings");
const delete_media_res = await clientDeleteMediaHandler({
user_id: target_user_id,
media_paradigm: "user-profile-image",
});
if (!delete_media_res.success) {
console.log("delete_media_res", delete_media_res);
throw new Error(
delete_media_res.msg ||
delete_media_res.error ||
`Couldn't delete old media`,
);
}
const upload_image_res = await clientUploadMediaHandler({
user_id: target_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: is_user_settings,
});
console.log("upload_image_res", upload_image_res);
if (!upload_image_res.success) {
throw new Error(
upload_image_res.msg || "Couldn't upload Image",
);
}
}
if (create_or_update_user_res.success && target_user_id) {
const delete_user_types = await adminCrudHandler({
action: "delete",
table: "user_types",
sql_query: {
query: {
user_id: {
value: target_user_id,
},
},
},
});
const user_types = form.user_types;
if (user_types?.[0]) {
const add_user_types =
await adminCrudHandler<BUN_SQLITE_BSQLITE_USER_TYPES>({
action: "insert",
table: "user_types",
insert_data: user_types.map((ut) => ({
user_id: target_user_id,
user_type: ut,
})),
});
if (existing_full?.id) {
window.location.reload();
} else {
window.location.pathname = `/admin/people/${target_user_id}`;
}
}
} else {
console.log("res", create_or_update_user_res);
console.log("form", form);
throw new Error(
create_or_update_user_res.msg || "Person Creation Failed",
);
}
} catch (error: any) {
setStatus({
error: true,
msg: error.message || "User Form Error",
});
setLoading(false);
}
}
@@ -0,0 +1,55 @@
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 submitUserForm from "../../(functions)/submit-user-form";
import UserFormName from "./user-form-name";
import Tag from "@moduletrace/twui/elements/Tag";
import UserFormAction from "./user-form-action";
import UserFormEmail from "./user-form-email";
import UserFormUsername from "./user-form-username";
import Row from "@moduletrace/twui/layout/Row";
import type { UserFormObject } from "@/src/types";
import UserFormUserTypes from "./user-form-user-types";
import UserFormBio from "./user-form-bio";
import UserFormImage from "./user-form-image";
type Props = {
existing_user?: UserFormObject;
existing_user_full?: BUN_SQLITE_BSQLITE_USERS;
};
export default function UserForm({ 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={() => {
submitUserForm(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>
<UserFormUserTypes {...init} />
<Row className="w-full grid xl:grid-cols-2 items-stretch">
<UserFormBio {...init} />
<UserFormImage {...init} />
</Row>
<UserFormAction {...init} />
</Form>
</>
);
}
@@ -0,0 +1,17 @@
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 UserFormAction({
setForm,
existing,
existing_full,
}: ReturnType<typeof useFormInit<BUN_SQLITE_BSQLITE_USERS>>) {
const title = existing_full?.id ? "Update User" : "Create New User";
return (
<>
<Button title={title}>{title}</Button>
</>
);
}
@@ -0,0 +1,31 @@
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
import TinyMCEEditor from "@moduletrace/twui/editors/TinyMCE";
import Button from "@moduletrace/twui/layout/Button";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormBio({
setForm,
existing,
existing_full,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_BSQLITE_USERS>>) {
const title = existing_full?.id ? "Update User" : "Create New User";
return (
<>
<TinyMCEEditor
defaultValue={form.bio}
changeHandler={(content) => {
setForm((prev) => ({
...prev,
bio: content,
}));
}}
placeholder="Write Something about yourself"
options={{ height: 300 }}
name="Bio"
showLabel
/>
</>
);
}
@@ -0,0 +1,26 @@
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
import Input from "@moduletrace/twui/form/Input";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormEmail({
setForm,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_BSQLITE_USERS>>) {
return (
<>
<Input
type="email"
placeholder="Email Addres"
onChange={(e) => {
setForm((prev) => ({
...prev,
email: e.target.value,
}));
}}
defaultValue={form.email}
showLabel
required
/>
</>
);
}
@@ -0,0 +1,54 @@
import ImageUpload from "@moduletrace/twui/form/ImageUpload";
import grabMediaURL from "@/src/functions/backend/media/grab-media-url";
import useFormInit from "@/src/hooks/use-form-init";
import type { UserFormObject } from "@/src/types";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
export default function UserFormImage({
setForm,
existing_full,
setImage,
}: ReturnType<typeof useFormInit<UserFormObject>>) {
const title = existing_full?.id ? "Update User" : "Create New User";
const person = existing_full as BUN_SQLITE_BSQLITE_USERS_JOIN | undefined;
const user_image_url = person?.profile_media_id
? grabMediaURL({
media: { id: person?.profile_media_id },
})
: undefined;
return (
<>
<ImageUpload
label="User Image"
existingImageUrl={user_image_url?.media_url}
onChangeHandler={(imgData) => {
setImage(imgData);
}}
setImgURL={(imgURL) => {
if (
typeof imgURL !== "string" &&
typeof imgURL !== "undefined"
) {
return;
}
setForm((prev) => ({
...prev,
image: imgURL || "",
image_thumbnail: imgURL || "",
}));
}}
restoreImageFn={() => {
setForm((prev) => ({
...prev,
image: user_image_url || "",
image_thumbnail: user_image_url || "",
}));
}}
/>
</>
);
}
@@ -0,0 +1,42 @@
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
import Input from "@moduletrace/twui/form/Input";
import Row from "@moduletrace/twui/layout/Row";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormName({
setForm,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_BSQLITE_USERS>>) {
return (
<Row className="w-full grid xl:grid-cols-2">
<Input
title="First Name"
placeholder="Eg. John"
onChange={(e) => {
setForm((prev) => ({
...prev,
first_name: e.target.value,
}));
}}
defaultValue={form.first_name}
autoComplete="given-name"
showLabel
required
/>
<Input
title="Last Name"
placeholder="Eg. Doe"
onChange={(e) => {
setForm((prev) => ({
...prev,
last_name: e.target.value,
}));
}}
defaultValue={form.last_name}
autoComplete="family-name"
showLabel
/>
</Row>
);
}
@@ -0,0 +1,33 @@
import SearchSelect from "@moduletrace/twui/form/SearchSelect";
import { UneditableUserTypes, UserTypes } from "@/src/dict/user-types-dict";
import useFormInit from "@/src/hooks/use-form-init";
import type { UserFormObject, UserType } from "@/src/types";
export default function UserFormUserTypes({
setForm,
form,
}: ReturnType<typeof useFormInit<UserFormObject>>) {
return (
<>
<SearchSelect
multiple
options={UserTypes.filter(
(ut) => !UneditableUserTypes.includes(ut.value),
).map((ut) => ({
value: ut.value,
}))}
defaultValues={form.user_types || []}
changeHandler={(value) => {
const user_types = Array.isArray(value)
? (value as UserType[])
: value
? ([value] as UserType[])
: [];
setForm({ ...form, user_types });
}}
title="User Types"
showLabel
/>
</>
);
}
@@ -0,0 +1,24 @@
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
import Input from "@moduletrace/twui/form/Input";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormUsername({
setForm,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_BSQLITE_USERS>>) {
return (
<>
<Input
placeholder="Username"
onChange={(e) => {
setForm((prev) => ({
...prev,
username: e.target.value,
}));
}}
defaultValue={form.username}
showLabel
/>
</>
);
}
@@ -0,0 +1,6 @@
import AdminHero from "@/src/components/general/admin-hero";
import Button from "@moduletrace/twui/layout/Button";
export default function Hero() {
return <AdminHero title="Create New Person" />;
}
@@ -0,0 +1,10 @@
import Section from "@moduletrace/twui/layout/Section";
import UserForm from "../(partials)/user-form";
export default function UserFormSection() {
return (
<Section>
<UserForm />
</Section>
);
}
@@ -0,0 +1,17 @@
import type { PagePropsType } from "@/src/types";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
const user_types = params.props?.user_types;
if (isReadOnlyUser({ user_types })) {
return {
redirect: {
destination: "/admin/people",
},
};
}
return {};
};
+18
View File
@@ -0,0 +1,18 @@
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
import Hero from "./(sections)/hero";
import UserFormSection from "./(sections)/user-form-section";
import { SiteData } from "@/src/data/site-data";
export default function AdminUsersCreatePage() {
return (
<>
<Hero />
<UserFormSection />
</>
);
}
export const meta: BunextPageModuleMeta = {
title: `Create Admin User | ${SiteData["SiteName"]}`,
description: "Create a new admin user in the dashboard.",
};
+18
View File
@@ -0,0 +1,18 @@
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
import Hero from "./(sections)/hero";
import PeopleListSection from "./(sections)/people-list-section";
import { SiteData } from "@/src/data/site-data";
export default function AdminUsersPage() {
return (
<>
<Hero />
<PeopleListSection />
</>
);
}
export const meta: BunextPageModuleMeta = {
title: `Admin People | ${SiteData["SiteName"]}`,
description: "Manage admin users in the dashboard.",
};