This commit is contained in:
Benjamin Toby 2025-12-02 16:30:46 +01:00
parent 979728e6c8
commit 0b7c70058d
31 changed files with 298 additions and 106 deletions

View File

@ -69,7 +69,7 @@ export default function PopoverComponent({
<Paper
{...props}
className={twMerge(
"max-w-[300px]",
"max-w-[300px] z-[250]",
"twui-popover-content",
props.className
)}

View File

@ -60,6 +60,8 @@
--radius-default-xs: 1px;
--radius-default-lg: 7px;
--radius-default-xl: 10px;
--container-container: 1200px;
}
@custom-variant dark (&:where(.dark, .dark *));
@ -155,3 +157,16 @@ option {
.normal-text {
@apply text-foreground-light dark:text-foreground-dark;
}
ol {
list-style: decimal;
}
ul {
list-style: disc;
}
ul,
ol {
margin-left: 25px;
}

View File

@ -115,7 +115,7 @@ export default function TWUIDocsLink({
<TWUIDocsLink
key={index}
docLink={link}
className="text-sm opacity-70"
className="opacity-70"
autoExpandAll={autoExpandAll}
child
/>

View File

@ -114,7 +114,7 @@ export default function AceEditor({
return function () {
editor.destroy();
};
}, [refresh, darkMode, ready, externalRefresh, content]);
}, [refresh, darkMode, ready, externalRefresh]);
React.useEffect(() => {
const htmlClassName = document.documentElement.className;
@ -145,7 +145,7 @@ export default function AceEditor({
} catch (error: any) {
return (
<React.Fragment>
<span className="text-sm m-0">
<span className="m-0">
Editor Error:{" "}
<b className="text-red-600">{error.message}</b>
</span>

View File

@ -3,9 +3,9 @@ import { RawEditorOptions, TinyMCE, Editor } from "./tinymce";
import { twMerge } from "tailwind-merge";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Border from "../../elements/Border";
import useTinyMCE from "./useTinyMCE";
export type TinyMCEEditorProps<KeyType extends string> = {
tinyMCE?: TinyMCE | null;
options?: RawEditorOptions;
editorRef?: React.MutableRefObject<Editor | null>;
setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
@ -26,8 +26,6 @@ export type TinyMCEEditorProps<KeyType extends string> = {
placeholder?: string;
};
let interval: any;
/**
* # Tiny MCE Editor Component
* @className_wrapper twui-rte-wrapper
@ -35,8 +33,7 @@ let interval: any;
export default function TinyMCEEditor<KeyType extends string>({
options,
editorRef,
setEditor,
tinyMCE,
setEditor: passedSetEditor,
wrapperProps,
defaultValue,
changeHandler,
@ -47,29 +44,49 @@ export default function TinyMCEEditor<KeyType extends string>({
useParentCSS,
placeholder,
}: TinyMCEEditorProps<KeyType>) {
const { tinyMCE } = useTinyMCE();
const editorComponentRef = React.useRef<HTMLDivElement>(null);
const EDITOR_VALUE_CHANGE_TIMEOUT = 500;
const FINAL_HEIGHT = options?.height || 500;
const [themeReady, setThemeReady] = React.useState(false);
const [ready, setReady] = React.useState(false);
const [darkMode, setDarkMode] = React.useState(false);
const [refresh, setRefresh] = React.useState(0);
const [editor, setEditor] = React.useState<Editor>();
const title = name ? twuiSlugToNormalText(name) : "Rich Text";
React.useEffect(() => {
const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setThemeReady(true);
}, 200);
}, []);
React.useEffect(() => {
if (!editorComponentRef.current || !themeReady) {
if (!tinyMCE) {
return;
}
tinyMCE?.init({
const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setThemeReady(true);
}, 200);
}, [tinyMCE]);
let valueTimeout: any;
const id = crypto.randomUUID();
React.useEffect(() => {
if (!editorComponentRef.current || !themeReady || !tinyMCE) {
return;
}
const baseUrl =
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public";
tinyMCE.init({
height: FINAL_HEIGHT,
menubar: false,
plugins:
@ -79,20 +96,30 @@ export default function TinyMCEEditor<KeyType extends string>({
content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }",
init_instance_callback: (editor) => {
setEditor?.(editor as any);
setEditor(editor as any);
if (editorRef) editorRef.current = editor as any;
if (defaultValue) editor.setContent(defaultValue);
setReady(true);
editor.on("change", (e) => {
changeHandler?.(editor.getContent());
// editor.on("change", (e) => {
// changeHandler?.(editor.getContent());
// });
editor.on("input", (e) => {
if (changeHandler) {
window.clearTimeout(valueTimeout);
valueTimeout = setTimeout(() => {
changeHandler(editor.getContent());
}, EDITOR_VALUE_CHANGE_TIMEOUT);
}
});
if (useParentCSS) {
useParentStyles(editor);
}
},
base_url: "https://www.datasquirel.com/tinymce-public",
base_url: baseUrl,
body_class: "twui-tinymce",
placeholder,
relative_urls: true,
@ -106,9 +133,14 @@ export default function TinyMCEEditor<KeyType extends string>({
});
return function () {
tinyMCE?.remove();
if (!ready) return;
const instance = editorComponentRef.current
? tinyMCE?.get(editorComponentRef.current?.id)
: undefined;
instance?.remove();
};
}, [tinyMCE, themeReady]);
}, [tinyMCE, themeReady, refresh]);
return (
<div
@ -129,7 +161,7 @@ export default function TinyMCEEditor<KeyType extends string>({
"bg-background-light dark:bg-background-dark text-gray-500",
"dark:text-white/80 rounded"
)}
htmlFor={name || "twui-tinymce"}
htmlFor={id}
>
{title}
</label>
@ -153,7 +185,7 @@ export default function TinyMCEEditor<KeyType extends string>({
"bg-slate-200 dark:bg-slate-700 rounded-sm w-full",
"twui-rte-wrapper"
)}
id={name || "twui-tinymce"}
id={id}
></div>
</Border>
</div>

View File

@ -4,32 +4,49 @@ import { TinyMCE } from "./tinymce";
let interval: any;
export default function useTinyMCE() {
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE | null>(null);
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE>();
const [refresh, setRefresh] = React.useState(0);
const [scriptLoaded, setScriptLoaded] = React.useState(false);
React.useEffect(() => {
// @ts-ignore
if (window.tinymce) {
console.log("Tinymce already exists");
// @ts-ignore
setTinyMCE(window.tinymce);
if (refresh >= 5) return;
const clientWindow = window as Window & { tinymce?: TinyMCE };
if (clientWindow.tinymce) {
setScriptLoaded(true);
return;
}
const script = document.createElement("script");
script.src =
"https://www.datasquirel.com/tinymce-public/tinymce.min.js";
const baseUrl =
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public";
script.src = `${baseUrl}/tinymce.min.js`;
script.async = true;
document.head.appendChild(script);
script.onload = () => {
// @ts-ignore
if (window.tinymce) {
// @ts-ignore
setTinyMCE(window.tinymce);
}
setScriptLoaded(true);
};
}, []);
document.head.appendChild(script);
}, [refresh]);
React.useEffect(() => {
if (!scriptLoaded) return;
const clientWindow = window as Window & { tinymce?: TinyMCE };
let tinyMCE = clientWindow.tinymce;
if (tinyMCE) {
setTinyMCE(tinyMCE);
} else {
setRefresh((prev) => prev + 1);
}
}, [scriptLoaded]);
return { tinyMCE };
}

View File

@ -9,6 +9,8 @@ export const TWUIDropdownContentPositions = [
"left",
"bottom-left",
"top-left",
"top",
"bottom",
"right",
"bottom-right",
"top-right",
@ -160,6 +162,8 @@ export default function Dropdown({
? "right-0 top-[100%]"
: position == "center"
? "left-[50%] -translate-x-[50%] top-[100%]"
: position == "top"
? "left-[50%] -translate-x-[50%] bottom-[100%]"
: "top-[100%]",
above ? "-translate-y-[120%]" : "",
open ? "flex" : "hidden",

View File

@ -2,19 +2,26 @@ import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import Center from "../layout/Center";
import Loading from "./Loading";
import Row from "../layout/Row";
import Span from "../layout/Span";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
loadingProps?: ComponentProps<typeof Loading>;
label?: string;
};
/**
* # Loading Overlay Component
* @className_wrapper twui-loading-overlay
*/
export default function LoadingOverlay({ loadingProps, ...props }: Props) {
export default function LoadingOverlay({
loadingProps,
label,
...props
}: Props) {
return (
<div
{...props}
@ -26,7 +33,10 @@ export default function LoadingOverlay({ loadingProps, ...props }: Props) {
)}
>
<Center>
<Loading {...loadingProps} />
<Row>
<Loading {...loadingProps} />
{label && <Span>{label}</Span>}
</Row>
</Center>
</div>
);

View File

@ -32,6 +32,7 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
trigger?: (typeof TWUIPopoverTriggers)[number];
debounce?: number;
onClose?: () => any;
hoverOpen?: boolean;
};
/**
@ -55,6 +56,7 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
trigger = "hover",
debounce = 500,
onClose,
hoverOpen,
} = props;
const [ready, setReady] = React.useState(false);
@ -65,6 +67,9 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
const modalRoot = document.getElementById(IDName);
if (modalRoot) {
if (isPopover) {
modalRoot.style.zIndex = "1000";
}
setReady(true);
} else {
const newModalRootEl = document.createElement("div");
@ -144,12 +149,12 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
onClick={(e) => setOpen(!open)}
ref={finalTargetRef}
onMouseEnter={
isPopover && trigger === "hover"
isPopover && (trigger === "hover" || hoverOpen)
? popoverEnterFn
: targetWrapperProps?.onMouseEnter
}
onMouseLeave={
isPopover && trigger === "hover"
isPopover && (trigger === "hover" || hoverOpen)
? popoverLeaveFn
: targetWrapperProps?.onMouseLeave
}

View File

@ -21,6 +21,7 @@ export type SearchProps<KeyType extends string> = DetailedHTMLProps<
>;
loading?: boolean;
placeholder?: string;
componentRef?: React.RefObject<HTMLInputElement | null>;
};
/**
@ -37,6 +38,7 @@ export default function Search<KeyType extends string>({
buttonProps,
loading,
placeholder,
componentRef,
...props
}: SearchProps<KeyType>) {
const [input, setInput] = React.useState(
@ -52,7 +54,7 @@ export default function Search<KeyType extends string>({
}, delay);
}, [input]);
const inputRef = React.useRef<HTMLInputElement>(null);
const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
if (props.autoFocus) {
@ -64,7 +66,7 @@ export default function Search<KeyType extends string>({
<Row
{...props}
className={twMerge(
"relative xl:flex-nowrap items-stretch gap-0",
"relative xl:flex-nowrap items-stretch gap-0 flex-nowrap",
"twui-search-wrapper",
props?.className
)}

View File

@ -31,7 +31,7 @@ export default function Table({ data }: Props) {
<th
key={header}
className={twMerge(
"px-3 py-2 text-left text-sm opacity-50",
"px-3 py-2 text-left opacity-50",
"font-semibold"
)}
title={header}
@ -58,7 +58,7 @@ export default function Table({ data }: Props) {
<td
key={`${header}-${index}`}
className={twMerge(
"px-3 py-2 whitespace-nowrap text-sm text-foreground-light",
"px-3 py-2 whitespace-nowrap text-foreground-light",
"dark:text-foreground-dark max-w-[200px] overflow-hidden",
"overflow-ellipsis"
)}

View File

@ -8,7 +8,7 @@ import twuiSlugify from "../utils/slugify";
export type TWUITabsObject = {
title: string;
value?: string;
content: React.ReactNode;
content?: React.ReactNode;
defaultActive?: boolean;
};
@ -35,6 +35,8 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
* @className twui-tab-buttons
* @className twui-tab-button-active
* @className twui-tab-buttons-wrapper
* @className twui-tab-buttons-container
* @className twui-tabs-border
*/
export default function Tabs({
tabsContentArray,
@ -91,13 +93,14 @@ export default function Tabs({
)}
>
<Border
className="p-0 w-full overflow-hidden"
className="p-0 w-full overflow-hidden twui-tabs-border"
{...tabsBorderProps}
>
<Row
className={twMerge(
"gap-0 items-stretch w-full flex-nowrap overflow-x-auto",
centered && "justify-center"
centered && "justify-center",
"twui-tab-buttons-container"
)}
>
{values.map((value, index) => {

View File

@ -18,6 +18,7 @@ type Props = {
loading?: boolean;
mdRes?: string;
setMdRes: React.Dispatch<React.SetStateAction<string>>;
placeholder?: string;
};
export default function AIPromptBlock({
@ -27,6 +28,7 @@ export default function AIPromptBlock({
loading = false,
mdRes = "",
setMdRes,
placeholder,
}: Props) {
const [prompt, setPrompt] = React.useState("");
const currentPromptRef = React.useRef("");
@ -43,7 +45,7 @@ export default function AIPromptBlock({
<MessageCircleMore
size={15}
opacity={0.5}
className="-mt-[1px]"
className="-mt-px"
/>
</Row>
</Card>
@ -53,11 +55,15 @@ export default function AIPromptBlock({
setStreamRes={setMdRes}
streamRes={mdRes}
history={history}
loading={loading}
/>
<Stack className="w-full relative">
{loading && <LoadingOverlay />}
<Textarea
placeholder={model ? `Prompt ${model}` : "Prompt AI"}
placeholder={
placeholder ||
(model ? `Prompt ${model}` : "Prompt AI")
}
wrapperProps={{ className: "outline-none" }}
wrapperWrapperProps={{ className: "w-full" }}
value={prompt}
@ -65,7 +71,7 @@ export default function AIPromptBlock({
setPrompt(e.target.value);
}}
onKeyDown={(e) => {
if (e.key == "Enter" && !e.ctrlKey) {
if (e.key == "Enter" && !e.ctrlKey && !e.shiftKey) {
e.preventDefault();
currentPromptRef.current = prompt;
setTimeout(() => {

View File

@ -8,12 +8,14 @@ type Props = {
streamRes: string;
setStreamRes: React.Dispatch<React.SetStateAction<string>>;
history: ChatCompletionMessageParam[];
loading?: boolean;
};
export default function AIPromptPreview({
setStreamRes,
streamRes,
history,
loading,
}: Props) {
const responseContentRef = React.useRef<HTMLDivElement>(null);
const isContentInterrupted = React.useRef(false);
@ -27,7 +29,7 @@ export default function AIPromptPreview({
}
}, [streamRes]);
if (!streamRes?.match(/./)) return null;
if (loading || !streamRes?.match(/./)) return null;
return (
<Stack className="w-full">

View File

@ -11,6 +11,7 @@ import fileInputToBase64 from "../utils/form/fileInputToBase64";
import Row from "../layout/Row";
import Input from "./Input";
import Loading from "../elements/Loading";
import Tag from "../elements/Tag";
type FileInputUtils = {
clearFileInput?: () => void;
@ -59,6 +60,7 @@ type ImageUploadProps = DetailedHTMLProps<
existingFile?: FileInputToBase64FunctionReturn;
existingFileUrl?: string;
icon?: ReactNode;
externalSetFileURL?: React.Dispatch<string | undefined>;
labelSpanProps?: ComponentProps<typeof Span>;
loading?: boolean;
multiple?: boolean;
@ -86,6 +88,7 @@ export default function FileUpload({
multiple,
onClear,
changeHandler,
externalSetFileURL,
...props
}: ImageUploadProps) {
const [file, setFile] = React.useState<
@ -97,6 +100,7 @@ export default function FileUpload({
const [fileDraggedOver, setFileDraggedOver] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const tempFileURLRef = React.useRef<string>(null);
React.useEffect(() => {
if (existingFileUrl) {
@ -244,26 +248,37 @@ export default function FileUpload({
className="w-full relative h-full items-center justify-center overflow-hidden"
{...previewImageWrapperProps}
>
{disablePreview ? (
<Span className="opacity-50" size="small">
Image Uploaded!
</Span>
) : fileUrl.match(/\.pdf$|\.txt$/) ? (
<Row>
<FileArchive size={36} strokeWidth={1} />
<Stack className="gap-0">
<Span size="smaller" className="opacity-70">
{fileUrl}
</Span>
</Stack>
</Row>
) : (
<img
src={fileUrl}
className="w-full object-contain overflow-hidden"
{...previewImageProps}
/>
)}
<Stack className="w-full">
{disablePreview ? (
<Span className="opacity-50" size="small">
Image Uploaded!
</Span>
) : fileUrl.match(/\.pdf$|\.txt$/) ? (
<Row>
<FileArchive size={36} strokeWidth={1} />
<Stack className="gap-0">
<Span size="smaller" className="opacity-70">
{fileUrl}
</Span>
</Stack>
</Row>
) : (
<img
src={fileUrl}
className="w-full object-contain overflow-hidden"
{...previewImageProps}
/>
)}
<Tag
variant="outlined"
color="gray"
className="w-full py-2 text-sm"
>
{fileUrl}
</Tag>
</Stack>
<Button
variant="ghost"
className={twMerge(
@ -345,6 +360,59 @@ export default function FileUpload({
>
{label || "Click to Upload File"}
</Span>
{externalSetFileURL ? (
<Row
className="flex-nowrap gap-0 items-stretch"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<Input
placeholder="Add Media URL"
className="text-sm"
wrapperProps={{ className: "h-full" }}
wrapperWrapperProps={{
className: "h-full",
}}
changeHandler={(value) => {
tempFileURLRef.current = value;
}}
onKeyUp={(e) => {
if (e.key == "Enter") {
if (tempFileURLRef.current) {
setFileUrl(
tempFileURLRef.current
);
externalSetFileURL(
tempFileURLRef.current
);
}
}
}}
/>
<Button
title="Add Media URL"
variant="outlined"
className="py-0 px-3"
color="gray"
onClick={(e) => {
e.preventDefault();
if (tempFileURLRef.current) {
setFileUrl(
tempFileURLRef.current
);
externalSetFileURL(
tempFileURLRef.current
);
}
}}
>
<span className="text-2xl">+</span>
</Button>
</Row>
) : null}
</Stack>
</Center>
</Card>

View File

@ -207,14 +207,16 @@ export default function Input<KeyType extends string>(
window.clearTimeout(timeout);
if (validationRegex && !validationFunction) {
if (validationRegex) {
timeout = setTimeout(() => {
setValidity({
isValid: validationRegex.test(val),
msg: "Value mismatch",
});
}, finalDebounce);
} else if (validationFunction) {
}
if (validationFunction) {
window.clearTimeout(validationFnTimeout);
validationFnTimeout = setTimeout(() => {

View File

@ -232,9 +232,9 @@ export default function Select<
}
hoverOpen
>
<Card className="min-w-[250px] text-sm p-6">
<Card className="min-w-[250px] p-6">
{typeof info == "string" ? (
<Span className="text-sm">{info}</Span>
<Span>{info}</Span>
) : (
info
)}

View File

@ -258,7 +258,7 @@ export default function Button({
props.disabled ? "opacity-40 cursor-not-allowed" : "",
"twui-button-general",
size == "small"
? "px-3 py-1.5 text-sm twui-button-small"
? "px-3 py-1.5 twui-button-small text-sm"
: size == "smaller"
? "px-2 py-1 text-xs twui-button-smaller"
: size == "large"

View File

@ -12,7 +12,7 @@ export default function Container({
<div
{...props}
className={twMerge(
"flex w-full max-w-[1200px] gap-4 justify-between",
"flex w-full max-w-container gap-4 justify-between",
"flex-wrap flex-col xl:flex-row items-start xl:items-center",
"twui-container",
props.className

View File

@ -12,7 +12,7 @@ export default function H5({
<h5
{...props}
className={twMerge(
"text-sm mb-4",
"mb-4",
"twui-headings twui-heading",
"twui-h5",
props.className

View File

@ -18,7 +18,7 @@ export default function Spacer({ horizontal, ...props }: Props) {
<div
{...props}
className={twMerge(
"grow",
"",
horizontal ? "w-10" : "w-full h-10",
"twui-spacer",
props.className

View File

@ -23,6 +23,7 @@ export function useMDXComponents(params?: Params) {
pre: ({ children, ...props }) => {
if (React.isValidElement(children) && children.props) {
return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}>
{/* @ts-ignore */}
{children.props.children}
@ -30,6 +31,7 @@ export function useMDXComponents(params?: Params) {
);
}
return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}>
{children}
</CodeBlock>
@ -62,6 +64,7 @@ export function useMDXComponents(params?: Params) {
);
},
img: (props) => (
// @ts-ignore
<img
{...props}
className="w-full h-auto shadow-lg rounded-default overflow-hidden"

View File

@ -22,6 +22,7 @@ export default function useMDXComponents({
pre: ({ children, ...props }) => {
if (React.isValidElement(children) && children.props) {
return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}>
{/* @ts-ignore */}
{children.props.children}
@ -29,6 +30,7 @@ export default function useMDXComponents({
);
}
return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}>
{children}
</CodeBlock>

View File

@ -1,17 +1,20 @@
import _ from "lodash";
export const FetchAPIMethods = [
"POST",
"GET",
"DELETE",
"PUT",
"PATCH",
"post",
"get",
"delete",
"put",
"patch",
] as const;
type FetchApiOptions<T extends { [k: string]: any } = { [k: string]: any }> = {
method:
| "POST"
| "GET"
| "DELETE"
| "PUT"
| "PATCH"
| "post"
| "get"
| "delete"
| "put"
| "patch";
method: (typeof FetchAPIMethods)[number];
body?: T | string;
headers?: FetchHeader;
};

View File

@ -35,7 +35,7 @@ export default async function fileInputToBase64({
resolve(reader.result?.toString());
};
reader.onerror = function (/** @type {*} */ error: any) {
console.log("Error: ", error.message);
console.log("File Input to Base64 Error: ", error.message);
};
}
);

View File

@ -4,14 +4,28 @@ import Header from "./Header";
import Footer from "./Footer";
import { twMerge } from "tailwind-merge";
import MobileMenu from "./(sections)/MobileMenu";
import Head from "next/head";
type Props = PropsWithChildren & {};
type Props = PropsWithChildren & {
meta?: {
title?: string;
description?: string;
};
};
export default function Layout({ children }: Props) {
export default function Layout({ children, meta }: Props) {
const [menuOpen, setMenuOpen] = React.useState(false);
return (
<div className="flex flex-row items-stretch w-full min-h-screen">
<Head>
<title>
{meta?.title || "10X Software/Devops Engineer | Tben.me"}
</title>
{meta?.description && (
<meta name="description" content={meta?.description} />
)}
</Head>
<Aside />
<div className={twMerge("flex flex-col items-start gap-0", "grow")}>
<Header {...{ menuOpen, setMenuOpen }} />

View File

@ -3,7 +3,7 @@ import Main from "@/components/pages/about";
export default function ContactPage() {
return (
<Layout>
<Layout meta={{ title: "About Me | Tben.me" }}>
<Main />
</Layout>
);

View File

@ -3,7 +3,7 @@ import Main from "@/components/pages/contact";
export default function ContactPage() {
return (
<Layout>
<Layout meta={{ title: "Contact Me | Tben.me" }}>
<Main />
</Layout>
);

View File

@ -1,5 +1,4 @@
import Layout from "@/layouts/main";
import H1 from "@/components/lib/layout/H1";
import Main from "@/components/pages/Home";
import AboutSection from "@/components/pages/Home/(sections)/AboutSection";
import Divider from "@/components/lib/layout/Divider";
@ -8,7 +7,12 @@ import MyWorkSection from "@/components/pages/Home/(sections)/MyWorkSection";
export default function Home() {
return (
<Layout>
<Layout
meta={{
description:
"Software Engineer, DevOps Engineer, Full Stack Developer, Software Architect, Philosopher, Solar Energy Enthusiast.",
}}
>
<Main />
<Divider />
<AboutSection />

View File

@ -5,7 +5,7 @@ import MySkillsSection from "@/components/pages/Home/(sections)/MySkillsSection"
export default function SkillsPage() {
return (
<Layout>
<Layout meta={{ title: "My Skills | Tben.me" }}>
<Main />
<Divider />
<MySkillsSection noTitle expand />

View File

@ -5,7 +5,7 @@ import MyWorkSection from "@/components/pages/Home/(sections)/MyWorkSection";
export default function WorkPage() {
return (
<Layout>
<Layout meta={{ title: "My Work | Tben.me" }}>
<Main />
<Divider />
<MyWorkSection noTitle expand />