First Commit

This commit is contained in:
Benjamin Toby
2024-12-09 16:36:17 +01:00
parent f24b493566
commit b037cec100
128 changed files with 3281 additions and 193 deletions
+167
View File
@@ -0,0 +1,167 @@
export const skills = {
Devops: {
description: "Server management, deployment, and automation",
href: "/work/devops",
portfolio: [
{
title: "Server Management",
description: "Server management, deployment, and automation",
href: "/work/devops/server-management",
image: "/images/work/devops/server-management.png",
technologies: [
"Linux",
"Docker",
"Shell Scripting",
"Kubernetes",
"Terraform",
"AWS",
"Hetzner",
"Azure",
"Google Cloud",
"Debian",
"Ubuntu",
"Alpine",
"...",
],
},
{
title: "Email Server Setup",
description:
"Self Hosted Email solution. Setup and Deployment.",
href: "/work/devops/server-management",
image: "/images/work/devops/server-management.png",
technologies: ["Linux", "Docker", "Mailinabox", "Hetzner"],
},
{
title: "DNS Configuration",
description: "Domain names setup, configuration, and security.",
image: "/images/work/devops/server-management.png",
technologies: [
"Cloudflare DNS",
"Cloudflare Zero Trust",
"Cloudflare Registry",
],
},
{
title: "Web Servers and Reverse Proxies",
description:
"Web Server Configuration and deployment. Plus SSL Certificates provisioning.",
image: "/images/work/devops/server-management.png",
technologies: [
"NGINX",
"Node JS",
"Bun JS",
"Certbot",
"Let's Encrypt",
],
},
{
title: "Continuous Integration and Deployment",
description:
"Self hosted and managed solutions for deployment and continuous integration",
image: "/images/work/devops/server-management.png",
technologies: [
"Coolify",
"Vercel",
"Github Runners",
"Gitea Runners",
"Dokploy",
],
},
{
title: "Customer Service",
description:
"Self hosted and managed solutions for handling customers",
image: "/images/work/devops/server-management.png",
technologies: ["Chatwoot", "Tidio"],
},
],
},
"Full Stack": {
description: "Frontend and Backend development",
href: "/work/devops",
portfolio: [
{
title: "Server Development",
description:
"Easy development and development of Web servers and APIs",
image: "/images/work/devops/server-management.png",
technologies: ["Node JS", "Bun JS", "Shell Script", "Python"],
},
{
title: "React JS",
description: "Development of React JS applications for the web",
image: "/images/work/devops/server-management.png",
technologies: [
"React",
"Next JS",
"Vite",
"React Hooks",
"Functional Components",
],
},
{
title: "HTML & CSS",
description: "HTML and CSS Development for the web",
image: "/images/work/devops/server-management.png",
technologies: [
"HTML 5",
"CSS 3",
"SCSS",
"Less",
"Tailwind CSS",
],
},
{
title: "Database",
description: "Database Development and Management",
image: "/images/work/devops/server-management.png",
technologies: [
"MySQL",
"Mariadb",
"PostgreSQL",
"MongoDB",
"Redis",
"SQLite",
"Firebase",
"Supabase",
"Prisma",
"Mongoose",
],
},
{
title: "User Authentication",
description: "User Authentication and Authorization",
image: "/images/work/devops/server-management.png",
technologies: [
"Google Login",
"Github Login",
"Next Auth",
"Firebase",
"Cloudflare Zero Trust",
"Clerk",
],
},
],
},
"Software Development": {
description: "Application development for server, desktop, and web",
href: "/work/devops",
portfolio: [
{
title: "Node Modules Development",
description:
"Development of Node Modules for server, desktop, and web. Deployed on NPM package registry.",
image: "/images/work/devops/server-management.png",
technologies: ["Node JS", "Bun JS", "Shell Scripting", "Rsync"],
},
{
title: "Shell Scripting",
description:
"Development of shell scripts for automation and task execution",
image: "/images/work/devops/server-management.png",
technologies: ["Shell Script", "Bash Scripting", "GNU/Linux"],
},
],
},
};
+92
View File
@@ -0,0 +1,92 @@
export const work = {
Fullstack: {
href: "/work/full-stack",
portfolio: [
{
title: "Datasquirel",
description: "Clould-based SQL data management system.",
href: "https://datasquirel.com",
image: "/images/work/devops/server-management.png",
technologies: [
"Node JS",
"SQL",
"Mariadb",
"Shell Scripting",
"Docker",
"Docker Compose",
"Next JS",
"Tailwind CSS",
"Cloudflare Zero Trust",
],
},
{
title: "Summit Lending",
description: "Mortgage Broker in Utah",
href: "https://summitlending.com",
image: "/images/work/devops/server-management.png",
technologies: [
"Next JS",
"Tailwind CSS",
"Zapier",
"Google APIs",
"SQL",
"Spreadsheet to JSON",
],
},
{
title: "Coderank",
description: "A new age of remote work. Targeted at developers",
href: "https://summitlending.com",
image: "/images/work/devops/server-management.png",
technologies: [
"Next JS",
"Docker",
"VSCode Web Editor",
"NGINX Reverse Proxy",
],
},
],
},
Devops: {
href: "/work/devops",
portfolio: [
{
title: "Personal Mail Server",
description:
"Self Hosted Email solution for all my personal projects",
href: "https://box.mailben.xyz/mail",
image: "/images/work/devops/server-management.png",
technologies: ["Linux", "Docker", "Mailinabox"],
},
{
title: "Git, Packge, Container repository",
description:
"Self Hosted repository for Git projects, NPM modules, Docker images, and more",
href: "https://git.tben.me/tben",
image: "/images/work/devops/server-management.png",
technologies: ["Gitea", "Linux"],
},
],
},
Software: {
description: "Application development for server, desktop, and web",
href: "/work/devops",
portfolio: [
{
title: "Turbo Sync NPM Module",
description:
"The easiest way to synchronize local and remote directories in real time",
href: "https://git.tben.me/Moduletrace/-/packages/npm/turbosync",
image: "/images/work/devops/server-management.png",
technologies: ["Node JS", "Bun JS", "Shell Scripting", "Rsync"],
},
{
title: "Batchrun",
description: "Run multiple concurrent processes",
href: "https://git.tben.me/Moduletrace/-/packages/npm/@moduletrace%2Fbatchrun",
image: "/images/work/devops/server-management.png",
technologies: ["Node JS"],
},
],
},
};
@@ -0,0 +1,18 @@
import H2 from "@/components/lib/layout/H2";
import Section from "@/components/lib/layout/Section";
import Span from "@/components/lib/layout/Span";
import Stack from "@/components/lib/layout/Stack";
export default function AboutSection() {
return (
<Section>
<Stack className="w-full max-w-full xl:max-w-[50vw]">
<H2 className="leading-snug">About Me</H2>
<Span>
I'm passionate and dedicated to solving problems using the
best technologies available.
</Span>
</Stack>
</Section>
);
}
@@ -0,0 +1,159 @@
import Card from "@/components/lib/elements/Card";
import H2 from "@/components/lib/layout/H2";
import H3 from "@/components/lib/layout/H3";
import Section from "@/components/lib/layout/Section";
import Span from "@/components/lib/layout/Span";
import Stack from "@/components/lib/layout/Stack";
import { skills } from "../(data)/skills";
import React from "react";
import Row from "@/components/lib/layout/Row";
import { twMerge } from "tailwind-merge";
import Divider from "@/components/lib/layout/Divider";
type Props = {
noTitle?: boolean;
expand?: boolean;
};
export default function MySkillsSection({ noTitle, expand }: Props) {
const [category, setCategory] =
React.useState<keyof typeof skills>("Devops");
const categories = Object.keys(skills) as (keyof typeof skills)[];
if (expand) {
return (
<Section>
<Stack className="w-full">
{!noTitle && (
<React.Fragment>
<H2 className="leading-snug m-0">My Skills</H2>
<Span>
A summary of the vast array of tools I've
mastered over the years
</Span>
<Divider className="opacity-0 my-2" />
</React.Fragment>
)}
<Stack className="items-stretch gap-20 flex-row xl:flex-col flex-wrap md:flex-nowrap">
{categories.map((ctgr, i) => {
return (
<Stack key={i}>
<Stack className="gap-6">
<Stack className="gap-2">
<H3 className="m-0 text-lg">
{ctgr}
</H3>
<Span className="text-sm !leading-5">
{skills[ctgr].description}
</Span>
</Stack>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 w-full">
{skills[ctgr].portfolio.map(
(portfolio, index) => {
return (
<MySkillsCard
portfolio={
portfolio
}
key={index}
/>
);
}
)}
</div>
</Stack>
</Stack>
);
})}
</Stack>
</Stack>
</Section>
);
}
return (
<Section>
<Stack className="w-full">
{!noTitle && (
<React.Fragment>
<H2 className="leading-snug m-0">My Skills</H2>
<Span>
A summary of the vast array of tools I've mastered
over the years
</Span>
<Divider className="opacity-0 my-2" />
</React.Fragment>
)}
<Row className="flex-nowrap items-start gap-x-10 flex-col xl:flex-row gap-y-10">
<Stack className="xl:max-w-[200px] items-stretch gap-10 flex-row xl:flex-col flex-wrap md:flex-nowrap">
{categories.map((ctgr, i) => {
const isActive = category === ctgr;
return (
<Stack
key={i}
className={twMerge(
"cursor-pointer",
isActive
? ""
: "opacity-40 hover:opacity-70"
)}
onClick={() => setCategory(ctgr)}
>
<Stack className="gap-1">
<H3 className="m-0 text-lg">{ctgr}</H3>
<Span className="text-sm !leading-5">
{skills[ctgr].description}
</Span>
</Stack>
</Stack>
);
})}
</Stack>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 w-full">
{skills[category].portfolio.map((portfolio, index) => {
return (
<MySkillsCard
portfolio={portfolio}
key={index}
/>
);
})}
</div>
</Row>
</Stack>
</Section>
);
}
export function MySkillsCard({
portfolio,
}: {
portfolio: (typeof skills.Devops.portfolio)[number];
}) {
return (
<Card className="grow w-full items-start">
<Stack className="gap-4">
<H3 className="m-0">{portfolio.title}</H3>
<Span>{portfolio.description}</Span>
{portfolio.technologies?.[0] && (
<Row className="gap-4">
{portfolio.technologies.map((tch, _i) => (
<React.Fragment key={_i}>
<Span className="text-sm dark:text-white/40">
{tch}
</Span>
{_i < portfolio.technologies.length - 1 && (
<Divider vertical />
)}
</React.Fragment>
))}
</Row>
)}
</Stack>
</Card>
);
}
@@ -0,0 +1,154 @@
import Card from "@/components/lib/elements/Card";
import H2 from "@/components/lib/layout/H2";
import H3 from "@/components/lib/layout/H3";
import Section from "@/components/lib/layout/Section";
import Span from "@/components/lib/layout/Span";
import Stack from "@/components/lib/layout/Stack";
import { work } from "../(data)/work";
import Link from "@/components/lib/layout/Link";
import React from "react";
import Row from "@/components/lib/layout/Row";
import Divider from "@/components/lib/layout/Divider";
import { twMerge } from "tailwind-merge";
type Props = {
noTitle?: boolean;
expand?: boolean;
};
export default function MyWorkSection({ noTitle, expand }: Props) {
const categories = Object.keys(work) as (keyof typeof work)[];
const [category, setCategory] = React.useState<keyof typeof work>(
categories[0]
);
if (expand) {
return (
<Section>
<Stack className="w-full">
{!noTitle && (
<React.Fragment>
<H2 className="leading-snug m-0">My Work</H2>
<Span>Some work I've done in the past</Span>
<Divider className="opacity-0 my-2" />
</React.Fragment>
)}
<Stack className="items-stretch gap-20 flex-row xl:flex-col flex-wrap md:flex-nowrap">
{categories.map((ctgr, i) => {
const isActive = category === ctgr;
return (
<Stack key={i}>
<Stack className="gap-6">
<H3 className="m-0 text-lg">{ctgr}</H3>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 w-full">
{work[ctgr].portfolio.map(
(portfolio, index) => {
return (
<MyWorkPortfolioCard
portfolio={
portfolio
}
key={index}
/>
);
}
)}
</div>
</Stack>
</Stack>
);
})}
</Stack>
</Stack>
</Section>
);
}
return (
<Section>
<Stack className="w-full">
{!noTitle && (
<React.Fragment>
<H2 className="leading-snug m-0">My Work</H2>
<Span>Some work I've done in the past</Span>
<Divider className="opacity-0 my-2" />
</React.Fragment>
)}
<Row className="flex-nowrap items-start gap-x-14 flex-col xl:flex-row gap-y-10">
<Stack className="xl:max-w-[200px] items-stretch gap-6 flex-row xl:flex-col flex-wrap md:flex-nowrap">
{categories.map((ctgr, i) => {
const isActive = category === ctgr;
return (
<Stack
key={i}
className={twMerge(
"cursor-pointer",
isActive
? ""
: "opacity-40 hover:opacity-70"
)}
onClick={() => setCategory(ctgr)}
>
<Stack className="gap-1">
<H3 className="m-0 text-lg">{ctgr}</H3>
</Stack>
</Stack>
);
})}
</Stack>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 w-full">
{work[category].portfolio.map((portfolio, index) => {
return (
<MyWorkPortfolioCard
portfolio={portfolio}
key={index}
/>
);
})}
</div>
</Row>
</Stack>
</Section>
);
}
export function MyWorkPortfolioCard({
portfolio,
}: {
portfolio: (typeof work.Devops.portfolio)[number];
}) {
return (
<Card className="grow w-full items-start">
<Stack className="gap-4">
<H3 className="m-0">{portfolio.title}</H3>
<Link
target="_blank"
href={portfolio.href}
className="text-sm text-wrap break-all border-none"
>
{portfolio.href}
</Link>
<Span>{portfolio.description}</Span>
{portfolio.technologies?.[0] && (
<Row className="gap-4">
{portfolio.technologies.map((tch, _i) => (
<React.Fragment key={_i}>
<Span className="text-sm dark:text-white/40">
{tch}
</Span>
{_i < portfolio.technologies.length - 1 && (
<Divider vertical />
)}
</React.Fragment>
))}
</Row>
)}
</Stack>
</Card>
);
}
+42
View File
@@ -0,0 +1,42 @@
import Button from "@/components/lib/layout/Button";
import H1 from "@/components/lib/layout/H1";
import Row from "@/components/lib/layout/Row";
import Section from "@/components/lib/layout/Section";
import Span from "@/components/lib/layout/Span";
import Stack from "@/components/lib/layout/Stack";
import { Contact, ScrollText } from "lucide-react";
export default function Main() {
return (
<Section>
<Stack className="w-full max-w-full xl:max-w-[50vw]">
<Span>Howdy Tech Enthusiasts! I'm Benjamin Toby</Span>
<H1 className="leading-snug">
Software Engineer, DevOps Engineer, Full Stack Developer,
Software Architect, Philosopher, Solar Energy Enthusiast.
</H1>
<Row className="items-stretch flex-col md:flex-row w-full md:w-auto">
<Button
beforeIcon={
<Contact size={17} className="font-normal" />
}
href="/contact"
className="grow w-full"
>
Contact Me
</Button>
<Button
beforeIcon={
<ScrollText size={17} className="font-normal" />
}
href="/contact"
variant="outlined"
className="grow w-full"
>
Resume
</Button>
</Row>
</Stack>
</Section>
);
}