Initial community site — AI HUB Tilburg
Zero-dependency static site. config.json-driven. Deploy: docker compose up -d Publish: GitHub Pages / Cloudflare Pages / Tailscale Serve Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
a402cbaef5
11 changed files with 844 additions and 0 deletions
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "tilburg-hub",
|
||||||
|
"runtimeExecutable": "python",
|
||||||
|
"runtimeArgs": ["-m", "http.server", "3456", "--directory", "site"],
|
||||||
|
"port": 3456
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
112
README.md
Normal file
112
README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# AI HUB Tilburg — Community Site
|
||||||
|
|
||||||
|
Open-source community website template. Clone it, configure it, deploy it in 5 minutes.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <this-repo>
|
||||||
|
cd tilburg-hub
|
||||||
|
|
||||||
|
# 1. Edit your community details
|
||||||
|
nano config.json # or open in any editor
|
||||||
|
|
||||||
|
# 2. Run
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 3. Open http://localhost:3456
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. No database, no build step, no accounts.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All community settings live in `config.json`:
|
||||||
|
|
||||||
|
| Key | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `community.name` | Site title and nav logo |
|
||||||
|
| `community.tagline` | Short description |
|
||||||
|
| `join.whatsapp_invite` | The WhatsApp group invite link |
|
||||||
|
| `location.*` | Physical address shown on the site |
|
||||||
|
| `meetings.*` | Meeting schedule and format |
|
||||||
|
| `rules[]` | Community rules (array of strings) |
|
||||||
|
| `projects[]` | Member projects to showcase |
|
||||||
|
| `founding_members[]` | Listed in the footer |
|
||||||
|
|
||||||
|
## Rename for your community
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Replace all default placeholders in one command:
|
||||||
|
grep -rl 'AI HUB Tilburg' . | xargs sed -i 's/AI HUB Tilburg/Your Community Name/g'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update `config.json` with your details.
|
||||||
|
|
||||||
|
## Deploy publicly (free, no server required)
|
||||||
|
|
||||||
|
### Option A — GitHub Pages
|
||||||
|
1. Push this repo to GitHub
|
||||||
|
2. Go to Settings → Pages → Deploy from `main` branch `/site` folder
|
||||||
|
3. Done. Your community is live at `https://YOUR_ORG.github.io/tilburg-hub`
|
||||||
|
|
||||||
|
### Option B — Cloudflare Pages
|
||||||
|
1. Connect your GitHub repo to Cloudflare Pages
|
||||||
|
2. Root: `site/`, build command: *(none — static)*
|
||||||
|
3. Free HTTPS, global CDN
|
||||||
|
|
||||||
|
### Option C — MET Desktop (Docker, local network / Tailscale)
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
Then expose via Tailscale Serve or Cloudflare Tunnel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tailscale Serve (one-liner, works on Windows/Mac/Linux)
|
||||||
|
tailscale serve http://localhost:3456
|
||||||
|
|
||||||
|
# Or use the Cloudflare Tunnel block in docker-compose.yml (uncomment + add your token)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Evolution API integration (optional WhatsApp bot)
|
||||||
|
|
||||||
|
If you have an Evolution API instance connected to WhatsApp, you can wire a webhook to:
|
||||||
|
- Welcome new members when they join the group
|
||||||
|
- Post project updates to the group from the website
|
||||||
|
|
||||||
|
See `evolution/` folder for webhook scripts. Set your API key in `.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
EVOLUTION_API_URL=http://YOUR_SERVER:8120
|
||||||
|
EVOLUTION_API_KEY=your-key-here
|
||||||
|
EVOLUTION_INSTANCE=your-instance-name
|
||||||
|
WA_GROUP_JID=123456789@g.us
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tilburg-hub/
|
||||||
|
├── config.json ← ALL community settings (edit this)
|
||||||
|
├── docker-compose.yml ← one-command deploy
|
||||||
|
├── nginx.conf ← webserver config
|
||||||
|
├── site/
|
||||||
|
│ ├── index.html ← the page
|
||||||
|
│ ├── style.css ← styles (warm, neutral, not Atlas)
|
||||||
|
│ ├── app.js ← reads config.json, populates page
|
||||||
|
│ └── config.js ← placeholder (overwritten by app.js)
|
||||||
|
├── evolution/ ← optional WhatsApp webhook scripts
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
This site was built to be:
|
||||||
|
- **Independent** — no central server, no Atlas branding required
|
||||||
|
- **Forkable** — anyone can clone and run their own community
|
||||||
|
- **Zero-dependency** — pure HTML/CSS/JS, no framework, no npm
|
||||||
|
- **Config-first** — change everything without touching HTML
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — do what you want with it.
|
||||||
40
config.json
Normal file
40
config.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"community": {
|
||||||
|
"name": "AI HUB Tilburg",
|
||||||
|
"tagline": "Builders, thinkers, makers — geen leider, wel richting.",
|
||||||
|
"description": "Een open community voor iedereen in de regio Tilburg die bouwt, onderzoekt of experimenteert met AI, technologie en ondernemen. Geen agenda, geen hiërarchie. Je project is van jou.",
|
||||||
|
"founded": "2026",
|
||||||
|
"language": "nl"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"name": "Business Centre Tilburg",
|
||||||
|
"address": "Kraaivenstraat 36-14",
|
||||||
|
"city": "5048 AB Tilburg",
|
||||||
|
"country": "Nederland",
|
||||||
|
"maps_query": "Kraaivenstraat+36+Tilburg"
|
||||||
|
},
|
||||||
|
"meetings": {
|
||||||
|
"schedule": "Elke zaterdag",
|
||||||
|
"time": "12:00–14:00",
|
||||||
|
"format": "Inloop — kom als je kan, ga als je wil",
|
||||||
|
"note": "Check WhatsApp voor actuele locatie en updates"
|
||||||
|
},
|
||||||
|
"join": {
|
||||||
|
"whatsapp_invite": "https://chat.whatsapp.com/REPLACE_WITH_YOUR_LINK",
|
||||||
|
"cta_text": "Word lid via WhatsApp"
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
"Iedereen heeft zijn eigen project. Niemand beslist voor een ander.",
|
||||||
|
"Geen commerciële pitch zonder toestemming van de groep.",
|
||||||
|
"Opbouwend — vragen stellen is beter dan oordelen.",
|
||||||
|
"Aanwezig zijn als je kan. Online check-in altijd welkom.",
|
||||||
|
"Middelen en kennis delen als je wil — niet verplicht."
|
||||||
|
],
|
||||||
|
"founding_members": [
|
||||||
|
{ "name": "Atlas Corporation", "role": "Founding member · sponsor · facilitator", "url": "https://atlascorporation.nl" }
|
||||||
|
],
|
||||||
|
"projects": [],
|
||||||
|
"social": {
|
||||||
|
"whatsapp": true
|
||||||
|
}
|
||||||
|
}
|
||||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3456:80" # change 3456 to any free port on your machine
|
||||||
|
volumes:
|
||||||
|
- ./site:/usr/share/nginx/html:ro
|
||||||
|
- ./config.json:/usr/share/nginx/html/config.json:ro
|
||||||
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
labels:
|
||||||
|
- "community=tilburg-hub"
|
||||||
|
|
||||||
|
# Optional: Cloudflare Tunnel for public HTTPS without port forwarding
|
||||||
|
# tunnel:
|
||||||
|
# image: cloudflare/cloudflared:latest
|
||||||
|
# restart: unless-stopped
|
||||||
|
# command: tunnel --no-autoupdate run --token YOUR_TUNNEL_TOKEN
|
||||||
|
# depends_on: [web]
|
||||||
5
evolution/.env.example
Normal file
5
evolution/.env.example
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
EVOLUTION_API_URL=http://YOUR_SERVER:8120
|
||||||
|
EVOLUTION_API_KEY=your-api-key-here
|
||||||
|
EVOLUTION_INSTANCE=atlas
|
||||||
|
WA_GROUP_JID=123456789@g.us
|
||||||
|
BOT_PORT=8765
|
||||||
90
evolution/welcome_bot.py
Normal file
90
evolution/welcome_bot.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AI HUB Tilburg — WhatsApp welcome bot via Evolution API.
|
||||||
|
Sends a welcome message when a new member joins the group.
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
cp .env.example .env
|
||||||
|
# fill in your values
|
||||||
|
python3 welcome_bot.py --serve # starts a webhook server on :8765
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, json, sys, http.server, urllib.request, urllib.parse
|
||||||
|
|
||||||
|
EVO_URL = os.getenv("EVOLUTION_API_URL", "http://localhost:8120")
|
||||||
|
EVO_KEY = os.getenv("EVOLUTION_API_KEY", "")
|
||||||
|
INSTANCE = os.getenv("EVOLUTION_INSTANCE", "atlas")
|
||||||
|
GROUP_JID = os.getenv("WA_GROUP_JID", "") # 123456789@g.us
|
||||||
|
|
||||||
|
WELCOME_MESSAGE = """👋 Welkom bij AI HUB Tilburg!
|
||||||
|
|
||||||
|
We zijn een open community van builders, makers en denkers in de regio Tilburg. Geen leider, geen verplichtingen.
|
||||||
|
|
||||||
|
📍 We komen samen op zaterdag 12:00–14:00 bij Business Centre Tilburg.
|
||||||
|
|
||||||
|
Vertel gerust wie je bent en wat je bouwt. Niemand beslist voor jou — jouw project is van jou.
|
||||||
|
|
||||||
|
Fijn dat je erbij bent! 🙌"""
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(to_jid: str, text: str):
|
||||||
|
payload = json.dumps({"number": to_jid, "text": text}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{EVO_URL}/message/sendText/{INSTANCE}",
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json", "apikey": EVO_KEY},
|
||||||
|
method="POST"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as r:
|
||||||
|
print(f"[bot] sent to {to_jid}: HTTP {r.status}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[bot] error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self):
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(length)
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = json.loads(body)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Evolution API group participant update event
|
||||||
|
event_type = event.get("event", "")
|
||||||
|
if event_type == "group-participants.update":
|
||||||
|
data = event.get("data", {})
|
||||||
|
action = data.get("action", "")
|
||||||
|
jid = data.get("id", "")
|
||||||
|
|
||||||
|
if action in ("add", "invite") and jid:
|
||||||
|
print(f"[bot] new member: {jid}")
|
||||||
|
# Welcome in group
|
||||||
|
send_message(GROUP_JID, WELCOME_MESSAGE)
|
||||||
|
# Or DM the new member directly:
|
||||||
|
# send_message(jid, WELCOME_MESSAGE)
|
||||||
|
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass # suppress access logs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if "--serve" in sys.argv:
|
||||||
|
port = int(os.getenv("BOT_PORT", "8765"))
|
||||||
|
server = http.server.HTTPServer(("0.0.0.0", port), WebhookHandler)
|
||||||
|
print(f"[bot] listening on :{port}")
|
||||||
|
print(f"[bot] register this URL as Evolution webhook for instance '{INSTANCE}'")
|
||||||
|
server.serve_forever()
|
||||||
|
else:
|
||||||
|
print("Usage: python3 welcome_bot.py --serve")
|
||||||
|
print(" Then register http://YOUR_IP:8765 as a webhook in Evolution API")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
25
nginx.conf
Normal file
25
nginx.conf
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# serve config.json with correct MIME type + CORS for fetch()
|
||||||
|
location = /config.json {
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA fallback (not needed for static, but safe to have)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options SAMEORIGIN;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Referrer-Policy no-referrer-when-downgrade;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
||||||
|
}
|
||||||
86
site/app.js
Normal file
86
site/app.js
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
// AI HUB Tilburg — community site runtime
|
||||||
|
// Reads config.json and populates the page.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
function get(obj, path) {
|
||||||
|
return path.split('.').reduce(function (o, k) {
|
||||||
|
return o && o[k] !== undefined ? o[k] : null;
|
||||||
|
}, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyConfig(cfg) {
|
||||||
|
// Text nodes
|
||||||
|
document.querySelectorAll('[data-config]').forEach(function (el) {
|
||||||
|
var val = get(cfg, el.dataset.config);
|
||||||
|
if (val !== null) el.textContent = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
// href → direct config path
|
||||||
|
document.querySelectorAll('[data-config-href]').forEach(function (el) {
|
||||||
|
var val = get(cfg, el.dataset.configHref);
|
||||||
|
if (val) el.href = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
// href → Google Maps query
|
||||||
|
document.querySelectorAll('[data-config-href-maps]').forEach(function (el) {
|
||||||
|
var val = get(cfg, el.dataset.configHrefMaps);
|
||||||
|
if (val) el.href = 'https://www.google.com/maps/search/' + encodeURIComponent(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rules list
|
||||||
|
var rulesEl = document.getElementById('rules-list');
|
||||||
|
if (rulesEl && cfg.rules && cfg.rules.length) {
|
||||||
|
rulesEl.innerHTML = cfg.rules.map(function (r) {
|
||||||
|
return '<li>' + r + '</li>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projects grid
|
||||||
|
var grid = document.getElementById('projects-grid');
|
||||||
|
if (grid && cfg.projects && cfg.projects.length) {
|
||||||
|
grid.innerHTML = cfg.projects.map(function (p) {
|
||||||
|
return [
|
||||||
|
'<div class="project-card">',
|
||||||
|
' <div class="project-icon">' + (p.icon || '🔧') + '</div>',
|
||||||
|
' <h3>' + p.name + '</h3>',
|
||||||
|
' <p>' + p.description + '</p>',
|
||||||
|
p.url ? ' <a class="link" href="' + p.url + '" target="_blank" rel="noopener">Bekijk project →</a>' : '',
|
||||||
|
' <div class="project-meta" style="margin-top:12px;font-size:12px;color:var(--text-4);font-family:var(--font-mono)">' + (p.author || '') + '</div>',
|
||||||
|
'</div>'
|
||||||
|
].join('');
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Founding members in footer
|
||||||
|
var fLink = document.getElementById('founding-link');
|
||||||
|
if (fLink && cfg.founding_members && cfg.founding_members.length) {
|
||||||
|
var fm = cfg.founding_members[0];
|
||||||
|
fLink.textContent = fm.name;
|
||||||
|
if (fm.url) fLink.href = fm.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language
|
||||||
|
if (cfg.community && cfg.community.language) {
|
||||||
|
document.documentElement.lang = cfg.community.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page title
|
||||||
|
if (cfg.community && cfg.community.name) {
|
||||||
|
document.title = cfg.community.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load from config.json (works when served via HTTP)
|
||||||
|
fetch('../config.json')
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(applyConfig)
|
||||||
|
.catch(function () {
|
||||||
|
// Fallback: try same directory
|
||||||
|
fetch('config.json')
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(applyConfig)
|
||||||
|
.catch(function () {
|
||||||
|
console.info('[tilburg-hub] config.json not found — using HTML defaults');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
5
site/config.js
Normal file
5
site/config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Auto-generated from config.json by build.js
|
||||||
|
// Or served directly if no build step is used
|
||||||
|
// Edit config.json — not this file.
|
||||||
|
|
||||||
|
window.HUB_CONFIG = null; // populated by app.js via fetch("../config.json")
|
||||||
174
site/index.html
Normal file
174
site/index.html
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AI HUB Tilburg</title>
|
||||||
|
<meta name="description" content="Een open community voor builders, thinkers en makers in de regio Tilburg. Geen leider, wel richting.">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="config.js" defer></script>
|
||||||
|
<script src="app.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- NAV -->
|
||||||
|
<nav>
|
||||||
|
<div class="container nav-inner">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-dot"></span>
|
||||||
|
<span class="logo-text" data-config="community.name">AI HUB Tilburg</span>
|
||||||
|
</div>
|
||||||
|
<a href="#join" class="btn btn-sm">Word lid →</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="badge" data-config="community.founded">Opgericht 2026 · Tilburg</div>
|
||||||
|
<h1>Builders,<br>thinkers,<br><em>makers.</em></h1>
|
||||||
|
<p class="lead" data-config="community.description">
|
||||||
|
Een open community voor iedereen in de regio Tilburg die bouwt, onderzoekt of experimenteert met AI, technologie en ondernemen. Geen agenda, geen hiërarchie. Je project is van jou.
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href="#join" class="btn btn-primary" data-config-href="join.whatsapp_invite" data-config-text="join.cta_text">Word lid via WhatsApp</a>
|
||||||
|
<a href="#bijeenkomsten" class="btn btn-ghost">Wanneer & Waar →</a>
|
||||||
|
</div>
|
||||||
|
<div class="hero-meta">
|
||||||
|
<span>🟢 Actief</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span data-config="location.city">5048 AB Tilburg</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>Open voor iedereen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- WHAT -->
|
||||||
|
<section class="section" id="over">
|
||||||
|
<div class="container grid-2">
|
||||||
|
<div>
|
||||||
|
<span class="kicker">Wat is dit?</span>
|
||||||
|
<h2>Geen club, geen bedrijf.<br>Een verzamelpunt.</h2>
|
||||||
|
<p>
|
||||||
|
AI HUB Tilburg is een informele community. We hebben geen bestuur, geen contributie en geen gemeenschappelijke agenda. Iedereen werkt aan zijn eigen project — samen in dezelfde ruimte, met dezelfde energie.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Wat je hier vindt: mensen die vragen stellen, kennis delen, resources samenvoegen en soms gewoon laten zien waar ze mee bezig zijn.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-stack">
|
||||||
|
<div class="principle-card">
|
||||||
|
<span class="number">01</span>
|
||||||
|
<h3>Jouw project, jouw tempo</h3>
|
||||||
|
<p>Niemand stuurt jou. Je deelt wat je wil, wanneer je wil.</p>
|
||||||
|
</div>
|
||||||
|
<div class="principle-card">
|
||||||
|
<span class="number">02</span>
|
||||||
|
<h3>Middelen samenvoegen</h3>
|
||||||
|
<p>Kennis, netwerk, tools — als je iets kunt bieden, bied je het aan. Vrijwillig.</p>
|
||||||
|
</div>
|
||||||
|
<div class="principle-card">
|
||||||
|
<span class="number">03</span>
|
||||||
|
<h3>Compound groei</h3>
|
||||||
|
<p>Samen sneller dan alleen. Door te zien wat anderen doen, groei je zelf.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MEETINGS -->
|
||||||
|
<section class="section section-tinted" id="bijeenkomsten">
|
||||||
|
<div class="container">
|
||||||
|
<span class="kicker">Bijeenkomsten</span>
|
||||||
|
<h2>Elke week. Geen agenda.</h2>
|
||||||
|
<div class="meetings-grid">
|
||||||
|
<div class="meeting-card">
|
||||||
|
<div class="meeting-icon">📅</div>
|
||||||
|
<h3 data-config="meetings.schedule">Elke zaterdag</h3>
|
||||||
|
<p data-config="meetings.time">12:00–14:00</p>
|
||||||
|
<p class="sub" data-config="meetings.format">Inloop — kom als je kan, ga als je wil</p>
|
||||||
|
</div>
|
||||||
|
<div class="meeting-card">
|
||||||
|
<div class="meeting-icon">📍</div>
|
||||||
|
<h3 data-config="location.name">Business Centre Tilburg</h3>
|
||||||
|
<p data-config="location.address">Kraaivenstraat 36-14</p>
|
||||||
|
<p class="sub" data-config="location.city">5048 AB Tilburg</p>
|
||||||
|
<a href="#" class="link" id="maps-link" data-config-href-maps="location.maps_query">Open in Maps →</a>
|
||||||
|
</div>
|
||||||
|
<div class="meeting-card">
|
||||||
|
<div class="meeting-icon">💻</div>
|
||||||
|
<h3>Online check-in</h3>
|
||||||
|
<p>Kan je er niet bij zijn?</p>
|
||||||
|
<p class="sub" data-config="meetings.note">Check WhatsApp voor actuele locatie en updates</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- RULES -->
|
||||||
|
<section class="section" id="spelregels">
|
||||||
|
<div class="container container-narrow">
|
||||||
|
<span class="kicker">Spelregels</span>
|
||||||
|
<h2>Vijf afspraken. Niet meer.</h2>
|
||||||
|
<ol class="rules-list" id="rules-list">
|
||||||
|
<li>Iedereen heeft zijn eigen project. Niemand beslist voor een ander.</li>
|
||||||
|
<li>Geen commerciële pitch zonder toestemming van de groep.</li>
|
||||||
|
<li>Opbouwend — vragen stellen is beter dan oordelen.</li>
|
||||||
|
<li>Aanwezig zijn als je kan. Online check-in altijd welkom.</li>
|
||||||
|
<li>Middelen en kennis delen als je wil — niet verplicht.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PROJECTS -->
|
||||||
|
<section class="section section-tinted" id="projecten">
|
||||||
|
<div class="container">
|
||||||
|
<span class="kicker">Projecten</span>
|
||||||
|
<h2>Wat leden bouwen.</h2>
|
||||||
|
<p class="section-lead">Leden kunnen hier hun project toevoegen. Geen pitch — gewoon: wat ben je aan het doen?</p>
|
||||||
|
<div class="projects-grid" id="projects-grid">
|
||||||
|
<!-- Populated from config.json or submitted by members -->
|
||||||
|
<div class="project-card project-placeholder">
|
||||||
|
<div class="project-icon">🚧</div>
|
||||||
|
<p>Jouw project hier? Deel het in de WhatsApp groep.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- JOIN -->
|
||||||
|
<section class="section join-section" id="join">
|
||||||
|
<div class="container container-narrow text-center">
|
||||||
|
<span class="kicker">Doe mee</span>
|
||||||
|
<h2>Klaar om mee te bouwen?</h2>
|
||||||
|
<p>Sluit je aan via WhatsApp. Geen sollicitatie, geen formulier. Gewoon joinen en aanwezig zijn.</p>
|
||||||
|
<a class="btn btn-primary btn-lg" href="#" data-config-href="join.whatsapp_invite" id="join-btn">
|
||||||
|
📱 Word lid via WhatsApp
|
||||||
|
</a>
|
||||||
|
<p class="join-sub">Stuur een berichtje wie je bent en wat je bouwt.<br>Dat is alles.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<footer>
|
||||||
|
<div class="container footer-inner">
|
||||||
|
<div class="footer-left">
|
||||||
|
<span class="logo-text" data-config="community.name">AI HUB Tilburg</span>
|
||||||
|
<span class="footer-sub" data-config="location.city">Tilburg · Nederland</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<span class="footer-sub">Founding member:</span>
|
||||||
|
<a href="https://atlascorporation.nl" target="_blank" rel="noopener" id="founding-link">Atlas Corporation</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-copy">
|
||||||
|
<span>Open community · Geen commercieel belang · </span>
|
||||||
|
<a href="https://github.com/YOUR_ORG/tilburg-hub" target="_blank" rel="noopener">Broncode op GitHub</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
277
site/style.css
Normal file
277
site/style.css
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
/* AI HUB Tilburg — Community Site
|
||||||
|
Deliberately NOT Atlas dark/space theme.
|
||||||
|
Warm, neutral, maker-space energy. */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #FAFAF8;
|
||||||
|
--bg-tint: #F4F2EE;
|
||||||
|
--surface: #FFFFFF;
|
||||||
|
--border: #E8E4DE;
|
||||||
|
--text-1: #1A1714;
|
||||||
|
--text-2: #4A4540;
|
||||||
|
--text-3: #7A736C;
|
||||||
|
--text-4: #A8A098;
|
||||||
|
|
||||||
|
--accent: #2B6CB0;
|
||||||
|
--accent-warm: #C05621;
|
||||||
|
--accent-green:#276749;
|
||||||
|
|
||||||
|
--font-sans: 'Inter', system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Courier New', monospace;
|
||||||
|
|
||||||
|
--r: 8px;
|
||||||
|
--r-lg: 16px;
|
||||||
|
--shadow: 0 1px 4px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.04);
|
||||||
|
--shadow-lg: 0 8px 32px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; font-size: 16px; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-1);
|
||||||
|
line-height: 1.65;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LAYOUT */
|
||||||
|
.container { max-width: 1080px; margin: 0 auto; padding: 0 24px; }
|
||||||
|
.container-narrow { max-width: 680px; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: start; }
|
||||||
|
@media (max-width: 720px) { .grid-2 { grid-template-columns: 1fr; gap: 40px; } }
|
||||||
|
|
||||||
|
/* TYPOGRAPHY */
|
||||||
|
h1 { font-size: clamp(52px, 8vw, 96px); font-weight: 800; line-height: 1.0; letter-spacing: -0.03em; color: var(--text-1); }
|
||||||
|
h1 em { font-style: normal; color: var(--accent); }
|
||||||
|
h2 { font-size: clamp(28px, 4vw, 44px); font-weight: 700; line-height: 1.15; letter-spacing: -0.02em; margin-bottom: 20px; }
|
||||||
|
h3 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
|
||||||
|
p { color: var(--text-2); font-size: 16px; line-height: 1.75; margin-bottom: 16px; }
|
||||||
|
p:last-child { margin-bottom: 0; }
|
||||||
|
.lead { font-size: 19px; line-height: 1.7; color: var(--text-2); max-width: 56ch; margin-top: 24px; margin-bottom: 36px; }
|
||||||
|
.section-lead { font-size: 18px; color: var(--text-2); max-width: 52ch; margin-bottom: 40px; }
|
||||||
|
.sub { color: var(--text-3); font-size: 14px; margin-top: 4px; }
|
||||||
|
.link { color: var(--accent); font-size: 14px; font-weight: 500; text-decoration: none; display: inline-block; margin-top: 12px; }
|
||||||
|
.link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-3);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BUTTONS */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 8px;
|
||||||
|
padding: 12px 22px; border-radius: var(--r);
|
||||||
|
font-size: 15px; font-weight: 600; text-decoration: none;
|
||||||
|
cursor: pointer; border: none; transition: all 0.15s ease;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.btn-sm { padding: 8px 16px; font-size: 13px; }
|
||||||
|
.btn-lg { padding: 16px 32px; font-size: 17px; }
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent); color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(43,108,176,0.25);
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background: #2558a0; transform: translateY(-1px); box-shadow: 0 4px 16px rgba(43,108,176,0.3); }
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent; color: var(--text-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover { border-color: var(--text-3); color: var(--text-1); }
|
||||||
|
|
||||||
|
/* NAV */
|
||||||
|
nav {
|
||||||
|
position: sticky; top: 0; z-index: 100;
|
||||||
|
background: rgba(250,250,248,0.92);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.nav-inner {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.logo { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.logo-dot {
|
||||||
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
animation: pulse 2.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.6; transform: scale(0.85); }
|
||||||
|
}
|
||||||
|
.logo-text { font-size: 15px; font-weight: 700; color: var(--text-1); }
|
||||||
|
|
||||||
|
/* HERO */
|
||||||
|
.hero {
|
||||||
|
padding: 96px 0 80px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -40%; right: -20%;
|
||||||
|
width: 600px; height: 600px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(43,108,176,0.06) 0%, transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.hero h1 { margin-bottom: 0; }
|
||||||
|
.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
|
||||||
|
.hero-meta {
|
||||||
|
display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
|
||||||
|
margin-top: 32px;
|
||||||
|
font-family: var(--font-mono); font-size: 12px;
|
||||||
|
color: var(--text-3); letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--font-mono); font-size: 11px;
|
||||||
|
color: var(--accent); background: rgba(43,108,176,0.08);
|
||||||
|
border: 1px solid rgba(43,108,176,0.2);
|
||||||
|
border-radius: 999px; padding: 4px 12px;
|
||||||
|
margin-bottom: 24px; letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SECTIONS */
|
||||||
|
.section { padding: 96px 0; }
|
||||||
|
.section-tinted { background: var(--bg-tint); }
|
||||||
|
|
||||||
|
/* PRINCIPLE CARDS */
|
||||||
|
.card-stack { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
.principle-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
padding: 24px 28px;
|
||||||
|
position: relative;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.principle-card:hover { box-shadow: var(--shadow-lg); }
|
||||||
|
.number {
|
||||||
|
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
||||||
|
color: var(--text-4); letter-spacing: 0.1em;
|
||||||
|
display: block; margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.principle-card h3 { font-size: 16px; margin-bottom: 6px; }
|
||||||
|
.principle-card p { font-size: 14px; margin: 0; }
|
||||||
|
|
||||||
|
/* MEETINGS */
|
||||||
|
.meetings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
.meeting-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
padding: 28px 24px;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.meeting-card:hover { box-shadow: var(--shadow-lg); }
|
||||||
|
.meeting-icon { font-size: 28px; margin-bottom: 14px; }
|
||||||
|
.meeting-card h3 { font-size: 17px; margin-bottom: 6px; }
|
||||||
|
.meeting-card p { font-size: 15px; margin-bottom: 2px; }
|
||||||
|
|
||||||
|
/* RULES */
|
||||||
|
.rules-list {
|
||||||
|
list-style: none;
|
||||||
|
counter-reset: rule;
|
||||||
|
display: flex; flex-direction: column; gap: 16px;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
.rules-list li {
|
||||||
|
counter-increment: rule;
|
||||||
|
display: flex; align-items: flex-start; gap: 16px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r);
|
||||||
|
font-size: 16px; color: var(--text-1); line-height: 1.6;
|
||||||
|
}
|
||||||
|
.rules-list li::before {
|
||||||
|
content: counter(rule, decimal-leading-zero);
|
||||||
|
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
||||||
|
color: var(--text-4); white-space: nowrap; margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PROJECTS */
|
||||||
|
.projects-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.project-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
padding: 28px;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.project-card:hover { box-shadow: var(--shadow-lg); }
|
||||||
|
.project-icon { font-size: 32px; margin-bottom: 14px; }
|
||||||
|
.project-placeholder {
|
||||||
|
border-style: dashed;
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
justify-content: center; text-align: center;
|
||||||
|
min-height: 160px; color: var(--text-3);
|
||||||
|
}
|
||||||
|
.project-placeholder p { color: var(--text-3); font-size: 14px; }
|
||||||
|
|
||||||
|
/* JOIN */
|
||||||
|
.join-section {
|
||||||
|
background: var(--text-1);
|
||||||
|
color: #fff;
|
||||||
|
padding: 96px 0;
|
||||||
|
}
|
||||||
|
.join-section .kicker { color: rgba(255,255,255,0.4); }
|
||||||
|
.join-section h2 { color: #fff; }
|
||||||
|
.join-section p { color: rgba(255,255,255,0.65); max-width: 44ch; margin: 0 auto 36px; }
|
||||||
|
.join-sub { font-size: 14px; color: rgba(255,255,255,0.4); margin-top: 20px; text-align: center; }
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
footer {
|
||||||
|
background: var(--bg-tint);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
.footer-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.footer-copy {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-size: 12px; color: var(--text-4);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
.footer-copy a { color: var(--accent); text-decoration: none; }
|
||||||
|
.footer-left { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.footer-right { text-align: right; display: flex; flex-direction: column; gap: 4px; align-items: flex-end; }
|
||||||
|
.footer-sub { font-size: 12px; color: var(--text-3); }
|
||||||
|
.footer-right a { color: var(--accent); font-size: 14px; font-weight: 500; text-decoration: none; }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.footer-inner { grid-template-columns: 1fr; }
|
||||||
|
.footer-right { text-align: left; align-items: flex-start; }
|
||||||
|
.footer-copy { grid-column: 1; }
|
||||||
|
.hero { padding: 60px 0 56px; }
|
||||||
|
.section { padding: 64px 0; }
|
||||||
|
.join-section { padding: 64px 0; }
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue