feat: initial humanAI landing page
- Astro 6 static site, PL default (/) + EN (/en/) - All copy in src/i18n/pl.json and src/i18n/en.json - Sections: Hero, Problem, HowWeWork, Offers, Process, Contact, Footer - Security section hidden behind SHOW_SECURITY flag - Conservative tone for Industry and Med (human-in-the-loop) - Contact form via mailto prefill - Dockerfile multi-stage (node build -> nginx) - docker-compose.yml on npm_proxy network (NPM visible) - deploy.sh for SATURN -> VPS workflow
This commit is contained in:
commit
fc5741316f
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Stage 1: build
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: serve
|
||||||
|
FROM nginx:alpine AS server
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
74
README.md
Normal file
74
README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# humanAI landing page
|
||||||
|
|
||||||
|
Static bilingual landing page for **humanAI** (gethumanai.com).
|
||||||
|
Stack: Astro 6, static output, i18n PL (default `/`) + EN (`/en/`), nginx in Docker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Host: SATURN
|
||||||
|
npm install
|
||||||
|
npm run dev # dev server at http://localhost:4321
|
||||||
|
npm run build # build to ./dist/
|
||||||
|
npm run preview # preview built site at http://localhost:4321
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker local test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Host: SATURN
|
||||||
|
# Create a local test network if npm_proxy doesn't exist locally
|
||||||
|
docker network create npm_proxy 2>/dev/null || true
|
||||||
|
docker compose up --build
|
||||||
|
# site at http://localhost:80
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy to VPS
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Host: SATURN
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script:
|
||||||
|
1. Commits any pending changes and pushes to Forgejo (`oskar/gethumanai-landing`).
|
||||||
|
2. SSH-es to `ubuntu-4gb-hel1-1`, pulls the repo and runs `docker compose up -d --build`.
|
||||||
|
|
||||||
|
The container joins the `npm_proxy` Docker network. In **Nginx Proxy Manager** on the VPS,
|
||||||
|
create a proxy host: `gethumanai.com` → `http://humanai-landing:80`.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
i18n/
|
||||||
|
pl.json # Polish copy (all text)
|
||||||
|
en.json # English copy (all text)
|
||||||
|
index.ts # translation helpers
|
||||||
|
layouts/
|
||||||
|
Layout.astro # HTML shell, meta/OG/hreflang
|
||||||
|
components/
|
||||||
|
Nav.astro
|
||||||
|
Hero.astro
|
||||||
|
Problem.astro
|
||||||
|
HowWeWork.astro
|
||||||
|
Offers.astro
|
||||||
|
Process.astro
|
||||||
|
Security.astro # hidden on MVP (SHOW_SECURITY flag)
|
||||||
|
Contact.astro
|
||||||
|
Footer.astro
|
||||||
|
pages/
|
||||||
|
index.astro # PL — /
|
||||||
|
en/
|
||||||
|
index.astro # EN — /en/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enabling the Security section
|
||||||
|
|
||||||
|
Open `src/components/Security.astro` and set `SHOW_SECURITY = true`.
|
||||||
|
|
||||||
|
## Swapping the logo
|
||||||
|
|
||||||
|
The logo text and SVG icon are in `src/components/Nav.astro` (`.logo`).
|
||||||
13
astro.config.mjs
Normal file
13
astro.config.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'static',
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'pl',
|
||||||
|
locales: ['pl', 'en'],
|
||||||
|
routing: {
|
||||||
|
prefixDefaultLocale: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
39
deploy.sh
Executable file
39
deploy.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Host: SATURN
|
||||||
|
# Run this script from SATURN to commit, push and deploy to VPS.
|
||||||
|
|
||||||
|
VPS="ubuntu-4gb-hel1-1"
|
||||||
|
REPO_DIR="/home/oskar/projects/gethumanai-landing"
|
||||||
|
VPS_DEPLOY_DIR="/opt/gethumanai-landing"
|
||||||
|
FORGEJO_REMOTE="ssh://git@100.108.208.3:222/oskar/gethumanai-landing.git"
|
||||||
|
|
||||||
|
# ── 1. Commit & push (SATURN) ──────────────────────────────────────────────
|
||||||
|
# Host: SATURN
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: deploy $(date '+%Y-%m-%d %H:%M')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# ── 2. Pull & rebuild on VPS ───────────────────────────────────────────────
|
||||||
|
# Host: ubuntu-4gb-hel1-1
|
||||||
|
ssh "$VPS" bash -s <<'REMOTE'
|
||||||
|
set -euo pipefail
|
||||||
|
DEPLOY_DIR="/opt/gethumanai-landing"
|
||||||
|
|
||||||
|
if [ ! -d "$DEPLOY_DIR/.git" ]; then
|
||||||
|
git clone ssh://git@100.108.208.3:222/oskar/gethumanai-landing.git "$DEPLOY_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
git pull origin main
|
||||||
|
docker compose up -d --build --remove-orphans
|
||||||
|
echo "Deploy complete."
|
||||||
|
REMOTE
|
||||||
|
|
||||||
|
echo "Done — humanAI landing is live."
|
||||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
services:
|
||||||
|
humanai-landing:
|
||||||
|
build: .
|
||||||
|
container_name: humanai-landing
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- npm_proxy
|
||||||
|
# Port is NOT exposed to host — traffic goes through Nginx Proxy Manager only.
|
||||||
|
# In NPM, proxy host → http://humanai-landing:80
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm_proxy:
|
||||||
|
external: true
|
||||||
29
nginx.conf
Normal file
29
nginx.conf
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ $uri.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|woff2?|ttf|svg|ico|png|jpg|webp)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
}
|
||||||
4761
package-lock.json
generated
Normal file
4761
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "gethumanai-landing",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^6.4.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 655 B |
5
public/favicon.svg
Normal file
5
public/favicon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<rect width="32" height="32" rx="7" fill="#1a3d2e"/>
|
||||||
|
<circle cx="16" cy="16" r="8.5" stroke="white" stroke-width="1.4"/>
|
||||||
|
<path d="M16 7.5 A8.5 8.5 0 0 1 16 24.5" stroke="white" stroke-width="1.4" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 296 B |
176
src/components/Contact.astro
Normal file
176
src/components/Contact.astro
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
const contactId = locale === 'en' ? 'contact' : 'kontakt';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="contact-section" id={contactId} aria-labelledby="contact-heading">
|
||||||
|
<div class="container contact-inner">
|
||||||
|
<div class="contact-intro">
|
||||||
|
<span class="section-label" aria-hidden="true">
|
||||||
|
{locale === 'en' ? 'Contact' : 'Kontakt'}
|
||||||
|
</span>
|
||||||
|
<h2 id="contact-heading">{tr.contact.h2}</h2>
|
||||||
|
<p>{tr.contact.subtitle}</p>
|
||||||
|
<a href={`mailto:${tr.contact.email}`} class="contact-email">
|
||||||
|
{tr.contact.email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-form-wrap">
|
||||||
|
<form
|
||||||
|
class="contact-form"
|
||||||
|
id="contact-form"
|
||||||
|
aria-label={locale === 'en' ? 'Contact form' : 'Formularz kontaktowy'}
|
||||||
|
novalidate
|
||||||
|
>
|
||||||
|
<div class="field">
|
||||||
|
<label for="name">{tr.contact.field_name}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
autocomplete="name"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="email">{tr.contact.field_email}</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="message">{tr.contact.field_message}</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
rows="5"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary submit-btn">
|
||||||
|
{tr.contact.button}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script define:vars={{ email: tr.contact.email, locale }}>
|
||||||
|
// TODO: podłączyć backend — na razie mailto prefill
|
||||||
|
const form = document.getElementById('contact-form');
|
||||||
|
form?.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const fd = new FormData(form);
|
||||||
|
const name = fd.get('name') || '';
|
||||||
|
const emailVal = fd.get('email') || '';
|
||||||
|
const message = fd.get('message') || '';
|
||||||
|
const subject = locale === 'en'
|
||||||
|
? `Message from ${name} — humanAI`
|
||||||
|
: `Wiadomość od ${name} — humanAI`;
|
||||||
|
const body = locale === 'en'
|
||||||
|
? `From: ${name}\nEmail: ${emailVal}\n\n${message}`
|
||||||
|
: `Od: ${name}\nE-mail: ${emailVal}\n\n${message}`;
|
||||||
|
window.location.href = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.contact-section {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 3rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.contact-inner {
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-intro h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-intro p {
|
||||||
|
max-width: 38ch;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-email {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-email:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.7rem 0.9rem;
|
||||||
|
border: 1.5px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
width: 100%;
|
||||||
|
transition: border-color 0.18s;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
src/components/Footer.astro
Normal file
100
src/components/Footer.astro
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="footer" role="contentinfo">
|
||||||
|
<div class="container footer-inner">
|
||||||
|
<div class="footer-brand">
|
||||||
|
<span class="footer-logo">human<span class="logo-ai">AI</span></span>
|
||||||
|
<p class="footer-tagline">{tr.footer.tagline}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="footer-nav" aria-label={locale === 'en' ? 'Footer navigation' : 'Nawigacja stopki'}>
|
||||||
|
<ul role="list">
|
||||||
|
{tr.footer.links.map((link) => (
|
||||||
|
<li><a href={link.anchor}>{link.label}</a></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<div class="container">
|
||||||
|
<small>© {year} {tr.footer.copyright}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.footer {
|
||||||
|
background: var(--color-text);
|
||||||
|
color: #e8e8e4;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
padding-block: 3rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.footer-inner {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-ai {
|
||||||
|
color: #7fc5a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-tagline {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
max-width: 24ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav a {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.08);
|
||||||
|
padding-block: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom small {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgba(255,255,255,0.35);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
152
src/components/Hero.astro
Normal file
152
src/components/Hero.astro
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
const contactAnchor = locale === 'en' ? '#contact' : '#kontakt';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="hero" id="home" aria-labelledby="hero-heading">
|
||||||
|
<div class="container hero-inner">
|
||||||
|
<div class="hero-content">
|
||||||
|
<span class="hero-badge" role="text">{tr.hero.badge}</span>
|
||||||
|
<h1 id="hero-heading">{tr.hero.h1}</h1>
|
||||||
|
<p class="hero-sub">{tr.hero.subtitle}</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href={contactAnchor} class="btn-primary">{tr.hero.cta_primary}</a>
|
||||||
|
<a href={locale === 'en' ? '#offers' : '#oferty'} class="btn-outline">{tr.hero.cta_secondary}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pillars-row" role="list" aria-label={locale === 'en' ? 'Our four focus areas' : 'Cztery obszary zastosowań'}>
|
||||||
|
{(['home','business','industry','med'] as const).map((key) => (
|
||||||
|
<a
|
||||||
|
href={locale === 'en' ? `#${key}` : `#${key}`}
|
||||||
|
class={`pillar pillar-${key}`}
|
||||||
|
role="listitem"
|
||||||
|
aria-label={`${tr.pillars[key].title} — ${tr.pillars[key].tagline}`}
|
||||||
|
>
|
||||||
|
<span class="pillar-icon" aria-hidden="true">
|
||||||
|
{key === 'home' && (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{key === 'business' && (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{key === 'industry' && (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M2 20h20M4 20V10l5-5 5 5V20M14 20V14h6v6"/><line x1="9" y1="20" x2="9" y2="14"/><line x1="12" y1="20" x2="12" y2="14"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{key === 'med' && (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span class="pillar-title">{tr.pillars[key].title}</span>
|
||||||
|
<span class="pillar-tagline">{tr.pillars[key].tagline}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
padding-block: clamp(4rem, 10vw, 8rem) clamp(2rem, 5vw, 4rem);
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
max-width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
background: var(--color-surface-alt);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2.25rem, 7vw, 4rem);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub {
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.2rem);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
max-width: 52ch;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillars-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
transition: border-color 0.18s, box-shadow 0.18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 2px 12px rgba(26, 61, 46, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar-icon {
|
||||||
|
color: var(--color-accent);
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar-tagline {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
102
src/components/HowWeWork.astro
Normal file
102
src/components/HowWeWork.astro
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
key: 'local',
|
||||||
|
data: tr.how.local,
|
||||||
|
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/>
|
||||||
|
<line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'integrations',
|
||||||
|
data: tr.how.integrations,
|
||||||
|
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
|
||||||
|
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'human',
|
||||||
|
data: tr.how.human,
|
||||||
|
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="9" cy="7" r="4"/>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="how-section" aria-labelledby="how-heading">
|
||||||
|
<div class="container">
|
||||||
|
<span class="section-label" aria-hidden="true">
|
||||||
|
{locale === 'en' ? 'Our approach' : 'Nasze podejście'}
|
||||||
|
</span>
|
||||||
|
<h2 id="how-heading">{tr.how.h2}</h2>
|
||||||
|
|
||||||
|
<div class="features-grid" role="list">
|
||||||
|
{features.map(({ data, icon }) => (
|
||||||
|
<article class="feature-card" role="listitem">
|
||||||
|
<div class="feature-icon" aria-hidden="true" set:html={icon} />
|
||||||
|
<h3>{data.title}</h3>
|
||||||
|
<p>{data.body}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.how-section {
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.how-section h2 {
|
||||||
|
max-width: 40ch;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: 1.75rem;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
color: var(--color-accent);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 32ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
194
src/components/Nav.astro
Normal file
194
src/components/Nav.astro
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t, getAlternateUrl } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
locale: Locale;
|
||||||
|
url: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { locale, url } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
const altLocale: Locale = locale === 'pl' ? 'en' : 'pl';
|
||||||
|
const altUrl = getAlternateUrl(url, altLocale);
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="nav-wrap">
|
||||||
|
<nav class="container nav-inner" aria-label="Nawigacja główna">
|
||||||
|
<a href={locale === 'en' ? '/en/' : '/'} class="logo" aria-label="humanAI — strona główna">
|
||||||
|
<span class="logo-text">human<span class="logo-ai">AI</span></span>
|
||||||
|
<span class="logo-icon" aria-hidden="true">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<circle cx="10" cy="10" r="8.5" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
<path d="M10 1.5 A8.5 8.5 0 0 1 10 18.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="nav-toggle" id="nav-toggle" aria-expanded="false" aria-controls="nav-menu" aria-label="Otwórz menu">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul class="nav-links" id="nav-menu" role="list">
|
||||||
|
<li><a href={locale === 'en' ? '/en/#home' : '/#home'}>{tr.nav.home}</a></li>
|
||||||
|
<li><a href={locale === 'en' ? '/en/#business' : '/#business'}>{tr.nav.business}</a></li>
|
||||||
|
<li><a href={locale === 'en' ? '/en/#industry' : '/#industry'}>{tr.nav.industry}</a></li>
|
||||||
|
<li><a href={locale === 'en' ? '/en/#med' : '/#med'}>{tr.nav.med}</a></li>
|
||||||
|
<li><a href={locale === 'en' ? '/en/#contact' : '/#kontakt'}>{tr.nav.contact}</a></li>
|
||||||
|
<li class="lang-item">
|
||||||
|
<a href={altUrl} class="lang-switch" hreflang={altLocale} lang={altLocale}>
|
||||||
|
{tr.nav.lang_switch}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const toggle = document.getElementById('nav-toggle');
|
||||||
|
const menu = document.getElementById('nav-menu');
|
||||||
|
toggle?.addEventListener('click', () => {
|
||||||
|
const open = toggle.getAttribute('aria-expanded') === 'true';
|
||||||
|
toggle.setAttribute('aria-expanded', String(!open));
|
||||||
|
menu?.classList.toggle('is-open', !open);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-wrap {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(250, 250, 248, 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-ai {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
color: var(--color-accent);
|
||||||
|
opacity: 0.7;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-surface-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-item {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-switch {
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: var(--color-accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle span {
|
||||||
|
display: block;
|
||||||
|
width: 22px;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-text);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: transform 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle[aria-expanded="true"] span:nth-child(1) {
|
||||||
|
transform: translateY(6px) rotate(45deg);
|
||||||
|
}
|
||||||
|
.nav-toggle[aria-expanded="true"] span:nth-child(2) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.nav-toggle[aria-expanded="true"] span:nth-child(3) {
|
||||||
|
transform: translateY(-6px) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.nav-toggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 3.5rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding: 0.75rem 1.25rem 1rem;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links.is-open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-item {
|
||||||
|
margin-left: 0;
|
||||||
|
border-left: none;
|
||||||
|
padding-left: 0;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
167
src/components/Offers.astro
Normal file
167
src/components/Offers.astro
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
|
||||||
|
const offerKeys = ['home', 'business', 'industry', 'med'] as const;
|
||||||
|
|
||||||
|
const icons: Record<typeof offerKeys[number], string> = {
|
||||||
|
home: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
|
||||||
|
</svg>`,
|
||||||
|
business: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
|
||||||
|
<line x1="12" y1="12" x2="12" y2="17"/><line x1="9.5" y1="14.5" x2="14.5" y2="14.5"/>
|
||||||
|
</svg>`,
|
||||||
|
industry: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||||
|
</svg>`,
|
||||||
|
med: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||||
|
</svg>`,
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="offers-section" id="oferty" aria-labelledby="offers-heading">
|
||||||
|
<div class="container">
|
||||||
|
<span class="section-label" aria-hidden="true">
|
||||||
|
{locale === 'en' ? 'Solutions' : 'Oferta'}
|
||||||
|
</span>
|
||||||
|
<h2 id="offers-heading">{tr.offers.h2}</h2>
|
||||||
|
|
||||||
|
<div class="offers-grid">
|
||||||
|
{offerKeys.map((key) => {
|
||||||
|
const offer = tr.offers[key];
|
||||||
|
const isConservative = 'conservative' in offer && offer.conservative;
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
class={`offer-card offer-${key}`}
|
||||||
|
id={key}
|
||||||
|
aria-labelledby={`offer-${key}-title`}
|
||||||
|
>
|
||||||
|
<div class="offer-header">
|
||||||
|
<div class="offer-icon" aria-hidden="true" set:html={icons[key]} />
|
||||||
|
<div>
|
||||||
|
<h3 id={`offer-${key}-title`}>{offer.title}</h3>
|
||||||
|
<p class="offer-who">{offer.who}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="offer-desc">{offer.desc}</p>
|
||||||
|
{isConservative && (
|
||||||
|
<p class="conservative-note" role="note">
|
||||||
|
{locale === 'en'
|
||||||
|
? 'Advisory only — all outputs require human verification before any action.'
|
||||||
|
: 'Rola wyłącznie doradcza — każdy wynik wymaga weryfikacji przez człowieka przed podjęciem działania.'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<blockquote class="offer-example">
|
||||||
|
<p>{offer.example}</p>
|
||||||
|
</blockquote>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.offers-section {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.offers-section h2 {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offers-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.offers-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-card.offer-industry,
|
||||||
|
.offer-card.offer-med {
|
||||||
|
border-top: 3px solid var(--color-industry);
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-icon {
|
||||||
|
color: var(--color-accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-industry .offer-icon,
|
||||||
|
.offer-med .offer-icon {
|
||||||
|
color: var(--color-industry);
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-card h3 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-who {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conservative-note {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-industry);
|
||||||
|
background: rgba(44, 62, 80, 0.05);
|
||||||
|
border-left: 3px solid var(--color-industry);
|
||||||
|
padding: 0.6rem 0.85rem;
|
||||||
|
border-radius: 0 var(--radius) var(--radius) 0;
|
||||||
|
max-width: none;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-example {
|
||||||
|
background: var(--color-surface-alt);
|
||||||
|
border-left: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-example p {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/components/Problem.astro
Normal file
95
src/components/Problem.astro
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="problem-section" aria-labelledby="problem-heading">
|
||||||
|
<div class="container problem-inner">
|
||||||
|
<div class="problem-text">
|
||||||
|
<span class="section-label" aria-hidden="true">
|
||||||
|
{locale === 'en' ? 'The challenge' : 'Wyzwanie'}
|
||||||
|
</span>
|
||||||
|
<h2 id="problem-heading">{tr.problem.h2}</h2>
|
||||||
|
<p>{tr.problem.body}</p>
|
||||||
|
</div>
|
||||||
|
<div class="problem-visual" aria-hidden="true">
|
||||||
|
<div class="contrast-list">
|
||||||
|
<div class="contrast-bad">
|
||||||
|
<span class="contrast-icon">✗</span>
|
||||||
|
<span>{locale === 'en' ? 'Generic chatbot in the cloud' : 'Generyczny chatbot w chmurze'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-bad">
|
||||||
|
<span class="contrast-icon">✗</span>
|
||||||
|
<span>{locale === 'en' ? 'Data sent to third parties' : 'Dane wysyłane na zewnątrz'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-bad">
|
||||||
|
<span class="contrast-icon">✗</span>
|
||||||
|
<span>{locale === 'en' ? 'No integration with your tools' : 'Brak integracji z Twoimi narzędziami'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-bad">
|
||||||
|
<span class="contrast-icon">✗</span>
|
||||||
|
<span>{locale === 'en' ? 'One more tool to manage' : 'Kolejne narzędzie do obsługi'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.problem-section {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.problem-inner {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-text h2 {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-text p {
|
||||||
|
max-width: 48ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-bad {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.9rem 1.1rem;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-icon {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #b06060;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
102
src/components/Process.astro
Normal file
102
src/components/Process.astro
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="process-section" aria-labelledby="process-heading">
|
||||||
|
<div class="container">
|
||||||
|
<span class="section-label" aria-hidden="true">
|
||||||
|
{locale === 'en' ? 'Process' : 'Proces'}
|
||||||
|
</span>
|
||||||
|
<h2 id="process-heading">{tr.process.h2}</h2>
|
||||||
|
|
||||||
|
<ol class="steps" aria-label={locale === 'en' ? 'How we start — 3 steps' : 'Jak zaczynamy — 3 kroki'}>
|
||||||
|
{tr.process.steps.map((step, i) => (
|
||||||
|
<li class="step">
|
||||||
|
<div class="step-number" aria-hidden="true">{i + 1}</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>{step.title}</h3>
|
||||||
|
<p>{step.body}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.process-section {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-section h2 {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
max-width: 600px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 1.1rem;
|
||||||
|
top: 2.5rem;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:last-child::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content h3 {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content p {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
max-width: 44ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
src/components/Security.astro
Normal file
35
src/components/Security.astro
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props { locale: Locale }
|
||||||
|
const { locale } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
|
||||||
|
// Flag to show/hide this section on MVP
|
||||||
|
const SHOW_SECURITY = false;
|
||||||
|
---
|
||||||
|
|
||||||
|
{SHOW_SECURITY && (
|
||||||
|
<section class="security-section" aria-labelledby="security-heading">
|
||||||
|
<div class="container">
|
||||||
|
<span class="section-label" aria-hidden="true">
|
||||||
|
{locale === 'en' ? 'Security' : 'Bezpieczeństwo'}
|
||||||
|
</span>
|
||||||
|
<h2 id="security-heading">{tr.security.h2}</h2>
|
||||||
|
<p>{tr.security.body}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.security-section {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-section p {
|
||||||
|
max-width: 56ch;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
133
src/i18n/en.json
Normal file
133
src/i18n/en.json
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"dir": "ltr",
|
||||||
|
"meta": {
|
||||||
|
"title": "humanAI — the human side of AI",
|
||||||
|
"description": "Private, local AI systems deeply integrated with your environment. humanAI Home, Business, Industry and Med.",
|
||||||
|
"og_title": "humanAI — the human side of AI",
|
||||||
|
"og_description": "We build AI systems with a human in the decision loop. Private, local, deeply integrated."
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"business": "Business",
|
||||||
|
"industry": "Industry",
|
||||||
|
"med": "Med",
|
||||||
|
"contact": "Contact",
|
||||||
|
"lang_switch": "PL"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"badge": "Private, local AI systems",
|
||||||
|
"h1": "The human side of AI",
|
||||||
|
"subtitle": "We build AI systems deeply integrated with your environment, always with a human in the decision loop. Not another chatbot.",
|
||||||
|
"cta_primary": "Let's talk",
|
||||||
|
"cta_secondary": "See what we do"
|
||||||
|
},
|
||||||
|
"pillars": {
|
||||||
|
"home": {
|
||||||
|
"title": "Home",
|
||||||
|
"tagline": "Smart home with voice and automation"
|
||||||
|
},
|
||||||
|
"business": {
|
||||||
|
"title": "Business",
|
||||||
|
"tagline": "Knowledge, email, docs for SMBs"
|
||||||
|
},
|
||||||
|
"industry": {
|
||||||
|
"title": "Industry",
|
||||||
|
"tagline": "Telemetry and alerts for manufacturing"
|
||||||
|
},
|
||||||
|
"med": {
|
||||||
|
"title": "Med",
|
||||||
|
"tagline": "Clinical admin with human oversight"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problem": {
|
||||||
|
"h2": "AI promises a lot. Most deployments disappoint.",
|
||||||
|
"body": "Generic cloud chatbots don't know your context, send your data elsewhere, and don't integrate with what you actually use. The result: one more tool to manage, instead of help that works in the background."
|
||||||
|
},
|
||||||
|
"how": {
|
||||||
|
"h2": "We do this differently",
|
||||||
|
"local": {
|
||||||
|
"title": "Local and private",
|
||||||
|
"body": "Your data stays with you. Models run on your infrastructure, with no sensitive information sent to the cloud."
|
||||||
|
},
|
||||||
|
"integrations": {
|
||||||
|
"title": "Deep integrations",
|
||||||
|
"body": "We connect AI to the systems you actually use — from smart home (KNX, Home Assistant) to business tools."
|
||||||
|
},
|
||||||
|
"human": {
|
||||||
|
"title": "Human in the loop",
|
||||||
|
"body": "AI advises and prepares; people decide. Especially where the stakes are high."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"offers": {
|
||||||
|
"h2": "Four areas of application",
|
||||||
|
"home": {
|
||||||
|
"title": "humanAI Home",
|
||||||
|
"who": "Smart homes and private clients",
|
||||||
|
"desc": "AI on top of your smart home. Automations described in plain language, integrations with KNX, Loxone, FIBARO and Home Assistant.",
|
||||||
|
"example": "\"Set the house for the evening\" — lights, heating and music adapted to the time and your habits."
|
||||||
|
},
|
||||||
|
"business": {
|
||||||
|
"title": "humanAI Business",
|
||||||
|
"who": "Small and medium businesses",
|
||||||
|
"desc": "An AI knowledge base, an email and document assistant, and simple CRM/ERP workflows — all grounded in your own data.",
|
||||||
|
"example": "An assistant that knows your procedures, answers staff questions, and drafts replies to customer emails."
|
||||||
|
},
|
||||||
|
"industry": {
|
||||||
|
"title": "humanAI Industry",
|
||||||
|
"who": "Manufacturing plants",
|
||||||
|
"desc": "Telemetry dashboards, anomaly detection and a maintenance assistant. Advisory by design — the system flags, the team decides.",
|
||||||
|
"example": "An early warning about unusual machine behavior, with suggested causes to verify.",
|
||||||
|
"conservative": true
|
||||||
|
},
|
||||||
|
"med": {
|
||||||
|
"title": "humanAI Med",
|
||||||
|
"who": "Clinics and hospitals",
|
||||||
|
"desc": "Administrative AI — billing code classification and document workflow automation. Every output goes through human review.",
|
||||||
|
"example": "A draft billing-code suggestion, prepared for staff approval.",
|
||||||
|
"conservative": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"h2": "How we start",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"title": "A conversation",
|
||||||
|
"body": "We learn your context, systems and goal. No commitment."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "A pilot",
|
||||||
|
"body": "We deploy one narrow, measurable use case."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Rollout",
|
||||||
|
"body": "We scale what works and integrate it with the rest."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"h2": "Security built in from the start",
|
||||||
|
"body": "Local data, TLS encryption, admin access over a private network. We build so sensitive information never leaves your infrastructure."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"h2": "Let's talk about your case",
|
||||||
|
"subtitle": "Tell us what you need — we'll come back with a concrete proposal.",
|
||||||
|
"field_name": "Name",
|
||||||
|
"field_email": "Email",
|
||||||
|
"field_message": "Message",
|
||||||
|
"button": "Get in touch",
|
||||||
|
"email": "oskar@gethumanai.com"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"tagline": "The human side of AI",
|
||||||
|
"links": [
|
||||||
|
{ "label": "Home", "anchor": "#home" },
|
||||||
|
{ "label": "Business", "anchor": "#business" },
|
||||||
|
{ "label": "Industry", "anchor": "#industry" },
|
||||||
|
{ "label": "Med", "anchor": "#med" },
|
||||||
|
{ "label": "Contact", "anchor": "#contact" },
|
||||||
|
{ "label": "Privacy policy", "anchor": "/en/privacy-policy" }
|
||||||
|
],
|
||||||
|
"copyright": "humanAI"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/i18n/index.ts
Normal file
28
src/i18n/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import pl from './pl.json';
|
||||||
|
import en from './en.json';
|
||||||
|
|
||||||
|
export type Locale = 'pl' | 'en';
|
||||||
|
|
||||||
|
const translations = { pl, en } as const;
|
||||||
|
|
||||||
|
export function t(locale: Locale) {
|
||||||
|
return translations[locale];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLangFromUrl(url: URL): Locale {
|
||||||
|
const [, first] = url.pathname.split('/');
|
||||||
|
if (first === 'en') return 'en';
|
||||||
|
return 'pl';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAlternateUrl(url: URL, targetLocale: Locale): string {
|
||||||
|
const path = url.pathname;
|
||||||
|
if (targetLocale === 'en') {
|
||||||
|
if (path.startsWith('/en')) return path;
|
||||||
|
return '/en' + (path === '/' ? '/' : path);
|
||||||
|
} else {
|
||||||
|
if (path.startsWith('/en/')) return path.slice(3) || '/';
|
||||||
|
if (path === '/en') return '/';
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/i18n/pl.json
Normal file
133
src/i18n/pl.json
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
{
|
||||||
|
"lang": "pl",
|
||||||
|
"dir": "ltr",
|
||||||
|
"meta": {
|
||||||
|
"title": "humanAI — ludzka strona AI",
|
||||||
|
"description": "Prywatne, lokalne systemy AI głęboko zintegrowane z Twoim otoczeniem. humanAI Home, Business, Industry i Med.",
|
||||||
|
"og_title": "humanAI — ludzka strona AI",
|
||||||
|
"og_description": "Budujemy systemy AI z człowiekiem w pętli decyzji. Prywatne, lokalne, głęboko zintegrowane."
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"business": "Business",
|
||||||
|
"industry": "Industry",
|
||||||
|
"med": "Med",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"lang_switch": "EN"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"badge": "Prywatne, lokalne systemy AI",
|
||||||
|
"h1": "Ludzka strona AI",
|
||||||
|
"subtitle": "Budujemy systemy AI głęboko zintegrowane z Twoim otoczeniem i zawsze z człowiekiem w pętli decyzji. Nie kolejny chatbot.",
|
||||||
|
"cta_primary": "Porozmawiajmy",
|
||||||
|
"cta_secondary": "Zobacz, co robimy"
|
||||||
|
},
|
||||||
|
"pillars": {
|
||||||
|
"home": {
|
||||||
|
"title": "Home",
|
||||||
|
"tagline": "Smart home z głosem i automatyzacją"
|
||||||
|
},
|
||||||
|
"business": {
|
||||||
|
"title": "Business",
|
||||||
|
"tagline": "Wiedza, maile, dokumenty dla MŚP"
|
||||||
|
},
|
||||||
|
"industry": {
|
||||||
|
"title": "Industry",
|
||||||
|
"tagline": "Telemetria i alerty dla produkcji"
|
||||||
|
},
|
||||||
|
"med": {
|
||||||
|
"title": "Med",
|
||||||
|
"tagline": "Administracja kliniczna z nadzorem człowieka"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problem": {
|
||||||
|
"h2": "AI obiecuje dużo. Większość wdrożeń rozczarowuje.",
|
||||||
|
"body": "Generyczne chatboty w chmurze nie znają Twojego kontekstu, wysyłają dane na zewnątrz i nie integrują się z tym, czego naprawdę używasz. Efekt: kolejne narzędzie do obsługi, zamiast pomocy, która działa w tle."
|
||||||
|
},
|
||||||
|
"how": {
|
||||||
|
"h2": "Inaczej do tego podchodzimy",
|
||||||
|
"local": {
|
||||||
|
"title": "Lokalnie i prywatnie",
|
||||||
|
"body": "Twoje dane zostają u Ciebie. Modele działają na Twojej infrastrukturze, bez wysyłania wrażliwych informacji do chmury."
|
||||||
|
},
|
||||||
|
"integrations": {
|
||||||
|
"title": "Głębokie integracje",
|
||||||
|
"body": "Łączymy AI z systemami, których realnie używasz — od inteligentnego domu (KNX, Home Assistant) po narzędzia firmowe."
|
||||||
|
},
|
||||||
|
"human": {
|
||||||
|
"title": "Człowiek w pętli",
|
||||||
|
"body": "AI doradza i przygotowuje, decyzję podejmuje człowiek. Szczególnie tam, gdzie stawka jest wysoka."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"offers": {
|
||||||
|
"h2": "Cztery obszary zastosowań",
|
||||||
|
"home": {
|
||||||
|
"title": "humanAI Home",
|
||||||
|
"who": "Inteligentne domy i klienci prywatni",
|
||||||
|
"desc": "AI nad Twoim systemem smart home. Automatyzacje opisane zwykłym językiem, integracje z KNX, Loxone, FIBARO i Home Assistant.",
|
||||||
|
"example": "\"Przygotuj dom na wieczór\" — światło, ogrzewanie i muzyka dopasowane do pory i Twoich nawyków."
|
||||||
|
},
|
||||||
|
"business": {
|
||||||
|
"title": "humanAI Business",
|
||||||
|
"who": "Małe i średnie firmy",
|
||||||
|
"desc": "Baza wiedzy AI, asystent do maili i dokumentów oraz proste workflow CRM/ERP — wszystko oparte o Twoje dane.",
|
||||||
|
"example": "Asystent, który zna Twoje procedury, odpowiada pracownikom i przygotowuje odpowiedzi na maile klientów."
|
||||||
|
},
|
||||||
|
"industry": {
|
||||||
|
"title": "humanAI Industry",
|
||||||
|
"who": "Zakłady produkcyjne",
|
||||||
|
"desc": "Pulpity telemetrii, wykrywanie anomalii i asystent utrzymania ruchu. Rola doradcza — system sygnalizuje, decyzje należą do zespołu.",
|
||||||
|
"example": "Wczesne ostrzeżenie o nietypowym zachowaniu maszyny, z podpowiedzią możliwych przyczyn do weryfikacji.",
|
||||||
|
"conservative": true
|
||||||
|
},
|
||||||
|
"med": {
|
||||||
|
"title": "humanAI Med",
|
||||||
|
"who": "Kliniki i szpitale",
|
||||||
|
"desc": "AI administracyjne — klasyfikacja kodów rozliczeniowych i automatyzacja obiegu dokumentów. Każdy wynik przechodzi weryfikację człowieka.",
|
||||||
|
"example": "Wstępna propozycja kodowania rozliczeń, przygotowana do zatwierdzenia przez personel.",
|
||||||
|
"conservative": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"h2": "Jak zaczynamy",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"title": "Rozmowa",
|
||||||
|
"body": "Poznajemy Twój kontekst, systemy i cel. Bez zobowiązań."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pilotaż",
|
||||||
|
"body": "Wdrażamy jeden wąski, mierzalny przypadek użycia."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Wdrożenie",
|
||||||
|
"body": "Rozszerzamy to, co działa, i integrujemy z resztą."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"h2": "Bezpieczeństwo wbudowane od początku",
|
||||||
|
"body": "Dane lokalnie, szyfrowanie TLS, dostęp administracyjny przez prywatną sieć. Budujemy tak, żeby wrażliwe informacje nie opuszczały Twojej infrastruktury."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"h2": "Porozmawiajmy o Twoim przypadku",
|
||||||
|
"subtitle": "Opowiedz, czego potrzebujesz — odezwiemy się z konkretną propozycją.",
|
||||||
|
"field_name": "Imię",
|
||||||
|
"field_email": "E-mail",
|
||||||
|
"field_message": "Wiadomość",
|
||||||
|
"button": "Napisz do nas",
|
||||||
|
"email": "oskar@gethumanai.com"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"tagline": "Ludzka strona AI",
|
||||||
|
"links": [
|
||||||
|
{ "label": "Home", "anchor": "#home" },
|
||||||
|
{ "label": "Business", "anchor": "#business" },
|
||||||
|
{ "label": "Industry", "anchor": "#industry" },
|
||||||
|
{ "label": "Med", "anchor": "#med" },
|
||||||
|
{ "label": "Kontakt", "anchor": "#kontakt" },
|
||||||
|
{ "label": "Polityka prywatności", "anchor": "/polityka-prywatnosci" }
|
||||||
|
],
|
||||||
|
"copyright": "humanAI"
|
||||||
|
}
|
||||||
|
}
|
||||||
193
src/layouts/Layout.astro
Normal file
193
src/layouts/Layout.astro
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
---
|
||||||
|
import type { Locale } from '../i18n/index.ts';
|
||||||
|
import { t, getAlternateUrl } from '../i18n/index.ts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
locale: Locale;
|
||||||
|
url: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { locale, url } = Astro.props;
|
||||||
|
const tr = t(locale);
|
||||||
|
const altLocale: Locale = locale === 'pl' ? 'en' : 'pl';
|
||||||
|
const altUrl = getAlternateUrl(url, altLocale);
|
||||||
|
const canonicalUrl = url.href;
|
||||||
|
const plUrl = locale === 'pl' ? url.pathname : getAlternateUrl(url, 'pl');
|
||||||
|
const enUrl = locale === 'en' ? url.pathname : getAlternateUrl(url, 'en');
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={locale} dir={tr.dir}>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{tr.meta.title}</title>
|
||||||
|
<meta name="description" content={tr.meta.description} />
|
||||||
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
<link rel="alternate" hreflang="pl" href={`https://gethumanai.com${plUrl}`} />
|
||||||
|
<link rel="alternate" hreflang="en" href={`https://gethumanai.com${enUrl}`} />
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://gethumanai.com/" />
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content={canonicalUrl} />
|
||||||
|
<meta property="og:title" content={tr.meta.og_title} />
|
||||||
|
<meta property="og:description" content={tr.meta.og_description} />
|
||||||
|
<meta property="og:locale" content={locale === 'pl' ? 'pl_PL' : 'en_US'} />
|
||||||
|
<meta property="og:site_name" content="humanAI" />
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-bg: #fafaf8;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-alt: #f4f4f0;
|
||||||
|
--color-text: #1a1a18;
|
||||||
|
--color-text-muted: #6b6b65;
|
||||||
|
--color-accent: #1a3d2e;
|
||||||
|
--color-accent-hover: #122d21;
|
||||||
|
--color-border: #e4e4de;
|
||||||
|
--color-industry: #2c3e50;
|
||||||
|
--color-med: #2c3e50;
|
||||||
|
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
--radius: 6px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
--max-w: 1100px;
|
||||||
|
--section-py: clamp(4rem, 8vw, 7rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.65;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--max-w);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: clamp(1.25rem, 5vw, 2.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1.75rem;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: background 0.18s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--color-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1.75rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border: 1.5px solid var(--color-accent);
|
||||||
|
transition: background 0.18s, color 0.18s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding-block: var(--section-py);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(1.6rem, 4vw, 2.2rem);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
max-width: 60ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility */
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.text-center p { margin-inline: auto; }
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
src/pages/en/index.astro
Normal file
29
src/pages/en/index.astro
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
import Layout from '../../layouts/Layout.astro';
|
||||||
|
import Nav from '../../components/Nav.astro';
|
||||||
|
import Hero from '../../components/Hero.astro';
|
||||||
|
import Problem from '../../components/Problem.astro';
|
||||||
|
import HowWeWork from '../../components/HowWeWork.astro';
|
||||||
|
import Offers from '../../components/Offers.astro';
|
||||||
|
import Process from '../../components/Process.astro';
|
||||||
|
import Security from '../../components/Security.astro';
|
||||||
|
import Contact from '../../components/Contact.astro';
|
||||||
|
import Footer from '../../components/Footer.astro';
|
||||||
|
|
||||||
|
const locale = 'en' as const;
|
||||||
|
const url = Astro.url;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout locale={locale} url={url}>
|
||||||
|
<Nav locale={locale} url={url} />
|
||||||
|
<main id="main" tabindex="-1">
|
||||||
|
<Hero locale={locale} />
|
||||||
|
<Problem locale={locale} />
|
||||||
|
<HowWeWork locale={locale} />
|
||||||
|
<Offers locale={locale} />
|
||||||
|
<Process locale={locale} />
|
||||||
|
<Security locale={locale} />
|
||||||
|
<Contact locale={locale} />
|
||||||
|
</main>
|
||||||
|
<Footer locale={locale} />
|
||||||
|
</Layout>
|
||||||
29
src/pages/index.astro
Normal file
29
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
import Nav from '../components/Nav.astro';
|
||||||
|
import Hero from '../components/Hero.astro';
|
||||||
|
import Problem from '../components/Problem.astro';
|
||||||
|
import HowWeWork from '../components/HowWeWork.astro';
|
||||||
|
import Offers from '../components/Offers.astro';
|
||||||
|
import Process from '../components/Process.astro';
|
||||||
|
import Security from '../components/Security.astro';
|
||||||
|
import Contact from '../components/Contact.astro';
|
||||||
|
import Footer from '../components/Footer.astro';
|
||||||
|
|
||||||
|
const locale = 'pl' as const;
|
||||||
|
const url = Astro.url;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout locale={locale} url={url}>
|
||||||
|
<Nav locale={locale} url={url} />
|
||||||
|
<main id="main" tabindex="-1">
|
||||||
|
<Hero locale={locale} />
|
||||||
|
<Problem locale={locale} />
|
||||||
|
<HowWeWork locale={locale} />
|
||||||
|
<Offers locale={locale} />
|
||||||
|
<Process locale={locale} />
|
||||||
|
<Security locale={locale} />
|
||||||
|
<Contact locale={locale} />
|
||||||
|
</main>
|
||||||
|
<Footer locale={locale} />
|
||||||
|
</Layout>
|
||||||
5
tsconfig.json
Normal file
5
tsconfig.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue