Replace static Hello World with KenJim Technologies React app
- Vite + React frontend: hero, services grid, contact form - Express + nodemailer backend for contact form → kenji@kenjim.com - Multi-stage Docker builds; nginx inside frontend proxies /api to backend - SMTP config via .env (see .env.example) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -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
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
node_modules/
|
||||
frontend/node_modules/
|
||||
backend/node_modules/
|
||||
frontend/dist/
|
||||
7
backend/Dockerfile
Normal file
7
backend/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
RUN npm install --production
|
||||
COPY . .
|
||||
EXPOSE 3001
|
||||
CMD ["node", "server.js"]
|
||||
13
backend/package.json
Normal file
13
backend/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
48
backend/server.js
Normal file
48
backend/server.js
Normal file
@@ -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: `
|
||||
<p><strong>Name:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
|
||||
<p><strong>Service:</strong> ${service || 'General Inquiry'}</p>
|
||||
<hr/>
|
||||
<p>${message.replace(/\n/g, '<br>')}</p>
|
||||
`,
|
||||
})
|
||||
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'))
|
||||
@@ -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
|
||||
|
||||
11
frontend/Dockerfile
Normal file
11
frontend/Dockerfile
Normal file
@@ -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
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="KenJim Technologies — Enterprise Network Design, Security Solutions, AI Applications and Infrastructure Automation." />
|
||||
<title>KenJim Technologies</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
18
frontend/nginx.conf
Normal file
18
frontend/nginx.conf
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
18
frontend/package.json
Normal file
18
frontend/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
19
frontend/src/App.jsx
Normal file
19
frontend/src/App.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Nav />
|
||||
<main>
|
||||
<Hero />
|
||||
<Services />
|
||||
<Contact />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
130
frontend/src/components/Contact.jsx
Normal file
130
frontend/src/components/Contact.jsx
Normal file
@@ -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 (
|
||||
<section id="contact">
|
||||
<div className="container">
|
||||
<div className={styles.layout}>
|
||||
<div className={styles.info}>
|
||||
<p className="section-label">Get in Touch</p>
|
||||
<h2 className="section-title">Start a Conversation</h2>
|
||||
<p className={styles.infoText}>
|
||||
Whether you need a comprehensive network overhaul, a security audit,
|
||||
or want to explore how AI can transform your operations — we'd love
|
||||
to hear from you.
|
||||
</p>
|
||||
<div className={styles.contact}>
|
||||
<p className={styles.contactLabel}>Email us directly</p>
|
||||
<a href="mailto:kenji@kenjim.com">kenji@kenjim.com</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit} noValidate>
|
||||
<div className={styles.row}>
|
||||
<label className={styles.field}>
|
||||
<span>Name</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
placeholder="Your name"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className={styles.field}>
|
||||
<span>Email</span>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
placeholder="you@company.com"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className={styles.field}>
|
||||
<span>Service</span>
|
||||
<select name="service" value={form.service} onChange={handleChange}>
|
||||
<option value="">Select a service…</option>
|
||||
{SERVICES.map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className={styles.field}>
|
||||
<span>Message</span>
|
||||
<textarea
|
||||
name="message"
|
||||
value={form.message}
|
||||
onChange={handleChange}
|
||||
placeholder="Tell us about your project or requirements…"
|
||||
rows={5}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
{status === 'ok' && (
|
||||
<p className={styles.success}>
|
||||
✓ Message sent — we'll be in touch shortly.
|
||||
</p>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<p className={styles.error}>{errorMsg}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.submit}
|
||||
disabled={status === 'sending'}
|
||||
>
|
||||
{status === 'sending' ? 'Sending…' : 'Send Message'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
110
frontend/src/components/Contact.module.css
Normal file
110
frontend/src/components/Contact.module.css
Normal file
@@ -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;
|
||||
}
|
||||
19
frontend/src/components/Footer.jsx
Normal file
19
frontend/src/components/Footer.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import styles from './Footer.module.css'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.inner}>
|
||||
<span className={styles.logo}>
|
||||
<span className={styles.accent}>KenJim</span> Technologies
|
||||
</span>
|
||||
<span className={styles.copy}>
|
||||
© {new Date().getFullYear()} KenJim Technologies. All rights reserved.
|
||||
</span>
|
||||
<a href="mailto:kenji@kenjim.com" className={styles.email}>
|
||||
kenji@kenjim.com
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
20
frontend/src/components/Footer.module.css
Normal file
20
frontend/src/components/Footer.module.css
Normal file
@@ -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); }
|
||||
24
frontend/src/components/Hero.jsx
Normal file
24
frontend/src/components/Hero.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import styles from './Hero.module.css'
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className={styles.hero}>
|
||||
<div className={`container ${styles.inner}`}>
|
||||
<p className="section-label">Enterprise Technology Consulting</p>
|
||||
<h1 className={styles.headline}>
|
||||
Engineering the Infrastructure<br />
|
||||
<span className={styles.accent}>that Powers the Future</span>
|
||||
</h1>
|
||||
<p className={styles.sub}>
|
||||
KenJim Technologies delivers expert consulting in enterprise network design,
|
||||
cybersecurity, AI-driven applications, and infrastructure automation —
|
||||
built for organizations that demand reliability at scale.
|
||||
</p>
|
||||
<div className={styles.actions}>
|
||||
<a href="#contact" className={styles.btnPrimary}>Request a Consultation</a>
|
||||
<a href="#services" className={styles.btnSecondary}>Our Services</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
58
frontend/src/components/Hero.module.css
Normal file
58
frontend/src/components/Hero.module.css
Normal file
@@ -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; }
|
||||
17
frontend/src/components/Nav.jsx
Normal file
17
frontend/src/components/Nav.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import styles from './Nav.module.css'
|
||||
|
||||
export default function Nav() {
|
||||
return (
|
||||
<header className={styles.nav}>
|
||||
<div className={styles.inner}>
|
||||
<a href="#" className={styles.logo}>
|
||||
<span className={styles.logoAccent}>KenJim</span> Technologies
|
||||
</a>
|
||||
<nav className={styles.links}>
|
||||
<a href="#services">Services</a>
|
||||
<a href="#contact" className={styles.cta}>Contact Us</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
51
frontend/src/components/Nav.module.css
Normal file
51
frontend/src/components/Nav.module.css
Normal file
@@ -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); }
|
||||
74
frontend/src/components/Services.jsx
Normal file
74
frontend/src/components/Services.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import styles from './Services.module.css'
|
||||
|
||||
const services = [
|
||||
{
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zm0 0v-9m0 0H3m9 0h9M12 3v9" />
|
||||
</svg>
|
||||
),
|
||||
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: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Security Solutions',
|
||||
description:
|
||||
'End-to-end cybersecurity strategy, assessment, and implementation. Zero-trust architecture, firewall design, intrusion detection, and compliance frameworks.',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25zm.75-12h9v9h-9v-9z" />
|
||||
</svg>
|
||||
),
|
||||
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: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
),
|
||||
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 (
|
||||
<section id="services" className={styles.section}>
|
||||
<div className="container">
|
||||
<p className="section-label">What We Do</p>
|
||||
<h2 className="section-title">Consulting Services</h2>
|
||||
<p className="section-sub">
|
||||
We bring deep technical expertise across the full stack of enterprise technology.
|
||||
</p>
|
||||
<div className={styles.grid}>
|
||||
{services.map((s) => (
|
||||
<div key={s.title} className={styles.card}>
|
||||
<div className={styles.icon}>{s.icon}</div>
|
||||
<h3 className={styles.cardTitle}>{s.title}</h3>
|
||||
<p className={styles.cardDesc}>{s.description}</p>
|
||||
<a href="#contact" className={styles.cardLink}>
|
||||
Request a quote →
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
55
frontend/src/components/Services.module.css
Normal file
55
frontend/src/components/Services.module.css
Normal file
@@ -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; }
|
||||
56
frontend/src/index.css
Normal file
56
frontend/src/index.css
Normal file
@@ -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;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
)
|
||||
11
frontend/vite.config.js
Normal file
11
frontend/vite.config.js
Normal file
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>www.kenjim.com</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #0f172a;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
.card {
|
||||
text-align: center;
|
||||
padding: 3rem 4rem;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 1rem;
|
||||
background: #1e293b;
|
||||
}
|
||||
h1 { margin: 0 0 0.5rem; font-size: 2.5rem; }
|
||||
p { margin: 0; color: #94a3b8; font-size: 1.1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Hello World</h1>
|
||||
<p>www.kenjim.com</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user