diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..d8d2aab
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,11 @@
+# Copy to .env and fill in your SMTP credentials
+# Common options:
+# Gmail: SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_SECURE=false
+# Office365: SMTP_HOST=smtp.office365.com SMTP_PORT=587 SMTP_SECURE=false
+
+SMTP_HOST=smtp.gmail.com
+SMTP_PORT=587
+SMTP_SECURE=false
+SMTP_USER=your-sending-address@gmail.com
+SMTP_PASS=your-app-password
+CONTACT_TO=kenji@kenjim.com
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4624535
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.env
+node_modules/
+frontend/node_modules/
+backend/node_modules/
+frontend/dist/
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..aee0aaa
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:20-alpine
+WORKDIR /app
+COPY package.json .
+RUN npm install --production
+COPY . .
+EXPOSE 3001
+CMD ["node", "server.js"]
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..3e31c51
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "kenjim-technologies-api",
+ "version": "1.0.0",
+ "type": "commonjs",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.19.2",
+ "nodemailer": "^6.9.14"
+ }
+}
diff --git a/backend/server.js b/backend/server.js
new file mode 100644
index 0000000..e645e77
--- /dev/null
+++ b/backend/server.js
@@ -0,0 +1,48 @@
+const express = require('express')
+const nodemailer = require('nodemailer')
+const cors = require('cors')
+
+const app = express()
+app.use(cors())
+app.use(express.json())
+
+const transporter = nodemailer.createTransport({
+ host: process.env.SMTP_HOST,
+ port: parseInt(process.env.SMTP_PORT || '587'),
+ secure: process.env.SMTP_SECURE === 'true',
+ auth: {
+ user: process.env.SMTP_USER,
+ pass: process.env.SMTP_PASS,
+ },
+})
+
+app.post('/contact', async (req, res) => {
+ const { name, email, service, message } = req.body
+
+ if (!name || !email || !message) {
+ return res.status(400).json({ error: 'Name, email, and message are required.' })
+ }
+
+ try {
+ await transporter.sendMail({
+ from: `"KenJim Technologies" <${process.env.SMTP_USER}>`,
+ to: process.env.CONTACT_TO,
+ replyTo: email,
+ subject: `[kenjim.com] ${service || 'General Inquiry'} — ${name}`,
+ text: `Name: ${name}\nEmail: ${email}\nService: ${service || 'General Inquiry'}\n\n${message}`,
+ html: `
+
Name: ${name}
+ Email: ${email}
+ Service: ${service || 'General Inquiry'}
+
+ ${message.replace(/\n/g, ' ')}
+ `,
+ })
+ res.json({ ok: true })
+ } catch (err) {
+ console.error('Mail error:', err)
+ res.status(500).json({ error: 'Failed to send message. Please try again.' })
+ }
+})
+
+app.listen(3001, () => console.log('API listening on :3001'))
diff --git a/docker-compose.yml b/docker-compose.yml
index aeee399..a5937f1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,9 +1,15 @@
services:
- www:
- image: nginx:alpine
- container_name: www-kenjim
+ frontend:
+ build: ./frontend
+ container_name: www-kenjim-frontend
ports:
- "8080:80"
- volumes:
- - ./html:/usr/share/nginx/html:ro
+ depends_on:
+ - api
+ restart: always
+
+ api:
+ build: ./backend
+ container_name: www-kenjim-api
+ env_file: .env
restart: always
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..70f53d5
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,11 @@
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package.json .
+RUN npm install
+COPY . .
+RUN npm run build
+
+FROM nginx:alpine
+COPY --from=build /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+EXPOSE 80
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..76ef2f0
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ KenJim Technologies
+
+
+
+
+
+
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 0000000..3599995
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,18 @@
+server {
+ listen 80;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # SPA routing — return index.html for all non-file routes
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Proxy API calls to the backend container
+ location /api/ {
+ proxy_pass http://api:3001/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..52c47cc
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "kenjim-technologies",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.1",
+ "vite": "^5.4.0"
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..9b4275a
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,19 @@
+import Nav from './components/Nav'
+import Hero from './components/Hero'
+import Services from './components/Services'
+import Contact from './components/Contact'
+import Footer from './components/Footer'
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/components/Contact.jsx b/frontend/src/components/Contact.jsx
new file mode 100644
index 0000000..60651fe
--- /dev/null
+++ b/frontend/src/components/Contact.jsx
@@ -0,0 +1,130 @@
+import { useState } from 'react'
+import styles from './Contact.module.css'
+
+const SERVICES = [
+ 'Enterprise Network Design',
+ 'Security Solutions',
+ 'AI Applications',
+ 'Infrastructure Automation',
+ 'General Inquiry',
+]
+
+const INITIAL = { name: '', email: '', service: '', message: '' }
+
+export default function Contact() {
+ const [form, setForm] = useState(INITIAL)
+ const [status, setStatus] = useState(null) // null | 'sending' | 'ok' | 'error'
+ const [errorMsg, setErrorMsg] = useState('')
+
+ function handleChange(e) {
+ setForm((f) => ({ ...f, [e.target.name]: e.target.value }))
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ setStatus('sending')
+ setErrorMsg('')
+ try {
+ const res = await fetch('/api/contact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(form),
+ })
+ const data = await res.json()
+ if (!res.ok) throw new Error(data.error || 'Unknown error')
+ setStatus('ok')
+ setForm(INITIAL)
+ } catch (err) {
+ setStatus('error')
+ setErrorMsg(err.message)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/components/Contact.module.css b/frontend/src/components/Contact.module.css
new file mode 100644
index 0000000..32c7e85
--- /dev/null
+++ b/frontend/src/components/Contact.module.css
@@ -0,0 +1,110 @@
+.layout {
+ display: grid;
+ grid-template-columns: 1fr 1.4fr;
+ gap: 4rem;
+ align-items: start;
+}
+
+@media (max-width: 768px) {
+ .layout { grid-template-columns: 1fr; gap: 2.5rem; }
+}
+
+.infoText {
+ color: var(--text-muted);
+ line-height: 1.7;
+ margin-top: 1rem;
+ margin-bottom: 2rem;
+}
+
+.contactLabel {
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--text-muted);
+ margin-bottom: 0.25rem;
+}
+
+.contact a {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
+
+.row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+@media (max-width: 500px) {
+ .row { grid-template-columns: 1fr; }
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--text-muted);
+}
+
+.field input,
+.field select,
+.field textarea {
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: 0.375rem;
+ color: var(--text);
+ font-size: 0.95rem;
+ padding: 0.65rem 0.85rem;
+ width: 100%;
+ transition: border-color 0.2s;
+ font-family: inherit;
+ resize: vertical;
+}
+
+.field input:focus,
+.field select:focus,
+.field textarea:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.field select option { background: var(--bg-card); }
+
+.submit {
+ background: var(--accent);
+ color: #080d1a;
+ border: none;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-weight: 700;
+ padding: 0.8rem;
+ cursor: pointer;
+ transition: opacity 0.2s;
+ margin-top: 0.25rem;
+}
+
+.submit:hover:not(:disabled) { opacity: 0.88; }
+.submit:disabled { opacity: 0.5; cursor: not-allowed; }
+
+.success {
+ color: var(--success);
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+.error {
+ color: var(--error);
+ font-size: 0.9rem;
+}
diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx
new file mode 100644
index 0000000..61152ae
--- /dev/null
+++ b/frontend/src/components/Footer.jsx
@@ -0,0 +1,19 @@
+import styles from './Footer.module.css'
+
+export default function Footer() {
+ return (
+
+
+
+ KenJim Technologies
+
+
+ © {new Date().getFullYear()} KenJim Technologies. All rights reserved.
+
+
+ kenji@kenjim.com
+
+
+
+ )
+}
diff --git a/frontend/src/components/Footer.module.css b/frontend/src/components/Footer.module.css
new file mode 100644
index 0000000..e262a4e
--- /dev/null
+++ b/frontend/src/components/Footer.module.css
@@ -0,0 +1,20 @@
+.footer {
+ border-top: 1px solid var(--border);
+ padding: 2rem 1.5rem;
+}
+
+.inner {
+ max-width: 1100px;
+ margin: 0 auto;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.logo { font-weight: 700; font-size: 1rem; }
+.accent { color: var(--accent); }
+.copy { color: var(--text-muted); font-size: 0.875rem; }
+.email { color: var(--text-muted); font-size: 0.875rem; }
+.email:hover { color: var(--accent); }
diff --git a/frontend/src/components/Hero.jsx b/frontend/src/components/Hero.jsx
new file mode 100644
index 0000000..9e9bdf1
--- /dev/null
+++ b/frontend/src/components/Hero.jsx
@@ -0,0 +1,24 @@
+import styles from './Hero.module.css'
+
+export default function Hero() {
+ return (
+
+
+
Enterprise Technology Consulting
+
+ Engineering the Infrastructure
+ that Powers the Future
+
+
+ KenJim Technologies delivers expert consulting in enterprise network design,
+ cybersecurity, AI-driven applications, and infrastructure automation —
+ built for organizations that demand reliability at scale.
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/Hero.module.css b/frontend/src/components/Hero.module.css
new file mode 100644
index 0000000..e7e797d
--- /dev/null
+++ b/frontend/src/components/Hero.module.css
@@ -0,0 +1,58 @@
+.hero {
+ min-height: 90vh;
+ display: flex;
+ align-items: center;
+ padding: 6rem 1.5rem;
+ background:
+ radial-gradient(ellipse 80% 60% at 50% -10%, rgba(56,189,248,0.12) 0%, transparent 70%),
+ var(--bg);
+}
+
+.inner { max-width: 720px; }
+
+.headline {
+ font-size: clamp(2.2rem, 5vw, 3.5rem);
+ font-weight: 800;
+ line-height: 1.15;
+ margin-bottom: 1.5rem;
+}
+
+.accent { color: var(--accent); }
+
+.sub {
+ color: var(--text-muted);
+ font-size: 1.15rem;
+ line-height: 1.7;
+ margin-bottom: 2.5rem;
+ max-width: 600px;
+}
+
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.btnPrimary, .btnSecondary {
+ padding: 0.75rem 1.75rem;
+ border-radius: 0.5rem;
+ font-weight: 600;
+ font-size: 1rem;
+ text-decoration: none;
+ transition: opacity 0.2s;
+}
+
+.btnPrimary {
+ background: var(--accent);
+ color: #080d1a;
+}
+
+.btnPrimary:hover { opacity: 0.88; text-decoration: none; }
+
+.btnSecondary {
+ border: 1px solid var(--border);
+ color: var(--text);
+ background: transparent;
+}
+
+.btnSecondary:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
diff --git a/frontend/src/components/Nav.jsx b/frontend/src/components/Nav.jsx
new file mode 100644
index 0000000..a592a9e
--- /dev/null
+++ b/frontend/src/components/Nav.jsx
@@ -0,0 +1,17 @@
+import styles from './Nav.module.css'
+
+export default function Nav() {
+ return (
+
+ )
+}
diff --git a/frontend/src/components/Nav.module.css b/frontend/src/components/Nav.module.css
new file mode 100644
index 0000000..1dd5f35
--- /dev/null
+++ b/frontend/src/components/Nav.module.css
@@ -0,0 +1,51 @@
+.nav {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ background: rgba(8, 13, 26, 0.85);
+ backdrop-filter: blur(12px);
+ border-bottom: 1px solid var(--border);
+}
+
+.inner {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 1rem 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.logo {
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: var(--text);
+ text-decoration: none;
+}
+
+.logoAccent { color: var(--accent); }
+
+.links {
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+}
+
+.links a {
+ color: var(--text-muted);
+ font-size: 0.95rem;
+ text-decoration: none;
+ transition: color 0.2s;
+}
+
+.links a:hover { color: var(--text); }
+
+.cta {
+ background: var(--accent);
+ color: #080d1a !important;
+ padding: 0.45rem 1.1rem;
+ border-radius: 0.375rem;
+ font-weight: 600;
+}
+
+.cta:hover { background: var(--accent-dim); }
diff --git a/frontend/src/components/Services.jsx b/frontend/src/components/Services.jsx
new file mode 100644
index 0000000..2a77a38
--- /dev/null
+++ b/frontend/src/components/Services.jsx
@@ -0,0 +1,74 @@
+import styles from './Services.module.css'
+
+const services = [
+ {
+ icon: (
+
+
+
+ ),
+ title: 'Enterprise Network Design',
+ description:
+ 'Architecting resilient, high-performance networks for complex enterprise environments. From campus LAN to multi-site WAN, SD-WAN, and hybrid cloud connectivity.',
+ },
+ {
+ icon: (
+
+
+
+ ),
+ title: 'Security Solutions',
+ description:
+ 'End-to-end cybersecurity strategy, assessment, and implementation. Zero-trust architecture, firewall design, intrusion detection, and compliance frameworks.',
+ },
+ {
+ icon: (
+
+
+
+ ),
+ title: 'AI Applications',
+ description:
+ 'Custom AI and machine learning solutions tailored to your business workflows. LLM integration, intelligent automation, data pipelines, and AI-powered decision systems.',
+ },
+ {
+ icon: (
+
+
+
+ ),
+ title: 'Infrastructure Automation',
+ description:
+ 'Eliminate manual operations with Infrastructure as Code, CI/CD pipelines, configuration management, and container orchestration built for scale and reliability.',
+ },
+]
+
+export default function Services() {
+ return (
+
+
+
What We Do
+
Consulting Services
+
+ We bring deep technical expertise across the full stack of enterprise technology.
+
+
+ {services.map((s) => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/frontend/src/components/Services.module.css b/frontend/src/components/Services.module.css
new file mode 100644
index 0000000..a8fca9f
--- /dev/null
+++ b/frontend/src/components/Services.module.css
@@ -0,0 +1,55 @@
+.section {
+ background: var(--bg-card);
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 1.5rem;
+ margin-top: 3rem;
+}
+
+.card {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 0.75rem;
+ padding: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ transition: border-color 0.2s;
+}
+
+.card:hover { border-color: var(--accent); }
+
+.icon {
+ width: 2.5rem;
+ height: 2.5rem;
+ color: var(--accent);
+}
+
+.icon svg { width: 100%; height: 100%; }
+
+.cardTitle {
+ font-size: 1.1rem;
+ font-weight: 700;
+}
+
+.cardDesc {
+ color: var(--text-muted);
+ font-size: 0.95rem;
+ line-height: 1.65;
+ flex: 1;
+}
+
+.cardLink {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--accent);
+ text-decoration: none;
+ margin-top: 0.5rem;
+}
+
+.cardLink:hover { text-decoration: underline; }
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..430546a
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,56 @@
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+:root {
+ --bg: #080d1a;
+ --bg-card: #0f1829;
+ --bg-input: #0a1020;
+ --border: #1e2d45;
+ --accent: #38bdf8;
+ --accent-dim:#0ea5e9;
+ --text: #e2e8f0;
+ --text-muted:#64748b;
+ --success: #22c55e;
+ --error: #ef4444;
+}
+
+html { scroll-behavior: smooth; }
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ font-size: 1rem;
+ line-height: 1.6;
+}
+
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+section { padding: 6rem 1.5rem; }
+
+.container {
+ max-width: 1100px;
+ margin: 0 auto;
+}
+
+.section-label {
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 1rem;
+}
+
+.section-title {
+ font-size: clamp(1.75rem, 4vw, 2.5rem);
+ font-weight: 700;
+ line-height: 1.2;
+ margin-bottom: 1rem;
+}
+
+.section-sub {
+ color: var(--text-muted);
+ max-width: 560px;
+ font-size: 1.05rem;
+}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..e14d575
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+
+
+
+)
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..1279713
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': 'http://localhost:3001'
+ }
+ }
+})
diff --git a/html/index.html b/html/index.html
deleted file mode 100644
index 5d2884c..0000000
--- a/html/index.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
- www.kenjim.com
-
-
-
-
-
Hello World
-
www.kenjim.com
-
-
-