First Commit

This commit is contained in:
2026-09-12 13:56:36 +01:00
commit 4d75af0418
426 changed files with 36452 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
import H1 from "@/src/components/twui/layout/H1";
import Section from "@/src/components/twui/layout/Section";
import { useContext, type ReactNode } from "react";
import Row from "../twui/layout/Row";
import Stack from "../twui/layout/Stack";
import Divider from "../twui/layout/Divider";
import Breadcrumbs from "../twui/elements/Breadcrumbs";
import { AppContext } from "@/src/pages/__root";
import Span from "../twui/layout/Span";
type Props = {
title: string;
description?: string | ReactNode;
buttons?: ReactNode;
};
export default function AdminHero({ title, description, buttons }: Props) {
const { pageProps } = useContext(AppContext);
return (
<Section>
<Stack className="w-full items-stretch">
<Row className="w-full justify-between items-start">
<Stack>
<H1>{title}</H1>
{description ? (
typeof description == "string" ? (
<Span>{description}</Span>
) : (
description
)
) : null}
<Breadcrumbs
dividerProps={{
className: "-skew-10",
}}
currentLinkProps={{
className:
"text-slate-400! pointer-events-none",
}}
pageUrl={pageProps?.url?.pathname}
backButton
skipHome
/>
</Stack>
<Row>{buttons}</Row>
</Row>
<Divider className="my-4" />
</Stack>
</Section>
);
}
+24
View File
@@ -0,0 +1,24 @@
import Img from "@/src/components/twui/layout/Img";
import { twMerge } from "tailwind-merge";
type Props = {
src: string;
opacity?: number;
};
export default function BlurredImageBG({ src, opacity = 0.1 }: Props) {
/**
* Render component
*/
return (
<Img
alt="bg image"
src={src}
className={twMerge(
`absolute top-0 left-0 w-[150vw] h-full blur-xs object-cover`,
"z-0",
)}
style={{ opacity }}
/>
);
}
+23
View File
@@ -0,0 +1,23 @@
import Img from "@/src/components/twui/layout/Img";
import type { ComponentProps } from "react";
import { twMerge } from "tailwind-merge";
type Props = {
src: string;
img_props?: Omit<ComponentProps<typeof Img>, "alt">;
};
export default function ImageBG({ src, img_props }: Props) {
return (
<Img
{...img_props}
alt="bg image"
src={src}
className={twMerge(
`absolute top-0 left-0 w-full h-full object-cover`,
"z-0",
img_props?.className,
)}
/>
);
}
@@ -0,0 +1,39 @@
import Section from "@/src/components/twui/layout/Section";
import Container from "@/src/components/twui/layout/Container";
import Stack from "@/src/components/twui/layout/Stack";
import H1 from "@/src/components/twui/layout/H1";
import Span from "@/src/components/twui/layout/Span";
import BlurredImageBG from "@/src/components/general/blured-image-bg";
import { twMerge } from "tailwind-merge";
type Props = {
title: string;
sub_title?: string;
description?: string;
bg_image?: string;
};
export default function InnerPageHeroSection({
title,
bg_image,
description,
sub_title,
}: Props) {
return (
<Section className={twMerge("")}>
<Container className="relative z-10">
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
{sub_title ? (
<Span className="htag">{sub_title}</Span>
) : null}
<H1 className="text-center font-bold">{title}</H1>
{description ? (
<Span className="text-xl sm:text-2xl leading-snug text-center">
{description}
</Span>
) : null}
</Stack>
</Container>
</Section>
);
}
+59
View File
@@ -0,0 +1,59 @@
import { twMerge } from "tailwind-merge";
import Tag from "../twui/elements/Tag";
import Row from "../twui/layout/Row";
import { TriangleAlert, CircleCheck } from "lucide-react";
import type { PropsWithChildren } from "react";
import type { TWUIStatusType } from "../twui/hooks/useStatus";
import Paper from "../twui/elements/Paper";
import Center from "../twui/layout/Center";
import Stack from "../twui/layout/Stack";
import Border from "../twui/elements/Border";
import Logo from "./logo";
type Props = PropsWithChildren & {
title?: string;
className?: string;
status?: TWUIStatusType;
};
export default function LoginShell({ children, className, status }: Props) {
return (
<Stack className="w-screen h-screen justify-center! items-center! overflow-x-hidden overflow-y-auto">
<Logo />
<Border
className={twMerge(
"w-full lg:w-[600px] flex-col items-center",
"px-4 sm:px-6 py-8 shadow-md relative",
className,
)}
>
{status?.error && status.msg ? (
<Tag
variant="outlined"
color="error"
className="w-full py-1.5 outline-error/50 bg-error/5!"
>
<Row>
<TriangleAlert size={15} />
<span>{status.msg}</span>
</Row>
</Tag>
) : null}
{status?.success && status.msg ? (
<Tag
variant="outlined"
color="success"
className="w-full py-1.5 outline-success/50 bg-success/5!"
>
<Row>
<CircleCheck size={15} />
<span>{status.msg}</span>
</Row>
</Tag>
) : null}
{children}
</Border>
</Stack>
);
}
+21
View File
@@ -0,0 +1,21 @@
import type { ComponentProps } from "react";
import Img from "../twui/layout/Img";
type Props = {
icon_size?: number;
img_props?: Omit<ComponentProps<typeof Img>, "alt">;
white?: boolean;
};
export default function LogoIcon({ icon_size = 45, img_props, white }: Props) {
return (
<Img
size={icon_size}
{...img_props}
alt="Logo Icon"
src={
white ? "/logos/wgui-logo-white.webp" : "/logos/wgui-logo.webp"
}
/>
);
}
+57
View File
@@ -0,0 +1,57 @@
import Row from "../twui/layout/Row";
import Span from "../twui/layout/Span";
import type { ComponentProps, PropsWithChildren } from "react";
import { twMerge } from "tailwind-merge";
import Stack from "../twui/layout/Stack";
import LogoIcon from "./logo-icon";
import { SiteData } from "@/src/data/site-data";
type Props = {
icon_size?: number;
text_wrapper_props?: ComponentProps<typeof Stack>;
text_props?: ComponentProps<typeof Span>;
white?: boolean;
};
export default function Logo({
icon_size = 45,
text_props,
text_wrapper_props,
white,
}: Props) {
return (
<a href="/" className="logo-link">
<Row className="gap-2! flex-nowrap">
<LogoIcon icon_size={icon_size} white={white} />
<Stack
{...text_wrapper_props}
className={twMerge("gap-1!", text_wrapper_props?.className)}
>
<Stack className="gap-0!">
<HeroText text_props={text_props}>wg-ui</HeroText>
</Stack>
</Stack>
</Row>
</a>
);
}
function HeroText({
text_props,
children,
}: PropsWithChildren & {
text_props?: ComponentProps<typeof Span>;
}) {
return (
<Span
{...text_props}
className={twMerge(
"text-xl text-dark font-serif whitespace-nowrap",
"heading leading-[24px]",
text_props?.className,
)}
>
{children}
</Span>
);
}
+66
View File
@@ -0,0 +1,66 @@
import React, { type ComponentProps, type ReactNode } from "react";
import Section from "@/src/components/twui/layout/Section";
import Container from "@/src/components/twui/layout/Container";
import Stack from "@/src/components/twui/layout/Stack";
import H1 from "@/src/components/twui/layout/H1";
import Row from "@/src/components/twui/layout/Row";
import { twMerge } from "tailwind-merge";
import Span from "@/src/components/twui/layout/Span";
import BlurredImageBG from "@/src/components/general/blured-image-bg";
import ImageBG from "@/src/components/general/image-bg";
import type Img from "../twui/layout/Img";
type Props = {
title: string;
image_url?: string;
sub_title?: string;
cta_buttons?: ReactNode;
img_props?: Omit<ComponentProps<typeof Img>, "alt">;
img_component?: ReactNode;
};
export default function MainHeroSection({
title,
cta_buttons,
sub_title,
image_url,
img_props,
img_component,
}: Props) {
const img_comp = img_component ? (
img_component
) : image_url ? (
<ImageBG src={image_url} img_props={img_props} />
) : null;
return (
<React.Fragment>
<Section className={twMerge("relative")}>
<Container>
<Row className="grid xl:grid-cols-2 relative z-10 max-w-xl xl:max-w-none lg:pb-20 pt-60 lg:pt-20">
<Stack className="w-full gap-8">
<H1
className={twMerge(
"text-center md:text-left w-full",
)}
>
{title}
</H1>
{sub_title ? (
<Span
className={twMerge(
"text-lg sm:text-2xl leading-snug text-center md:text-left",
)}
>
{sub_title}
</Span>
) : null}
{cta_buttons}
</Stack>
{/* <Stack className="relative w-full xl:w-[700px] xl:h-full overflow-visible"></Stack> */}
</Row>
</Container>
</Section>
</React.Fragment>
);
}
+42
View File
@@ -0,0 +1,42 @@
import Img from "@/src/components/twui/layout/Img";
import { twMerge } from "tailwind-merge";
import Stack from "../twui/layout/Stack";
type Props = {
src: string;
width?: number;
height?: number;
alt?: string;
};
export default function RectImage({
src,
height = 300,
width = 200,
alt,
}: Props) {
const img_component = (
<Img
alt={alt || "image"}
src={src}
className={twMerge(`w-full h-full object-cover`)}
/>
);
return (
<>
<Stack
className={twMerge(`overflow-hidden hidden xl:flex`)}
style={{
width: `${width}px`,
height: `${height}px`,
}}
>
{img_component}
</Stack>
<Stack className={twMerge(`overflow-hidden flex xl:hidden w-full`)}>
{img_component}
</Stack>
</>
);
}
+53
View File
@@ -0,0 +1,53 @@
import Section from "@/src/components/twui/layout/Section";
import Container from "@/src/components/twui/layout/Container";
import H2 from "@/src/components/twui/layout/H2";
import Stack from "@/src/components/twui/layout/Stack";
import type { TWUILucideIconName } from "../twui/elements/lucide-icon";
import LucideIcon from "../twui/elements/lucide-icon";
import Span from "../twui/layout/Span";
import type { ReactNode } from "react";
type Props = {
tagline: string;
icon_name?: TWUILucideIconName;
sub_title?: string;
post_component?: ReactNode;
};
export default function TaglineSection({
tagline,
icon_name,
sub_title,
post_component,
}: Props) {
/**
* Render component
*/
return (
<Section className="bg-secondary relative">
<Container className="xl:py-10">
<Stack className="w-full items-center gap-6">
{icon_name && (
<LucideIcon
name={icon_name}
size={50}
className="text-accent"
/>
)}
<H2 className="text-center max-w-4xl xl:text-6xl! text-white">
{tagline}
</H2>
{sub_title ? (
<Span
className="text-white/80 max-w-4xl text-center"
size="larger"
>
{sub_title}
</Span>
) : null}
{post_component}
</Stack>
</Container>
</Section>
);
}
@@ -0,0 +1,55 @@
import Form from "@/src/components/twui/form/Form";
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
import useFormInit from "@/src/hooks/use-form-init";
import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db";
import UserFormName from "./user-form-name";
import Tag from "@/src/components/twui/elements/Tag";
import UserFormAction from "./user-form-action";
import UserFormEmail from "./user-form-email";
import UserFormUsername from "./user-form-username";
import type { UserFormObject } from "@/src/types";
import submitUserForm from "./submit-user-form";
import UserFormPassword from "./user-form-password";
type Props = {
existing_user?: UserFormObject;
existing_user_full?: BUN_SQLITE_WGUI_USERS;
is_first_user?: boolean;
};
export default function UserForm({
existing_user,
existing_user_full,
is_first_user,
}: Props) {
const init = useFormInit<UserFormObject>({
existing: existing_user,
existing_full: existing_user_full,
is_first_user,
});
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} />
<UserFormEmail {...init} />
<UserFormUsername {...init} />
<UserFormPassword {...init} />
{/* <UserFormUserTypes {...init} /> */}
{/* <UserFormBio {...init} /> */}
{/* <UserFormImage {...init} /> */}
<UserFormAction {...init} />
</Form>
</>
);
}
@@ -0,0 +1,153 @@
import type {
BUN_SQLITE_WGUI_USER_TYPES,
BUN_SQLITE_WGUI_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 { ApiReqParams, UserFormObject } from "@/src/types";
import _ from "lodash";
import fetchApi from "../../twui/utils/fetch/fetchApi";
import type { APIResponseObject } from "@moduletrace/bunext/types";
export default async function submitUserForm({
setLoading,
setStatus,
form,
existing_full,
existing,
image,
is_first_user,
}: 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_WGUI_USERS = _.pick<
UserFormObject,
keyof UserFormObject
>(form, ["first_name", "last_name", "email", "username", "bio"]);
setLoading(true);
const create_or_update_user_res = is_first_user
? await fetchApi<
ApiReqParams,
APIResponseObject<BUN_SQLITE_WGUI_USERS>
>(`/api/create-first-user.ts`, {
method: "POST",
body: {
insert_data: [new_user_data],
},
})
: 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_WGUI_USER_TYPES>({
action: "insert",
table: "user_types",
insert_data: user_types.map((ut) => ({
user_id: target_user_id,
user_type: ut,
})),
});
if (add_user_types.success) {
if (existing_full?.id) {
window.location.reload();
} else {
window.location.pathname = `/admin/people/${target_user_id}`;
}
} else {
console.log("add_user_types", add_user_types);
throw new Error(
add_user_types.msg || "Couldn't add user types",
);
}
}
} 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,20 @@
import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db";
import Button from "@/src/components/twui/layout/Button";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormAction({
status,
existing_full,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_USERS>>) {
const title = existing_full?.id ? "Update User" : "Create New User";
if (status?.error) {
return null;
}
return (
<>
<Button title={title}>{title}</Button>
</>
);
}
@@ -0,0 +1,31 @@
import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db";
import TinyMCEEditor from "@/src/components/twui/editors/TinyMCE";
import Button from "@/src/components/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_WGUI_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_WGUI_USERS } from "@/db/types/db";
import Input from "@/src/components/twui/form/Input";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormEmail({
setForm,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_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 "@/src/components/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_WGUI_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_WGUI_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_WGUI_USERS } from "@/db/types/db";
import Input from "@/src/components/twui/form/Input";
import Row from "@/src/components/twui/layout/Row";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormName({
setForm,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_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,51 @@
import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db";
import Input from "@/src/components/twui/form/Input";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormPassword({
setForm,
form,
existing_full,
setStatus,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_USERS>>) {
if (existing_full?.id) {
return null;
}
return (
<>
<Input
placeholder="Password"
type="password"
autoComplete="new-password"
onChange={(e) => {
setForm((prev) => ({
...prev,
password: e.target.value,
}));
}}
defaultValue={form.username}
showLabel
/>
<Input
placeholder="Confirm Password"
type="password"
autoComplete="new-password"
onChange={(e) => {
const confirm_psswd = e.target.value;
if (confirm_psswd && form.password !== confirm_psswd) {
setStatus({
error: true,
msg: `Passwords don't match`,
});
} else {
setStatus(undefined);
}
}}
defaultValue={form.username}
showLabel
/>
</>
);
}
@@ -0,0 +1,33 @@
import SearchSelect from "@/src/components/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_WGUI_USERS } from "@/db/types/db";
import Input from "@/src/components/twui/form/Input";
import useFormInit from "@/src/hooks/use-form-init";
export default function UserFormUsername({
setForm,
form,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_USERS>>) {
return (
<>
<Input
placeholder="Username"
onChange={(e) => {
setForm((prev) => ({
...prev,
username: e.target.value,
}));
}}
defaultValue={form.username}
showLabel
/>
</>
);
}